blob: 6c779728d62b21f81a8bc8399258e4d0c85303af [file] [log] [blame]
Edward Lesmes98eda3f2019-08-12 21:09:53 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
sheyang@google.com6ebaf782015-05-12 19:17:54 +000018import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010019import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000020import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000022import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import optparse
24import os
25import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010026import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000027import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070029import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000031import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000041import auth
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000042import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000043import dart_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000044import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000045import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000046import gerrit_util
iannucci@chromium.org9e849272014-04-04 00:31:55 +000047import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000048import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000049import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000050import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000051import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000053import presubmit_support
54import scm
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +000055import setup_color
Francois Dorayd42c6812017-05-30 15:10:20 -040056import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000057import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000059import watchlists
60
tandrii7400cf02016-06-21 08:48:07 -070061__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062
Edward Lemur0f58ae42019-04-30 17:24:12 +000063# Traces for git push will be stored in a traces directory inside the
64# depot_tools checkout.
65DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
66TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
67
68# When collecting traces, Git hashes will be reduced to 6 characters to reduce
69# the size after compression.
70GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
71# Used to redact the cookies from the gitcookies file.
72GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
73
Edward Lemur1b52d872019-05-09 21:12:12 +000074# The maximum number of traces we will keep. Multiplied by 3 since we store
75# 3 files per trace.
76MAX_TRACES = 3 * 10
Edward Lemur5737f022019-05-17 01:24:00 +000077# Message to be displayed to the user to inform where to find the traces for a
78# git-cl upload execution.
Edward Lemur0f58ae42019-04-30 17:24:12 +000079TRACES_MESSAGE = (
Edward Lemur1b52d872019-05-09 21:12:12 +000080'\n'
Edward Lemur5737f022019-05-17 01:24:00 +000081'The traces of this git-cl execution have been recorded at:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +000082' %(trace_name)s-traces.zip\n'
Edward Lemur5737f022019-05-17 01:24:00 +000083'Copies of your gitcookies file and git config have been recorded at:\n'
84' %(trace_name)s-git-info.zip\n')
Edward Lemur1b52d872019-05-09 21:12:12 +000085# Format of the message to be stored as part of the traces to give developers a
86# better context when they go through traces.
87TRACES_README_FORMAT = (
88'Date: %(now)s\n'
89'\n'
90'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
91'Title: %(title)s\n'
92'\n'
93'%(description)s\n'
94'\n'
95'Execution time: %(execution_time)s\n'
96'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +000097
tandrii9d2c7a32016-06-22 03:42:45 -070098COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080099POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000100DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000101REFS_THAT_ALIAS_TO_OTHER_REFS = {
102 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
103 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
104}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000105
thestig@chromium.org44202a22014-03-11 19:22:18 +0000106# Valid extensions for files we want to lint.
107DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
108DEFAULT_LINT_IGNORE_REGEX = r"$^"
109
Aiden Bennerc08566e2018-10-03 17:52:42 +0000110# File name for yapf style config files.
111YAPF_CONFIG_FILENAME = '.style.yapf'
112
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000113# Buildbucket master name prefix for Buildbot masters.
borenet6c0efe62016-10-19 08:13:29 -0700114MASTER_PREFIX = 'master.'
115
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000116# Shortcut since it quickly becomes repetitive.
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000117Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000118
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000119# Initialized in main()
120settings = None
121
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100122# Used by tests/git_cl_test.py to add extra logging.
123# Inside the weirdly failing test, add this:
124# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700125# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100126_IS_BEING_TESTED = False
127
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000128
Christopher Lamf732cd52017-01-24 12:40:11 +1100129def DieWithError(message, change_desc=None):
130 if change_desc:
131 SaveDescriptionBackup(change_desc)
132
vapiera7fbd5a2016-06-16 09:17:49 -0700133 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000134 sys.exit(1)
135
136
Christopher Lamf732cd52017-01-24 12:40:11 +1100137def SaveDescriptionBackup(change_desc):
138 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000139 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100140 backup_file = open(backup_path, 'w')
141 backup_file.write(change_desc.description)
142 backup_file.close()
143
144
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000145def GetNoGitPagerEnv():
146 env = os.environ.copy()
147 # 'cat' is a magical git string that disables pagers on all platforms.
148 env['GIT_PAGER'] = 'cat'
149 return env
150
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000151
bsep@chromium.org627d9002016-04-29 00:00:52 +0000152def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000153 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000154 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000155 except subprocess2.CalledProcessError as e:
156 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000157 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000158 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000159 'Command "%s" failed.\n%s' % (
160 ' '.join(args), error_message or e.stdout or ''))
161 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000162
163
164def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000165 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000166 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000167
168
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000169def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000170 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700171 if suppress_stderr:
172 stderr = subprocess2.VOID
173 else:
174 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000175 try:
tandrii5d48c322016-08-18 16:19:37 -0700176 (out, _), code = subprocess2.communicate(['git'] + args,
177 env=GetNoGitPagerEnv(),
178 stdout=subprocess2.PIPE,
179 stderr=stderr)
180 return code, out
181 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900182 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700183 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000184
185
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000186def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000187 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000188 return RunGitWithCode(args, suppress_stderr=True)[1]
189
190
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000191def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000192 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000193 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000194 return (version.startswith(prefix) and
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000195 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000196
197
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000198def BranchExists(branch):
199 """Return True if specified branch exists."""
200 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
201 suppress_stderr=True)
202 return not code
203
204
tandrii2a16b952016-10-19 07:09:44 -0700205def time_sleep(seconds):
206 # Use this so that it can be mocked in tests without interfering with python
207 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700208 return time.sleep(seconds)
209
210
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000211def time_time():
212 # Use this so that it can be mocked in tests without interfering with python
213 # system machinery.
214 return time.time()
215
216
Edward Lemur1b52d872019-05-09 21:12:12 +0000217def datetime_now():
218 # Use this so that it can be mocked in tests without interfering with python
219 # system machinery.
220 return datetime.datetime.now()
221
222
maruel@chromium.org90541732011-04-01 17:54:18 +0000223def ask_for_data(prompt):
224 try:
225 return raw_input(prompt)
226 except KeyboardInterrupt:
227 # Hide the exception.
228 sys.exit(1)
229
230
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100231def confirm_or_exit(prefix='', action='confirm'):
232 """Asks user to press enter to continue or press Ctrl+C to abort."""
233 if not prefix or prefix.endswith('\n'):
234 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100235 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100236 mid = ' Press'
237 elif prefix.endswith(' '):
238 mid = 'press'
239 else:
240 mid = ' press'
241 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
242
243
244def ask_for_explicit_yes(prompt):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000245 """Returns whether user typed 'y' or 'yes' to confirm the given prompt."""
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100246 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
247 while True:
248 if 'yes'.startswith(result):
249 return True
250 if 'no'.startswith(result):
251 return False
252 result = ask_for_data('Please, type yes or no: ').lower()
253
254
tandrii5d48c322016-08-18 16:19:37 -0700255def _git_branch_config_key(branch, key):
256 """Helper method to return Git config key for a branch."""
257 assert branch, 'branch name is required to set git config for it'
258 return 'branch.%s.%s' % (branch, key)
259
260
261def _git_get_branch_config_value(key, default=None, value_type=str,
262 branch=False):
263 """Returns git config value of given or current branch if any.
264
265 Returns default in all other cases.
266 """
267 assert value_type in (int, str, bool)
268 if branch is False: # Distinguishing default arg value from None.
269 branch = GetCurrentBranch()
270
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000271 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700272 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000273
tandrii5d48c322016-08-18 16:19:37 -0700274 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700275 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700276 args.append('--bool')
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000277 # `git config` also has --int, but apparently git config suffers from integer
tandrii33a46ff2016-08-23 05:53:40 -0700278 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700279 args.append(_git_branch_config_key(branch, key))
280 code, out = RunGitWithCode(args)
281 if code == 0:
282 value = out.strip()
283 if value_type == int:
284 return int(value)
285 if value_type == bool:
286 return bool(value.lower() == 'true')
287 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000288 return default
289
290
tandrii5d48c322016-08-18 16:19:37 -0700291def _git_set_branch_config_value(key, value, branch=None, **kwargs):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000292 """Sets or unsets the git branch config value.
tandrii5d48c322016-08-18 16:19:37 -0700293
Quinten Yearsleyd242ed72019-07-25 17:17:55 +0000294 If value is None, the key will be unset, otherwise it will be set.
295 If no branch is given, the currently checked out branch is used.
tandrii5d48c322016-08-18 16:19:37 -0700296 """
297 if not branch:
298 branch = GetCurrentBranch()
299 assert branch, 'a branch name OR currently checked out branch is required'
300 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700301 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700302 if value is None:
303 args.append('--unset')
304 elif isinstance(value, bool):
305 args.append('--bool')
306 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700307 else:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000308 # `git config` also has --int, but apparently git config suffers from
309 # integer overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700310 value = str(value)
311 args.append(_git_branch_config_key(branch, key))
312 if value is not None:
313 args.append(value)
314 RunGit(args, **kwargs)
315
316
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100317def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700318 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100319
320 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
321 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000322 # Git also stores timezone offset, but it only affects visual display;
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100323 # actual point in time is defined by this timestamp only.
324 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
325
326
327def _git_amend_head(message, committer_timestamp):
328 """Amends commit with new message and desired committer_timestamp.
329
330 Sets committer timezone to UTC.
331 """
332 env = os.environ.copy()
333 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
334 return RunGit(['commit', '--amend', '-m', message], env=env)
335
336
machenbach@chromium.org45453142015-09-15 08:45:22 +0000337def _get_properties_from_options(options):
338 properties = dict(x.split('=', 1) for x in options.properties)
339 for key, val in properties.iteritems():
340 try:
341 properties[key] = json.loads(val)
342 except ValueError:
343 pass # If a value couldn't be evaluated, treat it as a string.
344 return properties
345
346
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000347def _prefix_master(master):
348 """Convert user-specified master name to full master name.
349
350 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
351 name, while the developers always use shortened master name
352 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
353 function does the conversion for buildbucket migration.
354 """
borenet6c0efe62016-10-19 08:13:29 -0700355 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000356 return master
borenet6c0efe62016-10-19 08:13:29 -0700357 return '%s%s' % (MASTER_PREFIX, master)
358
359
360def _unprefix_master(bucket):
361 """Convert bucket name to shortened master name.
362
363 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
364 name, while the developers always use shortened master name
365 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
366 function does the conversion for buildbucket migration.
367 """
368 if bucket.startswith(MASTER_PREFIX):
369 return bucket[len(MASTER_PREFIX):]
370 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000371
372
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000373def _buildbucket_retry(operation_name, http, *args, **kwargs):
374 """Retries requests to buildbucket service and returns parsed json content."""
375 try_count = 0
376 while True:
377 response, content = http.request(*args, **kwargs)
378 try:
379 content_json = json.loads(content)
380 except ValueError:
381 content_json = None
382
383 # Buildbucket could return an error even if status==200.
384 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000385 error = content_json.get('error')
386 if error.get('code') == 403:
387 raise BuildbucketResponseException(
388 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000389 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000390 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000391 raise BuildbucketResponseException(msg)
392
393 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700394 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000395 raise BuildbucketResponseException(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000396 'Buildbucket returned invalid JSON content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700397 'Please file bugs at http://crbug.com, '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000398 'component "Infra>Platform>Buildbucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000399 content)
400 return content_json
401 if response.status < 500 or try_count >= 2:
402 raise httplib2.HttpLib2Error(content)
403
404 # status >= 500 means transient failures.
405 logging.debug('Transient errors when %s. Will retry.', operation_name)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000406 time_sleep(0.5 + (1.5 * try_count))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000407 try_count += 1
408 assert False, 'unreachable'
409
410
qyearsley1fdfcb62016-10-24 13:22:03 -0700411def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700412 """Returns a dict mapping bucket names to builders and tests,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000413 for triggering tryjobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700414 """
qyearsleydd49f942016-10-28 11:57:22 -0700415 # If no bots are listed, we try to get a set of builders and tests based
416 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700417 if not options.bot:
418 change = changelist.GetChange(
419 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700420 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700421 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700422 change=change,
423 changed_files=change.LocalPaths(),
424 repository_root=settings.GetRoot(),
425 default_presubmit=None,
426 project=None,
427 verbose=options.verbose,
428 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700429 if masters is None:
430 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100431 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700432
qyearsley1fdfcb62016-10-24 13:22:03 -0700433 if options.bucket:
434 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700435 if options.master:
Andrii Shyshkalov75424372019-08-30 22:48:15 +0000436 print(
437 'WARNING: "-m <master>" option is deprecated. Use -B <bucket> instead.')
qyearsleydd49f942016-10-28 11:57:22 -0700438 return {_prefix_master(options.master): {b: [] for b in options.bot}}
Andrii Shyshkalov75424372019-08-30 22:48:15 +0000439 option_parser.error(
440 'Please specify the bucket, e.g. "-B luci.chromium.try".')
qyearsley1fdfcb62016-10-24 13:22:03 -0700441
442
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800443def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000444 """Sends a request to Buildbucket to trigger tryjobs for a changelist.
qyearsley1fdfcb62016-10-24 13:22:03 -0700445
446 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700447 auth_config: AuthConfig for Buildbucket.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000448 changelist: Changelist that the tryjobs are associated with.
qyearsley1fdfcb62016-10-24 13:22:03 -0700449 buckets: A nested dict mapping bucket names to builders to tests.
450 options: Command-line options.
451 """
tandriide281ae2016-10-12 06:02:30 -0700452 assert changelist.GetIssue(), 'CL must be uploaded first'
453 codereview_url = changelist.GetCodereviewServer()
454 assert codereview_url, 'CL must be uploaded first'
455 patchset = patchset or changelist.GetMostRecentPatchset()
456 assert patchset, 'CL must be uploaded first'
457
458 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700459 # Cache the buildbucket credentials under the codereview host key, so that
460 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700461 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000462 http = authenticator.authorize(httplib2.Http())
463 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700464
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465 buildbucket_put_url = (
466 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000467 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000468 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700469 hostname=codereview_host,
470 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000471 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700472
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700473 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800474 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700475 if options.clobber:
476 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700477 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700478 if extra_properties:
479 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000480
481 batch_req_body = {'builds': []}
482 print_text = []
483 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700484 for bucket, builders_and_tests in sorted(buckets.iteritems()):
485 print_text.append('Bucket: %s' % bucket)
486 master = None
487 if bucket.startswith(MASTER_PREFIX):
488 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000489 for builder, tests in sorted(builders_and_tests.iteritems()):
490 print_text.append(' %s: %s' % (builder, tests))
491 parameters = {
492 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000493 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100494 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000495 'revision': options.revision,
496 }],
tandrii8c5a3532016-11-04 07:52:02 -0700497 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000499 if 'presubmit' in builder.lower():
500 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000501 if tests:
502 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700503
504 tags = [
505 'builder:%s' % builder,
506 'buildset:%s' % buildset,
507 'user_agent:git_cl_try',
508 ]
509 if master:
510 parameters['properties']['master'] = master
511 tags.append('master:%s' % master)
512
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000513 batch_req_body['builds'].append(
514 {
515 'bucket': bucket,
516 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000517 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700518 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000519 }
520 )
521
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000522 _buildbucket_retry(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000523 'triggering tryjobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000524 http,
525 buildbucket_put_url,
526 'PUT',
527 body=json.dumps(batch_req_body),
528 headers={'Content-Type': 'application/json'}
529 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000530 print_text.append('To see results here, run: git cl try-results')
531 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700532 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000533
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000534
tandrii221ab252016-10-06 08:12:04 -0700535def fetch_try_jobs(auth_config, changelist, buildbucket_host,
536 patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000537 """Fetches tryjobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000538
qyearsley53f48a12016-09-01 10:45:13 -0700539 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000540 """
tandrii221ab252016-10-06 08:12:04 -0700541 assert buildbucket_host
542 assert changelist.GetIssue(), 'CL must be uploaded first'
543 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
544 patchset = patchset or changelist.GetMostRecentPatchset()
545 assert patchset, 'CL must be uploaded first'
546
547 codereview_url = changelist.GetCodereviewServer()
548 codereview_host = urlparse.urlparse(codereview_url).hostname
549 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 if authenticator.has_cached_credentials():
551 http = authenticator.authorize(httplib2.Http())
552 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700553 print('Warning: Some results might be missing because %s' %
554 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700555 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 http = httplib2.Http()
557
558 http.force_exception_to_status_code = True
559
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000560 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700561 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700563 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000564 params = {'tag': 'buildset:%s' % buildset}
565
566 builds = {}
567 while True:
568 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700569 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 params=urllib.urlencode(params))
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000571 content = _buildbucket_retry('fetching tryjobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 for build in content.get('builds', []):
573 builds[build['id']] = build
574 if 'next_cursor' in content:
575 params['start_cursor'] = content['next_cursor']
576 else:
577 break
578 return builds
579
580
qyearsleyeab3c042016-08-24 09:18:28 -0700581def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000582 """Prints nicely result of fetch_try_jobs."""
583 if not builds:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000584 print('No tryjobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 return
586
587 # Make a copy, because we'll be modifying builds dictionary.
588 builds = builds.copy()
589 builder_names_cache = {}
590
591 def get_builder(b):
592 try:
593 return builder_names_cache[b['id']]
594 except KeyError:
595 try:
596 parameters = json.loads(b['parameters_json'])
597 name = parameters['builder_name']
598 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700599 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700600 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000601 name = None
602 builder_names_cache[b['id']] = name
603 return name
604
605 def get_bucket(b):
606 bucket = b['bucket']
607 if bucket.startswith('master.'):
608 return bucket[len('master.'):]
609 return bucket
610
611 if options.print_master:
612 name_fmt = '%%-%ds %%-%ds' % (
613 max(len(str(get_bucket(b))) for b in builds.itervalues()),
614 max(len(str(get_builder(b))) for b in builds.itervalues()))
615 def get_name(b):
616 return name_fmt % (get_bucket(b), get_builder(b))
617 else:
618 name_fmt = '%%-%ds' % (
619 max(len(str(get_builder(b))) for b in builds.itervalues()))
620 def get_name(b):
621 return name_fmt % get_builder(b)
622
623 def sort_key(b):
624 return b['status'], b.get('result'), get_name(b), b.get('url')
625
626 def pop(title, f, color=None, **kwargs):
627 """Pop matching builds from `builds` dict and print them."""
628
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000629 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000630 colorize = str
631 else:
632 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
633
634 result = []
635 for b in builds.values():
636 if all(b.get(k) == v for k, v in kwargs.iteritems()):
637 builds.pop(b['id'])
638 result.append(b)
639 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700640 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000641 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700642 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000643
644 total = len(builds)
645 pop(status='COMPLETED', result='SUCCESS',
646 title='Successes:', color=Fore.GREEN,
647 f=lambda b: (get_name(b), b.get('url')))
648 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
649 title='Infra Failures:', color=Fore.MAGENTA,
650 f=lambda b: (get_name(b), b.get('url')))
651 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
652 title='Failures:', color=Fore.RED,
653 f=lambda b: (get_name(b), b.get('url')))
654 pop(status='COMPLETED', result='CANCELED',
655 title='Canceled:', color=Fore.MAGENTA,
656 f=lambda b: (get_name(b),))
657 pop(status='COMPLETED', result='FAILURE',
658 failure_reason='INVALID_BUILD_DEFINITION',
659 title='Wrong master/builder name:', color=Fore.MAGENTA,
660 f=lambda b: (get_name(b),))
661 pop(status='COMPLETED', result='FAILURE',
662 title='Other failures:',
663 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
664 pop(status='COMPLETED',
665 title='Other finished:',
666 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
667 pop(status='STARTED',
668 title='Started:', color=Fore.YELLOW,
669 f=lambda b: (get_name(b), b.get('url')))
670 pop(status='SCHEDULED',
671 title='Scheduled:',
672 f=lambda b: (get_name(b), 'id=%s' % b['id']))
673 # The last section is just in case buildbucket API changes OR there is a bug.
674 pop(title='Other:',
675 f=lambda b: (get_name(b), 'id=%s' % b['id']))
676 assert len(builds) == 0
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000677 print('Total: %d tryjobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000678
679
Aiden Bennerc08566e2018-10-03 17:52:42 +0000680def _ComputeDiffLineRanges(files, upstream_commit):
681 """Gets the changed line ranges for each file since upstream_commit.
682
683 Parses a git diff on provided files and returns a dict that maps a file name
684 to an ordered list of range tuples in the form (start_line, count).
685 Ranges are in the same format as a git diff.
686 """
687 # If files is empty then diff_output will be a full diff.
688 if len(files) == 0:
689 return {}
690
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000691 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000692 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
693 diff_output = RunGit(diff_cmd)
694
695 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
696 # 2 capture groups
697 # 0 == fname of diff file
698 # 1 == 'diff_start,diff_count' or 'diff_start'
699 # will match each of
700 # diff --git a/foo.foo b/foo.py
701 # @@ -12,2 +14,3 @@
702 # @@ -12,2 +17 @@
703 # running re.findall on the above string with pattern will give
704 # [('foo.py', ''), ('', '14,3'), ('', '17')]
705
706 curr_file = None
707 line_diffs = {}
708 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
709 if match[0] != '':
710 # Will match the second filename in diff --git a/a.py b/b.py.
711 curr_file = match[0]
712 line_diffs[curr_file] = []
713 else:
714 # Matches +14,3
715 if ',' in match[1]:
716 diff_start, diff_count = match[1].split(',')
717 else:
718 # Single line changes are of the form +12 instead of +12,1.
719 diff_start = match[1]
720 diff_count = 1
721
722 diff_start = int(diff_start)
723 diff_count = int(diff_count)
724
725 # If diff_count == 0 this is a removal we can ignore.
726 line_diffs[curr_file].append((diff_start, diff_count))
727
728 return line_diffs
729
730
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000731def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000732 """Checks if a yapf file is in any parent directory of fpath until top_dir.
733
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000734 Recursively checks parent directories to find yapf file and if no yapf file
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000735 is found returns None. Uses yapf_config_cache as a cache for previously found
736 configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000737 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000738 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000739 # Return result if we've already computed it.
740 if fpath in yapf_config_cache:
741 return yapf_config_cache[fpath]
742
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000743 parent_dir = os.path.dirname(fpath)
744 if os.path.isfile(fpath):
745 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000746 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000747 # Otherwise fpath is a directory
748 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
749 if os.path.isfile(yapf_file):
750 ret = yapf_file
751 elif fpath == top_dir or parent_dir == fpath:
752 # If we're at the top level directory, or if we're at root
753 # there is no provided style.
754 ret = None
755 else:
756 # Otherwise recurse on the current directory.
757 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000758 yapf_config_cache[fpath] = ret
759 return ret
760
761
qyearsley53f48a12016-09-01 10:45:13 -0700762def write_try_results_json(output_file, builds):
763 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
764
765 The input |builds| dict is assumed to be generated by Buildbucket.
766 Buildbucket documentation: http://goo.gl/G0s101
767 """
768
769 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800770 """Extracts some of the information from one build dict."""
771 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700772 return {
773 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700774 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800775 'builder_name': parameters.get('builder_name'),
776 'created_ts': build.get('created_ts'),
777 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700778 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800779 'result': build.get('result'),
780 'status': build.get('status'),
781 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700782 'url': build.get('url'),
783 }
784
785 converted = []
786 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000787 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700788 write_json(output_file, converted)
789
790
Aaron Gable13101a62018-02-09 13:20:41 -0800791def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000792 """Prints statistics about the change to the user."""
793 # --no-ext-diff is broken in some versions of Git, so try to work around
794 # this by overriding the environment (but there is still a problem if the
795 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000796 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000797 if 'GIT_EXTERNAL_DIFF' in env:
798 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000799
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000800 try:
801 stdout = sys.stdout.fileno()
802 except AttributeError:
803 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000804 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800805 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000806 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000807
808
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000809class BuildbucketResponseException(Exception):
810 pass
811
812
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813class Settings(object):
814 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000815 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000816 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817 self.tree_status_url = None
818 self.viewvc_url = None
819 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000820 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000821 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000822 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000823 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
825 def LazyUpdateIfNeeded(self):
826 """Updates the settings from a codereview.settings file, if available."""
827 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000828 # The only value that actually changes the behavior is
829 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000830 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000831 error_ok=True
832 ).strip().lower()
833
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000834 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000835 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836 LoadCodereviewSettingsFromFile(cr_settings_file)
837 self.updated = True
838
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000839 @staticmethod
840 def GetRelativeRoot():
841 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000844 if self.root is None:
845 self.root = os.path.abspath(self.GetRelativeRoot())
846 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 def GetTreeStatusUrl(self, error_ok=False):
849 if not self.tree_status_url:
850 error_message = ('You must configure your tree status URL by running '
851 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000852 self.tree_status_url = self._GetConfig(
853 'rietveld.tree-status-url', error_ok=error_ok,
854 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000855 return self.tree_status_url
856
857 def GetViewVCUrl(self):
858 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000859 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860 return self.viewvc_url
861
rmistry@google.com90752582014-01-14 21:04:50 +0000862 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000863 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000864
rmistry@google.com5626a922015-02-26 14:03:30 +0000865 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000866 run_post_upload_hook = self._GetConfig(
867 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000868 return run_post_upload_hook == "True"
869
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000870 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000871 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000872
ukai@chromium.orge8077812012-02-03 03:41:46 +0000873 def GetIsGerrit(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000874 """Returns True if this repo is associated with Gerrit."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000875 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700876 self.is_gerrit = (
877 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000878 return self.is_gerrit
879
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000880 def GetSquashGerritUploads(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000881 """Returns True if uploads to Gerrit should be squashed by default."""
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000882 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700883 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
884 if self.squash_gerrit_uploads is None:
885 # Default is squash now (http://crbug.com/611892#c23).
886 self.squash_gerrit_uploads = not (
887 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
888 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000889 return self.squash_gerrit_uploads
890
tandriia60502f2016-06-20 02:01:53 -0700891 def GetSquashGerritUploadsOverride(self):
892 """Return True or False if codereview.settings should be overridden.
893
894 Returns None if no override has been defined.
895 """
896 # See also http://crbug.com/611892#c23
897 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
898 error_ok=True).strip()
899 if result == 'true':
900 return True
901 if result == 'false':
902 return False
903 return None
904
tandrii@chromium.org28253532016-04-14 13:46:56 +0000905 def GetGerritSkipEnsureAuthenticated(self):
906 """Return True if EnsureAuthenticated should not be done for Gerrit
907 uploads."""
908 if self.gerrit_skip_ensure_authenticated is None:
909 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000910 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000911 error_ok=True).strip() == 'true')
912 return self.gerrit_skip_ensure_authenticated
913
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000914 def GetGitEditor(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +0000915 """Returns the editor specified in the git config, or None if none is."""
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000916 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000917 # Git requires single quotes for paths with spaces. We need to replace
918 # them with double quotes for Windows to treat such paths as a single
919 # path.
920 self.git_editor = self._GetConfig(
921 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000922 return self.git_editor or None
923
thestig@chromium.org44202a22014-03-11 19:22:18 +0000924 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000925 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000926 DEFAULT_LINT_REGEX)
927
928 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000929 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000930 DEFAULT_LINT_IGNORE_REGEX)
931
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932 def _GetConfig(self, param, **kwargs):
933 self.LazyUpdateIfNeeded()
934 return RunGit(['config', param], **kwargs).strip()
935
936
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100937@contextlib.contextmanager
938def _get_gerrit_project_config_file(remote_url):
939 """Context manager to fetch and store Gerrit's project.config from
940 refs/meta/config branch and store it in temp file.
941
942 Provides a temporary filename or None if there was error.
943 """
944 error, _ = RunGitWithCode([
945 'fetch', remote_url,
946 '+refs/meta/config:refs/git_cl/meta/config'])
947 if error:
948 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700949 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100950 (remote_url, error))
951 yield None
952 return
953
954 error, project_config_data = RunGitWithCode(
955 ['show', 'refs/git_cl/meta/config:project.config'])
956 if error:
957 print('WARNING: project.config file not found')
958 yield None
959 return
960
961 with gclient_utils.temporary_directory() as tempdir:
962 project_config_file = os.path.join(tempdir, 'project.config')
963 gclient_utils.FileWrite(project_config_file, project_config_data)
964 yield project_config_file
965
966
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967def ShortBranchName(branch):
968 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000969 return branch.replace('refs/heads/', '', 1)
970
971
972def GetCurrentBranchRef():
973 """Returns branch ref (e.g., refs/heads/master) or None."""
974 return RunGit(['symbolic-ref', 'HEAD'],
975 stderr=subprocess2.VOID, error_ok=True).strip() or None
976
977
978def GetCurrentBranch():
979 """Returns current branch or None.
980
981 For refs/heads/* branches, returns just last part. For others, full ref.
982 """
983 branchref = GetCurrentBranchRef()
984 if branchref:
985 return ShortBranchName(branchref)
986 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987
988
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000989class _CQState(object):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +0000990 """Enum for states of CL with respect to CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000991 NONE = 'none'
992 DRY_RUN = 'dry_run'
993 COMMIT = 'commit'
994
995 ALL_STATES = [NONE, DRY_RUN, COMMIT]
996
997
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000998class _ParsedIssueNumberArgument(object):
Edward Lemurf38bc172019-09-03 21:02:13 +0000999 def __init__(self, issue=None, patchset=None, hostname=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001000 self.issue = issue
1001 self.patchset = patchset
1002 self.hostname = hostname
1003
1004 @property
1005 def valid(self):
1006 return self.issue is not None
1007
1008
Edward Lemurf38bc172019-09-03 21:02:13 +00001009def ParseIssueNumberArgument(arg):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001010 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1011 fail_result = _ParsedIssueNumberArgument()
1012
1013 if arg.isdigit():
Edward Lemurf38bc172019-09-03 21:02:13 +00001014 return _ParsedIssueNumberArgument(issue=int(arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001015 if not arg.startswith('http'):
1016 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001017
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001018 url = gclient_utils.UpgradeToHttps(arg)
1019 try:
1020 parsed_url = urlparse.urlparse(url)
1021 except ValueError:
1022 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001023
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001024 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001025
1026
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001027def _create_description_from_log(args):
1028 """Pulls out the commit log to use as a base for the CL description."""
1029 log_args = []
1030 if len(args) == 1 and not args[0].endswith('.'):
1031 log_args = [args[0] + '..']
1032 elif len(args) == 1 and args[0].endswith('...'):
1033 log_args = [args[0][:-1]]
1034 elif len(args) == 2:
1035 log_args = [args[0] + '..' + args[1]]
1036 else:
1037 log_args = args[:] # Hope for the best!
1038 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1039
1040
Aaron Gablea45ee112016-11-22 15:14:38 -08001041class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001042 def __init__(self, issue, url):
1043 self.issue = issue
1044 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001045 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001046
1047 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001048 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001049 self.issue, self.url)
1050
1051
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001052_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001053 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001054 # TODO(tandrii): these two aren't known in Gerrit.
1055 'approval', 'disapproval'])
1056
1057
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001058class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001059 """Changelist works with one changelist in local branch.
1060
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001061 Notes:
1062 * Not safe for concurrent multi-{thread,process} use.
1063 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001064 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001065 """
1066
Edward Lemurf38bc172019-09-03 21:02:13 +00001067 def __init__(self, branchref=None, issue=None, **kwargs):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001068 """Create a new ChangeList instance.
1069
Edward Lemurf38bc172019-09-03 21:02:13 +00001070 **kwargs will be passed directly to Gerrit implementation.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001071 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001073 global settings
1074 if not settings:
1075 # Happens when git_cl.py is used as a utility library.
1076 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001077
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 self.branchref = branchref
1079 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001080 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081 self.branch = ShortBranchName(self.branchref)
1082 else:
1083 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001085 self.lookedup_issue = False
1086 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087 self.has_description = False
1088 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001089 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001091 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001092 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001093 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001094 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001095
Edward Lemurf38bc172019-09-03 21:02:13 +00001096 self._codereview_impl = _GerritChangelistImpl(self, **kwargs)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001097
1098 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001099 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001100
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001101 The return value is a string suitable for passing to git cl with the --cc
1102 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001103 """
1104 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001105 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001106 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001107 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1108 return self.cc
1109
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001110 def GetCCListWithoutDefault(self):
1111 """Return the users cc'd on this CL excluding default ones."""
1112 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001113 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001114 return self.cc
1115
Daniel Cheng7227d212017-11-17 08:12:37 -08001116 def ExtendCC(self, more_cc):
1117 """Extends the list of users to cc on this CL based on the changed files."""
1118 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119
1120 def GetBranch(self):
1121 """Returns the short branch name, e.g. 'master'."""
1122 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001123 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001124 if not branchref:
1125 return None
1126 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127 self.branch = ShortBranchName(self.branchref)
1128 return self.branch
1129
1130 def GetBranchRef(self):
1131 """Returns the full branch name, e.g. 'refs/heads/master'."""
1132 self.GetBranch() # Poke the lazy loader.
1133 return self.branchref
1134
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001135 def ClearBranch(self):
1136 """Clears cached branch data of this object."""
1137 self.branch = self.branchref = None
1138
tandrii5d48c322016-08-18 16:19:37 -07001139 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1140 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1141 kwargs['branch'] = self.GetBranch()
1142 return _git_get_branch_config_value(key, default, **kwargs)
1143
1144 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1145 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1146 assert self.GetBranch(), (
1147 'this CL must have an associated branch to %sset %s%s' %
1148 ('un' if value is None else '',
1149 key,
1150 '' if value is None else ' to %r' % value))
1151 kwargs['branch'] = self.GetBranch()
1152 return _git_set_branch_config_value(key, value, **kwargs)
1153
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001154 @staticmethod
1155 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001156 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 e.g. 'origin', 'refs/heads/master'
1158 """
1159 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001160 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1161
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001163 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001165 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1166 error_ok=True).strip()
1167 if upstream_branch:
1168 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001170 # Else, try to guess the origin remote.
1171 remote_branches = RunGit(['branch', '-r']).split()
1172 if 'origin/master' in remote_branches:
1173 # Fall back on origin/master if it exits.
1174 remote = 'origin'
1175 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001177 DieWithError(
1178 'Unable to determine default branch to diff against.\n'
1179 'Either pass complete "git diff"-style arguments, like\n'
1180 ' git cl upload origin/master\n'
1181 'or verify this branch is set up to track another \n'
1182 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001183
1184 return remote, upstream_branch
1185
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001186 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001187 upstream_branch = self.GetUpstreamBranch()
1188 if not BranchExists(upstream_branch):
1189 DieWithError('The upstream for the current branch (%s) does not exist '
1190 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001191 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001192 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001193
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194 def GetUpstreamBranch(self):
1195 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001196 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001197 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001198 upstream_branch = upstream_branch.replace('refs/heads/',
1199 'refs/remotes/%s/' % remote)
1200 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1201 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 self.upstream_branch = upstream_branch
1203 return self.upstream_branch
1204
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001205 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001206 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001207 remote, branch = None, self.GetBranch()
1208 seen_branches = set()
1209 while branch not in seen_branches:
1210 seen_branches.add(branch)
1211 remote, branch = self.FetchUpstreamTuple(branch)
1212 branch = ShortBranchName(branch)
1213 if remote != '.' or branch.startswith('refs/remotes'):
1214 break
1215 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001216 remotes = RunGit(['remote'], error_ok=True).split()
1217 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001218 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001219 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001220 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001221 logging.warn('Could not determine which remote this change is '
1222 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001223 else:
1224 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001225 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001226 branch = 'HEAD'
1227 if branch.startswith('refs/remotes'):
1228 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001229 elif branch.startswith('refs/branch-heads/'):
1230 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001231 else:
1232 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001233 return self._remote
1234
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001235 def GitSanityChecks(self, upstream_git_obj):
1236 """Checks git repo status and ensures diff is from local commits."""
1237
sbc@chromium.org79706062015-01-14 21:18:12 +00001238 if upstream_git_obj is None:
1239 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001240 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001241 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001242 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001243 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001244 return False
1245
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001246 # Verify the commit we're diffing against is in our current branch.
1247 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1248 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1249 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001250 print('ERROR: %s is not in the current branch. You may need to rebase '
1251 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001252 return False
1253
1254 # List the commits inside the diff, and verify they are all local.
1255 commits_in_diff = RunGit(
1256 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1257 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1258 remote_branch = remote_branch.strip()
1259 if code != 0:
1260 _, remote_branch = self.GetRemoteBranch()
1261
1262 commits_in_remote = RunGit(
1263 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1264
1265 common_commits = set(commits_in_diff) & set(commits_in_remote)
1266 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001267 print('ERROR: Your diff contains %d commits already in %s.\n'
1268 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1269 'the diff. If you are using a custom git flow, you can override'
1270 ' the reference used for this check with "git config '
1271 'gitcl.remotebranch <git-ref>".' % (
1272 len(common_commits), remote_branch, upstream_git_obj),
1273 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001274 return False
1275 return True
1276
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001277 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001278 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001279
1280 Returns None if it is not set.
1281 """
tandrii5d48c322016-08-18 16:19:37 -07001282 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001283
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284 def GetRemoteUrl(self):
1285 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1286
1287 Returns None if there is no remote.
1288 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001289 is_cached, value = self._cached_remote_url
1290 if is_cached:
1291 return value
1292
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001293 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001294 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1295
Edward Lemur298f2cf2019-02-22 21:40:39 +00001296 # Check if the remote url can be parsed as an URL.
1297 host = urlparse.urlparse(url).netloc
1298 if host:
1299 self._cached_remote_url = (True, url)
1300 return url
1301
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001302 # If it cannot be parsed as an url, assume it is a local directory,
1303 # probably a git cache.
Edward Lemur298f2cf2019-02-22 21:40:39 +00001304 logging.warning('"%s" doesn\'t appear to point to a git host. '
1305 'Interpreting it as a local directory.', url)
1306 if not os.path.isdir(url):
1307 logging.error(
1308 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
1309 remote, url, self.GetBranch())
1310 return None
1311
1312 cache_path = url
1313 url = RunGit(['config', 'remote.%s.url' % remote],
1314 error_ok=True,
1315 cwd=url).strip()
1316
1317 host = urlparse.urlparse(url).netloc
1318 if not host:
1319 logging.error(
1320 'Remote "%(remote)s" for branch "%(branch)s" points to '
1321 '"%(cache_path)s", but it is misconfigured.\n'
1322 '"%(cache_path)s" must be a git repo and must have a remote named '
1323 '"%(remote)s" pointing to the git host.', {
1324 'remote': remote,
1325 'cache_path': cache_path,
1326 'branch': self.GetBranch()})
1327 return None
1328
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001329 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001330 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001332 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001333 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001334 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001335 self.issue = self._GitGetBranchConfigValue(
1336 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001337 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 return self.issue
1339
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 def GetIssueURL(self):
1341 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001342 issue = self.GetIssue()
1343 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001344 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001345 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001347 def GetDescription(self, pretty=False, force=False):
1348 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001350 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351 self.has_description = True
1352 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001353 # Set width to 72 columns + 2 space indent.
1354 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001356 lines = self.description.splitlines()
1357 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 return self.description
1359
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001360 def GetDescriptionFooters(self):
1361 """Returns (non_footer_lines, footers) for the commit message.
1362
1363 Returns:
1364 non_footer_lines (list(str)) - Simple list of description lines without
1365 any footer. The lines do not contain newlines, nor does the list contain
1366 the empty line between the message and the footers.
1367 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1368 [("Change-Id", "Ideadbeef...."), ...]
1369 """
1370 raw_description = self.GetDescription()
1371 msg_lines, _, footers = git_footers.split_footers(raw_description)
1372 if footers:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001373 msg_lines = msg_lines[:len(msg_lines) - 1]
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001374 return msg_lines, footers
1375
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001376 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001377 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001378 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001379 self.patchset = self._GitGetBranchConfigValue(
1380 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001381 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 return self.patchset
1383
1384 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001385 """Set this branch's patchset. If patchset=0, clears the patchset."""
1386 assert self.GetBranch()
1387 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001388 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001389 else:
1390 self.patchset = int(patchset)
1391 self._GitSetBranchConfigValue(
1392 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001394 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001395 """Set this branch's issue. If issue isn't given, clears the issue."""
1396 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001398 issue = int(issue)
1399 self._GitSetBranchConfigValue(
1400 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001401 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001402 codereview_server = self._codereview_impl.GetCodereviewServer()
1403 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001404 self._GitSetBranchConfigValue(
1405 self._codereview_impl.CodereviewServerConfigKey(),
1406 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407 else:
tandrii5d48c322016-08-18 16:19:37 -07001408 # Reset all of these just to be clean.
1409 reset_suffixes = [
1410 'last-upload-hash',
1411 self._codereview_impl.IssueConfigKey(),
1412 self._codereview_impl.PatchsetConfigKey(),
1413 self._codereview_impl.CodereviewServerConfigKey(),
1414 ] + self._PostUnsetIssueProperties()
1415 for prop in reset_suffixes:
1416 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001417 msg = RunGit(['log', '-1', '--format=%B']).strip()
1418 if msg and git_footers.get_footer_change_id(msg):
1419 print('WARNING: The change patched into this branch has a Change-Id. '
1420 'Removing it.')
1421 RunGit(['commit', '--amend', '-m',
1422 git_footers.remove_footer(msg, 'Change-Id')])
Edward Lemurf38bc172019-09-03 21:02:13 +00001423 self.lookedup_issue = True
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001424 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001425 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426
dnjba1b0f32016-09-02 12:37:42 -07001427 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001428 if not self.GitSanityChecks(upstream_branch):
1429 DieWithError('\nGit sanity check failure')
1430
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001431 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001432 if not root:
1433 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001434 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001435
1436 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001437 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001438 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001439 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001440 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001441 except subprocess2.CalledProcessError:
1442 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001443 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001444 'This branch probably doesn\'t exist anymore. To reset the\n'
1445 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001446 ' git branch --set-upstream-to origin/master %s\n'
1447 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001448 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001449
maruel@chromium.org52424302012-08-29 15:14:30 +00001450 issue = self.GetIssue()
1451 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001452 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001453 description = self.GetDescription()
1454 else:
1455 # If the change was never uploaded, use the log messages of all commits
1456 # up to the branch point, as git cl upload will prefill the description
1457 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001458 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1459 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001460
1461 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001462 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001463 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001464 name,
1465 description,
1466 absroot,
1467 files,
1468 issue,
1469 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001470 author,
1471 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001472
dsansomee2d6fd92016-09-08 00:10:47 -07001473 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001474 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001475 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001476 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001477
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001478 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1479 """Sets the description for this CL remotely.
1480
1481 You can get description_lines and footers with GetDescriptionFooters.
1482
1483 Args:
1484 description_lines (list(str)) - List of CL description lines without
1485 newline characters.
1486 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1487 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1488 `List-Of-Tokens`). It will be case-normalized so that each token is
1489 title-cased.
1490 """
1491 new_description = '\n'.join(description_lines)
1492 if footers:
1493 new_description += '\n'
1494 for k, v in footers:
1495 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1496 if not git_footers.FOOTER_PATTERN.match(foot):
1497 raise ValueError('Invalid footer %r' % foot)
1498 new_description += foot + '\n'
1499 self.UpdateDescription(new_description, force)
1500
Edward Lesmes8e282792018-04-03 18:50:29 -04001501 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001502 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1503 try:
Edward Lemur2c48f242019-06-04 16:14:09 +00001504 start = time_time()
1505 result = presubmit_support.DoPresubmitChecks(change, committing,
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001506 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1507 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001508 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1509 parallel=parallel)
Edward Lemur2c48f242019-06-04 16:14:09 +00001510 metrics.collector.add_repeated('sub_commands', {
1511 'command': 'presubmit',
1512 'execution_time': time_time() - start,
1513 'exit_code': 0 if result.should_continue() else 1,
1514 })
1515 return result
vapierfd77ac72016-06-16 08:33:57 -07001516 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001517 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001518
Edward Lemurf38bc172019-09-03 21:02:13 +00001519 def CMDPatchIssue(self, issue_arg, nocommit):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001520 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001521 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1522 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001523 else:
1524 # Assume url.
1525 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1526 urlparse.urlparse(issue_arg))
1527 if not parsed_issue_arg or not parsed_issue_arg.valid:
1528 DieWithError('Failed to parse issue argument "%s". '
1529 'Must be an issue number or a valid URL.' % issue_arg)
1530 return self._codereview_impl.CMDPatchWithParsedIssue(
Edward Lemurf38bc172019-09-03 21:02:13 +00001531 parsed_issue_arg, nocommit, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001532
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001533 def CMDUpload(self, options, git_diff_args, orig_args):
1534 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001535 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001536 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001537 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001538 else:
1539 if self.GetBranch() is None:
1540 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1541
1542 # Default to diffing against common ancestor of upstream branch
1543 base_branch = self.GetCommonAncestorWithUpstream()
1544 git_diff_args = [base_branch, 'HEAD']
1545
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001546 # Fast best-effort checks to abort before running potentially expensive
1547 # hooks if uploading is likely to fail anyway. Passing these checks does
1548 # not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001549 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001550 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001551
1552 # Apply watchlists on upload.
1553 change = self.GetChange(base_branch, None)
1554 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1555 files = [f.LocalPath() for f in change.AffectedFiles()]
1556 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001557 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001558
1559 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001560 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001561 # Set the reviewer list now so that presubmit checks can access it.
1562 change_description = ChangeDescription(change.FullDescriptionText())
1563 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001564 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001565 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001566 change)
1567 change.SetDescriptionText(change_description.description)
1568 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001569 may_prompt=not options.force,
1570 verbose=options.verbose,
1571 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001572 if not hook_results.should_continue():
1573 return 1
1574 if not options.reviewers and hook_results.reviewers:
1575 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001576 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001577
Aaron Gable13101a62018-02-09 13:20:41 -08001578 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001579 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001580 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001581 _git_set_branch_config_value('last-upload-hash',
1582 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001583 # Run post upload hooks, if specified.
1584 if settings.GetRunPostUploadHook():
1585 presubmit_support.DoPostUploadExecuter(
1586 change,
1587 self,
1588 settings.GetRoot(),
1589 options.verbose,
1590 sys.stdout)
1591
1592 # Upload all dependencies if specified.
1593 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001594 print()
1595 print('--dependencies has been specified.')
1596 print('All dependent local branches will be re-uploaded.')
1597 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001598 # Remove the dependencies flag from args so that we do not end up in a
1599 # loop.
1600 orig_args.remove('--dependencies')
1601 ret = upload_branch_deps(self, orig_args)
1602 return ret
1603
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001604 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001605 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001606
1607 Issue must have been already uploaded and known.
1608 """
1609 assert new_state in _CQState.ALL_STATES
1610 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001611 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001612 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001613 return 0
1614 except KeyboardInterrupt:
1615 raise
1616 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001617 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001618 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001619 ' * Your project has no CQ,\n'
1620 ' * You don\'t have permission to change the CQ state,\n'
1621 ' * There\'s a bug in this code (see stack trace below).\n'
1622 'Consider specifying which bots to trigger manually or asking your '
1623 'project owners for permissions or contacting Chrome Infra at:\n'
1624 'https://www.chromium.org/infra\n\n' %
1625 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001626 # Still raise exception so that stack trace is printed.
1627 raise
1628
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001629 # Forward methods to codereview specific implementation.
1630
Aaron Gable636b13f2017-07-14 10:42:48 -07001631 def AddComment(self, message, publish=None):
1632 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001633
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001634 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001635 """Returns list of _CommentSummary for each comment.
1636
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001637 args:
1638 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001639 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001640 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001641
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001642 def CloseIssue(self):
1643 return self._codereview_impl.CloseIssue()
1644
1645 def GetStatus(self):
1646 return self._codereview_impl.GetStatus()
1647
1648 def GetCodereviewServer(self):
1649 return self._codereview_impl.GetCodereviewServer()
1650
tandriide281ae2016-10-12 06:02:30 -07001651 def GetIssueOwner(self):
1652 """Get owner from codereview, which may differ from this checkout."""
1653 return self._codereview_impl.GetIssueOwner()
1654
Edward Lemur707d70b2018-02-07 00:50:14 +01001655 def GetReviewers(self):
1656 return self._codereview_impl.GetReviewers()
1657
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001658 def GetMostRecentPatchset(self):
1659 return self._codereview_impl.GetMostRecentPatchset()
1660
tandriide281ae2016-10-12 06:02:30 -07001661 def CannotTriggerTryJobReason(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001662 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001663 return self._codereview_impl.CannotTriggerTryJobReason()
1664
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001665 def GetTryJobProperties(self, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001666 """Returns dictionary of properties to launch tryjob."""
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001667 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001668
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001669 def __getattr__(self, attr):
1670 # This is because lots of untested code accesses Rietveld-specific stuff
1671 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001672 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001673 # Note that child method defines __getattr__ as well, and forwards it here,
1674 # because _RietveldChangelistImpl is not cleaned up yet, and given
1675 # deprecation of Rietveld, it should probably be just removed.
1676 # Until that time, avoid infinite recursion by bypassing __getattr__
1677 # of implementation class.
1678 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001679
1680
1681class _ChangelistCodereviewBase(object):
1682 """Abstract base class encapsulating codereview specifics of a changelist."""
1683 def __init__(self, changelist):
1684 self._changelist = changelist # instance of Changelist
1685
1686 def __getattr__(self, attr):
1687 # Forward methods to changelist.
1688 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1689 # _RietveldChangelistImpl to avoid this hack?
1690 return getattr(self._changelist, attr)
1691
1692 def GetStatus(self):
1693 """Apply a rough heuristic to give a simple summary of an issue's review
1694 or CQ status, assuming adherence to a common workflow.
1695
1696 Returns None if no issue for this branch, or specific string keywords.
1697 """
1698 raise NotImplementedError()
1699
1700 def GetCodereviewServer(self):
1701 """Returns server URL without end slash, like "https://codereview.com"."""
1702 raise NotImplementedError()
1703
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001704 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001705 """Fetches and returns description from the codereview server."""
1706 raise NotImplementedError()
1707
tandrii5d48c322016-08-18 16:19:37 -07001708 @classmethod
1709 def IssueConfigKey(cls):
1710 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001711 raise NotImplementedError()
1712
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001713 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001714 def PatchsetConfigKey(cls):
1715 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001716 raise NotImplementedError()
1717
tandrii5d48c322016-08-18 16:19:37 -07001718 @classmethod
1719 def CodereviewServerConfigKey(cls):
1720 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001721 raise NotImplementedError()
1722
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001723 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001724 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001725 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001726
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001727 def GetGerritObjForPresubmit(self):
1728 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1729 return None
1730
dsansomee2d6fd92016-09-08 00:10:47 -07001731 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001732 """Update the description on codereview site."""
1733 raise NotImplementedError()
1734
Aaron Gable636b13f2017-07-14 10:42:48 -07001735 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001736 """Posts a comment to the codereview site."""
1737 raise NotImplementedError()
1738
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001739 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001740 raise NotImplementedError()
1741
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001742 def CloseIssue(self):
1743 """Closes the issue."""
1744 raise NotImplementedError()
1745
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001746 def GetMostRecentPatchset(self):
1747 """Returns the most recent patchset number from the codereview site."""
1748 raise NotImplementedError()
1749
Edward Lemurf38bc172019-09-03 21:02:13 +00001750 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001751 """Fetches and applies the issue.
1752
1753 Arguments:
1754 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
Edward Lemurf38bc172019-09-03 21:02:13 +00001755 nocommit: do not commit the patch, thus leave the tree dirty.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001756 """
1757 raise NotImplementedError()
1758
1759 @staticmethod
1760 def ParseIssueURL(parsed_url):
1761 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1762 failed."""
1763 raise NotImplementedError()
1764
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001765 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001766 """Best effort check that user is authenticated with codereview server.
1767
1768 Arguments:
1769 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001770 refresh: whether to attempt to refresh credentials. Ignored if not
1771 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001772 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001773 raise NotImplementedError()
1774
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001775 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001776 """Best effort check that uploading isn't supposed to fail for predictable
1777 reasons.
1778
1779 This method should raise informative exception if uploading shouldn't
1780 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001781
1782 Arguments:
1783 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001784 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001785 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001786
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001787 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001788 """Uploads a change to codereview."""
1789 raise NotImplementedError()
1790
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001791 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001792 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001793
1794 Issue must have been already uploaded and known.
1795 """
1796 raise NotImplementedError()
1797
tandriie113dfd2016-10-11 10:20:12 -07001798 def CannotTriggerTryJobReason(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001799 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001800 raise NotImplementedError()
1801
tandriide281ae2016-10-12 06:02:30 -07001802 def GetIssueOwner(self):
1803 raise NotImplementedError()
1804
Edward Lemur707d70b2018-02-07 00:50:14 +01001805 def GetReviewers(self):
1806 raise NotImplementedError()
1807
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001808 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001809 raise NotImplementedError()
1810
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001811
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001812class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001813 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001814 # auth_config is Rietveld thing, kept here to preserve interface only.
1815 super(_GerritChangelistImpl, self).__init__(changelist)
1816 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001817 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001818 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001819 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001820 # Map from change number (issue) to its detail cache.
1821 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001822
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001823 if codereview_host is not None:
1824 assert not codereview_host.startswith('https://'), codereview_host
1825 self._gerrit_host = codereview_host
1826 self._gerrit_server = 'https://%s' % codereview_host
1827
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828 def _GetGerritHost(self):
1829 # Lazy load of configs.
1830 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001831 if self._gerrit_host and '.' not in self._gerrit_host:
1832 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1833 # This happens for internal stuff http://crbug.com/614312.
1834 parsed = urlparse.urlparse(self.GetRemoteUrl())
1835 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001836 print('WARNING: using non-https URLs for remote is likely broken\n'
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00001837 ' Your current remote is: %s' % self.GetRemoteUrl())
tandriie32e3ea2016-06-22 02:52:48 -07001838 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1839 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001840 return self._gerrit_host
1841
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001842 def _GetGitHost(self):
1843 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001844 remote_url = self.GetRemoteUrl()
1845 if not remote_url:
1846 return None
1847 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001848
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001849 def GetCodereviewServer(self):
1850 if not self._gerrit_server:
1851 # If we're on a branch then get the server potentially associated
1852 # with that branch.
1853 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001854 self._gerrit_server = self._GitGetBranchConfigValue(
1855 self.CodereviewServerConfigKey())
1856 if self._gerrit_server:
1857 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001858 if not self._gerrit_server:
1859 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1860 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001861 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001862 parts[0] = parts[0] + '-review'
1863 self._gerrit_host = '.'.join(parts)
1864 self._gerrit_server = 'https://%s' % self._gerrit_host
1865 return self._gerrit_server
1866
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001867 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001868 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001869 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001870 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001871 logging.warn('can\'t detect Gerrit project.')
1872 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001873 project = urlparse.urlparse(remote_url).path.strip('/')
1874 if project.endswith('.git'):
1875 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001876 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1877 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1878 # gitiles/git-over-https protocol. E.g.,
1879 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1880 # as
1881 # https://chromium.googlesource.com/v8/v8
1882 if project.startswith('a/'):
1883 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001884 return project
1885
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001886 def _GerritChangeIdentifier(self):
1887 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1888
1889 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001890 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001891 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001892 project = self._GetGerritProject()
1893 if project:
1894 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1895 # Fall back on still unique, but less efficient change number.
1896 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001897
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001898 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001899 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001900 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001901
tandrii5d48c322016-08-18 16:19:37 -07001902 @classmethod
1903 def PatchsetConfigKey(cls):
1904 return 'gerritpatchset'
1905
1906 @classmethod
1907 def CodereviewServerConfigKey(cls):
1908 return 'gerritserver'
1909
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001910 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001911 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00001912 if settings.GetGerritSkipEnsureAuthenticated():
1913 # For projects with unusual authentication schemes.
1914 # See http://crbug.com/603378.
1915 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001916
1917 # Check presence of cookies only if using cookies-based auth method.
1918 cookie_auth = gerrit_util.Authenticator.get()
1919 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001920 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001921
Daniel Chengcf6269b2019-05-18 01:02:12 +00001922 if urlparse.urlparse(self.GetRemoteUrl()).scheme != 'https':
1923 print('WARNING: Ignoring branch %s with non-https remote %s' %
1924 (self._changelist.branch, self.GetRemoteUrl()))
1925 return
1926
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001927 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001928 self.GetCodereviewServer()
1929 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00001930 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001931
1932 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
1933 git_auth = cookie_auth.get_auth_header(git_host)
1934 if gerrit_auth and git_auth:
1935 if gerrit_auth == git_auth:
1936 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001937 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00001938 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001939 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001940 ' %s\n'
1941 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001942 ' Consider running the following command:\n'
1943 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001944 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00001945 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02001946 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02001947 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001948 cookie_auth.get_new_password_message(git_host)))
1949 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001950 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001951 return
1952 else:
1953 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02001954 ([] if gerrit_auth else [self._gerrit_host]) +
1955 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001956 DieWithError('Credentials for the following hosts are required:\n'
1957 ' %s\n'
1958 'These are read from %s (or legacy %s)\n'
1959 '%s' % (
1960 '\n '.join(missing),
1961 cookie_auth.get_gitcookies_path(),
1962 cookie_auth.get_netrc_path(),
1963 cookie_auth.get_new_password_message(git_host)))
1964
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001965 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001966 if not self.GetIssue():
1967 return
1968
1969 # Warm change details cache now to avoid RPCs later, reducing latency for
1970 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001971 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00001972 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001973
1974 status = self._GetChangeDetail()['status']
1975 if status in ('MERGED', 'ABANDONED'):
1976 DieWithError('Change %s has been %s, new uploads are not allowed' %
1977 (self.GetIssueURL(),
1978 'submitted' if status == 'MERGED' else 'abandoned'))
1979
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001980 # TODO(vadimsh): For some reason the chunk of code below was skipped if
1981 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
1982 # Apparently this check is not very important? Otherwise get_auth_email
1983 # could have been added to other implementations of Authenticator.
1984 cookies_auth = gerrit_util.Authenticator.get()
1985 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001986 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00001987
1988 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001989 if self.GetIssueOwner() == cookies_user:
1990 return
1991 logging.debug('change %s owner is %s, cookies user is %s',
1992 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001993 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001994 # so ask what Gerrit thinks of this user.
1995 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
1996 if details['email'] == self.GetIssueOwner():
1997 return
1998 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001999 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002000 'as %s.\n'
2001 'Uploading may fail due to lack of permissions.' %
2002 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2003 confirm_or_exit(action='upload')
2004
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002005 def _PostUnsetIssueProperties(self):
2006 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002007 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002008
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002009 def GetGerritObjForPresubmit(self):
2010 return presubmit_support.GerritAccessor(self._GetGerritHost())
2011
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002012 def GetStatus(self):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002013 """Applies a rough heuristic to give a simple summary of an issue's review
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002014 or CQ status, assuming adherence to a common workflow.
2015
2016 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002017 * 'error' - error from review tool (including deleted issues)
2018 * 'unsent' - no reviewers added
2019 * 'waiting' - waiting for review
2020 * 'reply' - waiting for uploader to reply to review
2021 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002022 * 'dry-run' - dry-running in the CQ
2023 * 'commit' - in the CQ
Aaron Gable9ab38c62017-04-06 14:36:33 -07002024 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002025 """
2026 if not self.GetIssue():
2027 return None
2028
2029 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002030 data = self._GetChangeDetail([
2031 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002032 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002033 return 'error'
2034
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002035 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002036 return 'closed'
2037
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002038 cq_label = data['labels'].get('Commit-Queue', {})
2039 max_cq_vote = 0
2040 for vote in cq_label.get('all', []):
2041 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2042 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002043 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002044 if max_cq_vote == 1:
2045 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002046
Aaron Gable9ab38c62017-04-06 14:36:33 -07002047 if data['labels'].get('Code-Review', {}).get('approved'):
2048 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002049
2050 if not data.get('reviewers', {}).get('REVIEWER', []):
2051 return 'unsent'
2052
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002053 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002054 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2055 last_message_author = messages.pop().get('author', {})
2056 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002057 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2058 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002059 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002060 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002061 if last_message_author.get('_account_id') == owner:
2062 # Most recent message was by owner.
2063 return 'waiting'
2064 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002065 # Some reply from non-owner.
2066 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002067
2068 # Somehow there are no messages even though there are reviewers.
2069 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002070
2071 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002072 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002073 patchset = data['revisions'][data['current_revision']]['_number']
2074 self.SetPatchset(patchset)
2075 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002076
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002077 def FetchDescription(self, force=False):
2078 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2079 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002080 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002081 return data['revisions'][current_rev]['commit']['message'].encode(
2082 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002083
dsansomee2d6fd92016-09-08 00:10:47 -07002084 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002085 if gerrit_util.HasPendingChangeEdit(
2086 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002087 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002088 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002089 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002090 'unpublished edit. Either publish the edit in the Gerrit web UI '
2091 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002092
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002093 gerrit_util.DeletePendingChangeEdit(
2094 self._GetGerritHost(), self._GerritChangeIdentifier())
2095 gerrit_util.SetCommitMessage(
2096 self._GetGerritHost(), self._GerritChangeIdentifier(),
2097 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002098
Aaron Gable636b13f2017-07-14 10:42:48 -07002099 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002100 gerrit_util.SetReview(
2101 self._GetGerritHost(), self._GerritChangeIdentifier(),
2102 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002103
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002104 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002105 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002106 # CURRENT_REVISION is included to get the latest patchset so that
2107 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002108 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002109 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2110 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002111 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002112 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002113 robot_file_comments = gerrit_util.GetChangeRobotComments(
2114 self._GetGerritHost(), self._GerritChangeIdentifier())
2115
2116 # Add the robot comments onto the list of comments, but only
2117 # keep those that are from the latest pachset.
2118 latest_patch_set = self.GetMostRecentPatchset()
2119 for path, robot_comments in robot_file_comments.iteritems():
2120 line_comments = file_comments.setdefault(path, [])
2121 line_comments.extend(
2122 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002123
2124 # Build dictionary of file comments for easy access and sorting later.
2125 # {author+date: {path: {patchset: {line: url+message}}}}
2126 comments = collections.defaultdict(
2127 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2128 for path, line_comments in file_comments.iteritems():
2129 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002130 tag = comment.get('tag', '')
2131 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002132 continue
2133 key = (comment['author']['email'], comment['updated'])
2134 if comment.get('side', 'REVISION') == 'PARENT':
2135 patchset = 'Base'
2136 else:
2137 patchset = 'PS%d' % comment['patch_set']
2138 line = comment.get('line', 0)
2139 url = ('https://%s/c/%s/%s/%s#%s%s' %
2140 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2141 'b' if comment.get('side') == 'PARENT' else '',
2142 str(line) if line else ''))
2143 comments[key][path][patchset][line] = (url, comment['message'])
2144
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002145 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002146 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002147 summary = self._BuildCommentSummary(msg, comments, readable)
2148 if summary:
2149 summaries.append(summary)
2150 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002151
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002152 @staticmethod
2153 def _BuildCommentSummary(msg, comments, readable):
2154 key = (msg['author']['email'], msg['date'])
2155 # Don't bother showing autogenerated messages that don't have associated
2156 # file or line comments. this will filter out most autogenerated
2157 # messages, but will keep robot comments like those from Tricium.
2158 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2159 if is_autogenerated and not comments.get(key):
2160 return None
2161 message = msg['message']
2162 # Gerrit spits out nanoseconds.
2163 assert len(msg['date'].split('.')[-1]) == 9
2164 date = datetime.datetime.strptime(msg['date'][:-3],
2165 '%Y-%m-%d %H:%M:%S.%f')
2166 if key in comments:
2167 message += '\n'
2168 for path, patchsets in sorted(comments.get(key, {}).items()):
2169 if readable:
2170 message += '\n%s' % path
2171 for patchset, lines in sorted(patchsets.items()):
2172 for line, (url, content) in sorted(lines.items()):
2173 if line:
2174 line_str = 'Line %d' % line
2175 path_str = '%s:%d:' % (path, line)
2176 else:
2177 line_str = 'File comment'
2178 path_str = '%s:0:' % path
2179 if readable:
2180 message += '\n %s, %s: %s' % (patchset, line_str, url)
2181 message += '\n %s\n' % content
2182 else:
2183 message += '\n%s ' % path_str
2184 message += '\n%s\n' % content
2185
2186 return _CommentSummary(
2187 date=date,
2188 message=message,
2189 sender=msg['author']['email'],
2190 autogenerated=is_autogenerated,
2191 # These could be inferred from the text messages and correlated with
2192 # Code-Review label maximum, however this is not reliable.
2193 # Leaving as is until the need arises.
2194 approval=False,
2195 disapproval=False,
2196 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002197
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002198 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002199 gerrit_util.AbandonChange(
2200 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002201
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002202 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002203 gerrit_util.SubmitChange(
2204 self._GetGerritHost(), self._GerritChangeIdentifier(),
2205 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002206
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002207 def _GetChangeDetail(self, options=None, no_cache=False):
2208 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002209
2210 If fresh data is needed, set no_cache=True which will clear cache and
2211 thus new data will be fetched from Gerrit.
2212 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002213 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002214 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002215
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002216 # Optimization to avoid multiple RPCs:
2217 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2218 'CURRENT_COMMIT' not in options):
2219 options.append('CURRENT_COMMIT')
2220
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002221 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002222 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002223 options = [o.upper() for o in options]
2224
2225 # Check in cache first unless no_cache is True.
2226 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002227 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002228 else:
2229 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002230 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002231 # Assumption: data fetched before with extra options is suitable
2232 # for return for a smaller set of options.
2233 # For example, if we cached data for
2234 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2235 # and request is for options=[CURRENT_REVISION],
2236 # THEN we can return prior cached data.
2237 if options_set.issubset(cached_options_set):
2238 return data
2239
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002240 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002241 data = gerrit_util.GetChangeDetail(
2242 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002243 except gerrit_util.GerritError as e:
2244 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002245 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002246 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002247
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002248 self._detail_cache.setdefault(cache_key, []).append(
2249 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002250 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002251
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002252 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002253 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002254 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002255 data = gerrit_util.GetChangeCommit(
2256 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002257 except gerrit_util.GerritError as e:
2258 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002259 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002260 raise
agable32978d92016-11-01 12:55:02 -07002261 return data
2262
Karen Qian40c19422019-03-13 21:28:29 +00002263 def _IsCqConfigured(self):
2264 detail = self._GetChangeDetail(['LABELS'])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002265 if u'Commit-Queue' not in detail.get('labels', {}):
Karen Qian40c19422019-03-13 21:28:29 +00002266 return False
2267 # TODO(crbug/753213): Remove temporary hack
2268 if ('https://chromium.googlesource.com/chromium/src' ==
2269 self._changelist.GetRemoteUrl() and
2270 detail['branch'].startswith('refs/branch-heads/')):
2271 return False
2272 return True
2273
Olivier Robin75ee7252018-04-13 10:02:56 +02002274 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002275 if git_common.is_dirty_git_tree('land'):
2276 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002277
tandriid60367b2016-06-22 05:25:12 -07002278 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002279 if not force and self._IsCqConfigured():
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00002280 confirm_or_exit('\nIt seems this repository has a CQ, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002281 'which can test and land changes for you. '
2282 'Are you sure you wish to bypass it?\n',
2283 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002284 differs = True
tandriic4344b52016-08-29 06:04:54 -07002285 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002286 # Note: git diff outputs nothing if there is no diff.
2287 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002288 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002289 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002290 if detail['current_revision'] == last_upload:
2291 differs = False
2292 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002293 print('WARNING: Local branch contents differ from latest uploaded '
2294 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002295 if differs:
2296 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002297 confirm_or_exit(
2298 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2299 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002300 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002301 elif not bypass_hooks:
2302 hook_results = self.RunHook(
2303 committing=True,
2304 may_prompt=not force,
2305 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002306 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2307 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002308 if not hook_results.should_continue():
2309 return 1
2310
2311 self.SubmitIssue(wait_for_merge=True)
2312 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002313 links = self._GetChangeCommit().get('web_links', [])
2314 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002315 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002316 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002317 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002318 return 0
2319
Edward Lemurf38bc172019-09-03 21:02:13 +00002320 def CMDPatchWithParsedIssue(self, parsed_issue_arg, nocommit, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002321 assert parsed_issue_arg.valid
2322
2323 self._changelist.issue = parsed_issue_arg.issue
2324
2325 if parsed_issue_arg.hostname:
2326 self._gerrit_host = parsed_issue_arg.hostname
2327 self._gerrit_server = 'https://%s' % self._gerrit_host
2328
tandriic2405f52016-10-10 08:13:15 -07002329 try:
2330 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002331 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002332 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002333
2334 if not parsed_issue_arg.patchset:
2335 # Use current revision by default.
2336 revision_info = detail['revisions'][detail['current_revision']]
2337 patchset = int(revision_info['_number'])
2338 else:
2339 patchset = parsed_issue_arg.patchset
2340 for revision_info in detail['revisions'].itervalues():
2341 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2342 break
2343 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002344 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002345 (parsed_issue_arg.patchset, self.GetIssue()))
2346
Aaron Gable697a91b2018-01-19 15:20:15 -08002347 remote_url = self._changelist.GetRemoteUrl()
2348 if remote_url.endswith('.git'):
2349 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002350 remote_url = remote_url.rstrip('/')
2351
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002352 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002353 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002354
2355 if remote_url != fetch_info['url']:
2356 DieWithError('Trying to patch a change from %s but this repo appears '
2357 'to be %s.' % (fetch_info['url'], remote_url))
2358
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002359 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002360
Aaron Gable62619a32017-06-16 08:22:09 -07002361 if force:
2362 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2363 print('Checked out commit for change %i patchset %i locally' %
2364 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002365 elif nocommit:
2366 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2367 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002368 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002369 RunGit(['cherry-pick', 'FETCH_HEAD'])
2370 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002371 (parsed_issue_arg.issue, patchset))
2372 print('Note: this created a local commit which does not have '
2373 'the same hash as the one uploaded for review. This will make '
2374 'uploading changes based on top of this branch difficult.\n'
2375 'If you want to do that, use "git cl patch --force" instead.')
2376
Stefan Zagerd08043c2017-10-12 12:07:02 -07002377 if self.GetBranch():
2378 self.SetIssue(parsed_issue_arg.issue)
2379 self.SetPatchset(patchset)
2380 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2381 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2382 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2383 else:
2384 print('WARNING: You are in detached HEAD state.\n'
2385 'The patch has been applied to your checkout, but you will not be '
2386 'able to upload a new patch set to the gerrit issue.\n'
2387 'Try using the \'-b\' option if you would like to work on a '
2388 'branch and/or upload a new patch set.')
2389
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002390 return 0
2391
2392 @staticmethod
2393 def ParseIssueURL(parsed_url):
2394 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2395 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002396 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2397 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002398 # Short urls like https://domain/<issue_number> can be used, but don't allow
2399 # specifying the patchset (you'd 404), but we allow that here.
2400 if parsed_url.path == '/':
2401 part = parsed_url.fragment
2402 else:
2403 part = parsed_url.path
Bruce Dawson9c062012019-05-02 19:20:28 +00002404 match = re.match(r'(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002405 if match:
2406 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002407 issue=int(match.group(3)),
2408 patchset=int(match.group(5)) if match.group(5) else None,
Edward Lemurf38bc172019-09-03 21:02:13 +00002409 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002410 return None
2411
tandrii16e0b4e2016-06-07 10:34:28 -07002412 def _GerritCommitMsgHookCheck(self, offer_removal):
2413 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2414 if not os.path.exists(hook):
2415 return
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002416 # Crude attempt to distinguish Gerrit Codereview hook from a potentially
2417 # custom developer-made one.
tandrii16e0b4e2016-06-07 10:34:28 -07002418 data = gclient_utils.FileRead(hook)
2419 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2420 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002421 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002422 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002423 'and may interfere with it in subtle ways.\n'
2424 'We recommend you remove the commit-msg hook.')
2425 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002426 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002427 gclient_utils.rm_file_or_tree(hook)
2428 print('Gerrit commit-msg hook removed.')
2429 else:
2430 print('OK, will keep Gerrit commit-msg hook in place.')
2431
Edward Lemur1b52d872019-05-09 21:12:12 +00002432 def _CleanUpOldTraces(self):
2433 """Keep only the last |MAX_TRACES| traces."""
2434 try:
2435 traces = sorted([
2436 os.path.join(TRACES_DIR, f)
2437 for f in os.listdir(TRACES_DIR)
2438 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2439 and not f.startswith('tmp'))
2440 ])
2441 traces_to_delete = traces[:-MAX_TRACES]
2442 for trace in traces_to_delete:
Daniel Chengcf6269b2019-05-18 01:02:12 +00002443 os.remove(trace)
Edward Lemur1b52d872019-05-09 21:12:12 +00002444 except OSError:
2445 print('WARNING: Failed to remove old git traces from\n'
2446 ' %s'
2447 'Consider removing them manually.' % TRACES_DIR)
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002448
Edward Lemur5737f022019-05-17 01:24:00 +00002449 def _WriteGitPushTraces(self, trace_name, traces_dir, git_push_metadata):
Edward Lemur1b52d872019-05-09 21:12:12 +00002450 """Zip and write the git push traces stored in traces_dir."""
2451 gclient_utils.safe_makedirs(TRACES_DIR)
Edward Lemur1b52d872019-05-09 21:12:12 +00002452 traces_zip = trace_name + '-traces'
2453 traces_readme = trace_name + '-README'
Michael Mosse7f0b4c2019-05-08 04:36:24 +00002454 # Create a temporary dir to store git config and gitcookies in. It will be
2455 # compressed and stored next to the traces.
2456 git_info_dir = tempfile.mkdtemp()
Edward Lemur1b52d872019-05-09 21:12:12 +00002457 git_info_zip = trace_name + '-git-info'
2458
Edward Lemur5737f022019-05-17 01:24:00 +00002459 git_push_metadata['now'] = datetime_now().strftime('%c')
Eric Boren67c48202019-05-30 16:52:51 +00002460 if sys.stdin.encoding and sys.stdin.encoding != 'utf-8':
sangwoo.ko7a614332019-05-22 02:46:19 +00002461 git_push_metadata['now'] = git_push_metadata['now'].decode(
2462 sys.stdin.encoding)
2463
Edward Lemur1b52d872019-05-09 21:12:12 +00002464 git_push_metadata['trace_name'] = trace_name
2465 gclient_utils.FileWrite(
2466 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2467
2468 # Keep only the first 6 characters of the git hashes on the packet
2469 # trace. This greatly decreases size after compression.
2470 packet_traces = os.path.join(traces_dir, 'trace-packet')
2471 if os.path.isfile(packet_traces):
2472 contents = gclient_utils.FileRead(packet_traces)
2473 gclient_utils.FileWrite(
2474 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2475 shutil.make_archive(traces_zip, 'zip', traces_dir)
2476
2477 # Collect and compress the git config and gitcookies.
2478 git_config = RunGit(['config', '-l'])
2479 gclient_utils.FileWrite(
2480 os.path.join(git_info_dir, 'git-config'),
2481 git_config)
2482
2483 cookie_auth = gerrit_util.Authenticator.get()
2484 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2485 gitcookies_path = cookie_auth.get_gitcookies_path()
2486 if os.path.isfile(gitcookies_path):
2487 gitcookies = gclient_utils.FileRead(gitcookies_path)
2488 gclient_utils.FileWrite(
2489 os.path.join(git_info_dir, 'gitcookies'),
2490 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2491 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2492
Edward Lemur1b52d872019-05-09 21:12:12 +00002493 gclient_utils.rmtree(git_info_dir)
2494
2495 def _RunGitPushWithTraces(
2496 self, change_desc, refspec, refspec_opts, git_push_metadata):
2497 """Run git push and collect the traces resulting from the execution."""
2498 # Create a temporary directory to store traces in. Traces will be compressed
2499 # and stored in a 'traces' dir inside depot_tools.
2500 traces_dir = tempfile.mkdtemp()
Edward Lemur5737f022019-05-17 01:24:00 +00002501 trace_name = os.path.join(
2502 TRACES_DIR, datetime_now().strftime('%Y%m%dT%H%M%S.%f'))
Edward Lemur0f58ae42019-04-30 17:24:12 +00002503
2504 env = os.environ.copy()
2505 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2506 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Jonathan Nieder9779b142019-05-29 23:19:29 +00002507 env['GIT_TRACE2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
Edward Lemur0f58ae42019-04-30 17:24:12 +00002508 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2509 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2510 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2511
2512 try:
2513 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002514 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002515 before_push = time_time()
2516 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002517 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002518 env=env,
2519 print_stdout=True,
2520 # Flush after every line: useful for seeing progress when running as
2521 # recipe.
2522 filter_fn=lambda _: sys.stdout.flush())
2523 except subprocess2.CalledProcessError as e:
2524 push_returncode = e.returncode
2525 DieWithError('Failed to create a change. Please examine output above '
2526 'for the reason of the failure.\n'
2527 'Hint: run command below to diagnose common Git/Gerrit '
2528 'credential problems:\n'
Edward Lemur5737f022019-05-17 01:24:00 +00002529 ' git cl creds-check\n'
2530 '\n'
2531 'If git-cl is not working correctly, file a bug under the '
2532 'Infra>SDK component including the files below.\n'
2533 'Review the files before upload, since they might contain '
2534 'sensitive information.\n'
2535 'Set the Restrict-View-Google label so that they are not '
2536 'publicly accessible.\n'
2537 + TRACES_MESSAGE % {'trace_name': trace_name},
Edward Lemur0f58ae42019-04-30 17:24:12 +00002538 change_desc)
2539 finally:
2540 execution_time = time_time() - before_push
2541 metrics.collector.add_repeated('sub_commands', {
2542 'command': 'git push',
2543 'execution_time': execution_time,
2544 'exit_code': push_returncode,
2545 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2546 })
2547
Edward Lemur1b52d872019-05-09 21:12:12 +00002548 git_push_metadata['execution_time'] = execution_time
2549 git_push_metadata['exit_code'] = push_returncode
Edward Lemur5737f022019-05-17 01:24:00 +00002550 self._WriteGitPushTraces(trace_name, traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002551
Edward Lemur1b52d872019-05-09 21:12:12 +00002552 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002553 gclient_utils.rmtree(traces_dir)
2554
2555 return push_stdout
2556
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002557 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002558 """Upload the current branch to Gerrit."""
Mike Frysingera989d552019-08-14 20:51:23 +00002559 if options.squash is None:
tandriia60502f2016-06-20 02:01:53 -07002560 # Load default for user, repo, squash=true, in this order.
2561 options.squash = settings.GetSquashGerritUploads()
tandrii26f3e4e2016-06-10 08:37:04 -07002562
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002563 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002564 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002565 # This may be None; default fallback value is determined in logic below.
2566 title = options.title
2567
Dominic Battre7d1c4842017-10-27 09:17:28 +02002568 # Extract bug number from branch name.
2569 bug = options.bug
2570 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2571 if not bug and match:
2572 bug = match.group(1)
2573
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002574 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002575 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002576 if self.GetIssue():
2577 # Try to get the message from a previous upload.
2578 message = self.GetDescription()
2579 if not message:
2580 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002581 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002582 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002583 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002584 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002585 # When uploading a subsequent patchset, -m|--message is taken
2586 # as the patchset title if --title was not provided.
2587 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002588 else:
2589 default_title = RunGit(
2590 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002591 if options.force:
2592 title = default_title
2593 else:
2594 title = ask_for_data(
2595 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002596 change_id = self._GetChangeDetail()['change_id']
2597 while True:
2598 footer_change_ids = git_footers.get_footer_change_id(message)
2599 if footer_change_ids == [change_id]:
2600 break
2601 if not footer_change_ids:
2602 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002603 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002604 continue
2605 # There is already a valid footer but with different or several ids.
2606 # Doing this automatically is non-trivial as we don't want to lose
2607 # existing other footers, yet we want to append just 1 desired
2608 # Change-Id. Thus, just create a new footer, but let user verify the
2609 # new description.
2610 message = '%s\n\nChange-Id: %s' % (message, change_id)
2611 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002612 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002613 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002614 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002615 'Please, check the proposed correction to the description, '
2616 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2617 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2618 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002619 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002620 if not options.force:
2621 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002622 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002623 message = change_desc.description
2624 if not message:
2625 DieWithError("Description is empty. Aborting...")
2626 # Continue the while loop.
2627 # Sanity check of this code - we should end up with proper message
2628 # footer.
2629 assert [change_id] == git_footers.get_footer_change_id(message)
2630 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002631 else: # if not self.GetIssue()
2632 if options.message:
2633 message = options.message
2634 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002635 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002636 if options.title:
2637 message = options.title + '\n\n' + message
2638 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002639
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002640 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002641 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002642 # On first upload, patchset title is always this string, while
2643 # --title flag gets converted to first line of message.
2644 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002645 if not change_desc.description:
2646 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002647 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002648 if len(change_ids) > 1:
2649 DieWithError('too many Change-Id footers, at most 1 allowed.')
2650 if not change_ids:
2651 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002652 change_desc.set_description(git_footers.add_footer_change_id(
2653 change_desc.description,
2654 GenerateGerritChangeId(change_desc.description)))
2655 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002656 assert len(change_ids) == 1
2657 change_id = change_ids[0]
2658
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002659 if options.reviewers or options.tbrs or options.add_owners_to:
2660 change_desc.update_reviewers(options.reviewers, options.tbrs,
2661 options.add_owners_to, change)
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00002662 if options.preserve_tryjobs:
2663 change_desc.set_preserve_tryjobs()
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002664
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002665 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002666 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2667 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002668 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002669 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2670 desc_tempfile.write(change_desc.description)
2671 desc_tempfile.close()
2672 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2673 '-F', desc_tempfile.name]).strip()
2674 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002675 else:
2676 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002677 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002678 if not change_desc.description:
2679 DieWithError("Description is empty. Aborting...")
2680
2681 if not git_footers.get_footer_change_id(change_desc.description):
2682 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002683 change_desc.set_description(
2684 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002685 if options.reviewers or options.tbrs or options.add_owners_to:
2686 change_desc.update_reviewers(options.reviewers, options.tbrs,
2687 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002688 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002689 # For no-squash mode, we assume the remote called "origin" is the one we
2690 # want. It is not worthwhile to support different workflows for
2691 # no-squash mode.
2692 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002693 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2694
2695 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002696 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002697 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2698 ref_to_push)]).splitlines()
2699 if len(commits) > 1:
2700 print('WARNING: This will upload %d commits. Run the following command '
2701 'to see which commits will be uploaded: ' % len(commits))
2702 print('git log %s..%s' % (parent, ref_to_push))
2703 print('You can also use `git squash-branch` to squash these into a '
2704 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002705 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002706
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002707 if options.reviewers or options.tbrs or options.add_owners_to:
2708 change_desc.update_reviewers(options.reviewers, options.tbrs,
2709 options.add_owners_to, change)
2710
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002711 reviewers = sorted(change_desc.get_reviewers())
2712 # Add cc's from the CC_LIST and --cc flag (if any).
2713 if not options.private and not options.no_autocc:
2714 cc = self.GetCCList().split(',')
2715 else:
2716 cc = []
2717 if options.cc:
2718 cc.extend(options.cc)
2719 cc = filter(None, [email.strip() for email in cc])
2720 if change_desc.get_cced():
2721 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002722 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2723 valid_accounts = set(reviewers + cc)
2724 # TODO(crbug/877717): relax this for all hosts.
2725 else:
2726 valid_accounts = gerrit_util.ValidAccounts(
2727 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002728 logging.info('accounts %s are recognized, %s invalid',
2729 sorted(valid_accounts),
2730 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002731
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002732 # Extra options that can be specified at push time. Doc:
2733 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002734 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002735
Aaron Gable844cf292017-06-28 11:32:59 -07002736 # By default, new changes are started in WIP mode, and subsequent patchsets
2737 # don't send email. At any time, passing --send-mail will mark the change
2738 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002739 if options.send_mail:
2740 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002741 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002742 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002743 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002744 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002745 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002746
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002747 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002748 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002749
Aaron Gable9b713dd2016-12-14 16:04:21 -08002750 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002751 # Punctuation and whitespace in |title| must be percent-encoded.
2752 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002753
agablec6787972016-09-09 16:13:34 -07002754 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002755 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002756
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002757 for r in sorted(reviewers):
2758 if r in valid_accounts:
2759 refspec_opts.append('r=%s' % r)
2760 reviewers.remove(r)
2761 else:
2762 # TODO(tandrii): this should probably be a hard failure.
2763 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2764 % r)
2765 for c in sorted(cc):
2766 # refspec option will be rejected if cc doesn't correspond to an
2767 # account, even though REST call to add such arbitrary cc may succeed.
2768 if c in valid_accounts:
2769 refspec_opts.append('cc=%s' % c)
2770 cc.remove(c)
2771
rmistry9eadede2016-09-19 11:22:43 -07002772 if options.topic:
2773 # Documentation on Gerrit topics is here:
2774 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002775 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002776
Edward Lemur687ca902018-12-05 02:30:30 +00002777 if options.enable_auto_submit:
2778 refspec_opts.append('l=Auto-Submit+1')
2779 if options.use_commit_queue:
2780 refspec_opts.append('l=Commit-Queue+2')
2781 elif options.cq_dry_run:
2782 refspec_opts.append('l=Commit-Queue+1')
2783
2784 if change_desc.get_reviewers(tbr_only=True):
2785 score = gerrit_util.GetCodeReviewTbrScore(
2786 self._GetGerritHost(),
2787 self._GetGerritProject())
2788 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002789
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002790 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002791 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002792 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002793 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002794 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2795
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002796 refspec_suffix = ''
2797 if refspec_opts:
2798 refspec_suffix = '%' + ','.join(refspec_opts)
2799 assert ' ' not in refspec_suffix, (
2800 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2801 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2802
Edward Lemur1b52d872019-05-09 21:12:12 +00002803 git_push_metadata = {
2804 'gerrit_host': self._GetGerritHost(),
2805 'title': title or '<untitled>',
2806 'change_id': change_id,
2807 'description': change_desc.description,
2808 }
2809 push_stdout = self._RunGitPushWithTraces(
2810 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002811
2812 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002813 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002814 change_numbers = [m.group(1)
2815 for m in map(regex.match, push_stdout.splitlines())
2816 if m]
2817 if len(change_numbers) != 1:
2818 DieWithError(
2819 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002820 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002821 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002822 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002823
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002824 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002825 # GetIssue() is not set in case of non-squash uploads according to tests.
2826 # TODO(agable): non-squash uploads in git cl should be removed.
2827 gerrit_util.AddReviewers(
2828 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002829 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002830 reviewers, cc,
2831 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002832
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002833 return 0
2834
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002835 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2836 change_desc):
2837 """Computes parent of the generated commit to be uploaded to Gerrit.
2838
2839 Returns revision or a ref name.
2840 """
2841 if custom_cl_base:
2842 # Try to avoid creating additional unintended CLs when uploading, unless
2843 # user wants to take this risk.
2844 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2845 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2846 local_ref_of_target_remote])
2847 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002848 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002849 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2850 'If you proceed with upload, more than 1 CL may be created by '
2851 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2852 'If you are certain that specified base `%s` has already been '
2853 'uploaded to Gerrit as another CL, you may proceed.\n' %
2854 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2855 if not force:
2856 confirm_or_exit(
2857 'Do you take responsibility for cleaning up potential mess '
2858 'resulting from proceeding with upload?',
2859 action='upload')
2860 return custom_cl_base
2861
Aaron Gablef97e33d2017-03-30 15:44:27 -07002862 if remote != '.':
2863 return self.GetCommonAncestorWithUpstream()
2864
2865 # If our upstream branch is local, we base our squashed commit on its
2866 # squashed version.
2867 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2868
Aaron Gablef97e33d2017-03-30 15:44:27 -07002869 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002870 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002871
2872 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002873 # TODO(tandrii): consider checking parent change in Gerrit and using its
2874 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2875 # the tree hash of the parent branch. The upside is less likely bogus
2876 # requests to reupload parent change just because it's uploadhash is
2877 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002878 parent = RunGit(['config',
2879 'branch.%s.gerritsquashhash' % upstream_branch_name],
2880 error_ok=True).strip()
2881 # Verify that the upstream branch has been uploaded too, otherwise
2882 # Gerrit will create additional CLs when uploading.
2883 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2884 RunGitSilent(['rev-parse', parent + ':'])):
2885 DieWithError(
2886 '\nUpload upstream branch %s first.\n'
2887 'It is likely that this branch has been rebased since its last '
2888 'upload, so you just need to upload it again.\n'
2889 '(If you uploaded it with --no-squash, then branch dependencies '
2890 'are not supported, and you should reupload with --squash.)'
2891 % upstream_branch_name,
2892 change_desc)
2893 return parent
2894
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002895 def _AddChangeIdToCommitMessage(self, options, args):
2896 """Re-commits using the current message, assumes the commit hook is in
2897 place.
2898 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002899 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002900 git_command = ['commit', '--amend', '-m', log_desc]
2901 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002902 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002903 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002904 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002905 return new_log_desc
2906 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002907 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002908
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002909 def SetCQState(self, new_state):
2910 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002911 vote_map = {
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002912 _CQState.NONE: 0,
2913 _CQState.DRY_RUN: 1,
2914 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002915 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002916 labels = {'Commit-Queue': vote_map[new_state]}
2917 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002918 gerrit_util.SetReview(
2919 self._GetGerritHost(), self._GerritChangeIdentifier(),
2920 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002921
tandriie113dfd2016-10-11 10:20:12 -07002922 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002923 try:
2924 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08002925 except GerritChangeNotExists:
2926 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07002927
2928 if data['status'] in ('ABANDONED', 'MERGED'):
2929 return 'CL %s is closed' % self.GetIssue()
2930
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002931 def GetTryJobProperties(self, patchset=None):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00002932 """Returns dictionary of properties to launch a tryjob."""
tandrii8c5a3532016-11-04 07:52:02 -07002933 data = self._GetChangeDetail(['ALL_REVISIONS'])
2934 patchset = int(patchset or self.GetPatchset())
2935 assert patchset
2936 revision_data = None # Pylint wants it to be defined.
2937 for revision_data in data['revisions'].itervalues():
2938 if int(revision_data['_number']) == patchset:
2939 break
2940 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002941 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07002942 (patchset, self.GetIssue()))
2943 return {
2944 'patch_issue': self.GetIssue(),
2945 'patch_set': patchset or self.GetPatchset(),
2946 'patch_project': data['project'],
2947 'patch_storage': 'gerrit',
2948 'patch_ref': revision_data['fetch']['http']['ref'],
2949 'patch_repository_url': revision_data['fetch']['http']['url'],
2950 'patch_gerrit_url': self.GetCodereviewServer(),
2951 }
tandriie113dfd2016-10-11 10:20:12 -07002952
tandriide281ae2016-10-12 06:02:30 -07002953 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002954 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002955
Edward Lemur707d70b2018-02-07 00:50:14 +01002956 def GetReviewers(self):
2957 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00002958 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01002959
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002960
2961_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002962 'gerrit': _GerritChangelistImpl,
2963}
2964
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002965
iannuccie53c9352016-08-17 14:40:40 -07002966def _add_codereview_issue_select_options(parser, extra=""):
2967 _add_codereview_select_options(parser)
2968
2969 text = ('Operate on this issue number instead of the current branch\'s '
2970 'implicit issue.')
2971 if extra:
2972 text += ' '+extra
2973 parser.add_option('-i', '--issue', type=int, help=text)
2974
2975
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002976def _add_codereview_select_options(parser):
Edward Lemurf38bc172019-09-03 21:02:13 +00002977 """Appends --gerrit option to force specific codereview."""
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002978 parser.codereview_group = optparse.OptionGroup(
Edward Lemurf38bc172019-09-03 21:02:13 +00002979 parser, 'DEPRECATED! Codereview override options')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002980 parser.add_option_group(parser.codereview_group)
2981 parser.codereview_group.add_option(
2982 '--gerrit', action='store_true',
Edward Lemurf38bc172019-09-03 21:02:13 +00002983 help='Deprecated. Noop. Do not use.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002984
2985
2986def _process_codereview_select_options(parser, options):
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002987 options.forced_codereview = None
2988 if options.gerrit:
2989 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002990
2991
tandriif9aefb72016-07-01 09:06:51 -07002992def _get_bug_line_values(default_project, bugs):
2993 """Given default_project and comma separated list of bugs, yields bug line
2994 values.
2995
2996 Each bug can be either:
2997 * a number, which is combined with default_project
2998 * string, which is left as is.
2999
3000 This function may produce more than one line, because bugdroid expects one
3001 project per line.
3002
3003 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3004 ['v8:123', 'chromium:789']
3005 """
3006 default_bugs = []
3007 others = []
3008 for bug in bugs.split(','):
3009 bug = bug.strip()
3010 if bug:
3011 try:
3012 default_bugs.append(int(bug))
3013 except ValueError:
3014 others.append(bug)
3015
3016 if default_bugs:
3017 default_bugs = ','.join(map(str, default_bugs))
3018 if default_project:
3019 yield '%s:%s' % (default_project, default_bugs)
3020 else:
3021 yield default_bugs
3022 for other in sorted(others):
3023 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3024 yield other
3025
3026
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003027class ChangeDescription(object):
3028 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003029 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003030 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003031 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003032 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003033 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3034 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3035 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3036 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003037
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003038 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003039 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003040
agable@chromium.org42c20792013-09-12 17:34:49 +00003041 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003042 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003043 return '\n'.join(self._description_lines)
3044
3045 def set_description(self, desc):
3046 if isinstance(desc, basestring):
3047 lines = desc.splitlines()
3048 else:
3049 lines = [line.rstrip() for line in desc]
3050 while lines and not lines[0]:
3051 lines.pop(0)
3052 while lines and not lines[-1]:
3053 lines.pop(-1)
3054 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003055
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003056 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3057 """Rewrites the R=/TBR= line(s) as a single line each.
3058
3059 Args:
3060 reviewers (list(str)) - list of additional emails to use for reviewers.
3061 tbrs (list(str)) - list of additional emails to use for TBRs.
3062 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3063 the change that are missing OWNER coverage. If this is not None, you
3064 must also pass a value for `change`.
3065 change (Change) - The Change that should be used for OWNERS lookups.
3066 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003067 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003068 assert isinstance(tbrs, list), tbrs
3069
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003070 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003071 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003072
3073 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003074 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003075
3076 reviewers = set(reviewers)
3077 tbrs = set(tbrs)
3078 LOOKUP = {
3079 'TBR': tbrs,
3080 'R': reviewers,
3081 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003082
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003083 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003084 regexp = re.compile(self.R_LINE)
3085 matches = [regexp.match(line) for line in self._description_lines]
3086 new_desc = [l for i, l in enumerate(self._description_lines)
3087 if not matches[i]]
3088 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003089
agable@chromium.org42c20792013-09-12 17:34:49 +00003090 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003091
3092 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003093 for match in matches:
3094 if not match:
3095 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003096 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3097
3098 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003099 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003100 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003101 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003102 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003103 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003104 LOOKUP[add_owners_to].update(
3105 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003106
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003107 # If any folks ended up in both groups, remove them from tbrs.
3108 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003109
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003110 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3111 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003112
3113 # Put the new lines in the description where the old first R= line was.
3114 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3115 if 0 <= line_loc < len(self._description_lines):
3116 if new_tbr_line:
3117 self._description_lines.insert(line_loc, new_tbr_line)
3118 if new_r_line:
3119 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003120 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003121 if new_r_line:
3122 self.append_footer(new_r_line)
3123 if new_tbr_line:
3124 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003125
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00003126 def set_preserve_tryjobs(self):
3127 """Ensures description footer contains 'Cq-Do-Not-Cancel-Tryjobs: true'."""
3128 footers = git_footers.parse_footers(self.description)
3129 for v in footers.get('Cq-Do-Not-Cancel-Tryjobs', []):
3130 if v.lower() == 'true':
3131 return
3132 self.append_footer('Cq-Do-Not-Cancel-Tryjobs: true')
3133
Aaron Gable3a16ed12017-03-23 10:51:55 -07003134 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003135 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003136 self.set_description([
3137 '# Enter a description of the change.',
3138 '# This will be displayed on the codereview site.',
3139 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003140 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003141 '--------------------',
3142 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003143
agable@chromium.org42c20792013-09-12 17:34:49 +00003144 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003145 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00003146 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003147 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003148 if git_footer:
3149 self.append_footer('Bug: %s' % ', '.join(values))
3150 else:
3151 for value in values:
3152 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003153
agable@chromium.org42c20792013-09-12 17:34:49 +00003154 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003155 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003156 if not content:
3157 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003158 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003159
Bruce Dawson2377b012018-01-11 16:46:49 -08003160 # Strip off comments and default inserted "Bug:" line.
3161 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003162 (line.startswith('#') or
3163 line.rstrip() == "Bug:" or
3164 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003165 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003166 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003167 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003168
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003169 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003170 """Adds a footer line to the description.
3171
3172 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3173 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3174 that Gerrit footers are always at the end.
3175 """
3176 parsed_footer_line = git_footers.parse_footer(line)
3177 if parsed_footer_line:
3178 # Line is a gerrit footer in the form: Footer-Key: any value.
3179 # Thus, must be appended observing Gerrit footer rules.
3180 self.set_description(
3181 git_footers.add_footer(self.description,
3182 key=parsed_footer_line[0],
3183 value=parsed_footer_line[1]))
3184 return
3185
3186 if not self._description_lines:
3187 self._description_lines.append(line)
3188 return
3189
3190 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3191 if gerrit_footers:
3192 # git_footers.split_footers ensures that there is an empty line before
3193 # actual (gerrit) footers, if any. We have to keep it that way.
3194 assert top_lines and top_lines[-1] == ''
3195 top_lines, separator = top_lines[:-1], top_lines[-1:]
3196 else:
3197 separator = [] # No need for separator if there are no gerrit_footers.
3198
3199 prev_line = top_lines[-1] if top_lines else ''
3200 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3201 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3202 top_lines.append('')
3203 top_lines.append(line)
3204 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003205
tandrii99a72f22016-08-17 14:33:24 -07003206 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003207 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003208 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003209 reviewers = [match.group(2).strip()
3210 for match in matches
3211 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003212 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003213
bradnelsond975b302016-10-23 12:20:23 -07003214 def get_cced(self):
3215 """Retrieves the list of reviewers."""
3216 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3217 cced = [match.group(2).strip() for match in matches if match]
3218 return cleanup_list(cced)
3219
Nodir Turakulov23b82142017-11-16 11:04:25 -08003220 def get_hash_tags(self):
3221 """Extracts and sanitizes a list of Gerrit hashtags."""
3222 subject = (self._description_lines or ('',))[0]
3223 subject = re.sub(
3224 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3225
3226 tags = []
3227 start = 0
3228 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3229 while True:
3230 m = bracket_exp.match(subject, start)
3231 if not m:
3232 break
3233 tags.append(self.sanitize_hash_tag(m.group(1)))
3234 start = m.end()
3235
3236 if not tags:
3237 # Try "Tag: " prefix.
3238 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3239 if m:
3240 tags.append(self.sanitize_hash_tag(m.group(1)))
3241 return tags
3242
3243 @classmethod
3244 def sanitize_hash_tag(cls, tag):
3245 """Returns a sanitized Gerrit hash tag.
3246
3247 A sanitized hashtag can be used as a git push refspec parameter value.
3248 """
3249 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3250
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003251 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3252 """Updates this commit description given the parent.
3253
3254 This is essentially what Gnumbd used to do.
3255 Consult https://goo.gl/WMmpDe for more details.
3256 """
3257 assert parent_msg # No, orphan branch creation isn't supported.
3258 assert parent_hash
3259 assert dest_ref
3260 parent_footer_map = git_footers.parse_footers(parent_msg)
3261 # This will also happily parse svn-position, which GnumbD is no longer
3262 # supporting. While we'd generate correct footers, the verifier plugin
3263 # installed in Gerrit will block such commit (ie git push below will fail).
3264 parent_position = git_footers.get_position(parent_footer_map)
3265
3266 # Cherry-picks may have last line obscuring their prior footers,
3267 # from git_footers perspective. This is also what Gnumbd did.
3268 cp_line = None
3269 if (self._description_lines and
3270 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3271 cp_line = self._description_lines.pop()
3272
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003273 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003274
3275 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3276 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003277 for i, line in enumerate(footer_lines):
3278 k, v = git_footers.parse_footer(line) or (None, None)
3279 if k and k.startswith('Cr-'):
3280 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003281
3282 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003283 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003284 if parent_position[0] == dest_ref:
3285 # Same branch as parent.
3286 number = int(parent_position[1]) + 1
3287 else:
3288 number = 1 # New branch, and extra lineage.
3289 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3290 int(parent_position[1])))
3291
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003292 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3293 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003294
3295 self._description_lines = top_lines
3296 if cp_line:
3297 self._description_lines.append(cp_line)
3298 if self._description_lines[-1] != '':
3299 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003300 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003301
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003302
Aaron Gablea1bab272017-04-11 16:38:18 -07003303def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003304 """Retrieves the reviewers that approved a CL from the issue properties with
3305 messages.
3306
3307 Note that the list may contain reviewers that are not committer, thus are not
3308 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003309
3310 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003311 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003312 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003313 return sorted(
3314 set(
3315 message['sender']
3316 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003317 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003318 )
3319 )
3320
3321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003322def FindCodereviewSettingsFile(filename='codereview.settings'):
3323 """Finds the given file starting in the cwd and going up.
3324
3325 Only looks up to the top of the repository unless an
3326 'inherit-review-settings-ok' file exists in the root of the repository.
3327 """
3328 inherit_ok_file = 'inherit-review-settings-ok'
3329 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003330 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003331 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3332 root = '/'
3333 while True:
3334 if filename in os.listdir(cwd):
3335 if os.path.isfile(os.path.join(cwd, filename)):
3336 return open(os.path.join(cwd, filename))
3337 if cwd == root:
3338 break
3339 cwd = os.path.dirname(cwd)
3340
3341
3342def LoadCodereviewSettingsFromFile(fileobj):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003343 """Parses a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003344 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003345
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003346 def SetProperty(name, setting, unset_error_ok=False):
3347 fullname = 'rietveld.' + name
3348 if setting in keyvals:
3349 RunGit(['config', fullname, keyvals[setting]])
3350 else:
3351 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3352
tandrii48df5812016-10-17 03:55:37 -07003353 if not keyvals.get('GERRIT_HOST', False):
3354 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003355 # Only server setting is required. Other settings can be absent.
3356 # In that case, we ignore errors raised during option deletion attempt.
3357 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3358 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3359 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003360 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003361 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3362 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003363 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3364 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003365
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003366 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003367 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003368
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003369 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003370 RunGit(['config', 'gerrit.squash-uploads',
3371 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003372
tandrii@chromium.org28253532016-04-14 13:46:56 +00003373 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003374 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003375 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003377 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003378 # should be of the form
3379 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3380 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003381 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3382 keyvals['ORIGIN_URL_CONFIG']])
3383
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003384
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003385def urlretrieve(source, destination):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003386 """Downloads a network object to a local file, like urllib.urlretrieve.
3387
3388 This is necessary because urllib is broken for SSL connections via a proxy.
3389 """
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003390 with open(destination, 'w') as f:
3391 f.write(urllib2.urlopen(source).read())
3392
3393
ukai@chromium.org712d6102013-11-27 00:52:58 +00003394def hasSheBang(fname):
3395 """Checks fname is a #! script."""
3396 with open(fname) as f:
3397 return f.read(2).startswith('#!')
3398
3399
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003400# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3401def DownloadHooks(*args, **kwargs):
3402 pass
3403
3404
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003405def DownloadGerritHook(force):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003406 """Downloads and installs a Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003407
3408 Args:
3409 force: True to update hooks. False to install hooks if not present.
3410 """
3411 if not settings.GetIsGerrit():
3412 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003413 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003414 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3415 if not os.access(dst, os.X_OK):
3416 if os.path.exists(dst):
3417 if not force:
3418 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003419 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003420 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003421 if not hasSheBang(dst):
3422 DieWithError('Not a script: %s\n'
3423 'You need to download from\n%s\n'
3424 'into .git/hooks/commit-msg and '
3425 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003426 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3427 except Exception:
3428 if os.path.exists(dst):
3429 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003430 DieWithError('\nFailed to download hooks.\n'
3431 'You need to download from\n%s\n'
3432 'into .git/hooks/commit-msg and '
3433 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003434
3435
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003436class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003437 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003438
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003439 _GOOGLESOURCE = 'googlesource.com'
3440
3441 def __init__(self):
3442 # Cached list of [host, identity, source], where source is either
3443 # .gitcookies or .netrc.
3444 self._all_hosts = None
3445
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003446 def ensure_configured_gitcookies(self):
3447 """Runs checks and suggests fixes to make git use .gitcookies from default
3448 path."""
3449 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3450 configured_path = RunGitSilent(
3451 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003452 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003453 if configured_path:
3454 self._ensure_default_gitcookies_path(configured_path, default)
3455 else:
3456 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003457
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003458 @staticmethod
3459 def _ensure_default_gitcookies_path(configured_path, default_path):
3460 assert configured_path
3461 if configured_path == default_path:
3462 print('git is already configured to use your .gitcookies from %s' %
3463 configured_path)
3464 return
3465
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003466 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003467 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3468 (configured_path, default_path))
3469
3470 if not os.path.exists(configured_path):
3471 print('However, your configured .gitcookies file is missing.')
3472 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3473 action='reconfigure')
3474 RunGit(['config', '--global', 'http.cookiefile', default_path])
3475 return
3476
3477 if os.path.exists(default_path):
3478 print('WARNING: default .gitcookies file already exists %s' %
3479 default_path)
3480 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3481 default_path)
3482
3483 confirm_or_exit('Move existing .gitcookies to default location?',
3484 action='move')
3485 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003486 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003487 print('Moved and reconfigured git to use .gitcookies from %s' %
3488 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003489
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003490 @staticmethod
3491 def _configure_gitcookies_path(default_path):
3492 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3493 if os.path.exists(netrc_path):
3494 print('You seem to be using outdated .netrc for git credentials: %s' %
3495 netrc_path)
3496 print('This tool will guide you through setting up recommended '
3497 '.gitcookies store for git credentials.\n'
3498 '\n'
3499 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3500 ' git config --global --unset http.cookiefile\n'
3501 ' mv %s %s.backup\n\n' % (default_path, default_path))
3502 confirm_or_exit(action='setup .gitcookies')
3503 RunGit(['config', '--global', 'http.cookiefile', default_path])
3504 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003505
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003506 def get_hosts_with_creds(self, include_netrc=False):
3507 if self._all_hosts is None:
3508 a = gerrit_util.CookiesAuthenticator()
3509 self._all_hosts = [
3510 (h, u, s)
3511 for h, u, s in itertools.chain(
3512 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3513 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3514 )
3515 if h.endswith(self._GOOGLESOURCE)
3516 ]
3517
3518 if include_netrc:
3519 return self._all_hosts
3520 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3521
3522 def print_current_creds(self, include_netrc=False):
3523 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3524 if not hosts:
3525 print('No Git/Gerrit credentials found')
3526 return
3527 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3528 header = [('Host', 'User', 'Which file'),
3529 ['=' * l for l in lengths]]
3530 for row in (header + hosts):
3531 print('\t'.join((('%%+%ds' % l) % s)
3532 for l, s in zip(lengths, row)))
3533
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003534 @staticmethod
3535 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003536 """Parses identity "git-<username>.domain" into <username> and domain."""
3537 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003538 # distinguishable from sub-domains. But we do know typical domains:
3539 if identity.endswith('.chromium.org'):
3540 domain = 'chromium.org'
3541 username = identity[:-len('.chromium.org')]
3542 else:
3543 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003544 if username.startswith('git-'):
3545 username = username[len('git-'):]
3546 return username, domain
3547
3548 def _get_usernames_of_domain(self, domain):
3549 """Returns list of usernames referenced by .gitcookies in a given domain."""
3550 identities_by_domain = {}
3551 for _, identity, _ in self.get_hosts_with_creds():
3552 username, domain = self._parse_identity(identity)
3553 identities_by_domain.setdefault(domain, []).append(username)
3554 return identities_by_domain.get(domain)
3555
3556 def _canonical_git_googlesource_host(self, host):
3557 """Normalizes Gerrit hosts (with '-review') to Git host."""
3558 assert host.endswith(self._GOOGLESOURCE)
3559 # Prefix doesn't include '.' at the end.
3560 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3561 if prefix.endswith('-review'):
3562 prefix = prefix[:-len('-review')]
3563 return prefix + '.' + self._GOOGLESOURCE
3564
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003565 def _canonical_gerrit_googlesource_host(self, host):
3566 git_host = self._canonical_git_googlesource_host(host)
3567 prefix = git_host.split('.', 1)[0]
3568 return prefix + '-review.' + self._GOOGLESOURCE
3569
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003570 def _get_counterpart_host(self, host):
3571 assert host.endswith(self._GOOGLESOURCE)
3572 git = self._canonical_git_googlesource_host(host)
3573 gerrit = self._canonical_gerrit_googlesource_host(git)
3574 return git if gerrit == host else gerrit
3575
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003576 def has_generic_host(self):
3577 """Returns whether generic .googlesource.com has been configured.
3578
3579 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3580 """
3581 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3582 if host == '.' + self._GOOGLESOURCE:
3583 return True
3584 return False
3585
3586 def _get_git_gerrit_identity_pairs(self):
3587 """Returns map from canonic host to pair of identities (Git, Gerrit).
3588
3589 One of identities might be None, meaning not configured.
3590 """
3591 host_to_identity_pairs = {}
3592 for host, identity, _ in self.get_hosts_with_creds():
3593 canonical = self._canonical_git_googlesource_host(host)
3594 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3595 idx = 0 if canonical == host else 1
3596 pair[idx] = identity
3597 return host_to_identity_pairs
3598
3599 def get_partially_configured_hosts(self):
3600 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003601 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3602 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3603 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003604
3605 def get_conflicting_hosts(self):
3606 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003607 host
3608 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003609 if None not in (i1, i2) and i1 != i2)
3610
3611 def get_duplicated_hosts(self):
3612 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3613 return set(host for host, count in counters.iteritems() if count > 1)
3614
3615 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3616 'chromium.googlesource.com': 'chromium.org',
3617 'chrome-internal.googlesource.com': 'google.com',
3618 }
3619
3620 def get_hosts_with_wrong_identities(self):
3621 """Finds hosts which **likely** reference wrong identities.
3622
3623 Note: skips hosts which have conflicting identities for Git and Gerrit.
3624 """
3625 hosts = set()
3626 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3627 pair = self._get_git_gerrit_identity_pairs().get(host)
3628 if pair and pair[0] == pair[1]:
3629 _, domain = self._parse_identity(pair[0])
3630 if domain != expected:
3631 hosts.add(host)
3632 return hosts
3633
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003634 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003635 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003636 hosts = sorted(hosts)
3637 assert hosts
3638 if extra_column_func is None:
3639 extras = [''] * len(hosts)
3640 else:
3641 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003642 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3643 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003644 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003645 lines.append(tmpl % he)
3646 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003647
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003648 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003649 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003650 yield ('.googlesource.com wildcard record detected',
3651 ['Chrome Infrastructure team recommends to list full host names '
3652 'explicitly.'],
3653 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003654
3655 dups = self.get_duplicated_hosts()
3656 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003657 yield ('The following hosts were defined twice',
3658 self._format_hosts(dups),
3659 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003660
3661 partial = self.get_partially_configured_hosts()
3662 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003663 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3664 'These hosts are missing',
3665 self._format_hosts(partial, lambda host: 'but %s defined' %
3666 self._get_counterpart_host(host)),
3667 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003668
3669 conflicting = self.get_conflicting_hosts()
3670 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003671 yield ('The following Git hosts have differing credentials from their '
3672 'Gerrit counterparts',
3673 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3674 tuple(self._get_git_gerrit_identity_pairs()[host])),
3675 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003676
3677 wrong = self.get_hosts_with_wrong_identities()
3678 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003679 yield ('These hosts likely use wrong identity',
3680 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3681 (self._get_git_gerrit_identity_pairs()[host][0],
3682 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3683 wrong)
3684
3685 def find_and_report_problems(self):
3686 """Returns True if there was at least one problem, else False."""
3687 found = False
3688 bad_hosts = set()
3689 for title, sublines, hosts in self._find_problems():
3690 if not found:
3691 found = True
3692 print('\n\n.gitcookies problem report:\n')
3693 bad_hosts.update(hosts or [])
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003694 print(' %s%s' % (title, (':' if sublines else '')))
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003695 if sublines:
3696 print()
3697 print(' %s' % '\n '.join(sublines))
3698 print()
3699
3700 if bad_hosts:
3701 assert found
3702 print(' You can manually remove corresponding lines in your %s file and '
3703 'visit the following URLs with correct account to generate '
3704 'correct credential lines:\n' %
3705 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3706 print(' %s' % '\n '.join(sorted(set(
3707 gerrit_util.CookiesAuthenticator().get_new_password_url(
3708 self._canonical_git_googlesource_host(host))
3709 for host in bad_hosts
3710 ))))
3711 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003712
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003713
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003714@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003715def CMDcreds_check(parser, args):
3716 """Checks credentials and suggests changes."""
3717 _, _ = parser.parse_args(args)
3718
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003719 # Code below checks .gitcookies. Abort if using something else.
3720 authn = gerrit_util.Authenticator.get()
3721 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3722 if isinstance(authn, gerrit_util.GceAuthenticator):
3723 DieWithError(
3724 'This command is not designed for GCE, are you on a bot?\n'
3725 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3726 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003727 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003728 'This command is not designed for bot environment. It checks '
3729 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003730
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003731 checker = _GitCookiesChecker()
3732 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003733
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003734 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003735 checker.print_current_creds(include_netrc=True)
3736
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003737 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003738 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003739 return 0
3740 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003741
3742
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003743@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003744def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003745 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003746 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3747 branch = ShortBranchName(branchref)
3748 _, args = parser.parse_args(args)
3749 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003750 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003751 return RunGit(['config', 'branch.%s.base-url' % branch],
3752 error_ok=False).strip()
3753 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003754 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003755 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3756 error_ok=False).strip()
3757
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003758
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003759def color_for_status(status):
3760 """Maps a Changelist status to color, for CMDstatus and other tools."""
3761 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003762 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003763 'waiting': Fore.BLUE,
3764 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003765 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003766 'lgtm': Fore.GREEN,
3767 'commit': Fore.MAGENTA,
3768 'closed': Fore.CYAN,
3769 'error': Fore.WHITE,
3770 }.get(status, Fore.WHITE)
3771
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003772
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003773def get_cl_statuses(changes, fine_grained, max_processes=None):
3774 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003775
3776 If fine_grained is true, this will fetch CL statuses from the server.
3777 Otherwise, simply indicate if there's a matching url for the given branches.
3778
3779 If max_processes is specified, it is used as the maximum number of processes
3780 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3781 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003782
3783 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003784 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003785 if not changes:
3786 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003787
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003788 if not fine_grained:
3789 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003790 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003791 for cl in changes:
3792 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003793 return
3794
3795 # First, sort out authentication issues.
3796 logging.debug('ensuring credentials exist')
3797 for cl in changes:
3798 cl.EnsureAuthenticated(force=False, refresh=True)
3799
3800 def fetch(cl):
3801 try:
3802 return (cl, cl.GetStatus())
3803 except:
3804 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003805 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003806 raise
3807
3808 threads_count = len(changes)
3809 if max_processes:
3810 threads_count = max(1, min(threads_count, max_processes))
3811 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3812
3813 pool = ThreadPool(threads_count)
3814 fetched_cls = set()
3815 try:
3816 it = pool.imap_unordered(fetch, changes).__iter__()
3817 while True:
3818 try:
3819 cl, status = it.next(timeout=5)
3820 except multiprocessing.TimeoutError:
3821 break
3822 fetched_cls.add(cl)
3823 yield cl, status
3824 finally:
3825 pool.close()
3826
3827 # Add any branches that failed to fetch.
3828 for cl in set(changes) - fetched_cls:
3829 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003830
rmistry@google.com2dd99862015-06-22 12:22:18 +00003831
3832def upload_branch_deps(cl, args):
3833 """Uploads CLs of local branches that are dependents of the current branch.
3834
3835 If the local branch dependency tree looks like:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003836
3837 test1 -> test2.1 -> test3.1
3838 -> test3.2
3839 -> test2.2 -> test3.3
rmistry@google.com2dd99862015-06-22 12:22:18 +00003840
3841 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3842 run on the dependent branches in this order:
3843 test2.1, test3.1, test3.2, test2.2, test3.3
3844
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003845 Note: This function does not rebase your local dependent branches. Use it
3846 when you make a change to the parent branch that will not conflict
3847 with its dependent branches, and you would like their dependencies
3848 updated in Rietveld.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003849 """
3850 if git_common.is_dirty_git_tree('upload-branch-deps'):
3851 return 1
3852
3853 root_branch = cl.GetBranch()
3854 if root_branch is None:
3855 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3856 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003857 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003858 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3859 'patchset dependencies without an uploaded CL.')
3860
3861 branches = RunGit(['for-each-ref',
3862 '--format=%(refname:short) %(upstream:short)',
3863 'refs/heads'])
3864 if not branches:
3865 print('No local branches found.')
3866 return 0
3867
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003868 # Create a dictionary of all local branches to the branches that are
3869 # dependent on it.
rmistry@google.com2dd99862015-06-22 12:22:18 +00003870 tracked_to_dependents = collections.defaultdict(list)
3871 for b in branches.splitlines():
3872 tokens = b.split()
3873 if len(tokens) == 2:
3874 branch_name, tracked = tokens
3875 tracked_to_dependents[tracked].append(branch_name)
3876
vapiera7fbd5a2016-06-16 09:17:49 -07003877 print()
3878 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003879 dependents = []
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003880
rmistry@google.com2dd99862015-06-22 12:22:18 +00003881 def traverse_dependents_preorder(branch, padding=''):
3882 dependents_to_process = tracked_to_dependents.get(branch, [])
3883 padding += ' '
3884 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003885 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003886 dependents.append(dependent)
3887 traverse_dependents_preorder(dependent, padding)
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00003888
rmistry@google.com2dd99862015-06-22 12:22:18 +00003889 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003890 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003891
3892 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003893 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003894 return 0
3895
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003896 confirm_or_exit('This command will checkout all dependent branches and run '
3897 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003898
rmistry@google.com2dd99862015-06-22 12:22:18 +00003899 # Record all dependents that failed to upload.
3900 failures = {}
3901 # Go through all dependents, checkout the branch and upload.
3902 try:
3903 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003904 print()
3905 print('--------------------------------------')
3906 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003907 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003908 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003909 try:
3910 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003911 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003912 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003913 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003914 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003915 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003916 finally:
3917 # Swap back to the original root branch.
3918 RunGit(['checkout', '-q', root_branch])
3919
vapiera7fbd5a2016-06-16 09:17:49 -07003920 print()
3921 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003922 for dependent_branch in dependents:
3923 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003924 print(' %s : %s' % (dependent_branch, upload_status))
3925 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003926
3927 return 0
3928
3929
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003930@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07003931def CMDarchive(parser, args):
3932 """Archives and deletes branches associated with closed changelists."""
3933 parser.add_option(
3934 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003935 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003936 parser.add_option(
3937 '-f', '--force', action='store_true',
3938 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003939 parser.add_option(
3940 '-d', '--dry-run', action='store_true',
3941 help='Skip the branch tagging and removal steps.')
3942 parser.add_option(
3943 '-t', '--notags', action='store_true',
3944 help='Do not tag archived branches. '
3945 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003946
3947 auth.add_auth_options(parser)
3948 options, args = parser.parse_args(args)
3949 if args:
3950 parser.error('Unsupported args: %s' % ' '.join(args))
3951 auth_config = auth.extract_auth_config_from_options(options)
3952
3953 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3954 if not branches:
3955 return 0
3956
vapiera7fbd5a2016-06-16 09:17:49 -07003957 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003958 changes = [Changelist(branchref=b, auth_config=auth_config)
3959 for b in branches.splitlines()]
3960 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3961 statuses = get_cl_statuses(changes,
3962 fine_grained=True,
3963 max_processes=options.maxjobs)
3964 proposal = [(cl.GetBranch(),
3965 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3966 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00003967 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07003968 proposal.sort()
3969
3970 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003971 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003972 return 0
3973
3974 current_branch = GetCurrentBranch()
3975
vapiera7fbd5a2016-06-16 09:17:49 -07003976 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003977 if options.notags:
3978 for next_item in proposal:
3979 print(' ' + next_item[0])
3980 else:
3981 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3982 for next_item in proposal:
3983 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003984
kmarshall9249e012016-08-23 12:02:16 -07003985 # Quit now on precondition failure or if instructed by the user, either
3986 # via an interactive prompt or by command line flags.
3987 if options.dry_run:
3988 print('\nNo changes were made (dry run).\n')
3989 return 0
3990 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003991 print('You are currently on a branch \'%s\' which is associated with a '
3992 'closed codereview issue, so archive cannot proceed. Please '
3993 'checkout another branch and run this command again.' %
3994 current_branch)
3995 return 1
kmarshall9249e012016-08-23 12:02:16 -07003996 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003997 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3998 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003999 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004000 return 1
4001
4002 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004003 if not options.notags:
4004 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004005 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004006
vapiera7fbd5a2016-06-16 09:17:49 -07004007 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004008
4009 return 0
4010
4011
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004012@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004013def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004014 """Show status of changelists.
4015
4016 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004017 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004018 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004019 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004020 - Red 'not LGTM'ed
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004021 - Magenta in the CQ
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004022 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004023 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004024
4025 Also see 'git cl comments'.
4026 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004027 parser.add_option(
4028 '--no-branch-color',
4029 action='store_true',
4030 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004031 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004032 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004033 parser.add_option('-f', '--fast', action='store_true',
4034 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004035 parser.add_option(
4036 '-j', '--maxjobs', action='store', type=int,
4037 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004038
4039 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004040 _add_codereview_issue_select_options(
4041 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004042 options, args = parser.parse_args(args)
Edward Lemurf38bc172019-09-03 21:02:13 +00004043 _process_codereview_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004044 if args:
4045 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004046 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004047
iannuccie53c9352016-08-17 14:40:40 -07004048 if options.issue is not None and not options.field:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004049 parser.error('--field must be specified with --issue.')
iannucci3c972b92016-08-17 13:24:10 -07004050
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004051 if options.field:
Edward Lemurf38bc172019-09-03 21:02:13 +00004052 cl = Changelist(auth_config=auth_config, issue=options.issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004053 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004054 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004055 elif options.field == 'id':
4056 issueid = cl.GetIssue()
4057 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004058 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004059 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004060 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004061 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004062 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004063 elif options.field == 'status':
4064 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065 elif options.field == 'url':
4066 url = cl.GetIssueURL()
4067 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004068 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004069 return 0
4070
4071 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4072 if not branches:
4073 print('No local branch found.')
4074 return 0
4075
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004076 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004077 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004078 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004079 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004080 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004081 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004082 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004083
Daniel McArdlea23bf592019-02-12 00:25:12 +00004084 current_branch = GetCurrentBranch()
4085
4086 def FormatBranchName(branch, colorize=False):
4087 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4088 an asterisk when it is the current branch."""
4089
4090 asterisk = ""
4091 color = Fore.RESET
4092 if branch == current_branch:
4093 asterisk = "* "
4094 color = Fore.GREEN
4095 branch_name = ShortBranchName(branch)
4096
4097 if colorize:
4098 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004099 return asterisk + branch_name
4100
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004101 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004102
4103 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004104 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4105 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004106 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004107 c, status = output.next()
4108 branch_statuses[c.GetBranch()] = status
4109 status = branch_statuses.pop(branch)
4110 url = cl.GetIssueURL()
4111 if url and (not status or status == 'error'):
4112 # The issue probably doesn't exist anymore.
4113 url += ' (broken)'
4114
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004115 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004116 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004117 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004118 color = ''
4119 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004120 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004121
Alan Cuttera3be9a52019-03-04 18:50:33 +00004122 branch_display = FormatBranchName(branch)
4123 padding = ' ' * (alignment - len(branch_display))
4124 if not options.no_branch_color:
4125 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004126
Alan Cuttera3be9a52019-03-04 18:50:33 +00004127 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4128 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004129
vapiera7fbd5a2016-06-16 09:17:49 -07004130 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004131 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004132 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004133 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004134 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004135 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004136 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004137 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004138 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004139 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004140 print('Issue description:')
4141 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004142 return 0
4143
4144
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004145def colorize_CMDstatus_doc():
4146 """To be called once in main() to add colors to git cl status help."""
4147 colors = [i for i in dir(Fore) if i[0].isupper()]
4148
4149 def colorize_line(line):
4150 for color in colors:
4151 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004152 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004153 indent = len(line) - len(line.lstrip(' ')) + 1
4154 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4155 return line
4156
4157 lines = CMDstatus.__doc__.splitlines()
4158 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4159
4160
phajdan.jre328cf92016-08-22 04:12:17 -07004161def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004162 if path == '-':
4163 json.dump(contents, sys.stdout)
4164 else:
4165 with open(path, 'w') as f:
4166 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004167
4168
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004169@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004170@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004171def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004172 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004173
4174 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004175 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004176 parser.add_option('-r', '--reverse', action='store_true',
4177 help='Lookup the branch(es) for the specified issues. If '
4178 'no issues are specified, all branches with mapped '
4179 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004180 parser.add_option('--json',
4181 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004182 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004183 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004184 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004185
dnj@chromium.org406c4402015-03-03 17:22:28 +00004186 if options.reverse:
4187 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004188 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004189 # Reverse issue lookup.
4190 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004191
4192 git_config = {}
4193 for config in RunGit(['config', '--get-regexp',
4194 r'branch\..*issue']).splitlines():
4195 name, _space, val = config.partition(' ')
4196 git_config[name] = val
4197
dnj@chromium.org406c4402015-03-03 17:22:28 +00004198 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004199 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4200 config_key = _git_branch_config_key(ShortBranchName(branch),
4201 cls.IssueConfigKey())
4202 issue = git_config.get(config_key)
4203 if issue:
4204 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004205 if not args:
4206 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004207 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004208 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004209 try:
4210 issue_num = int(issue)
4211 except ValueError:
4212 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004213 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004214 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004215 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004216 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004217 if options.json:
4218 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004219 return 0
4220
4221 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004222 issue = ParseIssueNumberArgument(args[0])
Aaron Gable78753da2017-06-15 10:35:49 -07004223 if not issue.valid:
4224 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4225 'or no argument to list it.\n'
4226 'Maybe you want to run git cl status?')
Edward Lemurf38bc172019-09-03 21:02:13 +00004227 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004228 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004229 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004230 cl = Changelist()
Aaron Gable78753da2017-06-15 10:35:49 -07004231 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4232 if options.json:
4233 write_json(options.json, {
4234 'issue': cl.GetIssue(),
4235 'issue_url': cl.GetIssueURL(),
4236 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004237 return 0
4238
4239
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004240@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004241def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004242 """Shows or posts review comments for any changelist."""
4243 parser.add_option('-a', '--add-comment', dest='comment',
4244 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004245 parser.add_option('-p', '--publish', action='store_true',
4246 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004247 parser.add_option('-i', '--issue', dest='issue',
Edward Lemurf38bc172019-09-03 21:02:13 +00004248 help='review issue id (defaults to current issue).')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004249 parser.add_option('-m', '--machine-readable', dest='readable',
4250 action='store_false', default=True,
4251 help='output comments in a format compatible with '
4252 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004253 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004254 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004255 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004256 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004257 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004258 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004259 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004260
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004261 issue = None
4262 if options.issue:
4263 try:
4264 issue = int(options.issue)
4265 except ValueError:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004266 DieWithError('A review issue ID is expected to be a number.')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004267
Edward Lemurf38bc172019-09-03 21:02:13 +00004268 cl = Changelist(issue=issue, auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004269
4270 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004271 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004272 return 0
4273
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004274 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4275 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004276 for comment in summary:
4277 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004278 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004279 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004280 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004281 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004282 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004283 elif comment.autogenerated:
4284 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004285 else:
4286 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004287 print('\n%s%s %s%s\n%s' % (
4288 color,
4289 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4290 comment.sender,
4291 Fore.RESET,
4292 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4293
smut@google.comc85ac942015-09-15 16:34:43 +00004294 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004295 def pre_serialize(c):
4296 dct = c.__dict__.copy()
4297 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4298 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004299 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004300 return 0
4301
4302
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004303@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004304@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004305def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004306 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004307 parser.add_option('-d', '--display', action='store_true',
4308 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004309 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004310 help='New description to set for this issue (- for stdin, '
4311 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004312 parser.add_option('-f', '--force', action='store_true',
4313 help='Delete any unpublished Gerrit edits for this issue '
4314 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004315
4316 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004317 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004318 options, args = parser.parse_args(args)
4319 _process_codereview_select_options(parser, options)
4320
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004321 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004322 if len(args) > 0:
Edward Lemurf38bc172019-09-03 21:02:13 +00004323 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004324 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004325 parser.error('Invalid issue ID or URL.')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004326
martiniss6eda05f2016-06-30 10:18:35 -07004327 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004328 'auth_config': auth.extract_auth_config_from_options(options),
martiniss6eda05f2016-06-30 10:18:35 -07004329 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004330 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004331 if target_issue_arg:
4332 kwargs['issue'] = target_issue_arg.issue
4333 kwargs['codereview_host'] = target_issue_arg.hostname
Edward Lemurf38bc172019-09-03 21:02:13 +00004334 if not args[0].isdigit() and not options.forced_codereview:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004335 detected_codereview_from_url = True
martiniss6eda05f2016-06-30 10:18:35 -07004336
4337 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004338 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004339 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004340 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004341
4342 if detected_codereview_from_url:
Edward Lemurf38bc172019-09-03 21:02:13 +00004343 logging.info('canonical issue/change URL: %s\n', cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004344
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004345 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004346
smut@google.com34fb6b12015-07-13 20:03:26 +00004347 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004348 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004349 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004350
4351 if options.new_description:
4352 text = options.new_description
4353 if text == '-':
4354 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004355 elif text == '+':
4356 base_branch = cl.GetCommonAncestorWithUpstream()
4357 change = cl.GetChange(base_branch, None, local_description=True)
4358 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004359
4360 description.set_description(text)
4361 else:
Edward Lemurf38bc172019-09-03 21:02:13 +00004362 description.prompt()
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004363 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004364 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004365 return 0
4366
4367
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004368@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004369def CMDlint(parser, args):
4370 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004371 parser.add_option('--filter', action='append', metavar='-x,+y',
4372 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004373 auth.add_auth_options(parser)
4374 options, args = parser.parse_args(args)
4375 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004376
4377 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004378 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004379 try:
4380 import cpplint
4381 import cpplint_chromium
4382 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004383 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004384 return 1
4385
4386 # Change the current working directory before calling lint so that it
4387 # shows the correct base.
4388 previous_cwd = os.getcwd()
4389 os.chdir(settings.GetRoot())
4390 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004391 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004392 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4393 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004394 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004395 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004396 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004397
4398 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004399 command = args + files
4400 if options.filter:
4401 command = ['--filter=' + ','.join(options.filter)] + command
4402 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004403
4404 white_regex = re.compile(settings.GetLintRegex())
4405 black_regex = re.compile(settings.GetLintIgnoreRegex())
4406 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4407 for filename in filenames:
4408 if white_regex.match(filename):
4409 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004410 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004411 else:
4412 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4413 extra_check_functions)
4414 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004415 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004416 finally:
4417 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004418 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004419 if cpplint._cpplint_state.error_count != 0:
4420 return 1
4421 return 0
4422
4423
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004424@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004425def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004426 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004427 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004428 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004429 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004430 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004431 parser.add_option('--all', action='store_true',
4432 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004433 parser.add_option('--parallel', action='store_true',
4434 help='Run all tests specified by input_api.RunTests in all '
4435 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004436 auth.add_auth_options(parser)
4437 options, args = parser.parse_args(args)
4438 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004439
sbc@chromium.org71437c02015-04-09 19:29:40 +00004440 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004441 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004442 return 1
4443
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004444 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004445 if args:
4446 base_branch = args[0]
4447 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004448 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004449 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004450
Aaron Gable8076c282017-11-29 14:39:41 -08004451 if options.all:
4452 base_change = cl.GetChange(base_branch, None)
4453 files = [('M', f) for f in base_change.AllFiles()]
4454 change = presubmit_support.GitChange(
4455 base_change.Name(),
4456 base_change.FullDescriptionText(),
4457 base_change.RepositoryRoot(),
4458 files,
4459 base_change.issue,
4460 base_change.patchset,
4461 base_change.author_email,
4462 base_change._upstream)
4463 else:
4464 change = cl.GetChange(base_branch, None)
4465
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004466 cl.RunHook(
4467 committing=not options.upload,
4468 may_prompt=False,
4469 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004470 change=change,
4471 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004472 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004473
4474
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004475def GenerateGerritChangeId(message):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004476 """Returns the Change ID footer value (Ixxxxxx...xxx).
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004477
4478 Works the same way as
4479 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4480 but can be called on demand on all platforms.
4481
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004482 The basic idea is to generate git hash of a state of the tree, original
4483 commit message, author/committer info and timestamps.
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004484 """
4485 lines = []
4486 tree_hash = RunGitSilent(['write-tree'])
4487 lines.append('tree %s' % tree_hash.strip())
4488 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4489 if code == 0:
4490 lines.append('parent %s' % parent.strip())
4491 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4492 lines.append('author %s' % author.strip())
4493 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4494 lines.append('committer %s' % committer.strip())
4495 lines.append('')
4496 # Note: Gerrit's commit-hook actually cleans message of some lines and
4497 # whitespace. This code is not doing this, but it clearly won't decrease
4498 # entropy.
4499 lines.append(message)
4500 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004501 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004502 return 'I%s' % change_hash.strip()
4503
4504
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004505def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004506 """Computes the remote branch ref to use for the CL.
4507
4508 Args:
4509 remote (str): The git remote for the CL.
4510 remote_branch (str): The git remote branch for the CL.
4511 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004512 """
4513 if not (remote and remote_branch):
4514 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004515
wittman@chromium.org455dc922015-01-26 20:15:50 +00004516 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004517 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004518 # refs, which are then translated into the remote full symbolic refs
4519 # below.
4520 if '/' not in target_branch:
4521 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4522 else:
4523 prefix_replacements = (
4524 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4525 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4526 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4527 )
4528 match = None
4529 for regex, replacement in prefix_replacements:
4530 match = re.search(regex, target_branch)
4531 if match:
4532 remote_branch = target_branch.replace(match.group(0), replacement)
4533 break
4534 if not match:
4535 # This is a branch path but not one we recognize; use as-is.
4536 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004537 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4538 # Handle the refs that need to land in different refs.
4539 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004540
wittman@chromium.org455dc922015-01-26 20:15:50 +00004541 # Create the true path to the remote branch.
4542 # Does the following translation:
4543 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4544 # * refs/remotes/origin/master -> refs/heads/master
4545 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4546 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4547 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4548 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4549 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4550 'refs/heads/')
4551 elif remote_branch.startswith('refs/remotes/branch-heads'):
4552 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004553
wittman@chromium.org455dc922015-01-26 20:15:50 +00004554 return remote_branch
4555
4556
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004557def cleanup_list(l):
4558 """Fixes a list so that comma separated items are put as individual items.
4559
4560 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4561 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4562 """
4563 items = sum((i.split(',') for i in l), [])
4564 stripped_items = (i.strip() for i in items)
4565 return sorted(filter(None, stripped_items))
4566
4567
Aaron Gable4db38df2017-11-03 14:59:07 -07004568@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004569@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004570def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004571 """Uploads the current changelist to codereview.
4572
4573 Can skip dependency patchset uploads for a branch by running:
4574 git config branch.branch_name.skip-deps-uploads True
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004575 To unset, run:
rmistry@google.com78948ed2015-07-08 23:09:57 +00004576 git config --unset branch.branch_name.skip-deps-uploads
4577 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004578
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004579 If the name of the checked out branch starts with "bug-" or "fix-" followed
4580 by a bug number, this bug number is automatically populated in the CL
Dominic Battre7d1c4842017-10-27 09:17:28 +02004581 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004582
4583 If subject contains text in square brackets or has "<text>: " prefix, such
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004584 text(s) is treated as Gerrit hashtags. For example, CLs with subjects:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004585 [git-cl] add support for hashtags
4586 Foo bar: implement foo
4587 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004588 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004589 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4590 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004591 parser.add_option('--bypass-watchlists', action='store_true',
4592 dest='bypass_watchlists',
4593 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004594 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004595 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004596 parser.add_option('--message', '-m', dest='message',
4597 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004598 parser.add_option('-b', '--bug',
4599 help='pre-populate the bug number(s) for this issue. '
4600 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004601 parser.add_option('--message-file', dest='message_file',
4602 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004603 parser.add_option('--title', '-t', dest='title',
4604 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004605 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004606 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004607 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004608 parser.add_option('--tbrs',
4609 action='append', default=[],
4610 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004611 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004612 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004613 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004614 parser.add_option('--hashtag', dest='hashtags',
4615 action='append', default=[],
4616 help=('Gerrit hashtag for new CL; '
4617 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004618 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004619 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004620 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004621 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004622 metavar='TARGET',
4623 help='Apply CL to remote ref TARGET. ' +
4624 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004625 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004626 help='Squash multiple commits into one')
Mike Frysingera989d552019-08-14 20:51:23 +00004627 parser.add_option('--no-squash', action='store_false', dest='squash',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004628 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004629 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004630 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004631 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4632 const='TBR', help='add a set of OWNERS to TBR')
4633 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4634 const='R', help='add a set of OWNERS to R')
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00004635 parser.add_option('-c', '--use-commit-queue', action='store_true',
4636 help='tell the CQ to commit this patchset; '
4637 'implies --send-mail')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004638 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4639 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004640 help='Send the patchset to do a CQ dry run right after '
4641 'upload.')
Andrii Shyshkalov71f0da32019-07-15 22:45:18 +00004642 parser.add_option('--preserve-tryjobs', action='store_true',
4643 help='instruct the CQ to let tryjobs running even after '
4644 'new patchsets are uploaded instead of canceling '
4645 'prior patchset\' tryjobs')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004646 parser.add_option('--dependencies', action='store_true',
4647 help='Uploads CLs of all the local branches that depend on '
4648 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004649 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4650 help='Sends your change to the CQ after an approval. Only '
4651 'works on repos that have the Auto-Submit label '
4652 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004653 parser.add_option('--parallel', action='store_true',
4654 help='Run all tests specified by input_api.RunTests in all '
4655 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004656
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004657 parser.add_option('--no-autocc', action='store_true',
4658 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004659 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004660 help='Set the review private. This implies --no-autocc.')
4661
rmistry@google.com2dd99862015-06-22 12:22:18 +00004662 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004663 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004664 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004665 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004666 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004667 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004668
sbc@chromium.org71437c02015-04-09 19:29:40 +00004669 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004670 return 1
4671
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004672 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004673 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004674 options.cc = cleanup_list(options.cc)
4675
tandriib80458a2016-06-23 12:20:07 -07004676 if options.message_file:
4677 if options.message:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004678 parser.error('Only one of --message and --message-file allowed.')
tandriib80458a2016-06-23 12:20:07 -07004679 options.message = gclient_utils.FileRead(options.message_file)
4680 options.message_file = None
4681
tandrii4d0545a2016-07-06 03:56:49 -07004682 if options.cq_dry_run and options.use_commit_queue:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004683 parser.error('Only one of --use-commit-queue and --cq-dry-run allowed.')
tandrii4d0545a2016-07-06 03:56:49 -07004684
Aaron Gableedbc4132017-09-11 13:22:28 -07004685 if options.use_commit_queue:
4686 options.send_mail = True
4687
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004688 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4689 settings.GetIsGerrit()
4690
Edward Lemurf38bc172019-09-03 21:02:13 +00004691 cl = Changelist(auth_config=auth_config)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004692
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004693 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004694
4695
Francois Dorayd42c6812017-05-30 15:10:20 -04004696@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004697@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004698def CMDsplit(parser, args):
4699 """Splits a branch into smaller branches and uploads CLs.
4700
4701 Creates a branch and uploads a CL for each group of files modified in the
4702 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004703 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004704 the shared OWNERS file.
4705 """
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004706 parser.add_option('-d', '--description', dest='description_file',
4707 help='A text file containing a CL description in which '
4708 '$directory will be replaced by each CL\'s directory.')
4709 parser.add_option('-c', '--comment', dest='comment_file',
4710 help='A text file containing a CL comment.')
4711 parser.add_option('-n', '--dry-run', dest='dry_run', action='store_true',
Chris Watkinsba28e462017-12-13 11:22:17 +11004712 default=False,
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004713 help='List the files and reviewers for each CL that would '
4714 'be created, but don\'t create branches or CLs.')
4715 parser.add_option('--cq-dry-run', action='store_true',
4716 help='If set, will do a cq dry run for each uploaded CL. '
4717 'Please be careful when doing this; more than ~10 CLs '
4718 'has the potential to overload our build '
4719 'infrastructure. Try to upload these not during high '
4720 'load times (usually 11-3 Mountain View time). Email '
4721 'infra-dev@chromium.org with any questions.')
Takuto Ikuta51eca592019-02-14 19:40:52 +00004722 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4723 default=True,
4724 help='Sends your change to the CQ after an approval. Only '
4725 'works on repos that have the Auto-Submit label '
4726 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004727 options, _ = parser.parse_args(args)
4728
4729 if not options.description_file:
4730 parser.error('No --description flag specified.')
4731
4732 def WrappedCMDupload(args):
4733 return CMDupload(OptionParser(), args)
4734
4735 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004736 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004737 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004738
4739
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004740@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004741@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004742def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004743 """DEPRECATED: Used to commit the current changelist via git-svn."""
4744 message = ('git-cl no longer supports committing to SVN repositories via '
4745 'git-svn. You probably want to use `git cl land` instead.')
4746 print(message)
4747 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004748
4749
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004750# Two special branches used by git cl land.
4751MERGE_BRANCH = 'git-cl-commit'
4752CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4753
4754
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004755@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004756@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004757def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004758 """Commits the current changelist via git.
4759
4760 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4761 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004762 """
4763 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4764 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004765 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004766 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004767 parser.add_option('--parallel', action='store_true',
4768 help='Run all tests specified by input_api.RunTests in all '
4769 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004770 auth.add_auth_options(parser)
4771 (options, args) = parser.parse_args(args)
4772 auth_config = auth.extract_auth_config_from_options(options)
4773
4774 cl = Changelist(auth_config=auth_config)
4775
Robert Iannucci2e73d432018-03-14 01:10:47 -07004776 if not cl.GetIssue():
4777 DieWithError('You must upload the change first to Gerrit.\n'
4778 ' If you would rather have `git cl land` upload '
4779 'automatically for you, see http://crbug.com/642759')
4780 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004781 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004782
4783
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004784@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004785@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004786def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004787 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004788 parser.add_option('-b', dest='newbranch',
4789 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004790 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004791 help='overwrite state on the current or chosen branch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004792 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
Edward Lemurf38bc172019-09-03 21:02:13 +00004793 help='don\'t commit after patch applies.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004794
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004795 group = optparse.OptionGroup(
4796 parser,
4797 'Options for continuing work on the current issue uploaded from a '
4798 'different clone (e.g. different machine). Must be used independently '
4799 'from the other options. No issue number should be specified, and the '
4800 'branch must have an issue number associated with it')
4801 group.add_option('--reapply', action='store_true', dest='reapply',
4802 help='Reset the branch and reapply the issue.\n'
4803 'CAUTION: This will undo any local changes in this '
4804 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004805
4806 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004807 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004808 parser.add_option_group(group)
4809
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004810 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004811 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004812 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004813 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004814 auth_config = auth.extract_auth_config_from_options(options)
4815
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004816 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004817 if options.newbranch:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004818 parser.error('--reapply works on the current branch only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004819 if len(args) > 0:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004820 parser.error('--reapply implies no additional arguments.')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004821
Edward Lemurf38bc172019-09-03 21:02:13 +00004822 cl = Changelist(auth_config=auth_config)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004823 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004824 parser.error('Current branch must have an associated issue.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004825
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004826 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004827 if upstream is None:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004828 parser.error('No upstream branch specified. Cannot reset branch.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004829
4830 RunGit(['reset', '--hard', upstream])
4831 if options.pull:
4832 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004833
Edward Lemurf38bc172019-09-03 21:02:13 +00004834 return cl.CMDPatchIssue(cl.GetIssue(), options.nocommit)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004835
4836 if len(args) != 1 or not args[0]:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004837 parser.error('Must specify issue number or URL.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004838
Edward Lemurf38bc172019-09-03 21:02:13 +00004839 target_issue_arg = ParseIssueNumberArgument(args[0])
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004840 if not target_issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004841 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004842
4843 cl_kwargs = {
4844 'auth_config': auth_config,
4845 'codereview_host': target_issue_arg.hostname,
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004846 }
4847 detected_codereview_from_url = False
Edward Lemurf38bc172019-09-03 21:02:13 +00004848 if not args[0].isdigit() and not options.forced_codereview:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004849 detected_codereview_from_url = True
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004850 cl_kwargs['issue'] = target_issue_arg.issue
4851
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004852 # We don't want uncommitted changes mixed up with the patch.
4853 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004854 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004855
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004856 if options.newbranch:
4857 if options.force:
4858 RunGit(['branch', '-D', options.newbranch],
4859 stderr=subprocess2.PIPE, error_ok=True)
4860 RunGit(['new-branch', options.newbranch])
4861
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004862 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004863
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004864 if detected_codereview_from_url:
Edward Lemurf38bc172019-09-03 21:02:13 +00004865 print('canonical issue/change URL: %s\n' % cl.GetIssueURL())
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004866
Edward Lemurf38bc172019-09-03 21:02:13 +00004867 return cl.CMDPatchWithParsedIssue(
4868 target_issue_arg, options.nocommit, options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004869
4870
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004871def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004872 """Fetches the tree status and returns either 'open', 'closed',
4873 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004874 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004875 if url:
4876 status = urllib2.urlopen(url).read().lower()
4877 if status.find('closed') != -1 or status == '0':
4878 return 'closed'
4879 elif status.find('open') != -1 or status == '1':
4880 return 'open'
4881 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004882 return 'unset'
4883
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004884
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004885def GetTreeStatusReason():
4886 """Fetches the tree status from a json url and returns the message
4887 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004888 url = settings.GetTreeStatusUrl()
4889 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004890 connection = urllib2.urlopen(json_url)
4891 status = json.loads(connection.read())
4892 connection.close()
4893 return status['message']
4894
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004895
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004896@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004897def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004898 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004899 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004900 status = GetTreeStatus()
4901 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004902 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004903 return 2
4904
vapiera7fbd5a2016-06-16 09:17:49 -07004905 print('The tree is %s' % status)
4906 print()
4907 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004908 if status != 'open':
4909 return 1
4910 return 0
4911
4912
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004913@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00004914def CMDtry(parser, args):
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004915 """Triggers tryjobs using either Buildbucket or CQ dry run."""
4916 group = optparse.OptionGroup(parser, 'Tryjob options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004917 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004918 '-b', '--bot', action='append',
4919 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4920 'times to specify multiple builders. ex: '
4921 '"-b win_rel -b win_layout". See '
4922 'the try server waterfall for the builders name and the tests '
4923 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004924 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004925 '-B', '--bucket', default='',
4926 help=('Buildbucket bucket to send the try requests.'))
4927 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004928 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004929 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004930 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004931 '-r', '--revision',
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004932 help='Revision to use for the tryjob; default: the revision will '
tandriif7b29d42016-10-07 08:45:41 -07004933 'be determined by the try recipe that builder runs, which usually '
4934 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004935 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004936 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004937 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004938 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004939 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08004940 '--category', default='git_cl_try', help='Specify custom build category.')
4941 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004942 '--project',
4943 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004944 'in recipe to determine to which repository or directory to '
4945 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004946 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004947 '-p', '--property', dest='properties', action='append', default=[],
4948 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004949 'key2=value2 etc. The value will be treated as '
4950 'json if decodable, or as string otherwise. '
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004951 'NOTE: using this may make your tryjob not usable for CQ, '
4952 'which will then schedule another tryjob with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004953 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004954 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4955 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004956 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004957 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09004958 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004959 options, args = parser.parse_args(args)
Edward Lemurf38bc172019-09-03 21:02:13 +00004960 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004961 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004962
Nodir Turakulovf6929a12017-10-09 12:34:44 -07004963 if options.master and options.master.startswith('luci.'):
4964 parser.error(
4965 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00004966 # Make sure that all properties are prop=value pairs.
4967 bad_params = [x for x in options.properties if '=' not in x]
4968 if bad_params:
4969 parser.error('Got properties with missing "=": %s' % bad_params)
4970
maruel@chromium.org15192402012-09-06 12:38:29 +00004971 if args:
4972 parser.error('Unknown arguments: %s' % args)
4973
Edward Lemurf38bc172019-09-03 21:02:13 +00004974 cl = Changelist(auth_config=auth_config, issue=options.issue)
maruel@chromium.org15192402012-09-06 12:38:29 +00004975 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00004976 parser.error('Need to upload first.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004977
Edward Lemurf38bc172019-09-03 21:02:13 +00004978 # HACK: warm up Gerrit change detail cache to save on RPCs.
4979 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01004980
tandriie113dfd2016-10-11 10:20:12 -07004981 error_message = cl.CannotTriggerTryJobReason()
4982 if error_message:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00004983 parser.error('Can\'t trigger tryjobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004984
borenet6c0efe62016-10-19 08:13:29 -07004985 if options.bucket and options.master:
4986 parser.error('Only one of --bucket and --master may be used.')
4987
qyearsley1fdfcb62016-10-24 13:22:03 -07004988 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004989
qyearsleydd49f942016-10-28 11:57:22 -07004990 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4991 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004992 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004993 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07004994 print('git cl try with no bots now defaults to CQ dry run.')
4995 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
4996 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00004997
borenet6c0efe62016-10-19 08:13:29 -07004998 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004999 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005000 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005001 'of bot requires an initial job from a parent (usually a builder). '
5002 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005003 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005004 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005005
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005006 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005007 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005008 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005009 except BuildbucketResponseException as ex:
5010 print('ERROR: %s' % ex)
5011 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005012 return 0
5013
5014
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005015@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005016def CMDtry_results(parser, args):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005017 """Prints info about results for tryjobs associated with the current CL."""
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005018 group = optparse.OptionGroup(parser, 'Tryjob results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005019 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005020 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005021 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005022 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005023 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005024 '--color', action='store_true', default=setup_color.IS_TTY,
5025 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005026 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005027 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5028 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005029 group.add_option(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005030 '--json', help=('Path of JSON output file to write tryjob results to,'
Stefan Zager1306bd02017-06-22 19:26:46 -07005031 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005032 parser.add_option_group(group)
5033 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005034 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005035 options, args = parser.parse_args(args)
Edward Lemurf38bc172019-09-03 21:02:13 +00005036 _process_codereview_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005037 if args:
5038 parser.error('Unrecognized args: %s' % ' '.join(args))
5039
5040 auth_config = auth.extract_auth_config_from_options(options)
Edward Lemurf38bc172019-09-03 21:02:13 +00005041 cl = Changelist(issue=options.issue, auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005042 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005043 parser.error('Need to upload first.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005044
tandrii221ab252016-10-06 08:12:04 -07005045 patchset = options.patchset
5046 if not patchset:
5047 patchset = cl.GetMostRecentPatchset()
5048 if not patchset:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005049 parser.error('Code review host doesn\'t know about issue %s. '
tandrii221ab252016-10-06 08:12:04 -07005050 'No access to issue or wrong issue number?\n'
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005051 'Either upload first, or pass --patchset explicitly.' %
tandrii221ab252016-10-06 08:12:04 -07005052 cl.GetIssue())
5053
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005054 try:
tandrii221ab252016-10-06 08:12:04 -07005055 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005056 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005057 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005058 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005059 if options.json:
5060 write_try_results_json(options.json, jobs)
5061 else:
5062 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005063 return 0
5064
5065
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005066@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005067@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005068def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005069 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005070 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005071 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005072 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005073
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005074 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005075 if args:
5076 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005077 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005078 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005079 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005080 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005081
5082 # Clear configured merge-base, if there is one.
5083 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005084 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005085 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005086 return 0
5087
5088
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005089@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005090def CMDweb(parser, args):
5091 """Opens the current CL in the web browser."""
5092 _, args = parser.parse_args(args)
5093 if args:
5094 parser.error('Unrecognized args: %s' % ' '.join(args))
5095
5096 issue_url = Changelist().GetIssueURL()
5097 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005098 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005099 return 1
5100
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005101 # Redirect I/O before invoking browser to hide its output. For example, this
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005102 # allows us to hide the "Created new window in existing browser session."
5103 # message from Chrome. Based on https://stackoverflow.com/a/2323563.
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005104 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005105 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005106 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005107 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005108 os.open(os.devnull, os.O_RDWR)
5109 try:
5110 webbrowser.open(issue_url)
5111 finally:
5112 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005113 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005114 return 0
5115
5116
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005117@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005118def CMDset_commit(parser, args):
Andrii Shyshkalov0e889b72019-07-15 22:28:48 +00005119 """Sets the commit bit to trigger the CQ."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005120 parser.add_option('-d', '--dry-run', action='store_true',
5121 help='trigger in dry run mode')
5122 parser.add_option('-c', '--clear', action='store_true',
5123 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005124 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005125 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005126 options, args = parser.parse_args(args)
Edward Lemurf38bc172019-09-03 21:02:13 +00005127 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005128 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005129 if args:
5130 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005131 if options.dry_run and options.clear:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005132 parser.error('Only one of --dry-run and --clear are allowed.')
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005133
Edward Lemurf38bc172019-09-03 21:02:13 +00005134 cl = Changelist(auth_config=auth_config, issue=options.issue)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005135 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005136 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005137 elif options.dry_run:
5138 state = _CQState.DRY_RUN
5139 else:
5140 state = _CQState.COMMIT
5141 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005142 parser.error('Must upload the issue first.')
tandrii9de9ec62016-07-13 03:01:59 -07005143 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005144 return 0
5145
5146
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005147@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005148def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005149 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005150 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005151 auth.add_auth_options(parser)
5152 options, args = parser.parse_args(args)
Edward Lemurf38bc172019-09-03 21:02:13 +00005153 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005154 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005155 if args:
5156 parser.error('Unrecognized args: %s' % ' '.join(args))
Edward Lemurf38bc172019-09-03 21:02:13 +00005157 cl = Changelist(auth_config=auth_config, issue=options.issue)
groby@chromium.org411034a2013-02-26 15:12:01 +00005158 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005159 if not cl.GetIssue():
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005160 DieWithError('ERROR: No issue to close.')
groby@chromium.org411034a2013-02-26 15:12:01 +00005161 cl.CloseIssue()
5162 return 0
5163
5164
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005165@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005166def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005167 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005168 parser.add_option(
5169 '--stat',
5170 action='store_true',
5171 dest='stat',
5172 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005173 auth.add_auth_options(parser)
5174 options, args = parser.parse_args(args)
5175 auth_config = auth.extract_auth_config_from_options(options)
5176 if args:
5177 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005178
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005179 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005180 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005181 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005182 if not issue:
5183 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005184
Aaron Gablea718c3e2017-08-28 17:47:28 -07005185 base = cl._GitGetBranchConfigValue('last-upload-hash')
5186 if not base:
5187 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5188 if not base:
5189 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5190 revision_info = detail['revisions'][detail['current_revision']]
5191 fetch_info = revision_info['fetch']['http']
5192 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5193 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005194
Aaron Gablea718c3e2017-08-28 17:47:28 -07005195 cmd = ['git', 'diff']
5196 if options.stat:
5197 cmd.append('--stat')
5198 cmd.append(base)
5199 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005200
5201 return 0
5202
5203
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005204@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005205def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005206 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005207 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005208 '--ignore-current',
5209 action='store_true',
5210 help='Ignore the CL\'s current reviewers and start from scratch.')
5211 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005212 '--ignore-self',
5213 action='store_true',
5214 help='Do not consider CL\'s author as an owners.')
5215 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005216 '--no-color',
5217 action='store_true',
5218 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005219 parser.add_option(
5220 '--batch',
5221 action='store_true',
5222 help='Do not run interactively, just suggest some')
Yang Guo6e269a02019-06-26 11:17:02 +00005223 # TODO: Consider moving this to another command, since other
5224 # git-cl owners commands deal with owners for a given CL.
5225 parser.add_option(
5226 '--show-all',
5227 action='store_true',
5228 help='Show all owners for a particular file')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005229 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005230 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005231 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005232
5233 author = RunGit(['config', 'user.email']).strip() or None
5234
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005235 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005236
Yang Guo6e269a02019-06-26 11:17:02 +00005237 if options.show_all:
5238 for arg in args:
5239 base_branch = cl.GetCommonAncestorWithUpstream()
5240 change = cl.GetChange(base_branch, None)
5241 database = owners.Database(change.RepositoryRoot(), file, os.path)
5242 database.load_data_needed_for([arg])
5243 print('Owners for %s:' % arg)
5244 for owner in sorted(database.all_possible_owners([arg], None)):
5245 print(' - %s' % owner)
5246 return 0
5247
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005248 if args:
5249 if len(args) > 1:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005250 parser.error('Unknown args.')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005251 base_branch = args[0]
5252 else:
5253 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005254 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005255
5256 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005257 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5258
5259 if options.batch:
5260 db = owners.Database(change.RepositoryRoot(), file, os.path)
5261 print('\n'.join(db.reviewers_for(affected_files, author)))
5262 return 0
5263
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005264 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005265 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005266 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005267 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005268 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005269 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005270 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005271 override_files=change.OriginalOwnersFiles(),
5272 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005273
5274
Aiden Bennerc08566e2018-10-03 17:52:42 +00005275def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005276 """Generates a diff command."""
5277 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005278 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5279
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005280 if allow_prefix:
5281 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5282 # case that diff.noprefix is set in the user's git config.
5283 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5284 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005285 diff_cmd += ['--no-prefix']
5286
5287 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005288
5289 if args:
5290 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005291 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005292 diff_cmd.append(arg)
5293 else:
5294 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005295
5296 return diff_cmd
5297
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005298
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005299def MatchingFileType(file_name, extensions):
Quinten Yearsleyd242ed72019-07-25 17:17:55 +00005300 """Returns True if the file name ends with one of the given extensions."""
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005301 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005302
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005303
enne@chromium.org555cfe42014-01-29 18:21:39 +00005304@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005305@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005306def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005307 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005308 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005309 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005310 parser.add_option('--full', action='store_true',
5311 help='Reformat the full content of all touched files')
5312 parser.add_option('--dry-run', action='store_true',
5313 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005314 parser.add_option(
5315 '--python',
5316 action='store_true',
5317 default=None,
5318 help='Enables python formatting on all python files.')
5319 parser.add_option(
5320 '--no-python',
5321 action='store_true',
5322 dest='python',
5323 help='Disables python formatting on all python files. '
5324 'Takes precedence over --python. '
5325 'If neither --python or --no-python are set, python '
5326 'files that have a .style.yapf file in an ancestor '
5327 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005328 parser.add_option('--js', action='store_true',
5329 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005330 parser.add_option('--diff', action='store_true',
5331 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005332 parser.add_option('--presubmit', action='store_true',
5333 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005334 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005335
Daniel Chengc55eecf2016-12-30 03:11:02 -08005336 # Normalize any remaining args against the current path, so paths relative to
5337 # the current directory are still resolved as expected.
5338 args = [os.path.join(os.getcwd(), arg) for arg in args]
5339
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005340 # git diff generates paths against the root of the repository. Change
5341 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005342 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005343 if rel_base_path:
5344 os.chdir(rel_base_path)
5345
digit@chromium.org29e47272013-05-17 17:01:46 +00005346 # Grab the merge-base commit, i.e. the upstream commit of the current
5347 # branch when it was created or the last time it was rebased. This is
5348 # to cover the case where the user may have called "git fetch origin",
5349 # moving the origin branch to a newer commit, but hasn't rebased yet.
5350 upstream_commit = None
5351 cl = Changelist()
5352 upstream_branch = cl.GetUpstreamBranch()
5353 if upstream_branch:
5354 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5355 upstream_commit = upstream_commit.strip()
5356
5357 if not upstream_commit:
5358 DieWithError('Could not find base commit for this branch. '
5359 'Are you in detached state?')
5360
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005361 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5362 diff_output = RunGit(changed_files_cmd)
5363 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005364 # Filter out files deleted by this CL
5365 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005366
Christopher Lamc5ba6922017-01-24 11:19:14 +11005367 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005368 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005369
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005370 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5371 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5372 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005373 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005374
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005375 top_dir = os.path.normpath(
5376 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5377
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005378 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5379 # formatted. This is used to block during the presubmit.
5380 return_value = 0
5381
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005382 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005383 # Locate the clang-format binary in the checkout
5384 try:
5385 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005386 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005387 DieWithError(e)
5388
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005389 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005390 cmd = [clang_format_tool]
5391 if not opts.dry_run and not opts.diff:
5392 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005393 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005394 if opts.diff:
5395 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005396 else:
5397 env = os.environ.copy()
5398 env['PATH'] = str(os.path.dirname(clang_format_tool))
5399 try:
5400 script = clang_format.FindClangFormatScriptInChromiumTree(
5401 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005402 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005403 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005404
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005405 cmd = [sys.executable, script, '-p0']
5406 if not opts.dry_run and not opts.diff:
5407 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005408
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005409 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5410 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005411
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005412 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5413 if opts.diff:
5414 sys.stdout.write(stdout)
5415 if opts.dry_run and len(stdout) > 0:
5416 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005417
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005418 # Similar code to above, but using yapf on .py files rather than clang-format
5419 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005420 py_explicitly_disabled = opts.python is not None and not opts.python
5421 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005422 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5423 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5424 if sys.platform.startswith('win'):
5425 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005426
Aiden Bennerc08566e2018-10-03 17:52:42 +00005427 # If we couldn't find a yapf file we'll default to the chromium style
5428 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005429 chromium_default_yapf_style = os.path.join(depot_tools_path,
5430 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005431 # Used for caching.
5432 yapf_configs = {}
5433 for f in python_diff_files:
5434 # Find the yapf style config for the current file, defaults to depot
5435 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005436 _FindYapfConfigFile(f, yapf_configs, top_dir)
5437
5438 # Turn on python formatting by default if a yapf config is specified.
5439 # This breaks in the case of this repo though since the specified
5440 # style file is also the global default.
5441 if opts.python is None:
5442 filtered_py_files = []
5443 for f in python_diff_files:
5444 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5445 filtered_py_files.append(f)
5446 else:
5447 filtered_py_files = python_diff_files
5448
5449 # Note: yapf still seems to fix indentation of the entire file
5450 # even if line ranges are specified.
5451 # See https://github.com/google/yapf/issues/499
5452 if not opts.full and filtered_py_files:
5453 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5454
5455 for f in filtered_py_files:
5456 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5457 if yapf_config is None:
5458 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005459
5460 cmd = [yapf_tool, '--style', yapf_config, f]
5461
5462 has_formattable_lines = False
5463 if not opts.full:
5464 # Only run yapf over changed line ranges.
5465 for diff_start, diff_len in py_line_diffs[f]:
5466 diff_end = diff_start + diff_len - 1
5467 # Yapf errors out if diff_end < diff_start but this
5468 # is a valid line range diff for a removal.
5469 if diff_end >= diff_start:
5470 has_formattable_lines = True
5471 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5472 # If all line diffs were removals we have nothing to format.
5473 if not has_formattable_lines:
5474 continue
5475
5476 if opts.diff or opts.dry_run:
5477 cmd += ['--diff']
5478 # Will return non-zero exit code if non-empty diff.
5479 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5480 if opts.diff:
5481 sys.stdout.write(stdout)
5482 elif len(stdout) > 0:
5483 return_value = 2
5484 else:
5485 cmd += ['-i']
5486 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005487
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005488 # Dart's formatter does not have the nice property of only operating on
5489 # modified chunks, so hard code full.
5490 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005491 try:
5492 command = [dart_format.FindDartFmtToolInChromiumTree()]
5493 if not opts.dry_run and not opts.diff:
5494 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005495 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005496
ppi@chromium.org6593d932016-03-03 15:41:15 +00005497 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005498 if opts.dry_run and stdout:
5499 return_value = 2
5500 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005501 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5502 'found in this checkout. Files in other languages are still '
5503 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005504
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005505 # Format GN build files. Always run on full build files for canonical form.
5506 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005507 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005508 if opts.dry_run or opts.diff:
5509 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005510 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005511 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5512 shell=sys.platform == 'win32',
5513 cwd=top_dir)
5514 if opts.dry_run and gn_ret == 2:
5515 return_value = 2 # Not formatted.
5516 elif opts.diff and gn_ret == 2:
5517 # TODO this should compute and print the actual diff.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005518 print('This change has GN build file diff for ' + gn_diff_file)
brettw4b8ed592016-08-05 16:19:12 -07005519 elif gn_ret != 0:
5520 # For non-dry run cases (and non-2 return values for dry-run), a
5521 # nonzero error code indicates a failure, probably because the file
5522 # doesn't parse.
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005523 DieWithError('gn format failed on ' + gn_diff_file +
5524 '\nTry running `gn format` on this file manually.')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005525
Ilya Shermane081cbe2017-08-15 17:51:04 -07005526 # Skip the metrics formatting from the global presubmit hook. These files have
5527 # a separate presubmit hook that issues an error if the files need formatting,
5528 # whereas the top-level presubmit script merely issues a warning. Formatting
5529 # these files is somewhat slow, so it's important not to duplicate the work.
5530 if not opts.presubmit:
5531 for xml_dir in GetDirtyMetricsDirs(diff_files):
5532 tool_dir = os.path.join(top_dir, xml_dir)
5533 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5534 if opts.dry_run or opts.diff:
5535 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005536 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005537 if opts.diff:
5538 sys.stdout.write(stdout)
5539 if opts.dry_run and stdout:
5540 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005541
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005542 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005543
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005544
Steven Holte2e664bf2017-04-21 13:10:47 -07005545def GetDirtyMetricsDirs(diff_files):
5546 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5547 metrics_xml_dirs = [
5548 os.path.join('tools', 'metrics', 'actions'),
5549 os.path.join('tools', 'metrics', 'histograms'),
5550 os.path.join('tools', 'metrics', 'rappor'),
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005551 os.path.join('tools', 'metrics', 'ukm'),
5552 ]
Steven Holte2e664bf2017-04-21 13:10:47 -07005553 for xml_dir in metrics_xml_dirs:
5554 if any(file.startswith(xml_dir) for file in xml_diff_files):
5555 yield xml_dir
5556
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005557
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005558@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005559@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005560def CMDcheckout(parser, args):
Edward Lemurf38bc172019-09-03 21:02:13 +00005561 """Checks out a branch associated with a given Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005562 _, args = parser.parse_args(args)
5563
5564 if len(args) != 1:
5565 parser.print_help()
5566 return 1
5567
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005568 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005569 if not issue_arg.valid:
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005570 parser.error('Invalid issue ID or URL.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005571
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005572 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005573
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005574 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005575 output = RunGit(['config', '--local', '--get-regexp',
5576 r'branch\..*\.%s' % issueprefix],
5577 error_ok=True)
5578 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005579 if issue == target_issue:
5580 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005581
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005582 branches = []
5583 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005584 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005585 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005586 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005587 return 1
5588 if len(branches) == 1:
5589 RunGit(['checkout', branches[0]])
5590 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005591 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005592 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005593 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005594 which = raw_input('Choose by index: ')
5595 try:
5596 RunGit(['checkout', branches[int(which)]])
5597 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005598 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005599 return 1
5600
5601 return 0
5602
5603
maruel@chromium.org29404b52014-09-08 22:58:00 +00005604def CMDlol(parser, args):
5605 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005606 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005607 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5608 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5609 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005610 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005611 return 0
5612
5613
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005614class OptionParser(optparse.OptionParser):
5615 """Creates the option parse and add --verbose support."""
5616 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005617 optparse.OptionParser.__init__(
5618 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005619 self.add_option(
5620 '-v', '--verbose', action='count', default=0,
5621 help='Use 2 times for more debugging info')
5622
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005623 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005624 try:
5625 return self._parse_args(args)
5626 finally:
5627 # Regardless of success or failure of args parsing, we want to report
5628 # metrics, but only after logging has been initialized (if parsing
5629 # succeeded).
5630 global settings
5631 settings = Settings()
5632
5633 if not metrics.DISABLE_METRICS_COLLECTION:
5634 # GetViewVCUrl ultimately calls logging method.
5635 project_url = settings.GetViewVCUrl().strip('/+')
5636 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5637 metrics.collector.add('project_urls', [project_url])
5638
5639 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005640 # Create an optparse.Values object that will store only the actual passed
5641 # options, without the defaults.
5642 actual_options = optparse.Values()
5643 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5644 # Create an optparse.Values object with the default options.
5645 options = optparse.Values(self.get_default_values().__dict__)
5646 # Update it with the options passed by the user.
5647 options._update_careful(actual_options.__dict__)
5648 # Store the options passed by the user in an _actual_options attribute.
5649 # We store only the keys, and not the values, since the values can contain
5650 # arbitrary information, which might be PII.
5651 metrics.collector.add('arguments', actual_options.__dict__.keys())
5652
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005653 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005654 logging.basicConfig(
5655 level=levels[min(options.verbose, len(levels) - 1)],
5656 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5657 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005658
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005659 return options, args
5660
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005661
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005662def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005663 if sys.hexversion < 0x02060000:
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005664 print('\nYour Python version %s is unsupported, please upgrade.\n' %
vapiera7fbd5a2016-06-16 09:17:49 -07005665 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005666 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005667
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005668 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005669 dispatcher = subcommand.CommandDispatcher(__name__)
5670 try:
5671 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005672 except auth.AuthenticationError as e:
5673 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005674 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005675 if e.code != 500:
5676 raise
5677 DieWithError(
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005678 ('App Engine is misbehaving and returned HTTP %d, again. Keep faith '
Quinten Yearsleyc4202212019-07-22 23:34:40 +00005679 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005680 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005681
5682
5683if __name__ == '__main__':
Quinten Yearsleyd9cbe7a2019-09-03 16:49:11 +00005684 # These affect sys.stdout, so do it outside of main() to simplify mocks in
5685 # the unit tests.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005686 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005687 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005688 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005689 sys.exit(main(sys.argv[1:]))