blob: 66b205337f1b5d0bb87ead77fd526ad56fff5665 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000032import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000033import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000035import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000036import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000037import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000038import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000039
40try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080041 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042except ImportError:
43 pass
44
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000046from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000059import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000060import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000061import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import presubmit_support
63import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
Edward Lemur0f58ae42019-04-30 17:24:12 +000071# Traces for git push will be stored in a traces directory inside the
72# depot_tools checkout.
73DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
74TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
75
76# When collecting traces, Git hashes will be reduced to 6 characters to reduce
77# the size after compression.
78GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
79# Used to redact the cookies from the gitcookies file.
80GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
81
Edward Lemurdc8e23d2019-05-07 00:45:48 +000082# The maximum number of traces we will keep. Multiplied by 3 since we store
83# 3 files per trace.
84MAX_TRACES = 3 * 10
85# Message to display to the user after git-cl has run, to inform them of the
86# traces we just collected.
Edward Lemur0f58ae42019-04-30 17:24:12 +000087TRACES_MESSAGE = (
Edward Lemurdc8e23d2019-05-07 00:45:48 +000088'\n'
89'When filing a bug for this push, be sure to include the traces found at:\n'
90' %(trace_name)s-traces.zip\n'
91'Consider including the git config and gitcookies, which we have packed for \n'
92'you at:\n'
93' %(trace_name)s-git-info.zip\n')
94# Format of the message to be stored as part of the traces to give developers a
95# better context when they go through traces.
96TRACES_README_FORMAT = (
97'Date: %(now)s\n'
98'\n'
99'Change: https://%(gerrit_host)s/q/%(change_id)s\n'
100'Title: %(title)s\n'
101'\n'
102'%(description)s\n'
103'\n'
104'Execution time: %(execution_time)s\n'
105'Exit code: %(exit_code)s\n') + TRACES_MESSAGE
Edward Lemur0f58ae42019-04-30 17:24:12 +0000106
tandrii9d2c7a32016-06-22 03:42:45 -0700107COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -0800108POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +0000110REFS_THAT_ALIAS_TO_OTHER_REFS = {
111 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
112 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
113}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
thestig@chromium.org44202a22014-03-11 19:22:18 +0000115# Valid extensions for files we want to lint.
116DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
117DEFAULT_LINT_IGNORE_REGEX = r"$^"
118
Aiden Bennerc08566e2018-10-03 17:52:42 +0000119# File name for yapf style config files.
120YAPF_CONFIG_FILENAME = '.style.yapf'
121
borenet6c0efe62016-10-19 08:13:29 -0700122# Buildbucket master name prefix.
123MASTER_PREFIX = 'master.'
124
maruel@chromium.org2e23ce32013-05-07 12:42:28 +0000125# Shortcut since it quickly becomes redundant.
126Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +0000127
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000128# Initialized in main()
129settings = None
130
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100131# Used by tests/git_cl_test.py to add extra logging.
132# Inside the weirdly failing test, add this:
133# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700134# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100135_IS_BEING_TESTED = False
136
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000137
Christopher Lamf732cd52017-01-24 12:40:11 +1100138def DieWithError(message, change_desc=None):
139 if change_desc:
140 SaveDescriptionBackup(change_desc)
141
vapiera7fbd5a2016-06-16 09:17:49 -0700142 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000143 sys.exit(1)
144
145
Christopher Lamf732cd52017-01-24 12:40:11 +1100146def SaveDescriptionBackup(change_desc):
147 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000148 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100149 backup_file = open(backup_path, 'w')
150 backup_file.write(change_desc.description)
151 backup_file.close()
152
153
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000154def GetNoGitPagerEnv():
155 env = os.environ.copy()
156 # 'cat' is a magical git string that disables pagers on all platforms.
157 env['GIT_PAGER'] = 'cat'
158 return env
159
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000160
bsep@chromium.org627d9002016-04-29 00:00:52 +0000161def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000162 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000163 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000164 except subprocess2.CalledProcessError as e:
165 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000166 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000167 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000168 'Command "%s" failed.\n%s' % (
169 ' '.join(args), error_message or e.stdout or ''))
170 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000171
172
173def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000174 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000175 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000176
177
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000178def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000179 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700180 if suppress_stderr:
181 stderr = subprocess2.VOID
182 else:
183 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000184 try:
tandrii5d48c322016-08-18 16:19:37 -0700185 (out, _), code = subprocess2.communicate(['git'] + args,
186 env=GetNoGitPagerEnv(),
187 stdout=subprocess2.PIPE,
188 stderr=stderr)
189 return code, out
190 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900191 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700192 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000193
194
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000195def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000196 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000197 return RunGitWithCode(args, suppress_stderr=True)[1]
198
199
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000200def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000201 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000202 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000203 return (version.startswith(prefix) and
204 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000205
206
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000207def BranchExists(branch):
208 """Return True if specified branch exists."""
209 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
210 suppress_stderr=True)
211 return not code
212
213
tandrii2a16b952016-10-19 07:09:44 -0700214def time_sleep(seconds):
215 # Use this so that it can be mocked in tests without interfering with python
216 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700217 return time.sleep(seconds)
218
219
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000220def time_time():
221 # Use this so that it can be mocked in tests without interfering with python
222 # system machinery.
223 return time.time()
224
225
Edward Lemurdc8e23d2019-05-07 00:45:48 +0000226def datetime_now():
227 # Use this so that it can be mocked in tests without interfering with python
228 # system machinery.
229 return datetime.datetime.now()
230
231
maruel@chromium.org90541732011-04-01 17:54:18 +0000232def ask_for_data(prompt):
233 try:
234 return raw_input(prompt)
235 except KeyboardInterrupt:
236 # Hide the exception.
237 sys.exit(1)
238
239
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100240def confirm_or_exit(prefix='', action='confirm'):
241 """Asks user to press enter to continue or press Ctrl+C to abort."""
242 if not prefix or prefix.endswith('\n'):
243 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100244 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100245 mid = ' Press'
246 elif prefix.endswith(' '):
247 mid = 'press'
248 else:
249 mid = ' press'
250 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
251
252
253def ask_for_explicit_yes(prompt):
254 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
255 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
256 while True:
257 if 'yes'.startswith(result):
258 return True
259 if 'no'.startswith(result):
260 return False
261 result = ask_for_data('Please, type yes or no: ').lower()
262
263
tandrii5d48c322016-08-18 16:19:37 -0700264def _git_branch_config_key(branch, key):
265 """Helper method to return Git config key for a branch."""
266 assert branch, 'branch name is required to set git config for it'
267 return 'branch.%s.%s' % (branch, key)
268
269
270def _git_get_branch_config_value(key, default=None, value_type=str,
271 branch=False):
272 """Returns git config value of given or current branch if any.
273
274 Returns default in all other cases.
275 """
276 assert value_type in (int, str, bool)
277 if branch is False: # Distinguishing default arg value from None.
278 branch = GetCurrentBranch()
279
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000280 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700281 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000282
tandrii5d48c322016-08-18 16:19:37 -0700283 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700284 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700285 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700286 # git config also has --int, but apparently git config suffers from integer
287 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700288 args.append(_git_branch_config_key(branch, key))
289 code, out = RunGitWithCode(args)
290 if code == 0:
291 value = out.strip()
292 if value_type == int:
293 return int(value)
294 if value_type == bool:
295 return bool(value.lower() == 'true')
296 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000297 return default
298
299
tandrii5d48c322016-08-18 16:19:37 -0700300def _git_set_branch_config_value(key, value, branch=None, **kwargs):
301 """Sets the value or unsets if it's None of a git branch config.
302
303 Valid, though not necessarily existing, branch must be provided,
304 otherwise currently checked out branch is used.
305 """
306 if not branch:
307 branch = GetCurrentBranch()
308 assert branch, 'a branch name OR currently checked out branch is required'
309 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700310 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700311 if value is None:
312 args.append('--unset')
313 elif isinstance(value, bool):
314 args.append('--bool')
315 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700316 else:
tandrii33a46ff2016-08-23 05:53:40 -0700317 # git config also has --int, but apparently git config suffers from integer
318 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700319 value = str(value)
320 args.append(_git_branch_config_key(branch, key))
321 if value is not None:
322 args.append(value)
323 RunGit(args, **kwargs)
324
325
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100326def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700327 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100328
329 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
330 """
331 # Git also stores timezone offset, but it only affects visual display,
332 # actual point in time is defined by this timestamp only.
333 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
334
335
336def _git_amend_head(message, committer_timestamp):
337 """Amends commit with new message and desired committer_timestamp.
338
339 Sets committer timezone to UTC.
340 """
341 env = os.environ.copy()
342 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
343 return RunGit(['commit', '--amend', '-m', message], env=env)
344
345
machenbach@chromium.org45453142015-09-15 08:45:22 +0000346def _get_properties_from_options(options):
347 properties = dict(x.split('=', 1) for x in options.properties)
348 for key, val in properties.iteritems():
349 try:
350 properties[key] = json.loads(val)
351 except ValueError:
352 pass # If a value couldn't be evaluated, treat it as a string.
353 return properties
354
355
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000356def _prefix_master(master):
357 """Convert user-specified master name to full master name.
358
359 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
360 name, while the developers always use shortened master name
361 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
362 function does the conversion for buildbucket migration.
363 """
borenet6c0efe62016-10-19 08:13:29 -0700364 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000365 return master
borenet6c0efe62016-10-19 08:13:29 -0700366 return '%s%s' % (MASTER_PREFIX, master)
367
368
369def _unprefix_master(bucket):
370 """Convert bucket name to shortened master name.
371
372 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
373 name, while the developers always use shortened master name
374 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
375 function does the conversion for buildbucket migration.
376 """
377 if bucket.startswith(MASTER_PREFIX):
378 return bucket[len(MASTER_PREFIX):]
379 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000380
381
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000382def _buildbucket_retry(operation_name, http, *args, **kwargs):
383 """Retries requests to buildbucket service and returns parsed json content."""
384 try_count = 0
385 while True:
386 response, content = http.request(*args, **kwargs)
387 try:
388 content_json = json.loads(content)
389 except ValueError:
390 content_json = None
391
392 # Buildbucket could return an error even if status==200.
393 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000394 error = content_json.get('error')
395 if error.get('code') == 403:
396 raise BuildbucketResponseException(
397 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000398 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000399 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000400 raise BuildbucketResponseException(msg)
401
402 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700403 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000404 raise BuildbucketResponseException(
405 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700406 'Please file bugs at http://crbug.com, '
407 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000408 content)
409 return content_json
410 if response.status < 500 or try_count >= 2:
411 raise httplib2.HttpLib2Error(content)
412
413 # status >= 500 means transient failures.
414 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700415 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000416 try_count += 1
417 assert False, 'unreachable'
418
419
qyearsley1fdfcb62016-10-24 13:22:03 -0700420def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700421 """Returns a dict mapping bucket names to builders and tests,
422 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700423 """
qyearsleydd49f942016-10-28 11:57:22 -0700424 # If no bots are listed, we try to get a set of builders and tests based
425 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700426 if not options.bot:
427 change = changelist.GetChange(
428 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700429 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700430 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700431 change=change,
432 changed_files=change.LocalPaths(),
433 repository_root=settings.GetRoot(),
434 default_presubmit=None,
435 project=None,
436 verbose=options.verbose,
437 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700438 if masters is None:
439 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100440 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700441
qyearsley1fdfcb62016-10-24 13:22:03 -0700442 if options.bucket:
443 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700444 if options.master:
445 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700446
qyearsleydd49f942016-10-28 11:57:22 -0700447 # If bots are listed but no master or bucket, then we need to find out
448 # the corresponding master for each bot.
449 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
450 if error_message:
451 option_parser.error(
452 'Tryserver master cannot be found because: %s\n'
453 'Please manually specify the tryserver master, e.g. '
454 '"-m tryserver.chromium.linux".' % error_message)
455 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700456
457
qyearsley123a4682016-10-26 09:12:17 -0700458def _get_bucket_map_for_builders(builders):
459 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700460 map_url = 'https://builders-map.appspot.com/'
461 try:
qyearsley123a4682016-10-26 09:12:17 -0700462 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700463 except urllib2.URLError as e:
464 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
465 (map_url, e))
466 except ValueError as e:
467 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700468 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700469 return None, 'Failed to build master map.'
470
qyearsley123a4682016-10-26 09:12:17 -0700471 bucket_map = {}
472 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800473 bucket = builders_map.get(builder, {}).get('bucket')
474 if bucket:
475 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700476 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700477
478
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800479def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700480 """Sends a request to Buildbucket to trigger try jobs for a changelist.
481
482 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700483 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700484 changelist: Changelist that the try jobs are associated with.
485 buckets: A nested dict mapping bucket names to builders to tests.
486 options: Command-line options.
487 """
tandriide281ae2016-10-12 06:02:30 -0700488 assert changelist.GetIssue(), 'CL must be uploaded first'
489 codereview_url = changelist.GetCodereviewServer()
490 assert codereview_url, 'CL must be uploaded first'
491 patchset = patchset or changelist.GetMostRecentPatchset()
492 assert patchset, 'CL must be uploaded first'
493
494 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700495 # Cache the buildbucket credentials under the codereview host key, so that
496 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700497 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 http = authenticator.authorize(httplib2.Http())
499 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700500
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000501 buildbucket_put_url = (
502 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000503 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000504 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700505 hostname=codereview_host,
506 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000507 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700508
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700509 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800510 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700511 if options.clobber:
512 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700513 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700514 if extra_properties:
515 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000516
517 batch_req_body = {'builds': []}
518 print_text = []
519 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700520 for bucket, builders_and_tests in sorted(buckets.iteritems()):
521 print_text.append('Bucket: %s' % bucket)
522 master = None
523 if bucket.startswith(MASTER_PREFIX):
524 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000525 for builder, tests in sorted(builders_and_tests.iteritems()):
526 print_text.append(' %s: %s' % (builder, tests))
527 parameters = {
528 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000529 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100530 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000531 'revision': options.revision,
532 }],
tandrii8c5a3532016-11-04 07:52:02 -0700533 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000534 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000535 if 'presubmit' in builder.lower():
536 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000537 if tests:
538 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700539
540 tags = [
541 'builder:%s' % builder,
542 'buildset:%s' % buildset,
543 'user_agent:git_cl_try',
544 ]
545 if master:
546 parameters['properties']['master'] = master
547 tags.append('master:%s' % master)
548
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000549 batch_req_body['builds'].append(
550 {
551 'bucket': bucket,
552 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700554 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000555 }
556 )
557
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700559 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 http,
561 buildbucket_put_url,
562 'PUT',
563 body=json.dumps(batch_req_body),
564 headers={'Content-Type': 'application/json'}
565 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000566 print_text.append('To see results here, run: git cl try-results')
567 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700568 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000569
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000570
tandrii221ab252016-10-06 08:12:04 -0700571def fetch_try_jobs(auth_config, changelist, buildbucket_host,
572 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700573 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000574
qyearsley53f48a12016-09-01 10:45:13 -0700575 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000576 """
tandrii221ab252016-10-06 08:12:04 -0700577 assert buildbucket_host
578 assert changelist.GetIssue(), 'CL must be uploaded first'
579 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
580 patchset = patchset or changelist.GetMostRecentPatchset()
581 assert patchset, 'CL must be uploaded first'
582
583 codereview_url = changelist.GetCodereviewServer()
584 codereview_host = urlparse.urlparse(codereview_url).hostname
585 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000586 if authenticator.has_cached_credentials():
587 http = authenticator.authorize(httplib2.Http())
588 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700589 print('Warning: Some results might be missing because %s' %
590 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700591 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000592 http = httplib2.Http()
593
594 http.force_exception_to_status_code = True
595
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000596 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700597 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700599 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000600 params = {'tag': 'buildset:%s' % buildset}
601
602 builds = {}
603 while True:
604 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700605 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000606 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700607 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000608 for build in content.get('builds', []):
609 builds[build['id']] = build
610 if 'next_cursor' in content:
611 params['start_cursor'] = content['next_cursor']
612 else:
613 break
614 return builds
615
616
qyearsleyeab3c042016-08-24 09:18:28 -0700617def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000618 """Prints nicely result of fetch_try_jobs."""
619 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700620 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000621 return
622
623 # Make a copy, because we'll be modifying builds dictionary.
624 builds = builds.copy()
625 builder_names_cache = {}
626
627 def get_builder(b):
628 try:
629 return builder_names_cache[b['id']]
630 except KeyError:
631 try:
632 parameters = json.loads(b['parameters_json'])
633 name = parameters['builder_name']
634 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700635 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700636 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637 name = None
638 builder_names_cache[b['id']] = name
639 return name
640
641 def get_bucket(b):
642 bucket = b['bucket']
643 if bucket.startswith('master.'):
644 return bucket[len('master.'):]
645 return bucket
646
647 if options.print_master:
648 name_fmt = '%%-%ds %%-%ds' % (
649 max(len(str(get_bucket(b))) for b in builds.itervalues()),
650 max(len(str(get_builder(b))) for b in builds.itervalues()))
651 def get_name(b):
652 return name_fmt % (get_bucket(b), get_builder(b))
653 else:
654 name_fmt = '%%-%ds' % (
655 max(len(str(get_builder(b))) for b in builds.itervalues()))
656 def get_name(b):
657 return name_fmt % get_builder(b)
658
659 def sort_key(b):
660 return b['status'], b.get('result'), get_name(b), b.get('url')
661
662 def pop(title, f, color=None, **kwargs):
663 """Pop matching builds from `builds` dict and print them."""
664
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000665 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000666 colorize = str
667 else:
668 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
669
670 result = []
671 for b in builds.values():
672 if all(b.get(k) == v for k, v in kwargs.iteritems()):
673 builds.pop(b['id'])
674 result.append(b)
675 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700676 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000677 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700678 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000679
680 total = len(builds)
681 pop(status='COMPLETED', result='SUCCESS',
682 title='Successes:', color=Fore.GREEN,
683 f=lambda b: (get_name(b), b.get('url')))
684 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
685 title='Infra Failures:', color=Fore.MAGENTA,
686 f=lambda b: (get_name(b), b.get('url')))
687 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
688 title='Failures:', color=Fore.RED,
689 f=lambda b: (get_name(b), b.get('url')))
690 pop(status='COMPLETED', result='CANCELED',
691 title='Canceled:', color=Fore.MAGENTA,
692 f=lambda b: (get_name(b),))
693 pop(status='COMPLETED', result='FAILURE',
694 failure_reason='INVALID_BUILD_DEFINITION',
695 title='Wrong master/builder name:', color=Fore.MAGENTA,
696 f=lambda b: (get_name(b),))
697 pop(status='COMPLETED', result='FAILURE',
698 title='Other failures:',
699 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
700 pop(status='COMPLETED',
701 title='Other finished:',
702 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
703 pop(status='STARTED',
704 title='Started:', color=Fore.YELLOW,
705 f=lambda b: (get_name(b), b.get('url')))
706 pop(status='SCHEDULED',
707 title='Scheduled:',
708 f=lambda b: (get_name(b), 'id=%s' % b['id']))
709 # The last section is just in case buildbucket API changes OR there is a bug.
710 pop(title='Other:',
711 f=lambda b: (get_name(b), 'id=%s' % b['id']))
712 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700713 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000714
715
Aiden Bennerc08566e2018-10-03 17:52:42 +0000716def _ComputeDiffLineRanges(files, upstream_commit):
717 """Gets the changed line ranges for each file since upstream_commit.
718
719 Parses a git diff on provided files and returns a dict that maps a file name
720 to an ordered list of range tuples in the form (start_line, count).
721 Ranges are in the same format as a git diff.
722 """
723 # If files is empty then diff_output will be a full diff.
724 if len(files) == 0:
725 return {}
726
Aiden Benner6c18a1a2018-11-23 20:18:23 +0000727 # Take the git diff and find the line ranges where there are changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000728 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
729 diff_output = RunGit(diff_cmd)
730
731 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
732 # 2 capture groups
733 # 0 == fname of diff file
734 # 1 == 'diff_start,diff_count' or 'diff_start'
735 # will match each of
736 # diff --git a/foo.foo b/foo.py
737 # @@ -12,2 +14,3 @@
738 # @@ -12,2 +17 @@
739 # running re.findall on the above string with pattern will give
740 # [('foo.py', ''), ('', '14,3'), ('', '17')]
741
742 curr_file = None
743 line_diffs = {}
744 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
745 if match[0] != '':
746 # Will match the second filename in diff --git a/a.py b/b.py.
747 curr_file = match[0]
748 line_diffs[curr_file] = []
749 else:
750 # Matches +14,3
751 if ',' in match[1]:
752 diff_start, diff_count = match[1].split(',')
753 else:
754 # Single line changes are of the form +12 instead of +12,1.
755 diff_start = match[1]
756 diff_count = 1
757
758 diff_start = int(diff_start)
759 diff_count = int(diff_count)
760
761 # If diff_count == 0 this is a removal we can ignore.
762 line_diffs[curr_file].append((diff_start, diff_count))
763
764 return line_diffs
765
766
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000767def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000768 """Checks if a yapf file is in any parent directory of fpath until top_dir.
769
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000770 Recursively checks parent directories to find yapf file and if no yapf file
771 is found returns None. Uses yapf_config_cache as a cache for
772 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000773 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000774 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000775 # Return result if we've already computed it.
776 if fpath in yapf_config_cache:
777 return yapf_config_cache[fpath]
778
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000779 parent_dir = os.path.dirname(fpath)
780 if os.path.isfile(fpath):
781 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000782 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000783 # Otherwise fpath is a directory
784 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
785 if os.path.isfile(yapf_file):
786 ret = yapf_file
787 elif fpath == top_dir or parent_dir == fpath:
788 # If we're at the top level directory, or if we're at root
789 # there is no provided style.
790 ret = None
791 else:
792 # Otherwise recurse on the current directory.
793 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000794 yapf_config_cache[fpath] = ret
795 return ret
796
797
qyearsley53f48a12016-09-01 10:45:13 -0700798def write_try_results_json(output_file, builds):
799 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
800
801 The input |builds| dict is assumed to be generated by Buildbucket.
802 Buildbucket documentation: http://goo.gl/G0s101
803 """
804
805 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800806 """Extracts some of the information from one build dict."""
807 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700808 return {
809 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700810 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800811 'builder_name': parameters.get('builder_name'),
812 'created_ts': build.get('created_ts'),
813 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700814 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800815 'result': build.get('result'),
816 'status': build.get('status'),
817 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700818 'url': build.get('url'),
819 }
820
821 converted = []
822 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000823 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700824 write_json(output_file, converted)
825
826
Aaron Gable13101a62018-02-09 13:20:41 -0800827def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000828 """Prints statistics about the change to the user."""
829 # --no-ext-diff is broken in some versions of Git, so try to work around
830 # this by overriding the environment (but there is still a problem if the
831 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000832 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000833 if 'GIT_EXTERNAL_DIFF' in env:
834 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000835
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000836 try:
837 stdout = sys.stdout.fileno()
838 except AttributeError:
839 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000840 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800841 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000842 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000843
844
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000845class BuildbucketResponseException(Exception):
846 pass
847
848
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849class Settings(object):
850 def __init__(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000852 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000853 self.tree_status_url = None
854 self.viewvc_url = None
855 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000856 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000857 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000858 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000859 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860
861 def LazyUpdateIfNeeded(self):
862 """Updates the settings from a codereview.settings file, if available."""
863 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000864 # The only value that actually changes the behavior is
865 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000866 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000867 error_ok=True
868 ).strip().lower()
869
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000871 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 LoadCodereviewSettingsFromFile(cr_settings_file)
873 self.updated = True
874
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000875 @staticmethod
876 def GetRelativeRoot():
877 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000878
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000880 if self.root is None:
881 self.root = os.path.abspath(self.GetRelativeRoot())
882 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 def GetTreeStatusUrl(self, error_ok=False):
885 if not self.tree_status_url:
886 error_message = ('You must configure your tree status URL by running '
887 '"git cl config".')
Edward Lemur61ea3072018-12-01 00:34:36 +0000888 self.tree_status_url = self._GetConfig(
889 'rietveld.tree-status-url', error_ok=error_ok,
890 error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000891 return self.tree_status_url
892
893 def GetViewVCUrl(self):
894 if not self.viewvc_url:
Edward Lemur61ea3072018-12-01 00:34:36 +0000895 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 return self.viewvc_url
897
rmistry@google.com90752582014-01-14 21:04:50 +0000898 def GetBugPrefix(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000899 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
rmistry@google.com78948ed2015-07-08 23:09:57 +0000900
rmistry@google.com5626a922015-02-26 14:03:30 +0000901 def GetRunPostUploadHook(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000902 run_post_upload_hook = self._GetConfig(
903 'rietveld.run-post-upload-hook', error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +0000904 return run_post_upload_hook == "True"
905
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000906 def GetDefaultCCList(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000907 return self._GetConfig('rietveld.cc', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000908
ukai@chromium.orge8077812012-02-03 03:41:46 +0000909 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700910 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000911 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700912 self.is_gerrit = (
913 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000914 return self.is_gerrit
915
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000916 def GetSquashGerritUploads(self):
917 """Return true if uploads to Gerrit should be squashed by default."""
918 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700919 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
920 if self.squash_gerrit_uploads is None:
921 # Default is squash now (http://crbug.com/611892#c23).
922 self.squash_gerrit_uploads = not (
923 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
924 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000925 return self.squash_gerrit_uploads
926
tandriia60502f2016-06-20 02:01:53 -0700927 def GetSquashGerritUploadsOverride(self):
928 """Return True or False if codereview.settings should be overridden.
929
930 Returns None if no override has been defined.
931 """
932 # See also http://crbug.com/611892#c23
933 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
934 error_ok=True).strip()
935 if result == 'true':
936 return True
937 if result == 'false':
938 return False
939 return None
940
tandrii@chromium.org28253532016-04-14 13:46:56 +0000941 def GetGerritSkipEnsureAuthenticated(self):
942 """Return True if EnsureAuthenticated should not be done for Gerrit
943 uploads."""
944 if self.gerrit_skip_ensure_authenticated is None:
945 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000946 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000947 error_ok=True).strip() == 'true')
948 return self.gerrit_skip_ensure_authenticated
949
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000950 def GetGitEditor(self):
951 """Return the editor specified in the git config, or None if none is."""
952 if self.git_editor is None:
Raul Tambre5a525872019-02-12 19:08:08 +0000953 # Git requires single quotes for paths with spaces. We need to replace
954 # them with double quotes for Windows to treat such paths as a single
955 # path.
956 self.git_editor = self._GetConfig(
957 'core.editor', error_ok=True).replace('\'', '"')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000958 return self.git_editor or None
959
thestig@chromium.org44202a22014-03-11 19:22:18 +0000960 def GetLintRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000961 return (self._GetConfig('rietveld.cpplint-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000962 DEFAULT_LINT_REGEX)
963
964 def GetLintIgnoreRegex(self):
Edward Lemur61ea3072018-12-01 00:34:36 +0000965 return (self._GetConfig('rietveld.cpplint-ignore-regex', error_ok=True) or
thestig@chromium.org44202a22014-03-11 19:22:18 +0000966 DEFAULT_LINT_IGNORE_REGEX)
967
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968 def _GetConfig(self, param, **kwargs):
969 self.LazyUpdateIfNeeded()
970 return RunGit(['config', param], **kwargs).strip()
971
972
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100973@contextlib.contextmanager
974def _get_gerrit_project_config_file(remote_url):
975 """Context manager to fetch and store Gerrit's project.config from
976 refs/meta/config branch and store it in temp file.
977
978 Provides a temporary filename or None if there was error.
979 """
980 error, _ = RunGitWithCode([
981 'fetch', remote_url,
982 '+refs/meta/config:refs/git_cl/meta/config'])
983 if error:
984 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700985 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100986 (remote_url, error))
987 yield None
988 return
989
990 error, project_config_data = RunGitWithCode(
991 ['show', 'refs/git_cl/meta/config:project.config'])
992 if error:
993 print('WARNING: project.config file not found')
994 yield None
995 return
996
997 with gclient_utils.temporary_directory() as tempdir:
998 project_config_file = os.path.join(tempdir, 'project.config')
999 gclient_utils.FileWrite(project_config_file, project_config_data)
1000 yield project_config_file
1001
1002
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003def ShortBranchName(branch):
1004 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001005 return branch.replace('refs/heads/', '', 1)
1006
1007
1008def GetCurrentBranchRef():
1009 """Returns branch ref (e.g., refs/heads/master) or None."""
1010 return RunGit(['symbolic-ref', 'HEAD'],
1011 stderr=subprocess2.VOID, error_ok=True).strip() or None
1012
1013
1014def GetCurrentBranch():
1015 """Returns current branch or None.
1016
1017 For refs/heads/* branches, returns just last part. For others, full ref.
1018 """
1019 branchref = GetCurrentBranchRef()
1020 if branchref:
1021 return ShortBranchName(branchref)
1022 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023
1024
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001025class _CQState(object):
1026 """Enum for states of CL with respect to Commit Queue."""
1027 NONE = 'none'
1028 DRY_RUN = 'dry_run'
1029 COMMIT = 'commit'
1030
1031 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1032
1033
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001034class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001035 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001036 self.issue = issue
1037 self.patchset = patchset
1038 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001039 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001040 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001041
1042 @property
1043 def valid(self):
1044 return self.issue is not None
1045
1046
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001047def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001048 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1049 fail_result = _ParsedIssueNumberArgument()
1050
1051 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001052 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001053 if not arg.startswith('http'):
1054 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001055
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001056 url = gclient_utils.UpgradeToHttps(arg)
1057 try:
1058 parsed_url = urlparse.urlparse(url)
1059 except ValueError:
1060 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001061
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001062 if codereview is not None:
1063 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1064 return parsed or fail_result
1065
Andrii Shyshkalov0a264d82018-11-21 00:36:16 +00001066 return _GerritChangelistImpl.ParseIssueURL(parsed_url) or fail_result
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001067
1068
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001069def _create_description_from_log(args):
1070 """Pulls out the commit log to use as a base for the CL description."""
1071 log_args = []
1072 if len(args) == 1 and not args[0].endswith('.'):
1073 log_args = [args[0] + '..']
1074 elif len(args) == 1 and args[0].endswith('...'):
1075 log_args = [args[0][:-1]]
1076 elif len(args) == 2:
1077 log_args = [args[0] + '..' + args[1]]
1078 else:
1079 log_args = args[:] # Hope for the best!
1080 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1081
1082
Aaron Gablea45ee112016-11-22 15:14:38 -08001083class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001084 def __init__(self, issue, url):
1085 self.issue = issue
1086 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001087 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001088
1089 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001090 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001091 self.issue, self.url)
1092
1093
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001094_CommentSummary = collections.namedtuple(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00001095 '_CommentSummary', ['date', 'message', 'sender', 'autogenerated',
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001096 # TODO(tandrii): these two aren't known in Gerrit.
1097 'approval', 'disapproval'])
1098
1099
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001101 """Changelist works with one changelist in local branch.
1102
1103 Supports two codereview backends: Rietveld or Gerrit, selected at object
1104 creation.
1105
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001106 Notes:
1107 * Not safe for concurrent multi-{thread,process} use.
1108 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001109 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001110 """
1111
1112 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1113 """Create a new ChangeList instance.
1114
1115 If issue is given, the codereview must be given too.
1116
1117 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1118 Otherwise, it's decided based on current configuration of the local branch,
1119 with default being 'rietveld' for backwards compatibility.
1120 See _load_codereview_impl for more details.
1121
1122 **kwargs will be passed directly to codereview implementation.
1123 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001124 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001125 global settings
1126 if not settings:
1127 # Happens when git_cl.py is used as a utility library.
1128 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001129
1130 if issue:
1131 assert codereview, 'codereview must be known, if issue is known'
1132
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133 self.branchref = branchref
1134 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001135 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001136 self.branch = ShortBranchName(self.branchref)
1137 else:
1138 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001140 self.lookedup_issue = False
1141 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 self.has_description = False
1143 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001144 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001146 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001147 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001148 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001149 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001150
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001151 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001152 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001153 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001154 assert self._codereview_impl
1155 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001156
1157 def _load_codereview_impl(self, codereview=None, **kwargs):
1158 if codereview:
Joe Masond87b0962018-12-03 21:04:46 +00001159 assert codereview in _CODEREVIEW_IMPLEMENTATIONS, (
1160 'codereview {} not in {}'.format(codereview,
1161 _CODEREVIEW_IMPLEMENTATIONS))
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001162 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1163 self._codereview = codereview
1164 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001165 return
1166
1167 # Automatic selection based on issue number set for a current branch.
1168 # Rietveld takes precedence over Gerrit.
1169 assert not self.issue
1170 # Whether we find issue or not, we are doing the lookup.
1171 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001172 if self.GetBranch():
1173 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1174 issue = _git_get_branch_config_value(
1175 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1176 if issue:
1177 self._codereview = codereview
1178 self._codereview_impl = cls(self, **kwargs)
1179 self.issue = int(issue)
1180 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001181
Bryce Thomascfc97122018-12-13 20:21:47 +00001182 # No issue is set for this branch, so default to gerrit.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001183 return self._load_codereview_impl(
Bryce Thomascfc97122018-12-13 20:21:47 +00001184 codereview='gerrit',
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001185 **kwargs)
1186
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001187 def IsGerrit(self):
1188 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001189
1190 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001191 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001192
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001193 The return value is a string suitable for passing to git cl with the --cc
1194 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001195 """
1196 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001197 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001198 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001199 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1200 return self.cc
1201
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001202 def GetCCListWithoutDefault(self):
1203 """Return the users cc'd on this CL excluding default ones."""
1204 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001205 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001206 return self.cc
1207
Daniel Cheng7227d212017-11-17 08:12:37 -08001208 def ExtendCC(self, more_cc):
1209 """Extends the list of users to cc on this CL based on the changed files."""
1210 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211
1212 def GetBranch(self):
1213 """Returns the short branch name, e.g. 'master'."""
1214 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001215 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001216 if not branchref:
1217 return None
1218 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219 self.branch = ShortBranchName(self.branchref)
1220 return self.branch
1221
1222 def GetBranchRef(self):
1223 """Returns the full branch name, e.g. 'refs/heads/master'."""
1224 self.GetBranch() # Poke the lazy loader.
1225 return self.branchref
1226
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001227 def ClearBranch(self):
1228 """Clears cached branch data of this object."""
1229 self.branch = self.branchref = None
1230
tandrii5d48c322016-08-18 16:19:37 -07001231 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1232 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1233 kwargs['branch'] = self.GetBranch()
1234 return _git_get_branch_config_value(key, default, **kwargs)
1235
1236 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1237 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1238 assert self.GetBranch(), (
1239 'this CL must have an associated branch to %sset %s%s' %
1240 ('un' if value is None else '',
1241 key,
1242 '' if value is None else ' to %r' % value))
1243 kwargs['branch'] = self.GetBranch()
1244 return _git_set_branch_config_value(key, value, **kwargs)
1245
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001246 @staticmethod
1247 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001248 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 e.g. 'origin', 'refs/heads/master'
1250 """
1251 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001252 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001255 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001257 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1258 error_ok=True).strip()
1259 if upstream_branch:
1260 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001262 # Else, try to guess the origin remote.
1263 remote_branches = RunGit(['branch', '-r']).split()
1264 if 'origin/master' in remote_branches:
1265 # Fall back on origin/master if it exits.
1266 remote = 'origin'
1267 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001269 DieWithError(
1270 'Unable to determine default branch to diff against.\n'
1271 'Either pass complete "git diff"-style arguments, like\n'
1272 ' git cl upload origin/master\n'
1273 'or verify this branch is set up to track another \n'
1274 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001275
1276 return remote, upstream_branch
1277
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001278 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001279 upstream_branch = self.GetUpstreamBranch()
1280 if not BranchExists(upstream_branch):
1281 DieWithError('The upstream for the current branch (%s) does not exist '
1282 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001283 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001284 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001285
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 def GetUpstreamBranch(self):
1287 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001288 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Raul Tambrefe1dbe12019-05-02 04:43:57 +00001289 if remote != '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001290 upstream_branch = upstream_branch.replace('refs/heads/',
1291 'refs/remotes/%s/' % remote)
1292 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1293 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294 self.upstream_branch = upstream_branch
1295 return self.upstream_branch
1296
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001297 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001298 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 remote, branch = None, self.GetBranch()
1300 seen_branches = set()
1301 while branch not in seen_branches:
1302 seen_branches.add(branch)
1303 remote, branch = self.FetchUpstreamTuple(branch)
1304 branch = ShortBranchName(branch)
1305 if remote != '.' or branch.startswith('refs/remotes'):
1306 break
1307 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001308 remotes = RunGit(['remote'], error_ok=True).split()
1309 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001310 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001311 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001312 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001313 logging.warn('Could not determine which remote this change is '
1314 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001315 else:
1316 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001317 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001318 branch = 'HEAD'
1319 if branch.startswith('refs/remotes'):
1320 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001321 elif branch.startswith('refs/branch-heads/'):
1322 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001323 else:
1324 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001325 return self._remote
1326
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 def GitSanityChecks(self, upstream_git_obj):
1328 """Checks git repo status and ensures diff is from local commits."""
1329
sbc@chromium.org79706062015-01-14 21:18:12 +00001330 if upstream_git_obj is None:
1331 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001332 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001333 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001334 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001335 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001336 return False
1337
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001338 # Verify the commit we're diffing against is in our current branch.
1339 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1340 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1341 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001342 print('ERROR: %s is not in the current branch. You may need to rebase '
1343 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001344 return False
1345
1346 # List the commits inside the diff, and verify they are all local.
1347 commits_in_diff = RunGit(
1348 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1349 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1350 remote_branch = remote_branch.strip()
1351 if code != 0:
1352 _, remote_branch = self.GetRemoteBranch()
1353
1354 commits_in_remote = RunGit(
1355 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1356
1357 common_commits = set(commits_in_diff) & set(commits_in_remote)
1358 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001359 print('ERROR: Your diff contains %d commits already in %s.\n'
1360 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1361 'the diff. If you are using a custom git flow, you can override'
1362 ' the reference used for this check with "git config '
1363 'gitcl.remotebranch <git-ref>".' % (
1364 len(common_commits), remote_branch, upstream_git_obj),
1365 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001366 return False
1367 return True
1368
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001369 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001370 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001371
1372 Returns None if it is not set.
1373 """
tandrii5d48c322016-08-18 16:19:37 -07001374 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001375
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001376 def GetRemoteUrl(self):
1377 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1378
1379 Returns None if there is no remote.
1380 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001381 is_cached, value = self._cached_remote_url
1382 if is_cached:
1383 return value
1384
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001385 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001386 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1387
Edward Lemur298f2cf2019-02-22 21:40:39 +00001388 # Check if the remote url can be parsed as an URL.
1389 host = urlparse.urlparse(url).netloc
1390 if host:
1391 self._cached_remote_url = (True, url)
1392 return url
1393
1394 # If it cannot be parsed as an url, assume it is a local directory, probably
1395 # a git cache.
1396 logging.warning('"%s" doesn\'t appear to point to a git host. '
1397 'Interpreting it as a local directory.', url)
1398 if not os.path.isdir(url):
1399 logging.error(
1400 'Remote "%s" for branch "%s" points to "%s", but it doesn\'t exist.',
1401 remote, url, self.GetBranch())
1402 return None
1403
1404 cache_path = url
1405 url = RunGit(['config', 'remote.%s.url' % remote],
1406 error_ok=True,
1407 cwd=url).strip()
1408
1409 host = urlparse.urlparse(url).netloc
1410 if not host:
1411 logging.error(
1412 'Remote "%(remote)s" for branch "%(branch)s" points to '
1413 '"%(cache_path)s", but it is misconfigured.\n'
1414 '"%(cache_path)s" must be a git repo and must have a remote named '
1415 '"%(remote)s" pointing to the git host.', {
1416 'remote': remote,
1417 'cache_path': cache_path,
1418 'branch': self.GetBranch()})
1419 return None
1420
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001421 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001422 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001424 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001425 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001426 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001427 self.issue = self._GitGetBranchConfigValue(
1428 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001429 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 return self.issue
1431
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432 def GetIssueURL(self):
1433 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001434 issue = self.GetIssue()
1435 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001436 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001437 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001439 def GetDescription(self, pretty=False, force=False):
1440 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001442 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 self.has_description = True
1444 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001445 # Set width to 72 columns + 2 space indent.
1446 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001448 lines = self.description.splitlines()
1449 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 return self.description
1451
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001452 def GetDescriptionFooters(self):
1453 """Returns (non_footer_lines, footers) for the commit message.
1454
1455 Returns:
1456 non_footer_lines (list(str)) - Simple list of description lines without
1457 any footer. The lines do not contain newlines, nor does the list contain
1458 the empty line between the message and the footers.
1459 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1460 [("Change-Id", "Ideadbeef...."), ...]
1461 """
1462 raw_description = self.GetDescription()
1463 msg_lines, _, footers = git_footers.split_footers(raw_description)
1464 if footers:
1465 msg_lines = msg_lines[:len(msg_lines)-1]
1466 return msg_lines, footers
1467
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001468 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001469 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001470 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001471 self.patchset = self._GitGetBranchConfigValue(
1472 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001473 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474 return self.patchset
1475
1476 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001477 """Set this branch's patchset. If patchset=0, clears the patchset."""
1478 assert self.GetBranch()
1479 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001480 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001481 else:
1482 self.patchset = int(patchset)
1483 self._GitSetBranchConfigValue(
1484 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001485
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001486 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001487 """Set this branch's issue. If issue isn't given, clears the issue."""
1488 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001490 issue = int(issue)
1491 self._GitSetBranchConfigValue(
1492 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001493 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001494 codereview_server = self._codereview_impl.GetCodereviewServer()
1495 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001496 self._GitSetBranchConfigValue(
1497 self._codereview_impl.CodereviewServerConfigKey(),
1498 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001499 else:
tandrii5d48c322016-08-18 16:19:37 -07001500 # Reset all of these just to be clean.
1501 reset_suffixes = [
1502 'last-upload-hash',
1503 self._codereview_impl.IssueConfigKey(),
1504 self._codereview_impl.PatchsetConfigKey(),
1505 self._codereview_impl.CodereviewServerConfigKey(),
1506 ] + self._PostUnsetIssueProperties()
1507 for prop in reset_suffixes:
1508 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001509 msg = RunGit(['log', '-1', '--format=%B']).strip()
1510 if msg and git_footers.get_footer_change_id(msg):
1511 print('WARNING: The change patched into this branch has a Change-Id. '
1512 'Removing it.')
1513 RunGit(['commit', '--amend', '-m',
1514 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001515 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001516 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001517
dnjba1b0f32016-09-02 12:37:42 -07001518 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001519 if not self.GitSanityChecks(upstream_branch):
1520 DieWithError('\nGit sanity check failure')
1521
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001522 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001523 if not root:
1524 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001525 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001526
1527 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001528 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001529 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001530 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001531 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001532 except subprocess2.CalledProcessError:
1533 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001534 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001535 'This branch probably doesn\'t exist anymore. To reset the\n'
1536 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001537 ' git branch --set-upstream-to origin/master %s\n'
1538 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001539 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001540
maruel@chromium.org52424302012-08-29 15:14:30 +00001541 issue = self.GetIssue()
1542 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001543 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001544 description = self.GetDescription()
1545 else:
1546 # If the change was never uploaded, use the log messages of all commits
1547 # up to the branch point, as git cl upload will prefill the description
1548 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001549 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1550 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001551
1552 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001553 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001554 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001555 name,
1556 description,
1557 absroot,
1558 files,
1559 issue,
1560 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001561 author,
1562 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001563
dsansomee2d6fd92016-09-08 00:10:47 -07001564 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001565 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001566 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001567 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001568
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001569 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1570 """Sets the description for this CL remotely.
1571
1572 You can get description_lines and footers with GetDescriptionFooters.
1573
1574 Args:
1575 description_lines (list(str)) - List of CL description lines without
1576 newline characters.
1577 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1578 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1579 `List-Of-Tokens`). It will be case-normalized so that each token is
1580 title-cased.
1581 """
1582 new_description = '\n'.join(description_lines)
1583 if footers:
1584 new_description += '\n'
1585 for k, v in footers:
1586 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1587 if not git_footers.FOOTER_PATTERN.match(foot):
1588 raise ValueError('Invalid footer %r' % foot)
1589 new_description += foot + '\n'
1590 self.UpdateDescription(new_description, force)
1591
Edward Lesmes8e282792018-04-03 18:50:29 -04001592 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001593 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1594 try:
1595 return presubmit_support.DoPresubmitChecks(change, committing,
1596 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1597 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001598 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1599 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001600 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001601 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001602
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001603 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1604 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001605 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1606 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001607 else:
1608 # Assume url.
1609 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1610 urlparse.urlparse(issue_arg))
1611 if not parsed_issue_arg or not parsed_issue_arg.valid:
1612 DieWithError('Failed to parse issue argument "%s". '
1613 'Must be an issue number or a valid URL.' % issue_arg)
1614 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001615 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001616
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001617 def CMDUpload(self, options, git_diff_args, orig_args):
1618 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001619 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001620 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001621 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001622 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001623 else:
1624 if self.GetBranch() is None:
1625 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1626
1627 # Default to diffing against common ancestor of upstream branch
1628 base_branch = self.GetCommonAncestorWithUpstream()
1629 git_diff_args = [base_branch, 'HEAD']
1630
Aaron Gablec4c40d12017-05-22 11:49:53 -07001631
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001632 # Fast best-effort checks to abort before running potentially
1633 # expensive hooks if uploading is likely to fail anyway. Passing these
1634 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001635 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001636 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001637
1638 # Apply watchlists on upload.
1639 change = self.GetChange(base_branch, None)
1640 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1641 files = [f.LocalPath() for f in change.AffectedFiles()]
1642 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001643 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001644
1645 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001646 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001647 # Set the reviewer list now so that presubmit checks can access it.
1648 change_description = ChangeDescription(change.FullDescriptionText())
1649 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001650 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001651 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001652 change)
1653 change.SetDescriptionText(change_description.description)
1654 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001655 may_prompt=not options.force,
1656 verbose=options.verbose,
1657 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001658 if not hook_results.should_continue():
1659 return 1
1660 if not options.reviewers and hook_results.reviewers:
1661 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001662 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001663
Aaron Gable13101a62018-02-09 13:20:41 -08001664 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001665 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001666 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001667 _git_set_branch_config_value('last-upload-hash',
1668 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001669 # Run post upload hooks, if specified.
1670 if settings.GetRunPostUploadHook():
1671 presubmit_support.DoPostUploadExecuter(
1672 change,
1673 self,
1674 settings.GetRoot(),
1675 options.verbose,
1676 sys.stdout)
1677
1678 # Upload all dependencies if specified.
1679 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001680 print()
1681 print('--dependencies has been specified.')
1682 print('All dependent local branches will be re-uploaded.')
1683 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001684 # Remove the dependencies flag from args so that we do not end up in a
1685 # loop.
1686 orig_args.remove('--dependencies')
1687 ret = upload_branch_deps(self, orig_args)
1688 return ret
1689
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001690 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001691 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001692
1693 Issue must have been already uploaded and known.
1694 """
1695 assert new_state in _CQState.ALL_STATES
1696 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001697 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001698 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001699 return 0
1700 except KeyboardInterrupt:
1701 raise
1702 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001703 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001704 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001705 ' * Your project has no CQ,\n'
1706 ' * You don\'t have permission to change the CQ state,\n'
1707 ' * There\'s a bug in this code (see stack trace below).\n'
1708 'Consider specifying which bots to trigger manually or asking your '
1709 'project owners for permissions or contacting Chrome Infra at:\n'
1710 'https://www.chromium.org/infra\n\n' %
1711 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001712 # Still raise exception so that stack trace is printed.
1713 raise
1714
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001715 # Forward methods to codereview specific implementation.
1716
Aaron Gable636b13f2017-07-14 10:42:48 -07001717 def AddComment(self, message, publish=None):
1718 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001719
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001720 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001721 """Returns list of _CommentSummary for each comment.
1722
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001723 args:
1724 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001725 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001726 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001727
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001728 def CloseIssue(self):
1729 return self._codereview_impl.CloseIssue()
1730
1731 def GetStatus(self):
1732 return self._codereview_impl.GetStatus()
1733
1734 def GetCodereviewServer(self):
1735 return self._codereview_impl.GetCodereviewServer()
1736
tandriide281ae2016-10-12 06:02:30 -07001737 def GetIssueOwner(self):
1738 """Get owner from codereview, which may differ from this checkout."""
1739 return self._codereview_impl.GetIssueOwner()
1740
Edward Lemur707d70b2018-02-07 00:50:14 +01001741 def GetReviewers(self):
1742 return self._codereview_impl.GetReviewers()
1743
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001744 def GetMostRecentPatchset(self):
1745 return self._codereview_impl.GetMostRecentPatchset()
1746
tandriide281ae2016-10-12 06:02:30 -07001747 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001748 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001749 return self._codereview_impl.CannotTriggerTryJobReason()
1750
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001751 def GetTryJobProperties(self, patchset=None):
1752 """Returns dictionary of properties to launch try job."""
1753 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001754
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 def __getattr__(self, attr):
1756 # This is because lots of untested code accesses Rietveld-specific stuff
1757 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001758 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001759 # Note that child method defines __getattr__ as well, and forwards it here,
1760 # because _RietveldChangelistImpl is not cleaned up yet, and given
1761 # deprecation of Rietveld, it should probably be just removed.
1762 # Until that time, avoid infinite recursion by bypassing __getattr__
1763 # of implementation class.
1764 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001765
1766
1767class _ChangelistCodereviewBase(object):
1768 """Abstract base class encapsulating codereview specifics of a changelist."""
1769 def __init__(self, changelist):
1770 self._changelist = changelist # instance of Changelist
1771
1772 def __getattr__(self, attr):
1773 # Forward methods to changelist.
1774 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1775 # _RietveldChangelistImpl to avoid this hack?
1776 return getattr(self._changelist, attr)
1777
1778 def GetStatus(self):
1779 """Apply a rough heuristic to give a simple summary of an issue's review
1780 or CQ status, assuming adherence to a common workflow.
1781
1782 Returns None if no issue for this branch, or specific string keywords.
1783 """
1784 raise NotImplementedError()
1785
1786 def GetCodereviewServer(self):
1787 """Returns server URL without end slash, like "https://codereview.com"."""
1788 raise NotImplementedError()
1789
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001790 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001791 """Fetches and returns description from the codereview server."""
1792 raise NotImplementedError()
1793
tandrii5d48c322016-08-18 16:19:37 -07001794 @classmethod
1795 def IssueConfigKey(cls):
1796 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001797 raise NotImplementedError()
1798
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001799 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001800 def PatchsetConfigKey(cls):
1801 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001802 raise NotImplementedError()
1803
tandrii5d48c322016-08-18 16:19:37 -07001804 @classmethod
1805 def CodereviewServerConfigKey(cls):
1806 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001807 raise NotImplementedError()
1808
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001809 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001810 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001811 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001812
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001813 def GetGerritObjForPresubmit(self):
1814 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1815 return None
1816
dsansomee2d6fd92016-09-08 00:10:47 -07001817 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001818 """Update the description on codereview site."""
1819 raise NotImplementedError()
1820
Aaron Gable636b13f2017-07-14 10:42:48 -07001821 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001822 """Posts a comment to the codereview site."""
1823 raise NotImplementedError()
1824
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001825 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001826 raise NotImplementedError()
1827
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001828 def CloseIssue(self):
1829 """Closes the issue."""
1830 raise NotImplementedError()
1831
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001832 def GetMostRecentPatchset(self):
1833 """Returns the most recent patchset number from the codereview site."""
1834 raise NotImplementedError()
1835
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001836 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001837 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001838 """Fetches and applies the issue.
1839
1840 Arguments:
1841 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1842 reject: if True, reject the failed patch instead of switching to 3-way
1843 merge. Rietveld only.
1844 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1845 only.
1846 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001847 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001848 """
1849 raise NotImplementedError()
1850
1851 @staticmethod
1852 def ParseIssueURL(parsed_url):
1853 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1854 failed."""
1855 raise NotImplementedError()
1856
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001857 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001858 """Best effort check that user is authenticated with codereview server.
1859
1860 Arguments:
1861 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001862 refresh: whether to attempt to refresh credentials. Ignored if not
1863 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001864 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001865 raise NotImplementedError()
1866
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001867 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001868 """Best effort check that uploading isn't supposed to fail for predictable
1869 reasons.
1870
1871 This method should raise informative exception if uploading shouldn't
1872 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001873
1874 Arguments:
1875 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001876 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001877 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001878
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001879 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001880 """Uploads a change to codereview."""
1881 raise NotImplementedError()
1882
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001883 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001884 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001885
1886 Issue must have been already uploaded and known.
1887 """
1888 raise NotImplementedError()
1889
tandriie113dfd2016-10-11 10:20:12 -07001890 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001891 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001892 raise NotImplementedError()
1893
tandriide281ae2016-10-12 06:02:30 -07001894 def GetIssueOwner(self):
1895 raise NotImplementedError()
1896
Edward Lemur707d70b2018-02-07 00:50:14 +01001897 def GetReviewers(self):
1898 raise NotImplementedError()
1899
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001900 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001901 raise NotImplementedError()
1902
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001903
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001904class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001905 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001906 # auth_config is Rietveld thing, kept here to preserve interface only.
1907 super(_GerritChangelistImpl, self).__init__(changelist)
1908 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001909 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001910 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001911 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01001912 # Map from change number (issue) to its detail cache.
1913 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001914
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001915 if codereview_host is not None:
1916 assert not codereview_host.startswith('https://'), codereview_host
1917 self._gerrit_host = codereview_host
1918 self._gerrit_server = 'https://%s' % codereview_host
1919
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 def _GetGerritHost(self):
1921 # Lazy load of configs.
1922 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07001923 if self._gerrit_host and '.' not in self._gerrit_host:
1924 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
1925 # This happens for internal stuff http://crbug.com/614312.
1926 parsed = urlparse.urlparse(self.GetRemoteUrl())
1927 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001928 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07001929 ' Your current remote is: %s' % self.GetRemoteUrl())
1930 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
1931 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001932 return self._gerrit_host
1933
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001934 def _GetGitHost(self):
1935 """Returns git host to be used when uploading change to Gerrit."""
Edward Lemur298f2cf2019-02-22 21:40:39 +00001936 remote_url = self.GetRemoteUrl()
1937 if not remote_url:
1938 return None
1939 return urlparse.urlparse(remote_url).netloc
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001940
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001941 def GetCodereviewServer(self):
1942 if not self._gerrit_server:
1943 # If we're on a branch then get the server potentially associated
1944 # with that branch.
1945 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001946 self._gerrit_server = self._GitGetBranchConfigValue(
1947 self.CodereviewServerConfigKey())
1948 if self._gerrit_server:
1949 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001950 if not self._gerrit_server:
1951 # We assume repo to be hosted on Gerrit, and hence Gerrit server
1952 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001953 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001954 parts[0] = parts[0] + '-review'
1955 self._gerrit_host = '.'.join(parts)
1956 self._gerrit_server = 'https://%s' % self._gerrit_host
1957 return self._gerrit_server
1958
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001959 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001960 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001961 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001962 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001963 logging.warn('can\'t detect Gerrit project.')
1964 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001965 project = urlparse.urlparse(remote_url).path.strip('/')
1966 if project.endswith('.git'):
1967 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00001968 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
1969 # 'a/' prefix, because 'a/' prefix is used to force authentication in
1970 # gitiles/git-over-https protocol. E.g.,
1971 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
1972 # as
1973 # https://chromium.googlesource.com/v8/v8
1974 if project.startswith('a/'):
1975 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00001976 return project
1977
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001978 def _GerritChangeIdentifier(self):
1979 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
1980
1981 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001982 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001983 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00001984 project = self._GetGerritProject()
1985 if project:
1986 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
1987 # Fall back on still unique, but less efficient change number.
1988 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00001989
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001990 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001991 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001992 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001993
tandrii5d48c322016-08-18 16:19:37 -07001994 @classmethod
1995 def PatchsetConfigKey(cls):
1996 return 'gerritpatchset'
1997
1998 @classmethod
1999 def CodereviewServerConfigKey(cls):
2000 return 'gerritserver'
2001
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002002 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002003 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002004 if settings.GetGerritSkipEnsureAuthenticated():
2005 # For projects with unusual authentication schemes.
2006 # See http://crbug.com/603378.
2007 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002008
2009 # Check presence of cookies only if using cookies-based auth method.
2010 cookie_auth = gerrit_util.Authenticator.get()
2011 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002012 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002013
2014 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002015 self.GetCodereviewServer()
2016 git_host = self._GetGitHost()
Edward Lemur298f2cf2019-02-22 21:40:39 +00002017 assert self._gerrit_server and self._gerrit_host and git_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002018
2019 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2020 git_auth = cookie_auth.get_auth_header(git_host)
2021 if gerrit_auth and git_auth:
2022 if gerrit_auth == git_auth:
2023 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002024 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
Raul Tambre80ee78e2019-05-06 22:41:05 +00002025 print(
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002026 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002027 ' %s\n'
2028 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002029 ' Consider running the following command:\n'
2030 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002031 ' %s\n'
Raul Tambre80ee78e2019-05-06 22:41:05 +00002032 ' %s' %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002033 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002034 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002035 cookie_auth.get_new_password_message(git_host)))
2036 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002037 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002038 return
2039 else:
2040 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002041 ([] if gerrit_auth else [self._gerrit_host]) +
2042 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002043 DieWithError('Credentials for the following hosts are required:\n'
2044 ' %s\n'
2045 'These are read from %s (or legacy %s)\n'
2046 '%s' % (
2047 '\n '.join(missing),
2048 cookie_auth.get_gitcookies_path(),
2049 cookie_auth.get_netrc_path(),
2050 cookie_auth.get_new_password_message(git_host)))
2051
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002052 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002053 if not self.GetIssue():
2054 return
2055
2056 # Warm change details cache now to avoid RPCs later, reducing latency for
2057 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002058 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002059 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002060
2061 status = self._GetChangeDetail()['status']
2062 if status in ('MERGED', 'ABANDONED'):
2063 DieWithError('Change %s has been %s, new uploads are not allowed' %
2064 (self.GetIssueURL(),
2065 'submitted' if status == 'MERGED' else 'abandoned'))
2066
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002067 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2068 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2069 # Apparently this check is not very important? Otherwise get_auth_email
2070 # could have been added to other implementations of Authenticator.
2071 cookies_auth = gerrit_util.Authenticator.get()
2072 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002073 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002074
2075 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002076 if self.GetIssueOwner() == cookies_user:
2077 return
2078 logging.debug('change %s owner is %s, cookies user is %s',
2079 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002080 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002081 # so ask what Gerrit thinks of this user.
2082 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2083 if details['email'] == self.GetIssueOwner():
2084 return
2085 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002086 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002087 'as %s.\n'
2088 'Uploading may fail due to lack of permissions.' %
2089 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2090 confirm_or_exit(action='upload')
2091
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002092 def _PostUnsetIssueProperties(self):
2093 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002094 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002095
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002096 def GetGerritObjForPresubmit(self):
2097 return presubmit_support.GerritAccessor(self._GetGerritHost())
2098
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002099 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002100 """Apply a rough heuristic to give a simple summary of an issue's review
2101 or CQ status, assuming adherence to a common workflow.
2102
2103 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002104 * 'error' - error from review tool (including deleted issues)
2105 * 'unsent' - no reviewers added
2106 * 'waiting' - waiting for review
2107 * 'reply' - waiting for uploader to reply to review
2108 * 'lgtm' - Code-Review label has been set
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002109 * 'dry-run' - dry-running in the commit queue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002110 * 'commit' - in the commit queue
2111 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002112 """
2113 if not self.GetIssue():
2114 return None
2115
2116 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002117 data = self._GetChangeDetail([
2118 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002119 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002120 return 'error'
2121
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002122 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002123 return 'closed'
2124
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002125 cq_label = data['labels'].get('Commit-Queue', {})
2126 max_cq_vote = 0
2127 for vote in cq_label.get('all', []):
2128 max_cq_vote = max(max_cq_vote, vote.get('value', 0))
2129 if max_cq_vote == 2:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002130 return 'commit'
Andrii Shyshkalovb8268ca2019-04-03 23:33:44 +00002131 if max_cq_vote == 1:
2132 return 'dry-run'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002133
Aaron Gable9ab38c62017-04-06 14:36:33 -07002134 if data['labels'].get('Code-Review', {}).get('approved'):
2135 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002136
2137 if not data.get('reviewers', {}).get('REVIEWER', []):
2138 return 'unsent'
2139
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002140 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002141 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2142 last_message_author = messages.pop().get('author', {})
2143 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002144 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2145 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002146 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002147 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002148 if last_message_author.get('_account_id') == owner:
2149 # Most recent message was by owner.
2150 return 'waiting'
2151 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002152 # Some reply from non-owner.
2153 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002154
2155 # Somehow there are no messages even though there are reviewers.
2156 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002157
2158 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002159 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002160 patchset = data['revisions'][data['current_revision']]['_number']
2161 self.SetPatchset(patchset)
2162 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002163
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002164 def FetchDescription(self, force=False):
2165 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2166 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002167 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002168 return data['revisions'][current_rev]['commit']['message'].encode(
2169 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002170
dsansomee2d6fd92016-09-08 00:10:47 -07002171 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002172 if gerrit_util.HasPendingChangeEdit(
2173 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002174 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002175 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002176 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002177 'unpublished edit. Either publish the edit in the Gerrit web UI '
2178 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002179
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002180 gerrit_util.DeletePendingChangeEdit(
2181 self._GetGerritHost(), self._GerritChangeIdentifier())
2182 gerrit_util.SetCommitMessage(
2183 self._GetGerritHost(), self._GerritChangeIdentifier(),
2184 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002185
Aaron Gable636b13f2017-07-14 10:42:48 -07002186 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002187 gerrit_util.SetReview(
2188 self._GetGerritHost(), self._GerritChangeIdentifier(),
2189 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002190
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002191 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002192 # DETAILED_ACCOUNTS is to get emails in accounts.
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002193 # CURRENT_REVISION is included to get the latest patchset so that
2194 # only the robot comments from the latest patchset can be shown.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002195 messages = self._GetChangeDetail(
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002196 options=['MESSAGES', 'DETAILED_ACCOUNTS',
2197 'CURRENT_REVISION']).get('messages', [])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002198 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002199 self._GetGerritHost(), self._GerritChangeIdentifier())
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002200 robot_file_comments = gerrit_util.GetChangeRobotComments(
2201 self._GetGerritHost(), self._GerritChangeIdentifier())
2202
2203 # Add the robot comments onto the list of comments, but only
2204 # keep those that are from the latest pachset.
2205 latest_patch_set = self.GetMostRecentPatchset()
2206 for path, robot_comments in robot_file_comments.iteritems():
2207 line_comments = file_comments.setdefault(path, [])
2208 line_comments.extend(
2209 [c for c in robot_comments if c['patch_set'] == latest_patch_set])
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002210
2211 # Build dictionary of file comments for easy access and sorting later.
2212 # {author+date: {path: {patchset: {line: url+message}}}}
2213 comments = collections.defaultdict(
2214 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2215 for path, line_comments in file_comments.iteritems():
2216 for comment in line_comments:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002217 tag = comment.get('tag', '')
2218 if tag.startswith('autogenerated') and 'robot_id' not in comment:
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002219 continue
2220 key = (comment['author']['email'], comment['updated'])
2221 if comment.get('side', 'REVISION') == 'PARENT':
2222 patchset = 'Base'
2223 else:
2224 patchset = 'PS%d' % comment['patch_set']
2225 line = comment.get('line', 0)
2226 url = ('https://%s/c/%s/%s/%s#%s%s' %
2227 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2228 'b' if comment.get('side') == 'PARENT' else '',
2229 str(line) if line else ''))
2230 comments[key][path][patchset][line] = (url, comment['message'])
2231
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002232 summaries = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002233 for msg in messages:
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002234 summary = self._BuildCommentSummary(msg, comments, readable)
2235 if summary:
2236 summaries.append(summary)
2237 return summaries
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002238
Quinten Yearsley0e617c02019-02-20 00:37:03 +00002239 @staticmethod
2240 def _BuildCommentSummary(msg, comments, readable):
2241 key = (msg['author']['email'], msg['date'])
2242 # Don't bother showing autogenerated messages that don't have associated
2243 # file or line comments. this will filter out most autogenerated
2244 # messages, but will keep robot comments like those from Tricium.
2245 is_autogenerated = msg.get('tag', '').startswith('autogenerated')
2246 if is_autogenerated and not comments.get(key):
2247 return None
2248 message = msg['message']
2249 # Gerrit spits out nanoseconds.
2250 assert len(msg['date'].split('.')[-1]) == 9
2251 date = datetime.datetime.strptime(msg['date'][:-3],
2252 '%Y-%m-%d %H:%M:%S.%f')
2253 if key in comments:
2254 message += '\n'
2255 for path, patchsets in sorted(comments.get(key, {}).items()):
2256 if readable:
2257 message += '\n%s' % path
2258 for patchset, lines in sorted(patchsets.items()):
2259 for line, (url, content) in sorted(lines.items()):
2260 if line:
2261 line_str = 'Line %d' % line
2262 path_str = '%s:%d:' % (path, line)
2263 else:
2264 line_str = 'File comment'
2265 path_str = '%s:0:' % path
2266 if readable:
2267 message += '\n %s, %s: %s' % (patchset, line_str, url)
2268 message += '\n %s\n' % content
2269 else:
2270 message += '\n%s ' % path_str
2271 message += '\n%s\n' % content
2272
2273 return _CommentSummary(
2274 date=date,
2275 message=message,
2276 sender=msg['author']['email'],
2277 autogenerated=is_autogenerated,
2278 # These could be inferred from the text messages and correlated with
2279 # Code-Review label maximum, however this is not reliable.
2280 # Leaving as is until the need arises.
2281 approval=False,
2282 disapproval=False,
2283 )
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002284
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002285 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002286 gerrit_util.AbandonChange(
2287 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002288
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002289 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002290 gerrit_util.SubmitChange(
2291 self._GetGerritHost(), self._GerritChangeIdentifier(),
2292 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002293
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002294 def _GetChangeDetail(self, options=None, no_cache=False):
2295 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002296
2297 If fresh data is needed, set no_cache=True which will clear cache and
2298 thus new data will be fetched from Gerrit.
2299 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002300 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002301 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002302
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002303 # Optimization to avoid multiple RPCs:
2304 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2305 'CURRENT_COMMIT' not in options):
2306 options.append('CURRENT_COMMIT')
2307
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002308 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002309 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002310 options = [o.upper() for o in options]
2311
2312 # Check in cache first unless no_cache is True.
2313 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002314 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002315 else:
2316 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002317 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002318 # Assumption: data fetched before with extra options is suitable
2319 # for return for a smaller set of options.
2320 # For example, if we cached data for
2321 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2322 # and request is for options=[CURRENT_REVISION],
2323 # THEN we can return prior cached data.
2324 if options_set.issubset(cached_options_set):
2325 return data
2326
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002327 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002328 data = gerrit_util.GetChangeDetail(
2329 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002330 except gerrit_util.GerritError as e:
2331 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002332 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002333 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002334
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002335 self._detail_cache.setdefault(cache_key, []).append(
2336 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002337 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002338
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002339 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002340 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002341 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002342 data = gerrit_util.GetChangeCommit(
2343 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002344 except gerrit_util.GerritError as e:
2345 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002346 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002347 raise
agable32978d92016-11-01 12:55:02 -07002348 return data
2349
Karen Qian40c19422019-03-13 21:28:29 +00002350 def _IsCqConfigured(self):
2351 detail = self._GetChangeDetail(['LABELS'])
2352 if not u'Commit-Queue' in detail.get('labels', {}):
2353 return False
2354 # TODO(crbug/753213): Remove temporary hack
2355 if ('https://chromium.googlesource.com/chromium/src' ==
2356 self._changelist.GetRemoteUrl() and
2357 detail['branch'].startswith('refs/branch-heads/')):
2358 return False
2359 return True
2360
Olivier Robin75ee7252018-04-13 10:02:56 +02002361 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002362 if git_common.is_dirty_git_tree('land'):
2363 return 1
Karen Qian40c19422019-03-13 21:28:29 +00002364
tandriid60367b2016-06-22 05:25:12 -07002365 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
Karen Qian40c19422019-03-13 21:28:29 +00002366 if not force and self._IsCqConfigured():
2367 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002368 'which can test and land changes for you. '
2369 'Are you sure you wish to bypass it?\n',
2370 action='bypass CQ')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002371 differs = True
tandriic4344b52016-08-29 06:04:54 -07002372 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002373 # Note: git diff outputs nothing if there is no diff.
2374 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002375 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002376 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002377 if detail['current_revision'] == last_upload:
2378 differs = False
2379 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002380 print('WARNING: Local branch contents differ from latest uploaded '
2381 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002382 if differs:
2383 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002384 confirm_or_exit(
2385 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2386 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002387 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002388 elif not bypass_hooks:
2389 hook_results = self.RunHook(
2390 committing=True,
2391 may_prompt=not force,
2392 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002393 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2394 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002395 if not hook_results.should_continue():
2396 return 1
2397
2398 self.SubmitIssue(wait_for_merge=True)
2399 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002400 links = self._GetChangeCommit().get('web_links', [])
2401 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002402 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002403 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002404 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002405 return 0
2406
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002407 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002408 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002409 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002410 assert not directory
2411 assert parsed_issue_arg.valid
2412
2413 self._changelist.issue = parsed_issue_arg.issue
2414
2415 if parsed_issue_arg.hostname:
2416 self._gerrit_host = parsed_issue_arg.hostname
2417 self._gerrit_server = 'https://%s' % self._gerrit_host
2418
tandriic2405f52016-10-10 08:13:15 -07002419 try:
2420 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002421 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002422 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002423
2424 if not parsed_issue_arg.patchset:
2425 # Use current revision by default.
2426 revision_info = detail['revisions'][detail['current_revision']]
2427 patchset = int(revision_info['_number'])
2428 else:
2429 patchset = parsed_issue_arg.patchset
2430 for revision_info in detail['revisions'].itervalues():
2431 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2432 break
2433 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002434 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002435 (parsed_issue_arg.patchset, self.GetIssue()))
2436
Aaron Gable697a91b2018-01-19 15:20:15 -08002437 remote_url = self._changelist.GetRemoteUrl()
2438 if remote_url.endswith('.git'):
2439 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002440 remote_url = remote_url.rstrip('/')
2441
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002442 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002443 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002444
2445 if remote_url != fetch_info['url']:
2446 DieWithError('Trying to patch a change from %s but this repo appears '
2447 'to be %s.' % (fetch_info['url'], remote_url))
2448
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002449 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002450
Aaron Gable62619a32017-06-16 08:22:09 -07002451 if force:
2452 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2453 print('Checked out commit for change %i patchset %i locally' %
2454 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002455 elif nocommit:
2456 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2457 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002458 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002459 RunGit(['cherry-pick', 'FETCH_HEAD'])
2460 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002461 (parsed_issue_arg.issue, patchset))
2462 print('Note: this created a local commit which does not have '
2463 'the same hash as the one uploaded for review. This will make '
2464 'uploading changes based on top of this branch difficult.\n'
2465 'If you want to do that, use "git cl patch --force" instead.')
2466
Stefan Zagerd08043c2017-10-12 12:07:02 -07002467 if self.GetBranch():
2468 self.SetIssue(parsed_issue_arg.issue)
2469 self.SetPatchset(patchset)
2470 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2471 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2472 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2473 else:
2474 print('WARNING: You are in detached HEAD state.\n'
2475 'The patch has been applied to your checkout, but you will not be '
2476 'able to upload a new patch set to the gerrit issue.\n'
2477 'Try using the \'-b\' option if you would like to work on a '
2478 'branch and/or upload a new patch set.')
2479
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002480 return 0
2481
2482 @staticmethod
2483 def ParseIssueURL(parsed_url):
2484 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2485 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002486 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2487 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002488 # Short urls like https://domain/<issue_number> can be used, but don't allow
2489 # specifying the patchset (you'd 404), but we allow that here.
2490 if parsed_url.path == '/':
2491 part = parsed_url.fragment
2492 else:
2493 part = parsed_url.path
Bruce Dawson9c062012019-05-02 19:20:28 +00002494 match = re.match(r'(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002495 if match:
2496 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002497 issue=int(match.group(3)),
2498 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002499 hostname=parsed_url.netloc,
2500 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002501 return None
2502
tandrii16e0b4e2016-06-07 10:34:28 -07002503 def _GerritCommitMsgHookCheck(self, offer_removal):
2504 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2505 if not os.path.exists(hook):
2506 return
2507 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2508 # custom developer made one.
2509 data = gclient_utils.FileRead(hook)
2510 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2511 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002512 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002513 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002514 'and may interfere with it in subtle ways.\n'
2515 'We recommend you remove the commit-msg hook.')
2516 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002517 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002518 gclient_utils.rm_file_or_tree(hook)
2519 print('Gerrit commit-msg hook removed.')
2520 else:
2521 print('OK, will keep Gerrit commit-msg hook in place.')
2522
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002523 def _CleanUpOldTraces(self):
2524 """Keep only the last |MAX_TRACES| traces."""
2525 try:
2526 traces = sorted([
2527 os.path.join(TRACES_DIR, f)
2528 for f in os.listdir(TRACES_DIR)
2529 if (os.path.isfile(os.path.join(TRACES_DIR, f))
2530 and not f.startswith('tmp'))
2531 ])
2532 traces_to_delete = traces[:-MAX_TRACES]
2533 for trace in traces_to_delete:
2534 os.remove(trace)
2535 except OSError:
2536 print('WARNING: Failed to remove old git traces from\n'
2537 ' %s'
2538 'Consider removing them manually.' % TRACES_DIR)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002539
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002540 def _WriteGitPushTraces(self, traces_dir, git_push_metadata):
2541 """Zip and write the git push traces stored in traces_dir."""
2542 gclient_utils.safe_makedirs(TRACES_DIR)
2543 now = datetime_now()
2544 trace_name = os.path.join(TRACES_DIR, now.strftime('%Y%m%dT%H%M%S.%f'))
2545 traces_zip = trace_name + '-traces'
2546 traces_readme = trace_name + '-README'
Edward Lemur0f58ae42019-04-30 17:24:12 +00002547 # Create a temporary dir to store git config and gitcookies in. It will be
2548 # compressed and stored next to the traces.
2549 git_info_dir = tempfile.mkdtemp()
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002550 git_info_zip = trace_name + '-git-info'
2551
2552 git_push_metadata['now'] = now.strftime('%c')
2553 git_push_metadata['trace_name'] = trace_name
2554 gclient_utils.FileWrite(
2555 traces_readme, TRACES_README_FORMAT % git_push_metadata)
2556
2557 # Keep only the first 6 characters of the git hashes on the packet
2558 # trace. This greatly decreases size after compression.
2559 packet_traces = os.path.join(traces_dir, 'trace-packet')
2560 contents = gclient_utils.FileRead(packet_traces)
2561 gclient_utils.FileWrite(
2562 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2563 shutil.make_archive(traces_zip, 'zip', traces_dir)
2564
2565 # Collect and compress the git config and gitcookies.
2566 git_config = RunGit(['config', '-l'])
2567 gclient_utils.FileWrite(
2568 os.path.join(git_info_dir, 'git-config'),
2569 git_config)
2570
2571 cookie_auth = gerrit_util.Authenticator.get()
2572 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2573 gitcookies_path = cookie_auth.get_gitcookies_path()
2574 gitcookies = gclient_utils.FileRead(gitcookies_path)
2575 gclient_utils.FileWrite(
2576 os.path.join(git_info_dir, 'gitcookies'),
2577 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2578 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2579
2580 print(TRACES_MESSAGE % {'trace_name': trace_name})
2581
2582 gclient_utils.rmtree(git_info_dir)
2583
2584 def _RunGitPushWithTraces(
2585 self, change_desc, refspec, refspec_opts, git_push_metadata):
2586 """Run git push and collect the traces resulting from the execution."""
2587 # Create a temporary directory to store traces in. Traces will be compressed
2588 # and stored in a 'traces' dir inside depot_tools.
2589 traces_dir = tempfile.mkdtemp()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002590
2591 env = os.environ.copy()
2592 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2593 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2594 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2595 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2596 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2597
2598 try:
2599 push_returncode = 0
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002600 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002601 before_push = time_time()
2602 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002603 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002604 env=env,
2605 print_stdout=True,
2606 # Flush after every line: useful for seeing progress when running as
2607 # recipe.
2608 filter_fn=lambda _: sys.stdout.flush())
2609 except subprocess2.CalledProcessError as e:
2610 push_returncode = e.returncode
2611 DieWithError('Failed to create a change. Please examine output above '
2612 'for the reason of the failure.\n'
2613 'Hint: run command below to diagnose common Git/Gerrit '
2614 'credential problems:\n'
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002615 ' git cl creds-check',
Edward Lemur0f58ae42019-04-30 17:24:12 +00002616 change_desc)
2617 finally:
2618 execution_time = time_time() - before_push
2619 metrics.collector.add_repeated('sub_commands', {
2620 'command': 'git push',
2621 'execution_time': execution_time,
2622 'exit_code': push_returncode,
2623 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2624 })
2625
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002626 git_push_metadata['execution_time'] = execution_time
2627 git_push_metadata['exit_code'] = push_returncode
2628 self._WriteGitPushTraces(traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002629
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002630 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002631 gclient_utils.rmtree(traces_dir)
2632
2633 return push_stdout
2634
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002635 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002636 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002637 if options.squash and options.no_squash:
2638 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002639
2640 if not options.squash and not options.no_squash:
2641 # Load default for user, repo, squash=true, in this order.
2642 options.squash = settings.GetSquashGerritUploads()
2643 elif options.no_squash:
2644 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002645
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002646 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002647 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002648 # This may be None; default fallback value is determined in logic below.
2649 title = options.title
2650
Dominic Battre7d1c4842017-10-27 09:17:28 +02002651 # Extract bug number from branch name.
2652 bug = options.bug
2653 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2654 if not bug and match:
2655 bug = match.group(1)
2656
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002657 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002658 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002659 if self.GetIssue():
2660 # Try to get the message from a previous upload.
2661 message = self.GetDescription()
2662 if not message:
2663 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002664 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002665 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002666 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002667 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002668 # When uploading a subsequent patchset, -m|--message is taken
2669 # as the patchset title if --title was not provided.
2670 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002671 else:
2672 default_title = RunGit(
2673 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002674 if options.force:
2675 title = default_title
2676 else:
2677 title = ask_for_data(
2678 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002679 change_id = self._GetChangeDetail()['change_id']
2680 while True:
2681 footer_change_ids = git_footers.get_footer_change_id(message)
2682 if footer_change_ids == [change_id]:
2683 break
2684 if not footer_change_ids:
2685 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002686 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002687 continue
2688 # There is already a valid footer but with different or several ids.
2689 # Doing this automatically is non-trivial as we don't want to lose
2690 # existing other footers, yet we want to append just 1 desired
2691 # Change-Id. Thus, just create a new footer, but let user verify the
2692 # new description.
2693 message = '%s\n\nChange-Id: %s' % (message, change_id)
2694 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002695 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002696 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002697 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002698 'Please, check the proposed correction to the description, '
2699 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2700 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2701 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002702 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002703 if not options.force:
2704 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002705 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002706 message = change_desc.description
2707 if not message:
2708 DieWithError("Description is empty. Aborting...")
2709 # Continue the while loop.
2710 # Sanity check of this code - we should end up with proper message
2711 # footer.
2712 assert [change_id] == git_footers.get_footer_change_id(message)
2713 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002714 else: # if not self.GetIssue()
2715 if options.message:
2716 message = options.message
2717 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002718 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002719 if options.title:
2720 message = options.title + '\n\n' + message
2721 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002722
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002723 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002724 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002725 # On first upload, patchset title is always this string, while
2726 # --title flag gets converted to first line of message.
2727 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002728 if not change_desc.description:
2729 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002730 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002731 if len(change_ids) > 1:
2732 DieWithError('too many Change-Id footers, at most 1 allowed.')
2733 if not change_ids:
2734 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002735 change_desc.set_description(git_footers.add_footer_change_id(
2736 change_desc.description,
2737 GenerateGerritChangeId(change_desc.description)))
2738 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002739 assert len(change_ids) == 1
2740 change_id = change_ids[0]
2741
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002742 if options.reviewers or options.tbrs or options.add_owners_to:
2743 change_desc.update_reviewers(options.reviewers, options.tbrs,
2744 options.add_owners_to, change)
2745
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002746 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002747 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2748 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002749 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002750 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2751 desc_tempfile.write(change_desc.description)
2752 desc_tempfile.close()
2753 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2754 '-F', desc_tempfile.name]).strip()
2755 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002756 else:
2757 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002758 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002759 if not change_desc.description:
2760 DieWithError("Description is empty. Aborting...")
2761
2762 if not git_footers.get_footer_change_id(change_desc.description):
2763 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002764 change_desc.set_description(
2765 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002766 if options.reviewers or options.tbrs or options.add_owners_to:
2767 change_desc.update_reviewers(options.reviewers, options.tbrs,
2768 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002769 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002770 # For no-squash mode, we assume the remote called "origin" is the one we
2771 # want. It is not worthwhile to support different workflows for
2772 # no-squash mode.
2773 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002774 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2775
2776 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002777 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002778 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2779 ref_to_push)]).splitlines()
2780 if len(commits) > 1:
2781 print('WARNING: This will upload %d commits. Run the following command '
2782 'to see which commits will be uploaded: ' % len(commits))
2783 print('git log %s..%s' % (parent, ref_to_push))
2784 print('You can also use `git squash-branch` to squash these into a '
2785 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002786 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002787
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002788 if options.reviewers or options.tbrs or options.add_owners_to:
2789 change_desc.update_reviewers(options.reviewers, options.tbrs,
2790 options.add_owners_to, change)
2791
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002792 reviewers = sorted(change_desc.get_reviewers())
2793 # Add cc's from the CC_LIST and --cc flag (if any).
2794 if not options.private and not options.no_autocc:
2795 cc = self.GetCCList().split(',')
2796 else:
2797 cc = []
2798 if options.cc:
2799 cc.extend(options.cc)
2800 cc = filter(None, [email.strip() for email in cc])
2801 if change_desc.get_cced():
2802 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002803 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2804 valid_accounts = set(reviewers + cc)
2805 # TODO(crbug/877717): relax this for all hosts.
2806 else:
2807 valid_accounts = gerrit_util.ValidAccounts(
2808 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002809 logging.info('accounts %s are recognized, %s invalid',
2810 sorted(valid_accounts),
2811 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002812
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002813 # Extra options that can be specified at push time. Doc:
2814 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002815 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002816
Aaron Gable844cf292017-06-28 11:32:59 -07002817 # By default, new changes are started in WIP mode, and subsequent patchsets
2818 # don't send email. At any time, passing --send-mail will mark the change
2819 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002820 if options.send_mail:
2821 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002822 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002823 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002824 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002825 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002826 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002827
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002828 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002829 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002830
Aaron Gable9b713dd2016-12-14 16:04:21 -08002831 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002832 # Punctuation and whitespace in |title| must be percent-encoded.
2833 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002834
agablec6787972016-09-09 16:13:34 -07002835 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002836 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002837
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002838 for r in sorted(reviewers):
2839 if r in valid_accounts:
2840 refspec_opts.append('r=%s' % r)
2841 reviewers.remove(r)
2842 else:
2843 # TODO(tandrii): this should probably be a hard failure.
2844 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2845 % r)
2846 for c in sorted(cc):
2847 # refspec option will be rejected if cc doesn't correspond to an
2848 # account, even though REST call to add such arbitrary cc may succeed.
2849 if c in valid_accounts:
2850 refspec_opts.append('cc=%s' % c)
2851 cc.remove(c)
2852
rmistry9eadede2016-09-19 11:22:43 -07002853 if options.topic:
2854 # Documentation on Gerrit topics is here:
2855 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002856 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002857
Edward Lemur687ca902018-12-05 02:30:30 +00002858 if options.enable_auto_submit:
2859 refspec_opts.append('l=Auto-Submit+1')
2860 if options.use_commit_queue:
2861 refspec_opts.append('l=Commit-Queue+2')
2862 elif options.cq_dry_run:
2863 refspec_opts.append('l=Commit-Queue+1')
2864
2865 if change_desc.get_reviewers(tbr_only=True):
2866 score = gerrit_util.GetCodeReviewTbrScore(
2867 self._GetGerritHost(),
2868 self._GetGerritProject())
2869 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002870
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002871 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002872 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002873 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002874 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002875 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2876
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002877 refspec_suffix = ''
2878 if refspec_opts:
2879 refspec_suffix = '%' + ','.join(refspec_opts)
2880 assert ' ' not in refspec_suffix, (
2881 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2882 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2883
Edward Lemurdc8e23d2019-05-07 00:45:48 +00002884 git_push_metadata = {
2885 'gerrit_host': self._GetGerritHost(),
2886 'title': title or '<untitled>',
2887 'change_id': change_id,
2888 'description': change_desc.description,
2889 }
2890 push_stdout = self._RunGitPushWithTraces(
2891 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002892
2893 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002894 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002895 change_numbers = [m.group(1)
2896 for m in map(regex.match, push_stdout.splitlines())
2897 if m]
2898 if len(change_numbers) != 1:
2899 DieWithError(
2900 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002901 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002902 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002903 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002904
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002905 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002906 # GetIssue() is not set in case of non-squash uploads according to tests.
2907 # TODO(agable): non-squash uploads in git cl should be removed.
2908 gerrit_util.AddReviewers(
2909 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002910 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002911 reviewers, cc,
2912 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002913
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002914 return 0
2915
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002916 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2917 change_desc):
2918 """Computes parent of the generated commit to be uploaded to Gerrit.
2919
2920 Returns revision or a ref name.
2921 """
2922 if custom_cl_base:
2923 # Try to avoid creating additional unintended CLs when uploading, unless
2924 # user wants to take this risk.
2925 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2926 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2927 local_ref_of_target_remote])
2928 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002929 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002930 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2931 'If you proceed with upload, more than 1 CL may be created by '
2932 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2933 'If you are certain that specified base `%s` has already been '
2934 'uploaded to Gerrit as another CL, you may proceed.\n' %
2935 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2936 if not force:
2937 confirm_or_exit(
2938 'Do you take responsibility for cleaning up potential mess '
2939 'resulting from proceeding with upload?',
2940 action='upload')
2941 return custom_cl_base
2942
Aaron Gablef97e33d2017-03-30 15:44:27 -07002943 if remote != '.':
2944 return self.GetCommonAncestorWithUpstream()
2945
2946 # If our upstream branch is local, we base our squashed commit on its
2947 # squashed version.
2948 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2949
Aaron Gablef97e33d2017-03-30 15:44:27 -07002950 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002951 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002952
2953 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002954 # TODO(tandrii): consider checking parent change in Gerrit and using its
2955 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2956 # the tree hash of the parent branch. The upside is less likely bogus
2957 # requests to reupload parent change just because it's uploadhash is
2958 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002959 parent = RunGit(['config',
2960 'branch.%s.gerritsquashhash' % upstream_branch_name],
2961 error_ok=True).strip()
2962 # Verify that the upstream branch has been uploaded too, otherwise
2963 # Gerrit will create additional CLs when uploading.
2964 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2965 RunGitSilent(['rev-parse', parent + ':'])):
2966 DieWithError(
2967 '\nUpload upstream branch %s first.\n'
2968 'It is likely that this branch has been rebased since its last '
2969 'upload, so you just need to upload it again.\n'
2970 '(If you uploaded it with --no-squash, then branch dependencies '
2971 'are not supported, and you should reupload with --squash.)'
2972 % upstream_branch_name,
2973 change_desc)
2974 return parent
2975
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002976 def _AddChangeIdToCommitMessage(self, options, args):
2977 """Re-commits using the current message, assumes the commit hook is in
2978 place.
2979 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002980 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002981 git_command = ['commit', '--amend', '-m', log_desc]
2982 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002983 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002984 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002985 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002986 return new_log_desc
2987 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002988 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002989
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002990 def SetCQState(self, new_state):
2991 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002992 vote_map = {
2993 _CQState.NONE: 0,
2994 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002995 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002996 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002997 labels = {'Commit-Queue': vote_map[new_state]}
2998 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002999 gerrit_util.SetReview(
3000 self._GetGerritHost(), self._GerritChangeIdentifier(),
3001 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003002
tandriie113dfd2016-10-11 10:20:12 -07003003 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003004 try:
3005 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003006 except GerritChangeNotExists:
3007 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003008
3009 if data['status'] in ('ABANDONED', 'MERGED'):
3010 return 'CL %s is closed' % self.GetIssue()
3011
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003012 def GetTryJobProperties(self, patchset=None):
3013 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003014 data = self._GetChangeDetail(['ALL_REVISIONS'])
3015 patchset = int(patchset or self.GetPatchset())
3016 assert patchset
3017 revision_data = None # Pylint wants it to be defined.
3018 for revision_data in data['revisions'].itervalues():
3019 if int(revision_data['_number']) == patchset:
3020 break
3021 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003022 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003023 (patchset, self.GetIssue()))
3024 return {
3025 'patch_issue': self.GetIssue(),
3026 'patch_set': patchset or self.GetPatchset(),
3027 'patch_project': data['project'],
3028 'patch_storage': 'gerrit',
3029 'patch_ref': revision_data['fetch']['http']['ref'],
3030 'patch_repository_url': revision_data['fetch']['http']['url'],
3031 'patch_gerrit_url': self.GetCodereviewServer(),
3032 }
tandriie113dfd2016-10-11 10:20:12 -07003033
tandriide281ae2016-10-12 06:02:30 -07003034 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003035 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003036
Edward Lemur707d70b2018-02-07 00:50:14 +01003037 def GetReviewers(self):
3038 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003039 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003040
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003041
3042_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003043 'gerrit': _GerritChangelistImpl,
3044}
3045
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003046
iannuccie53c9352016-08-17 14:40:40 -07003047def _add_codereview_issue_select_options(parser, extra=""):
3048 _add_codereview_select_options(parser)
3049
3050 text = ('Operate on this issue number instead of the current branch\'s '
3051 'implicit issue.')
3052 if extra:
3053 text += ' '+extra
3054 parser.add_option('-i', '--issue', type=int, help=text)
3055
3056
3057def _process_codereview_issue_select_options(parser, options):
3058 _process_codereview_select_options(parser, options)
3059 if options.issue is not None and not options.forced_codereview:
3060 parser.error('--issue must be specified with either --rietveld or --gerrit')
3061
3062
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003063def _add_codereview_select_options(parser):
3064 """Appends --gerrit and --rietveld options to force specific codereview."""
3065 parser.codereview_group = optparse.OptionGroup(
3066 parser, 'EXPERIMENTAL! Codereview override options')
3067 parser.add_option_group(parser.codereview_group)
3068 parser.codereview_group.add_option(
3069 '--gerrit', action='store_true',
3070 help='Force the use of Gerrit for codereview')
3071 parser.codereview_group.add_option(
3072 '--rietveld', action='store_true',
3073 help='Force the use of Rietveld for codereview')
3074
3075
3076def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003077 if options.rietveld:
3078 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003079 options.forced_codereview = None
3080 if options.gerrit:
3081 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003082
3083
tandriif9aefb72016-07-01 09:06:51 -07003084def _get_bug_line_values(default_project, bugs):
3085 """Given default_project and comma separated list of bugs, yields bug line
3086 values.
3087
3088 Each bug can be either:
3089 * a number, which is combined with default_project
3090 * string, which is left as is.
3091
3092 This function may produce more than one line, because bugdroid expects one
3093 project per line.
3094
3095 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3096 ['v8:123', 'chromium:789']
3097 """
3098 default_bugs = []
3099 others = []
3100 for bug in bugs.split(','):
3101 bug = bug.strip()
3102 if bug:
3103 try:
3104 default_bugs.append(int(bug))
3105 except ValueError:
3106 others.append(bug)
3107
3108 if default_bugs:
3109 default_bugs = ','.join(map(str, default_bugs))
3110 if default_project:
3111 yield '%s:%s' % (default_project, default_bugs)
3112 else:
3113 yield default_bugs
3114 for other in sorted(others):
3115 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3116 yield other
3117
3118
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003119class ChangeDescription(object):
3120 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003121 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003122 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003123 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003124 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003125 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3126 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3127 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3128 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003129
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003130 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003131 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003132
agable@chromium.org42c20792013-09-12 17:34:49 +00003133 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003134 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003135 return '\n'.join(self._description_lines)
3136
3137 def set_description(self, desc):
3138 if isinstance(desc, basestring):
3139 lines = desc.splitlines()
3140 else:
3141 lines = [line.rstrip() for line in desc]
3142 while lines and not lines[0]:
3143 lines.pop(0)
3144 while lines and not lines[-1]:
3145 lines.pop(-1)
3146 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003147
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003148 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3149 """Rewrites the R=/TBR= line(s) as a single line each.
3150
3151 Args:
3152 reviewers (list(str)) - list of additional emails to use for reviewers.
3153 tbrs (list(str)) - list of additional emails to use for TBRs.
3154 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3155 the change that are missing OWNER coverage. If this is not None, you
3156 must also pass a value for `change`.
3157 change (Change) - The Change that should be used for OWNERS lookups.
3158 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003159 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003160 assert isinstance(tbrs, list), tbrs
3161
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003162 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003163 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003164
3165 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003166 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003167
3168 reviewers = set(reviewers)
3169 tbrs = set(tbrs)
3170 LOOKUP = {
3171 'TBR': tbrs,
3172 'R': reviewers,
3173 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003174
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003175 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003176 regexp = re.compile(self.R_LINE)
3177 matches = [regexp.match(line) for line in self._description_lines]
3178 new_desc = [l for i, l in enumerate(self._description_lines)
3179 if not matches[i]]
3180 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003181
agable@chromium.org42c20792013-09-12 17:34:49 +00003182 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003183
3184 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003185 for match in matches:
3186 if not match:
3187 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003188 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3189
3190 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003191 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003192 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003193 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003194 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003195 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003196 LOOKUP[add_owners_to].update(
3197 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003198
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003199 # If any folks ended up in both groups, remove them from tbrs.
3200 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003201
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003202 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3203 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003204
3205 # Put the new lines in the description where the old first R= line was.
3206 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3207 if 0 <= line_loc < len(self._description_lines):
3208 if new_tbr_line:
3209 self._description_lines.insert(line_loc, new_tbr_line)
3210 if new_r_line:
3211 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003212 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003213 if new_r_line:
3214 self.append_footer(new_r_line)
3215 if new_tbr_line:
3216 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003217
Aaron Gable3a16ed12017-03-23 10:51:55 -07003218 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003219 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003220 self.set_description([
3221 '# Enter a description of the change.',
3222 '# This will be displayed on the codereview site.',
3223 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003224 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003225 '--------------------',
3226 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003227
agable@chromium.org42c20792013-09-12 17:34:49 +00003228 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003229 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00003230 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003231 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003232 if git_footer:
3233 self.append_footer('Bug: %s' % ', '.join(values))
3234 else:
3235 for value in values:
3236 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003237
agable@chromium.org42c20792013-09-12 17:34:49 +00003238 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003239 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003240 if not content:
3241 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003242 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003243
Bruce Dawson2377b012018-01-11 16:46:49 -08003244 # Strip off comments and default inserted "Bug:" line.
3245 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003246 (line.startswith('#') or
3247 line.rstrip() == "Bug:" or
3248 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003249 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003250 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003251 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003252
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003253 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003254 """Adds a footer line to the description.
3255
3256 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3257 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3258 that Gerrit footers are always at the end.
3259 """
3260 parsed_footer_line = git_footers.parse_footer(line)
3261 if parsed_footer_line:
3262 # Line is a gerrit footer in the form: Footer-Key: any value.
3263 # Thus, must be appended observing Gerrit footer rules.
3264 self.set_description(
3265 git_footers.add_footer(self.description,
3266 key=parsed_footer_line[0],
3267 value=parsed_footer_line[1]))
3268 return
3269
3270 if not self._description_lines:
3271 self._description_lines.append(line)
3272 return
3273
3274 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3275 if gerrit_footers:
3276 # git_footers.split_footers ensures that there is an empty line before
3277 # actual (gerrit) footers, if any. We have to keep it that way.
3278 assert top_lines and top_lines[-1] == ''
3279 top_lines, separator = top_lines[:-1], top_lines[-1:]
3280 else:
3281 separator = [] # No need for separator if there are no gerrit_footers.
3282
3283 prev_line = top_lines[-1] if top_lines else ''
3284 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3285 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3286 top_lines.append('')
3287 top_lines.append(line)
3288 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003289
tandrii99a72f22016-08-17 14:33:24 -07003290 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003291 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003292 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003293 reviewers = [match.group(2).strip()
3294 for match in matches
3295 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003296 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003297
bradnelsond975b302016-10-23 12:20:23 -07003298 def get_cced(self):
3299 """Retrieves the list of reviewers."""
3300 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3301 cced = [match.group(2).strip() for match in matches if match]
3302 return cleanup_list(cced)
3303
Nodir Turakulov23b82142017-11-16 11:04:25 -08003304 def get_hash_tags(self):
3305 """Extracts and sanitizes a list of Gerrit hashtags."""
3306 subject = (self._description_lines or ('',))[0]
3307 subject = re.sub(
3308 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3309
3310 tags = []
3311 start = 0
3312 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3313 while True:
3314 m = bracket_exp.match(subject, start)
3315 if not m:
3316 break
3317 tags.append(self.sanitize_hash_tag(m.group(1)))
3318 start = m.end()
3319
3320 if not tags:
3321 # Try "Tag: " prefix.
3322 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3323 if m:
3324 tags.append(self.sanitize_hash_tag(m.group(1)))
3325 return tags
3326
3327 @classmethod
3328 def sanitize_hash_tag(cls, tag):
3329 """Returns a sanitized Gerrit hash tag.
3330
3331 A sanitized hashtag can be used as a git push refspec parameter value.
3332 """
3333 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3334
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003335 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3336 """Updates this commit description given the parent.
3337
3338 This is essentially what Gnumbd used to do.
3339 Consult https://goo.gl/WMmpDe for more details.
3340 """
3341 assert parent_msg # No, orphan branch creation isn't supported.
3342 assert parent_hash
3343 assert dest_ref
3344 parent_footer_map = git_footers.parse_footers(parent_msg)
3345 # This will also happily parse svn-position, which GnumbD is no longer
3346 # supporting. While we'd generate correct footers, the verifier plugin
3347 # installed in Gerrit will block such commit (ie git push below will fail).
3348 parent_position = git_footers.get_position(parent_footer_map)
3349
3350 # Cherry-picks may have last line obscuring their prior footers,
3351 # from git_footers perspective. This is also what Gnumbd did.
3352 cp_line = None
3353 if (self._description_lines and
3354 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3355 cp_line = self._description_lines.pop()
3356
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003357 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003358
3359 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3360 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003361 for i, line in enumerate(footer_lines):
3362 k, v = git_footers.parse_footer(line) or (None, None)
3363 if k and k.startswith('Cr-'):
3364 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003365
3366 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003367 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003368 if parent_position[0] == dest_ref:
3369 # Same branch as parent.
3370 number = int(parent_position[1]) + 1
3371 else:
3372 number = 1 # New branch, and extra lineage.
3373 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3374 int(parent_position[1])))
3375
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003376 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3377 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003378
3379 self._description_lines = top_lines
3380 if cp_line:
3381 self._description_lines.append(cp_line)
3382 if self._description_lines[-1] != '':
3383 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003384 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003385
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003386
Aaron Gablea1bab272017-04-11 16:38:18 -07003387def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003388 """Retrieves the reviewers that approved a CL from the issue properties with
3389 messages.
3390
3391 Note that the list may contain reviewers that are not committer, thus are not
3392 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003393
3394 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003395 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003396 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003397 return sorted(
3398 set(
3399 message['sender']
3400 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003401 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003402 )
3403 )
3404
3405
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003406def FindCodereviewSettingsFile(filename='codereview.settings'):
3407 """Finds the given file starting in the cwd and going up.
3408
3409 Only looks up to the top of the repository unless an
3410 'inherit-review-settings-ok' file exists in the root of the repository.
3411 """
3412 inherit_ok_file = 'inherit-review-settings-ok'
3413 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003414 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003415 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3416 root = '/'
3417 while True:
3418 if filename in os.listdir(cwd):
3419 if os.path.isfile(os.path.join(cwd, filename)):
3420 return open(os.path.join(cwd, filename))
3421 if cwd == root:
3422 break
3423 cwd = os.path.dirname(cwd)
3424
3425
3426def LoadCodereviewSettingsFromFile(fileobj):
3427 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003428 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003429
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003430 def SetProperty(name, setting, unset_error_ok=False):
3431 fullname = 'rietveld.' + name
3432 if setting in keyvals:
3433 RunGit(['config', fullname, keyvals[setting]])
3434 else:
3435 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3436
tandrii48df5812016-10-17 03:55:37 -07003437 if not keyvals.get('GERRIT_HOST', False):
3438 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003439 # Only server setting is required. Other settings can be absent.
3440 # In that case, we ignore errors raised during option deletion attempt.
3441 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3442 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3443 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003444 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003445 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3446 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003447 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3448 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003449
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003450 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003451 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003452
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003453 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003454 RunGit(['config', 'gerrit.squash-uploads',
3455 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003456
tandrii@chromium.org28253532016-04-14 13:46:56 +00003457 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003458 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003459 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003461 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003462 # should be of the form
3463 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3464 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003465 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3466 keyvals['ORIGIN_URL_CONFIG']])
3467
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003468
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003469def urlretrieve(source, destination):
3470 """urllib is broken for SSL connections via a proxy therefore we
3471 can't use urllib.urlretrieve()."""
3472 with open(destination, 'w') as f:
3473 f.write(urllib2.urlopen(source).read())
3474
3475
ukai@chromium.org712d6102013-11-27 00:52:58 +00003476def hasSheBang(fname):
3477 """Checks fname is a #! script."""
3478 with open(fname) as f:
3479 return f.read(2).startswith('#!')
3480
3481
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003482# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3483def DownloadHooks(*args, **kwargs):
3484 pass
3485
3486
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003487def DownloadGerritHook(force):
3488 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003489
3490 Args:
3491 force: True to update hooks. False to install hooks if not present.
3492 """
3493 if not settings.GetIsGerrit():
3494 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003495 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003496 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3497 if not os.access(dst, os.X_OK):
3498 if os.path.exists(dst):
3499 if not force:
3500 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003501 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003502 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003503 if not hasSheBang(dst):
3504 DieWithError('Not a script: %s\n'
3505 'You need to download from\n%s\n'
3506 'into .git/hooks/commit-msg and '
3507 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003508 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3509 except Exception:
3510 if os.path.exists(dst):
3511 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003512 DieWithError('\nFailed to download hooks.\n'
3513 'You need to download from\n%s\n'
3514 'into .git/hooks/commit-msg and '
3515 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003516
3517
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003518class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003519 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003520
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003521 _GOOGLESOURCE = 'googlesource.com'
3522
3523 def __init__(self):
3524 # Cached list of [host, identity, source], where source is either
3525 # .gitcookies or .netrc.
3526 self._all_hosts = None
3527
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003528 def ensure_configured_gitcookies(self):
3529 """Runs checks and suggests fixes to make git use .gitcookies from default
3530 path."""
3531 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3532 configured_path = RunGitSilent(
3533 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003534 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003535 if configured_path:
3536 self._ensure_default_gitcookies_path(configured_path, default)
3537 else:
3538 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003539
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003540 @staticmethod
3541 def _ensure_default_gitcookies_path(configured_path, default_path):
3542 assert configured_path
3543 if configured_path == default_path:
3544 print('git is already configured to use your .gitcookies from %s' %
3545 configured_path)
3546 return
3547
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003548 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003549 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3550 (configured_path, default_path))
3551
3552 if not os.path.exists(configured_path):
3553 print('However, your configured .gitcookies file is missing.')
3554 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3555 action='reconfigure')
3556 RunGit(['config', '--global', 'http.cookiefile', default_path])
3557 return
3558
3559 if os.path.exists(default_path):
3560 print('WARNING: default .gitcookies file already exists %s' %
3561 default_path)
3562 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3563 default_path)
3564
3565 confirm_or_exit('Move existing .gitcookies to default location?',
3566 action='move')
3567 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003568 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003569 print('Moved and reconfigured git to use .gitcookies from %s' %
3570 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003571
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003572 @staticmethod
3573 def _configure_gitcookies_path(default_path):
3574 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3575 if os.path.exists(netrc_path):
3576 print('You seem to be using outdated .netrc for git credentials: %s' %
3577 netrc_path)
3578 print('This tool will guide you through setting up recommended '
3579 '.gitcookies store for git credentials.\n'
3580 '\n'
3581 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3582 ' git config --global --unset http.cookiefile\n'
3583 ' mv %s %s.backup\n\n' % (default_path, default_path))
3584 confirm_or_exit(action='setup .gitcookies')
3585 RunGit(['config', '--global', 'http.cookiefile', default_path])
3586 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003587
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003588 def get_hosts_with_creds(self, include_netrc=False):
3589 if self._all_hosts is None:
3590 a = gerrit_util.CookiesAuthenticator()
3591 self._all_hosts = [
3592 (h, u, s)
3593 for h, u, s in itertools.chain(
3594 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3595 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3596 )
3597 if h.endswith(self._GOOGLESOURCE)
3598 ]
3599
3600 if include_netrc:
3601 return self._all_hosts
3602 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3603
3604 def print_current_creds(self, include_netrc=False):
3605 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3606 if not hosts:
3607 print('No Git/Gerrit credentials found')
3608 return
3609 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3610 header = [('Host', 'User', 'Which file'),
3611 ['=' * l for l in lengths]]
3612 for row in (header + hosts):
3613 print('\t'.join((('%%+%ds' % l) % s)
3614 for l, s in zip(lengths, row)))
3615
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003616 @staticmethod
3617 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003618 """Parses identity "git-<username>.domain" into <username> and domain."""
3619 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003620 # distinguishable from sub-domains. But we do know typical domains:
3621 if identity.endswith('.chromium.org'):
3622 domain = 'chromium.org'
3623 username = identity[:-len('.chromium.org')]
3624 else:
3625 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003626 if username.startswith('git-'):
3627 username = username[len('git-'):]
3628 return username, domain
3629
3630 def _get_usernames_of_domain(self, domain):
3631 """Returns list of usernames referenced by .gitcookies in a given domain."""
3632 identities_by_domain = {}
3633 for _, identity, _ in self.get_hosts_with_creds():
3634 username, domain = self._parse_identity(identity)
3635 identities_by_domain.setdefault(domain, []).append(username)
3636 return identities_by_domain.get(domain)
3637
3638 def _canonical_git_googlesource_host(self, host):
3639 """Normalizes Gerrit hosts (with '-review') to Git host."""
3640 assert host.endswith(self._GOOGLESOURCE)
3641 # Prefix doesn't include '.' at the end.
3642 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3643 if prefix.endswith('-review'):
3644 prefix = prefix[:-len('-review')]
3645 return prefix + '.' + self._GOOGLESOURCE
3646
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003647 def _canonical_gerrit_googlesource_host(self, host):
3648 git_host = self._canonical_git_googlesource_host(host)
3649 prefix = git_host.split('.', 1)[0]
3650 return prefix + '-review.' + self._GOOGLESOURCE
3651
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003652 def _get_counterpart_host(self, host):
3653 assert host.endswith(self._GOOGLESOURCE)
3654 git = self._canonical_git_googlesource_host(host)
3655 gerrit = self._canonical_gerrit_googlesource_host(git)
3656 return git if gerrit == host else gerrit
3657
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003658 def has_generic_host(self):
3659 """Returns whether generic .googlesource.com has been configured.
3660
3661 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3662 """
3663 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3664 if host == '.' + self._GOOGLESOURCE:
3665 return True
3666 return False
3667
3668 def _get_git_gerrit_identity_pairs(self):
3669 """Returns map from canonic host to pair of identities (Git, Gerrit).
3670
3671 One of identities might be None, meaning not configured.
3672 """
3673 host_to_identity_pairs = {}
3674 for host, identity, _ in self.get_hosts_with_creds():
3675 canonical = self._canonical_git_googlesource_host(host)
3676 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3677 idx = 0 if canonical == host else 1
3678 pair[idx] = identity
3679 return host_to_identity_pairs
3680
3681 def get_partially_configured_hosts(self):
3682 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003683 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3684 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3685 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003686
3687 def get_conflicting_hosts(self):
3688 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003689 host
3690 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003691 if None not in (i1, i2) and i1 != i2)
3692
3693 def get_duplicated_hosts(self):
3694 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3695 return set(host for host, count in counters.iteritems() if count > 1)
3696
3697 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3698 'chromium.googlesource.com': 'chromium.org',
3699 'chrome-internal.googlesource.com': 'google.com',
3700 }
3701
3702 def get_hosts_with_wrong_identities(self):
3703 """Finds hosts which **likely** reference wrong identities.
3704
3705 Note: skips hosts which have conflicting identities for Git and Gerrit.
3706 """
3707 hosts = set()
3708 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3709 pair = self._get_git_gerrit_identity_pairs().get(host)
3710 if pair and pair[0] == pair[1]:
3711 _, domain = self._parse_identity(pair[0])
3712 if domain != expected:
3713 hosts.add(host)
3714 return hosts
3715
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003716 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003717 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003718 hosts = sorted(hosts)
3719 assert hosts
3720 if extra_column_func is None:
3721 extras = [''] * len(hosts)
3722 else:
3723 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003724 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3725 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003726 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003727 lines.append(tmpl % he)
3728 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003729
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003730 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003731 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003732 yield ('.googlesource.com wildcard record detected',
3733 ['Chrome Infrastructure team recommends to list full host names '
3734 'explicitly.'],
3735 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003736
3737 dups = self.get_duplicated_hosts()
3738 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003739 yield ('The following hosts were defined twice',
3740 self._format_hosts(dups),
3741 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003742
3743 partial = self.get_partially_configured_hosts()
3744 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003745 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3746 'These hosts are missing',
3747 self._format_hosts(partial, lambda host: 'but %s defined' %
3748 self._get_counterpart_host(host)),
3749 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003750
3751 conflicting = self.get_conflicting_hosts()
3752 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003753 yield ('The following Git hosts have differing credentials from their '
3754 'Gerrit counterparts',
3755 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3756 tuple(self._get_git_gerrit_identity_pairs()[host])),
3757 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003758
3759 wrong = self.get_hosts_with_wrong_identities()
3760 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003761 yield ('These hosts likely use wrong identity',
3762 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3763 (self._get_git_gerrit_identity_pairs()[host][0],
3764 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3765 wrong)
3766
3767 def find_and_report_problems(self):
3768 """Returns True if there was at least one problem, else False."""
3769 found = False
3770 bad_hosts = set()
3771 for title, sublines, hosts in self._find_problems():
3772 if not found:
3773 found = True
3774 print('\n\n.gitcookies problem report:\n')
3775 bad_hosts.update(hosts or [])
3776 print(' %s%s' % (title , (':' if sublines else '')))
3777 if sublines:
3778 print()
3779 print(' %s' % '\n '.join(sublines))
3780 print()
3781
3782 if bad_hosts:
3783 assert found
3784 print(' You can manually remove corresponding lines in your %s file and '
3785 'visit the following URLs with correct account to generate '
3786 'correct credential lines:\n' %
3787 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3788 print(' %s' % '\n '.join(sorted(set(
3789 gerrit_util.CookiesAuthenticator().get_new_password_url(
3790 self._canonical_git_googlesource_host(host))
3791 for host in bad_hosts
3792 ))))
3793 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003794
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003795
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003796@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003797def CMDcreds_check(parser, args):
3798 """Checks credentials and suggests changes."""
3799 _, _ = parser.parse_args(args)
3800
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003801 # Code below checks .gitcookies. Abort if using something else.
3802 authn = gerrit_util.Authenticator.get()
3803 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3804 if isinstance(authn, gerrit_util.GceAuthenticator):
3805 DieWithError(
3806 'This command is not designed for GCE, are you on a bot?\n'
3807 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3808 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003809 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003810 'This command is not designed for bot environment. It checks '
3811 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003812
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003813 checker = _GitCookiesChecker()
3814 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003815
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003816 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003817 checker.print_current_creds(include_netrc=True)
3818
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003819 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003820 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003821 return 0
3822 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003823
3824
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003825@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003826def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003827 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003828 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3829 branch = ShortBranchName(branchref)
3830 _, args = parser.parse_args(args)
3831 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003832 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003833 return RunGit(['config', 'branch.%s.base-url' % branch],
3834 error_ok=False).strip()
3835 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003836 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003837 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3838 error_ok=False).strip()
3839
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003840def color_for_status(status):
3841 """Maps a Changelist status to color, for CMDstatus and other tools."""
3842 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003843 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003844 'waiting': Fore.BLUE,
3845 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003846 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003847 'lgtm': Fore.GREEN,
3848 'commit': Fore.MAGENTA,
3849 'closed': Fore.CYAN,
3850 'error': Fore.WHITE,
3851 }.get(status, Fore.WHITE)
3852
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003853
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003854def get_cl_statuses(changes, fine_grained, max_processes=None):
3855 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003856
3857 If fine_grained is true, this will fetch CL statuses from the server.
3858 Otherwise, simply indicate if there's a matching url for the given branches.
3859
3860 If max_processes is specified, it is used as the maximum number of processes
3861 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3862 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003863
3864 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003865 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003866 if not changes:
3867 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003868
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003869 if not fine_grained:
3870 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003871 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003872 for cl in changes:
3873 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003874 return
3875
3876 # First, sort out authentication issues.
3877 logging.debug('ensuring credentials exist')
3878 for cl in changes:
3879 cl.EnsureAuthenticated(force=False, refresh=True)
3880
3881 def fetch(cl):
3882 try:
3883 return (cl, cl.GetStatus())
3884 except:
3885 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003886 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003887 raise
3888
3889 threads_count = len(changes)
3890 if max_processes:
3891 threads_count = max(1, min(threads_count, max_processes))
3892 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3893
3894 pool = ThreadPool(threads_count)
3895 fetched_cls = set()
3896 try:
3897 it = pool.imap_unordered(fetch, changes).__iter__()
3898 while True:
3899 try:
3900 cl, status = it.next(timeout=5)
3901 except multiprocessing.TimeoutError:
3902 break
3903 fetched_cls.add(cl)
3904 yield cl, status
3905 finally:
3906 pool.close()
3907
3908 # Add any branches that failed to fetch.
3909 for cl in set(changes) - fetched_cls:
3910 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003911
rmistry@google.com2dd99862015-06-22 12:22:18 +00003912
3913def upload_branch_deps(cl, args):
3914 """Uploads CLs of local branches that are dependents of the current branch.
3915
3916 If the local branch dependency tree looks like:
3917 test1 -> test2.1 -> test3.1
3918 -> test3.2
3919 -> test2.2 -> test3.3
3920
3921 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3922 run on the dependent branches in this order:
3923 test2.1, test3.1, test3.2, test2.2, test3.3
3924
3925 Note: This function does not rebase your local dependent branches. Use it when
3926 you make a change to the parent branch that will not conflict with its
3927 dependent branches, and you would like their dependencies updated in
3928 Rietveld.
3929 """
3930 if git_common.is_dirty_git_tree('upload-branch-deps'):
3931 return 1
3932
3933 root_branch = cl.GetBranch()
3934 if root_branch is None:
3935 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3936 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003937 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003938 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3939 'patchset dependencies without an uploaded CL.')
3940
3941 branches = RunGit(['for-each-ref',
3942 '--format=%(refname:short) %(upstream:short)',
3943 'refs/heads'])
3944 if not branches:
3945 print('No local branches found.')
3946 return 0
3947
3948 # Create a dictionary of all local branches to the branches that are dependent
3949 # on it.
3950 tracked_to_dependents = collections.defaultdict(list)
3951 for b in branches.splitlines():
3952 tokens = b.split()
3953 if len(tokens) == 2:
3954 branch_name, tracked = tokens
3955 tracked_to_dependents[tracked].append(branch_name)
3956
vapiera7fbd5a2016-06-16 09:17:49 -07003957 print()
3958 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003959 dependents = []
3960 def traverse_dependents_preorder(branch, padding=''):
3961 dependents_to_process = tracked_to_dependents.get(branch, [])
3962 padding += ' '
3963 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003964 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003965 dependents.append(dependent)
3966 traverse_dependents_preorder(dependent, padding)
3967 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003968 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003969
3970 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003971 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003972 return 0
3973
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003974 confirm_or_exit('This command will checkout all dependent branches and run '
3975 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003976
rmistry@google.com2dd99862015-06-22 12:22:18 +00003977 # Record all dependents that failed to upload.
3978 failures = {}
3979 # Go through all dependents, checkout the branch and upload.
3980 try:
3981 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003982 print()
3983 print('--------------------------------------')
3984 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003985 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003986 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003987 try:
3988 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003989 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003990 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003991 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003992 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003993 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003994 finally:
3995 # Swap back to the original root branch.
3996 RunGit(['checkout', '-q', root_branch])
3997
vapiera7fbd5a2016-06-16 09:17:49 -07003998 print()
3999 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004000 for dependent_branch in dependents:
4001 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004002 print(' %s : %s' % (dependent_branch, upload_status))
4003 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004004
4005 return 0
4006
4007
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004008@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004009def CMDarchive(parser, args):
4010 """Archives and deletes branches associated with closed changelists."""
4011 parser.add_option(
4012 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004013 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004014 parser.add_option(
4015 '-f', '--force', action='store_true',
4016 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004017 parser.add_option(
4018 '-d', '--dry-run', action='store_true',
4019 help='Skip the branch tagging and removal steps.')
4020 parser.add_option(
4021 '-t', '--notags', action='store_true',
4022 help='Do not tag archived branches. '
4023 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004024
4025 auth.add_auth_options(parser)
4026 options, args = parser.parse_args(args)
4027 if args:
4028 parser.error('Unsupported args: %s' % ' '.join(args))
4029 auth_config = auth.extract_auth_config_from_options(options)
4030
4031 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4032 if not branches:
4033 return 0
4034
vapiera7fbd5a2016-06-16 09:17:49 -07004035 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004036 changes = [Changelist(branchref=b, auth_config=auth_config)
4037 for b in branches.splitlines()]
4038 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4039 statuses = get_cl_statuses(changes,
4040 fine_grained=True,
4041 max_processes=options.maxjobs)
4042 proposal = [(cl.GetBranch(),
4043 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4044 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004045 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004046 proposal.sort()
4047
4048 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004049 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004050 return 0
4051
4052 current_branch = GetCurrentBranch()
4053
vapiera7fbd5a2016-06-16 09:17:49 -07004054 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004055 if options.notags:
4056 for next_item in proposal:
4057 print(' ' + next_item[0])
4058 else:
4059 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4060 for next_item in proposal:
4061 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004062
kmarshall9249e012016-08-23 12:02:16 -07004063 # Quit now on precondition failure or if instructed by the user, either
4064 # via an interactive prompt or by command line flags.
4065 if options.dry_run:
4066 print('\nNo changes were made (dry run).\n')
4067 return 0
4068 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004069 print('You are currently on a branch \'%s\' which is associated with a '
4070 'closed codereview issue, so archive cannot proceed. Please '
4071 'checkout another branch and run this command again.' %
4072 current_branch)
4073 return 1
kmarshall9249e012016-08-23 12:02:16 -07004074 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004075 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4076 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004077 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004078 return 1
4079
4080 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004081 if not options.notags:
4082 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004083 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004084
vapiera7fbd5a2016-06-16 09:17:49 -07004085 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004086
4087 return 0
4088
4089
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004090@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004091def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004092 """Show status of changelists.
4093
4094 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004095 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004096 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004097 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004098 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004099 - Magenta in the commit queue
4100 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004101 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004102
4103 Also see 'git cl comments'.
4104 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004105 parser.add_option(
4106 '--no-branch-color',
4107 action='store_true',
4108 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004109 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004110 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004111 parser.add_option('-f', '--fast', action='store_true',
4112 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004113 parser.add_option(
4114 '-j', '--maxjobs', action='store', type=int,
4115 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004116
4117 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004118 _add_codereview_issue_select_options(
4119 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004120 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004121 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004122 if args:
4123 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004124 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004125
iannuccie53c9352016-08-17 14:40:40 -07004126 if options.issue is not None and not options.field:
4127 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004128
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004129 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004130 cl = Changelist(auth_config=auth_config, issue=options.issue,
4131 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004132 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004133 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004134 elif options.field == 'id':
4135 issueid = cl.GetIssue()
4136 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004137 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004138 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004139 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004140 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004141 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004142 elif options.field == 'status':
4143 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004144 elif options.field == 'url':
4145 url = cl.GetIssueURL()
4146 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004147 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004148 return 0
4149
4150 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4151 if not branches:
4152 print('No local branch found.')
4153 return 0
4154
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004155 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004156 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004157 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004158 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004159 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004160 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004161 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004162
Daniel McArdlea23bf592019-02-12 00:25:12 +00004163 current_branch = GetCurrentBranch()
4164
4165 def FormatBranchName(branch, colorize=False):
4166 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4167 an asterisk when it is the current branch."""
4168
4169 asterisk = ""
4170 color = Fore.RESET
4171 if branch == current_branch:
4172 asterisk = "* "
4173 color = Fore.GREEN
4174 branch_name = ShortBranchName(branch)
4175
4176 if colorize:
4177 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004178 return asterisk + branch_name
4179
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004180 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004181
4182 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004183 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4184 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004185 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004186 c, status = output.next()
4187 branch_statuses[c.GetBranch()] = status
4188 status = branch_statuses.pop(branch)
4189 url = cl.GetIssueURL()
4190 if url and (not status or status == 'error'):
4191 # The issue probably doesn't exist anymore.
4192 url += ' (broken)'
4193
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004194 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004195 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004196 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004197 color = ''
4198 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004199 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004200
Alan Cuttera3be9a52019-03-04 18:50:33 +00004201 branch_display = FormatBranchName(branch)
4202 padding = ' ' * (alignment - len(branch_display))
4203 if not options.no_branch_color:
4204 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004205
Alan Cuttera3be9a52019-03-04 18:50:33 +00004206 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4207 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004208
vapiera7fbd5a2016-06-16 09:17:49 -07004209 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004210 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004211 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004212 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004213 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004214 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004215 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004216 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004217 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004218 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004219 print('Issue description:')
4220 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004221 return 0
4222
4223
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004224def colorize_CMDstatus_doc():
4225 """To be called once in main() to add colors to git cl status help."""
4226 colors = [i for i in dir(Fore) if i[0].isupper()]
4227
4228 def colorize_line(line):
4229 for color in colors:
4230 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004231 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004232 indent = len(line) - len(line.lstrip(' ')) + 1
4233 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4234 return line
4235
4236 lines = CMDstatus.__doc__.splitlines()
4237 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4238
4239
phajdan.jre328cf92016-08-22 04:12:17 -07004240def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004241 if path == '-':
4242 json.dump(contents, sys.stdout)
4243 else:
4244 with open(path, 'w') as f:
4245 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004246
4247
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004248@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004249@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004250def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004251 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004252
4253 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004254 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004255 parser.add_option('-r', '--reverse', action='store_true',
4256 help='Lookup the branch(es) for the specified issues. If '
4257 'no issues are specified, all branches with mapped '
4258 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004259 parser.add_option('--json',
4260 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004261 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004262 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004263 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004264
dnj@chromium.org406c4402015-03-03 17:22:28 +00004265 if options.reverse:
4266 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004267 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004268 # Reverse issue lookup.
4269 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004270
4271 git_config = {}
4272 for config in RunGit(['config', '--get-regexp',
4273 r'branch\..*issue']).splitlines():
4274 name, _space, val = config.partition(' ')
4275 git_config[name] = val
4276
dnj@chromium.org406c4402015-03-03 17:22:28 +00004277 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004278 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4279 config_key = _git_branch_config_key(ShortBranchName(branch),
4280 cls.IssueConfigKey())
4281 issue = git_config.get(config_key)
4282 if issue:
4283 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004284 if not args:
4285 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004286 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004287 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004288 try:
4289 issue_num = int(issue)
4290 except ValueError:
4291 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004292 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004293 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004294 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004295 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004296 if options.json:
4297 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004298 return 0
4299
4300 if len(args) > 0:
4301 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4302 if not issue.valid:
4303 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4304 'or no argument to list it.\n'
4305 'Maybe you want to run git cl status?')
4306 cl = Changelist(codereview=issue.codereview)
4307 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004308 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004309 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004310 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4311 if options.json:
4312 write_json(options.json, {
4313 'issue': cl.GetIssue(),
4314 'issue_url': cl.GetIssueURL(),
4315 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004316 return 0
4317
4318
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004319@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004320def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004321 """Shows or posts review comments for any changelist."""
4322 parser.add_option('-a', '--add-comment', dest='comment',
4323 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004324 parser.add_option('-p', '--publish', action='store_true',
4325 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004326 parser.add_option('-i', '--issue', dest='issue',
4327 help='review issue id (defaults to current issue). '
4328 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004329 parser.add_option('-m', '--machine-readable', dest='readable',
4330 action='store_false', default=True,
4331 help='output comments in a format compatible with '
4332 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004333 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004334 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004335 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004336 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004337 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004338 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004339 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004340
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004341 issue = None
4342 if options.issue:
4343 try:
4344 issue = int(options.issue)
4345 except ValueError:
4346 DieWithError('A review issue id is expected to be a number')
4347
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004348 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4349
4350 if not cl.IsGerrit():
4351 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004352
4353 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004354 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004355 return 0
4356
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004357 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4358 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004359 for comment in summary:
4360 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004361 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004362 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004363 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004364 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004365 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004366 elif comment.autogenerated:
4367 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004368 else:
4369 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004370 print('\n%s%s %s%s\n%s' % (
4371 color,
4372 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4373 comment.sender,
4374 Fore.RESET,
4375 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4376
smut@google.comc85ac942015-09-15 16:34:43 +00004377 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004378 def pre_serialize(c):
4379 dct = c.__dict__.copy()
4380 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4381 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004382 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004383 return 0
4384
4385
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004386@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004387@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004388def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004389 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004390 parser.add_option('-d', '--display', action='store_true',
4391 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004392 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004393 help='New description to set for this issue (- for stdin, '
4394 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004395 parser.add_option('-f', '--force', action='store_true',
4396 help='Delete any unpublished Gerrit edits for this issue '
4397 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004398
4399 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004400 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004401 options, args = parser.parse_args(args)
4402 _process_codereview_select_options(parser, options)
4403
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004404 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004405 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004406 target_issue_arg = ParseIssueNumberArgument(args[0],
4407 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004408 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004409 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004410
martiniss6eda05f2016-06-30 10:18:35 -07004411 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004412 'auth_config': auth.extract_auth_config_from_options(options),
4413 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004414 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004415 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004416 if target_issue_arg:
4417 kwargs['issue'] = target_issue_arg.issue
4418 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004419 if target_issue_arg.codereview and not options.forced_codereview:
4420 detected_codereview_from_url = True
4421 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004422
4423 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004424 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004425 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004426 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004427
4428 if detected_codereview_from_url:
4429 logging.info('canonical issue/change URL: %s (type: %s)\n',
4430 cl.GetIssueURL(), target_issue_arg.codereview)
4431
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004432 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004433
smut@google.com34fb6b12015-07-13 20:03:26 +00004434 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004435 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004436 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004437
4438 if options.new_description:
4439 text = options.new_description
4440 if text == '-':
4441 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004442 elif text == '+':
4443 base_branch = cl.GetCommonAncestorWithUpstream()
4444 change = cl.GetChange(base_branch, None, local_description=True)
4445 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004446
4447 description.set_description(text)
4448 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004449 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004450
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004451 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004452 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004453 return 0
4454
4455
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004456@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004457def CMDlint(parser, args):
4458 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004459 parser.add_option('--filter', action='append', metavar='-x,+y',
4460 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004461 auth.add_auth_options(parser)
4462 options, args = parser.parse_args(args)
4463 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004464
4465 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004466 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004467 try:
4468 import cpplint
4469 import cpplint_chromium
4470 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004471 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004472 return 1
4473
4474 # Change the current working directory before calling lint so that it
4475 # shows the correct base.
4476 previous_cwd = os.getcwd()
4477 os.chdir(settings.GetRoot())
4478 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004479 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004480 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4481 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004482 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004483 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004484 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004485
4486 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004487 command = args + files
4488 if options.filter:
4489 command = ['--filter=' + ','.join(options.filter)] + command
4490 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004491
4492 white_regex = re.compile(settings.GetLintRegex())
4493 black_regex = re.compile(settings.GetLintIgnoreRegex())
4494 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4495 for filename in filenames:
4496 if white_regex.match(filename):
4497 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004498 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004499 else:
4500 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4501 extra_check_functions)
4502 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004503 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004504 finally:
4505 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004506 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004507 if cpplint._cpplint_state.error_count != 0:
4508 return 1
4509 return 0
4510
4511
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004512@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004513def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004514 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004515 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004516 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004517 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004518 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004519 parser.add_option('--all', action='store_true',
4520 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004521 parser.add_option('--parallel', action='store_true',
4522 help='Run all tests specified by input_api.RunTests in all '
4523 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004524 auth.add_auth_options(parser)
4525 options, args = parser.parse_args(args)
4526 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004527
sbc@chromium.org71437c02015-04-09 19:29:40 +00004528 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004529 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004530 return 1
4531
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004532 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533 if args:
4534 base_branch = args[0]
4535 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004536 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004537 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004538
Aaron Gable8076c282017-11-29 14:39:41 -08004539 if options.all:
4540 base_change = cl.GetChange(base_branch, None)
4541 files = [('M', f) for f in base_change.AllFiles()]
4542 change = presubmit_support.GitChange(
4543 base_change.Name(),
4544 base_change.FullDescriptionText(),
4545 base_change.RepositoryRoot(),
4546 files,
4547 base_change.issue,
4548 base_change.patchset,
4549 base_change.author_email,
4550 base_change._upstream)
4551 else:
4552 change = cl.GetChange(base_branch, None)
4553
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004554 cl.RunHook(
4555 committing=not options.upload,
4556 may_prompt=False,
4557 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004558 change=change,
4559 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004560 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004561
4562
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004563def GenerateGerritChangeId(message):
4564 """Returns Ixxxxxx...xxx change id.
4565
4566 Works the same way as
4567 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4568 but can be called on demand on all platforms.
4569
4570 The basic idea is to generate git hash of a state of the tree, original commit
4571 message, author/committer info and timestamps.
4572 """
4573 lines = []
4574 tree_hash = RunGitSilent(['write-tree'])
4575 lines.append('tree %s' % tree_hash.strip())
4576 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4577 if code == 0:
4578 lines.append('parent %s' % parent.strip())
4579 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4580 lines.append('author %s' % author.strip())
4581 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4582 lines.append('committer %s' % committer.strip())
4583 lines.append('')
4584 # Note: Gerrit's commit-hook actually cleans message of some lines and
4585 # whitespace. This code is not doing this, but it clearly won't decrease
4586 # entropy.
4587 lines.append(message)
4588 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004589 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004590 return 'I%s' % change_hash.strip()
4591
4592
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004593def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004594 """Computes the remote branch ref to use for the CL.
4595
4596 Args:
4597 remote (str): The git remote for the CL.
4598 remote_branch (str): The git remote branch for the CL.
4599 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004600 """
4601 if not (remote and remote_branch):
4602 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004603
wittman@chromium.org455dc922015-01-26 20:15:50 +00004604 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004605 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004606 # refs, which are then translated into the remote full symbolic refs
4607 # below.
4608 if '/' not in target_branch:
4609 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4610 else:
4611 prefix_replacements = (
4612 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4613 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4614 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4615 )
4616 match = None
4617 for regex, replacement in prefix_replacements:
4618 match = re.search(regex, target_branch)
4619 if match:
4620 remote_branch = target_branch.replace(match.group(0), replacement)
4621 break
4622 if not match:
4623 # This is a branch path but not one we recognize; use as-is.
4624 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004625 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4626 # Handle the refs that need to land in different refs.
4627 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004628
wittman@chromium.org455dc922015-01-26 20:15:50 +00004629 # Create the true path to the remote branch.
4630 # Does the following translation:
4631 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4632 # * refs/remotes/origin/master -> refs/heads/master
4633 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4634 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4635 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4636 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4637 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4638 'refs/heads/')
4639 elif remote_branch.startswith('refs/remotes/branch-heads'):
4640 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004641
wittman@chromium.org455dc922015-01-26 20:15:50 +00004642 return remote_branch
4643
4644
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004645def cleanup_list(l):
4646 """Fixes a list so that comma separated items are put as individual items.
4647
4648 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4649 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4650 """
4651 items = sum((i.split(',') for i in l), [])
4652 stripped_items = (i.strip() for i in items)
4653 return sorted(filter(None, stripped_items))
4654
4655
Aaron Gable4db38df2017-11-03 14:59:07 -07004656@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004657@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004658def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004659 """Uploads the current changelist to codereview.
4660
4661 Can skip dependency patchset uploads for a branch by running:
4662 git config branch.branch_name.skip-deps-uploads True
4663 To unset run:
4664 git config --unset branch.branch_name.skip-deps-uploads
4665 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004666
4667 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4668 a bug number, this bug number is automatically populated in the CL
4669 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004670
4671 If subject contains text in square brackets or has "<text>: " prefix, such
4672 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4673 [git-cl] add support for hashtags
4674 Foo bar: implement foo
4675 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004676 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004677 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4678 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004679 parser.add_option('--bypass-watchlists', action='store_true',
4680 dest='bypass_watchlists',
4681 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004682 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004683 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004684 parser.add_option('--message', '-m', dest='message',
4685 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004686 parser.add_option('-b', '--bug',
4687 help='pre-populate the bug number(s) for this issue. '
4688 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004689 parser.add_option('--message-file', dest='message_file',
4690 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004691 parser.add_option('--title', '-t', dest='title',
4692 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004693 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004694 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004695 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004696 parser.add_option('--tbrs',
4697 action='append', default=[],
4698 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004699 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004700 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004701 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004702 parser.add_option('--hashtag', dest='hashtags',
4703 action='append', default=[],
4704 help=('Gerrit hashtag for new CL; '
4705 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004706 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004707 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004708 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004709 help='tell the commit queue to commit this patchset; '
4710 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004711 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004712 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004713 metavar='TARGET',
4714 help='Apply CL to remote ref TARGET. ' +
4715 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004716 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004717 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004718 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004719 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004720 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004721 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004722 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4723 const='TBR', help='add a set of OWNERS to TBR')
4724 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4725 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004726 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4727 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004728 help='Send the patchset to do a CQ dry run right after '
4729 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004730 parser.add_option('--dependencies', action='store_true',
4731 help='Uploads CLs of all the local branches that depend on '
4732 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004733 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4734 help='Sends your change to the CQ after an approval. Only '
4735 'works on repos that have the Auto-Submit label '
4736 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004737 parser.add_option('--parallel', action='store_true',
4738 help='Run all tests specified by input_api.RunTests in all '
4739 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004740
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004741 parser.add_option('--no-autocc', action='store_true',
4742 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004743 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004744 help='Set the review private. This implies --no-autocc.')
4745
rmistry@google.com2dd99862015-06-22 12:22:18 +00004746 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004747 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004748 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004749 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004750 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004751 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004752
sbc@chromium.org71437c02015-04-09 19:29:40 +00004753 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004754 return 1
4755
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004756 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004757 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004758 options.cc = cleanup_list(options.cc)
4759
tandriib80458a2016-06-23 12:20:07 -07004760 if options.message_file:
4761 if options.message:
4762 parser.error('only one of --message and --message-file allowed.')
4763 options.message = gclient_utils.FileRead(options.message_file)
4764 options.message_file = None
4765
tandrii4d0545a2016-07-06 03:56:49 -07004766 if options.cq_dry_run and options.use_commit_queue:
4767 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4768
Aaron Gableedbc4132017-09-11 13:22:28 -07004769 if options.use_commit_queue:
4770 options.send_mail = True
4771
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004772 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4773 settings.GetIsGerrit()
4774
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004775 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004776 if not cl.IsGerrit():
4777 # Error out with instructions for repos not yet configured for Gerrit.
4778 print('=====================================')
4779 print('NOTICE: Rietveld is no longer supported. '
4780 'You can upload changes to Gerrit with')
4781 print(' git cl upload --gerrit')
4782 print('or set Gerrit to be your default code review tool with')
4783 print(' git config gerrit.host true')
4784 print('=====================================')
4785 return 1
4786
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004787 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004788
4789
Francois Dorayd42c6812017-05-30 15:10:20 -04004790@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004791@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004792def CMDsplit(parser, args):
4793 """Splits a branch into smaller branches and uploads CLs.
4794
4795 Creates a branch and uploads a CL for each group of files modified in the
4796 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004797 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004798 the shared OWNERS file.
4799 """
4800 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004801 help="A text file containing a CL description in which "
4802 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004803 parser.add_option("-c", "--comment", dest="comment_file",
4804 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004805 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4806 default=False,
4807 help="List the files and reviewers for each CL that would "
4808 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004809 parser.add_option("--cq-dry-run", action='store_true',
4810 help="If set, will do a cq dry run for each uploaded CL. "
4811 "Please be careful when doing this; more than ~10 CLs "
4812 "has the potential to overload our build "
4813 "infrastructure. Try to upload these not during high "
4814 "load times (usually 11-3 Mountain View time). Email "
4815 "infra-dev@chromium.org with any questions.")
Takuto Ikuta51eca592019-02-14 19:40:52 +00004816 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4817 default=True,
4818 help='Sends your change to the CQ after an approval. Only '
4819 'works on repos that have the Auto-Submit label '
4820 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004821 options, _ = parser.parse_args(args)
4822
4823 if not options.description_file:
4824 parser.error('No --description flag specified.')
4825
4826 def WrappedCMDupload(args):
4827 return CMDupload(OptionParser(), args)
4828
4829 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004830 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004831 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004832
4833
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004834@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004835@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004836def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004837 """DEPRECATED: Used to commit the current changelist via git-svn."""
4838 message = ('git-cl no longer supports committing to SVN repositories via '
4839 'git-svn. You probably want to use `git cl land` instead.')
4840 print(message)
4841 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004842
4843
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004844# Two special branches used by git cl land.
4845MERGE_BRANCH = 'git-cl-commit'
4846CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4847
4848
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004849@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004850@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004851def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004852 """Commits the current changelist via git.
4853
4854 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4855 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004856 """
4857 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4858 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004859 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004860 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004861 parser.add_option('--parallel', action='store_true',
4862 help='Run all tests specified by input_api.RunTests in all '
4863 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004864 auth.add_auth_options(parser)
4865 (options, args) = parser.parse_args(args)
4866 auth_config = auth.extract_auth_config_from_options(options)
4867
4868 cl = Changelist(auth_config=auth_config)
4869
Robert Iannucci2e73d432018-03-14 01:10:47 -07004870 if not cl.IsGerrit():
4871 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004872
Robert Iannucci2e73d432018-03-14 01:10:47 -07004873 if not cl.GetIssue():
4874 DieWithError('You must upload the change first to Gerrit.\n'
4875 ' If you would rather have `git cl land` upload '
4876 'automatically for you, see http://crbug.com/642759')
4877 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004878 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004879
4880
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004881@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004882@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004883def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004884 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004885 parser.add_option('-b', dest='newbranch',
4886 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004887 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004888 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004889 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004890 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004891 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004892 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004893 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004894 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004895 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004896 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004897
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004898
4899 group = optparse.OptionGroup(
4900 parser,
4901 'Options for continuing work on the current issue uploaded from a '
4902 'different clone (e.g. different machine). Must be used independently '
4903 'from the other options. No issue number should be specified, and the '
4904 'branch must have an issue number associated with it')
4905 group.add_option('--reapply', action='store_true', dest='reapply',
4906 help='Reset the branch and reapply the issue.\n'
4907 'CAUTION: This will undo any local changes in this '
4908 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004909
4910 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004911 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004912 parser.add_option_group(group)
4913
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004914 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004915 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004916 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004917 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004918 auth_config = auth.extract_auth_config_from_options(options)
4919
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004920 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004921 if options.newbranch:
4922 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004923 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004924 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004925
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004926 cl = Changelist(auth_config=auth_config,
4927 codereview=options.forced_codereview)
4928 if not cl.GetIssue():
4929 parser.error('current branch must have an associated issue')
4930
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004931 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004932 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004933 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004934
4935 RunGit(['reset', '--hard', upstream])
4936 if options.pull:
4937 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004938
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004939 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4940 options.directory)
4941
4942 if len(args) != 1 or not args[0]:
4943 parser.error('Must specify issue number or url')
4944
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004945 target_issue_arg = ParseIssueNumberArgument(args[0],
4946 options.forced_codereview)
4947 if not target_issue_arg.valid:
4948 parser.error('invalid codereview url or CL id')
4949
4950 cl_kwargs = {
4951 'auth_config': auth_config,
4952 'codereview_host': target_issue_arg.hostname,
4953 'codereview': options.forced_codereview,
4954 }
4955 detected_codereview_from_url = False
4956 if target_issue_arg.codereview and not options.forced_codereview:
4957 detected_codereview_from_url = True
4958 cl_kwargs['codereview'] = target_issue_arg.codereview
4959 cl_kwargs['issue'] = target_issue_arg.issue
4960
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004961 # We don't want uncommitted changes mixed up with the patch.
4962 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004963 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004964
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004965 if options.newbranch:
4966 if options.force:
4967 RunGit(['branch', '-D', options.newbranch],
4968 stderr=subprocess2.PIPE, error_ok=True)
4969 RunGit(['new-branch', options.newbranch])
4970
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004971 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004972
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004973 if cl.IsGerrit():
4974 if options.reject:
4975 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004976 if options.directory:
4977 parser.error('--directory is not supported with Gerrit codereview.')
4978
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004979 if detected_codereview_from_url:
4980 print('canonical issue/change URL: %s (type: %s)\n' %
4981 (cl.GetIssueURL(), target_issue_arg.codereview))
4982
4983 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07004984 options.nocommit, options.directory,
4985 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004986
4987
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004988def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004989 """Fetches the tree status and returns either 'open', 'closed',
4990 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004991 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004992 if url:
4993 status = urllib2.urlopen(url).read().lower()
4994 if status.find('closed') != -1 or status == '0':
4995 return 'closed'
4996 elif status.find('open') != -1 or status == '1':
4997 return 'open'
4998 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004999 return 'unset'
5000
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005001
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005002def GetTreeStatusReason():
5003 """Fetches the tree status from a json url and returns the message
5004 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005005 url = settings.GetTreeStatusUrl()
5006 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005007 connection = urllib2.urlopen(json_url)
5008 status = json.loads(connection.read())
5009 connection.close()
5010 return status['message']
5011
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005012
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005013@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005014def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005015 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005016 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005017 status = GetTreeStatus()
5018 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005019 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005020 return 2
5021
vapiera7fbd5a2016-06-16 09:17:49 -07005022 print('The tree is %s' % status)
5023 print()
5024 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005025 if status != 'open':
5026 return 1
5027 return 0
5028
5029
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005030@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005031def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005032 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005033 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005034 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005035 '-b', '--bot', action='append',
5036 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5037 'times to specify multiple builders. ex: '
5038 '"-b win_rel -b win_layout". See '
5039 'the try server waterfall for the builders name and the tests '
5040 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005041 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005042 '-B', '--bucket', default='',
5043 help=('Buildbucket bucket to send the try requests.'))
5044 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005045 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005046 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005047 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005048 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005049 help='Revision to use for the try job; default: the revision will '
5050 'be determined by the try recipe that builder runs, which usually '
5051 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005052 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005053 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005054 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005055 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005056 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005057 '--category', default='git_cl_try', help='Specify custom build category.')
5058 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005059 '--project',
5060 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005061 'in recipe to determine to which repository or directory to '
5062 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005063 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005064 '-p', '--property', dest='properties', action='append', default=[],
5065 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005066 'key2=value2 etc. The value will be treated as '
5067 'json if decodable, or as string otherwise. '
5068 'NOTE: using this may make your try job not usable for CQ, '
5069 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005070 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005071 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5072 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005073 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005074 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005075 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005076 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005077 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005078 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005079
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005080 if options.master and options.master.startswith('luci.'):
5081 parser.error(
5082 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005083 # Make sure that all properties are prop=value pairs.
5084 bad_params = [x for x in options.properties if '=' not in x]
5085 if bad_params:
5086 parser.error('Got properties with missing "=": %s' % bad_params)
5087
maruel@chromium.org15192402012-09-06 12:38:29 +00005088 if args:
5089 parser.error('Unknown arguments: %s' % args)
5090
Koji Ishii31c14782018-01-08 17:17:33 +09005091 cl = Changelist(auth_config=auth_config, issue=options.issue,
5092 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005093 if not cl.GetIssue():
5094 parser.error('Need to upload first')
5095
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005096 if cl.IsGerrit():
5097 # HACK: warm up Gerrit change detail cache to save on RPCs.
5098 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5099
tandriie113dfd2016-10-11 10:20:12 -07005100 error_message = cl.CannotTriggerTryJobReason()
5101 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005102 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005103
borenet6c0efe62016-10-19 08:13:29 -07005104 if options.bucket and options.master:
5105 parser.error('Only one of --bucket and --master may be used.')
5106
qyearsley1fdfcb62016-10-24 13:22:03 -07005107 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005108
qyearsleydd49f942016-10-28 11:57:22 -07005109 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5110 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005111 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005112 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005113 print('git cl try with no bots now defaults to CQ dry run.')
5114 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5115 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005116
borenet6c0efe62016-10-19 08:13:29 -07005117 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005118 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005119 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005120 'of bot requires an initial job from a parent (usually a builder). '
5121 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005122 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005123 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005124
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005125 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005126 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005127 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005128 except BuildbucketResponseException as ex:
5129 print('ERROR: %s' % ex)
5130 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005131 return 0
5132
5133
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005134@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005135def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005136 """Prints info about try jobs associated with current CL."""
5137 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005138 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005139 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005140 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005141 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005142 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005143 '--color', action='store_true', default=setup_color.IS_TTY,
5144 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005145 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005146 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5147 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005148 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005149 '--json', help=('Path of JSON output file to write try job results to,'
5150 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005151 parser.add_option_group(group)
5152 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005153 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005154 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005155 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005156 if args:
5157 parser.error('Unrecognized args: %s' % ' '.join(args))
5158
5159 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005160 cl = Changelist(
5161 issue=options.issue, codereview=options.forced_codereview,
5162 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005163 if not cl.GetIssue():
5164 parser.error('Need to upload first')
5165
tandrii221ab252016-10-06 08:12:04 -07005166 patchset = options.patchset
5167 if not patchset:
5168 patchset = cl.GetMostRecentPatchset()
5169 if not patchset:
5170 parser.error('Codereview doesn\'t know about issue %s. '
5171 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005172 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005173 cl.GetIssue())
5174
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005175 try:
tandrii221ab252016-10-06 08:12:04 -07005176 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005177 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005178 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005179 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005180 if options.json:
5181 write_try_results_json(options.json, jobs)
5182 else:
5183 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005184 return 0
5185
5186
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005187@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005188@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005189def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005190 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005191 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005192 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005193 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005194
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005195 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005196 if args:
5197 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005198 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005199 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005200 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005201 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005202
5203 # Clear configured merge-base, if there is one.
5204 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005205 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005206 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005207 return 0
5208
5209
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005210@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005211def CMDweb(parser, args):
5212 """Opens the current CL in the web browser."""
5213 _, args = parser.parse_args(args)
5214 if args:
5215 parser.error('Unrecognized args: %s' % ' '.join(args))
5216
5217 issue_url = Changelist().GetIssueURL()
5218 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005219 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005220 return 1
5221
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005222 # Redirect I/O before invoking browser to hide its output. For example, this
5223 # allows to hide "Created new window in existing browser session." message
5224 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5225 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005226 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005227 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005228 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005229 os.open(os.devnull, os.O_RDWR)
5230 try:
5231 webbrowser.open(issue_url)
5232 finally:
5233 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005234 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005235 return 0
5236
5237
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005238@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005239def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005240 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005241 parser.add_option('-d', '--dry-run', action='store_true',
5242 help='trigger in dry run mode')
5243 parser.add_option('-c', '--clear', action='store_true',
5244 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005245 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005246 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005247 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005248 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005249 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005250 if args:
5251 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005252 if options.dry_run and options.clear:
5253 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5254
iannuccie53c9352016-08-17 14:40:40 -07005255 cl = Changelist(auth_config=auth_config, issue=options.issue,
5256 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005257 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005258 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005259 elif options.dry_run:
5260 state = _CQState.DRY_RUN
5261 else:
5262 state = _CQState.COMMIT
5263 if not cl.GetIssue():
5264 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005265 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005266 return 0
5267
5268
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005269@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005270def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005271 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005272 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005273 auth.add_auth_options(parser)
5274 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005275 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005276 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005277 if args:
5278 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005279 cl = Changelist(auth_config=auth_config, issue=options.issue,
5280 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005281 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005282 if not cl.GetIssue():
5283 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005284 cl.CloseIssue()
5285 return 0
5286
5287
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005288@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005289def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005290 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005291 parser.add_option(
5292 '--stat',
5293 action='store_true',
5294 dest='stat',
5295 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005296 auth.add_auth_options(parser)
5297 options, args = parser.parse_args(args)
5298 auth_config = auth.extract_auth_config_from_options(options)
5299 if args:
5300 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005301
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005302 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005303 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005304 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005305 if not issue:
5306 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005307
Aaron Gablea718c3e2017-08-28 17:47:28 -07005308 base = cl._GitGetBranchConfigValue('last-upload-hash')
5309 if not base:
5310 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5311 if not base:
5312 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5313 revision_info = detail['revisions'][detail['current_revision']]
5314 fetch_info = revision_info['fetch']['http']
5315 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5316 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005317
Aaron Gablea718c3e2017-08-28 17:47:28 -07005318 cmd = ['git', 'diff']
5319 if options.stat:
5320 cmd.append('--stat')
5321 cmd.append(base)
5322 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005323
5324 return 0
5325
5326
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005327@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005328def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005329 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005330 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005331 '--ignore-current',
5332 action='store_true',
5333 help='Ignore the CL\'s current reviewers and start from scratch.')
5334 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005335 '--ignore-self',
5336 action='store_true',
5337 help='Do not consider CL\'s author as an owners.')
5338 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005339 '--no-color',
5340 action='store_true',
5341 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005342 parser.add_option(
5343 '--batch',
5344 action='store_true',
5345 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005346 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005347 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005348 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005349
5350 author = RunGit(['config', 'user.email']).strip() or None
5351
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005352 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005353
5354 if args:
5355 if len(args) > 1:
5356 parser.error('Unknown args')
5357 base_branch = args[0]
5358 else:
5359 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005360 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005361
5362 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005363 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5364
5365 if options.batch:
5366 db = owners.Database(change.RepositoryRoot(), file, os.path)
5367 print('\n'.join(db.reviewers_for(affected_files, author)))
5368 return 0
5369
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005370 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005371 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005372 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005373 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005374 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005375 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005376 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005377 override_files=change.OriginalOwnersFiles(),
5378 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005379
5380
Aiden Bennerc08566e2018-10-03 17:52:42 +00005381def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005382 """Generates a diff command."""
5383 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005384 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5385
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005386 if allow_prefix:
5387 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5388 # case that diff.noprefix is set in the user's git config.
5389 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5390 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005391 diff_cmd += ['--no-prefix']
5392
5393 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005394
5395 if args:
5396 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005397 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005398 diff_cmd.append(arg)
5399 else:
5400 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005401
5402 return diff_cmd
5403
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005404
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005405def MatchingFileType(file_name, extensions):
5406 """Returns true if the file name ends with one of the given extensions."""
5407 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005408
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005409
enne@chromium.org555cfe42014-01-29 18:21:39 +00005410@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005411@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005412def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005413 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005414 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005415 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005416 parser.add_option('--full', action='store_true',
5417 help='Reformat the full content of all touched files')
5418 parser.add_option('--dry-run', action='store_true',
5419 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005420 parser.add_option(
5421 '--python',
5422 action='store_true',
5423 default=None,
5424 help='Enables python formatting on all python files.')
5425 parser.add_option(
5426 '--no-python',
5427 action='store_true',
5428 dest='python',
5429 help='Disables python formatting on all python files. '
5430 'Takes precedence over --python. '
5431 'If neither --python or --no-python are set, python '
5432 'files that have a .style.yapf file in an ancestor '
5433 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005434 parser.add_option('--js', action='store_true',
5435 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005436 parser.add_option('--diff', action='store_true',
5437 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005438 parser.add_option('--presubmit', action='store_true',
5439 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005440 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005441
Daniel Chengc55eecf2016-12-30 03:11:02 -08005442 # Normalize any remaining args against the current path, so paths relative to
5443 # the current directory are still resolved as expected.
5444 args = [os.path.join(os.getcwd(), arg) for arg in args]
5445
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005446 # git diff generates paths against the root of the repository. Change
5447 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005448 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005449 if rel_base_path:
5450 os.chdir(rel_base_path)
5451
digit@chromium.org29e47272013-05-17 17:01:46 +00005452 # Grab the merge-base commit, i.e. the upstream commit of the current
5453 # branch when it was created or the last time it was rebased. This is
5454 # to cover the case where the user may have called "git fetch origin",
5455 # moving the origin branch to a newer commit, but hasn't rebased yet.
5456 upstream_commit = None
5457 cl = Changelist()
5458 upstream_branch = cl.GetUpstreamBranch()
5459 if upstream_branch:
5460 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5461 upstream_commit = upstream_commit.strip()
5462
5463 if not upstream_commit:
5464 DieWithError('Could not find base commit for this branch. '
5465 'Are you in detached state?')
5466
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005467 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5468 diff_output = RunGit(changed_files_cmd)
5469 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005470 # Filter out files deleted by this CL
5471 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005472
Christopher Lamc5ba6922017-01-24 11:19:14 +11005473 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005474 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005475
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005476 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5477 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5478 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005479 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005480
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005481 top_dir = os.path.normpath(
5482 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5483
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005484 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5485 # formatted. This is used to block during the presubmit.
5486 return_value = 0
5487
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005488 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005489 # Locate the clang-format binary in the checkout
5490 try:
5491 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005492 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005493 DieWithError(e)
5494
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005495 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005496 cmd = [clang_format_tool]
5497 if not opts.dry_run and not opts.diff:
5498 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005499 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005500 if opts.diff:
5501 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005502 else:
5503 env = os.environ.copy()
5504 env['PATH'] = str(os.path.dirname(clang_format_tool))
5505 try:
5506 script = clang_format.FindClangFormatScriptInChromiumTree(
5507 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005508 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005509 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005510
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005511 cmd = [sys.executable, script, '-p0']
5512 if not opts.dry_run and not opts.diff:
5513 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005514
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005515 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5516 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005517
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005518 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5519 if opts.diff:
5520 sys.stdout.write(stdout)
5521 if opts.dry_run and len(stdout) > 0:
5522 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005523
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005524 # Similar code to above, but using yapf on .py files rather than clang-format
5525 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005526 py_explicitly_disabled = opts.python is not None and not opts.python
5527 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005528 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5529 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5530 if sys.platform.startswith('win'):
5531 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005532
Aiden Bennerc08566e2018-10-03 17:52:42 +00005533 # If we couldn't find a yapf file we'll default to the chromium style
5534 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005535 chromium_default_yapf_style = os.path.join(depot_tools_path,
5536 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005537 # Used for caching.
5538 yapf_configs = {}
5539 for f in python_diff_files:
5540 # Find the yapf style config for the current file, defaults to depot
5541 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005542 _FindYapfConfigFile(f, yapf_configs, top_dir)
5543
5544 # Turn on python formatting by default if a yapf config is specified.
5545 # This breaks in the case of this repo though since the specified
5546 # style file is also the global default.
5547 if opts.python is None:
5548 filtered_py_files = []
5549 for f in python_diff_files:
5550 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5551 filtered_py_files.append(f)
5552 else:
5553 filtered_py_files = python_diff_files
5554
5555 # Note: yapf still seems to fix indentation of the entire file
5556 # even if line ranges are specified.
5557 # See https://github.com/google/yapf/issues/499
5558 if not opts.full and filtered_py_files:
5559 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5560
5561 for f in filtered_py_files:
5562 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5563 if yapf_config is None:
5564 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005565
5566 cmd = [yapf_tool, '--style', yapf_config, f]
5567
5568 has_formattable_lines = False
5569 if not opts.full:
5570 # Only run yapf over changed line ranges.
5571 for diff_start, diff_len in py_line_diffs[f]:
5572 diff_end = diff_start + diff_len - 1
5573 # Yapf errors out if diff_end < diff_start but this
5574 # is a valid line range diff for a removal.
5575 if diff_end >= diff_start:
5576 has_formattable_lines = True
5577 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5578 # If all line diffs were removals we have nothing to format.
5579 if not has_formattable_lines:
5580 continue
5581
5582 if opts.diff or opts.dry_run:
5583 cmd += ['--diff']
5584 # Will return non-zero exit code if non-empty diff.
5585 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5586 if opts.diff:
5587 sys.stdout.write(stdout)
5588 elif len(stdout) > 0:
5589 return_value = 2
5590 else:
5591 cmd += ['-i']
5592 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005593
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005594 # Dart's formatter does not have the nice property of only operating on
5595 # modified chunks, so hard code full.
5596 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005597 try:
5598 command = [dart_format.FindDartFmtToolInChromiumTree()]
5599 if not opts.dry_run and not opts.diff:
5600 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005601 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005602
ppi@chromium.org6593d932016-03-03 15:41:15 +00005603 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005604 if opts.dry_run and stdout:
5605 return_value = 2
5606 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005607 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5608 'found in this checkout. Files in other languages are still '
5609 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005610
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005611 # Format GN build files. Always run on full build files for canonical form.
5612 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005613 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005614 if opts.dry_run or opts.diff:
5615 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005616 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005617 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5618 shell=sys.platform == 'win32',
5619 cwd=top_dir)
5620 if opts.dry_run and gn_ret == 2:
5621 return_value = 2 # Not formatted.
5622 elif opts.diff and gn_ret == 2:
5623 # TODO this should compute and print the actual diff.
5624 print("This change has GN build file diff for " + gn_diff_file)
5625 elif gn_ret != 0:
5626 # For non-dry run cases (and non-2 return values for dry-run), a
5627 # nonzero error code indicates a failure, probably because the file
5628 # doesn't parse.
5629 DieWithError("gn format failed on " + gn_diff_file +
5630 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005631
Ilya Shermane081cbe2017-08-15 17:51:04 -07005632 # Skip the metrics formatting from the global presubmit hook. These files have
5633 # a separate presubmit hook that issues an error if the files need formatting,
5634 # whereas the top-level presubmit script merely issues a warning. Formatting
5635 # these files is somewhat slow, so it's important not to duplicate the work.
5636 if not opts.presubmit:
5637 for xml_dir in GetDirtyMetricsDirs(diff_files):
5638 tool_dir = os.path.join(top_dir, xml_dir)
5639 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5640 if opts.dry_run or opts.diff:
5641 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005642 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005643 if opts.diff:
5644 sys.stdout.write(stdout)
5645 if opts.dry_run and stdout:
5646 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005647
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005648 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005649
Steven Holte2e664bf2017-04-21 13:10:47 -07005650def GetDirtyMetricsDirs(diff_files):
5651 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5652 metrics_xml_dirs = [
5653 os.path.join('tools', 'metrics', 'actions'),
5654 os.path.join('tools', 'metrics', 'histograms'),
5655 os.path.join('tools', 'metrics', 'rappor'),
5656 os.path.join('tools', 'metrics', 'ukm')]
5657 for xml_dir in metrics_xml_dirs:
5658 if any(file.startswith(xml_dir) for file in xml_diff_files):
5659 yield xml_dir
5660
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005661
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005662@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005663@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005664def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005665 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005666 _, args = parser.parse_args(args)
5667
5668 if len(args) != 1:
5669 parser.print_help()
5670 return 1
5671
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005672 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005673 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005674 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005675
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005676 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005677
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005678 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005679 output = RunGit(['config', '--local', '--get-regexp',
5680 r'branch\..*\.%s' % issueprefix],
5681 error_ok=True)
5682 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005683 if issue == target_issue:
5684 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005685
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005686 branches = []
5687 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005688 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005689 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005690 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005691 return 1
5692 if len(branches) == 1:
5693 RunGit(['checkout', branches[0]])
5694 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005695 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005696 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005697 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005698 which = raw_input('Choose by index: ')
5699 try:
5700 RunGit(['checkout', branches[int(which)]])
5701 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005702 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005703 return 1
5704
5705 return 0
5706
5707
maruel@chromium.org29404b52014-09-08 22:58:00 +00005708def CMDlol(parser, args):
5709 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005710 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005711 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5712 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5713 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005714 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005715 return 0
5716
5717
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005718class OptionParser(optparse.OptionParser):
5719 """Creates the option parse and add --verbose support."""
5720 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005721 optparse.OptionParser.__init__(
5722 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005723 self.add_option(
5724 '-v', '--verbose', action='count', default=0,
5725 help='Use 2 times for more debugging info')
5726
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005727 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005728 try:
5729 return self._parse_args(args)
5730 finally:
5731 # Regardless of success or failure of args parsing, we want to report
5732 # metrics, but only after logging has been initialized (if parsing
5733 # succeeded).
5734 global settings
5735 settings = Settings()
5736
5737 if not metrics.DISABLE_METRICS_COLLECTION:
5738 # GetViewVCUrl ultimately calls logging method.
5739 project_url = settings.GetViewVCUrl().strip('/+')
5740 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5741 metrics.collector.add('project_urls', [project_url])
5742
5743 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005744 # Create an optparse.Values object that will store only the actual passed
5745 # options, without the defaults.
5746 actual_options = optparse.Values()
5747 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5748 # Create an optparse.Values object with the default options.
5749 options = optparse.Values(self.get_default_values().__dict__)
5750 # Update it with the options passed by the user.
5751 options._update_careful(actual_options.__dict__)
5752 # Store the options passed by the user in an _actual_options attribute.
5753 # We store only the keys, and not the values, since the values can contain
5754 # arbitrary information, which might be PII.
5755 metrics.collector.add('arguments', actual_options.__dict__.keys())
5756
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005757 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005758 logging.basicConfig(
5759 level=levels[min(options.verbose, len(levels) - 1)],
5760 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5761 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005762
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005763 return options, args
5764
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005765
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005766def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005767 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005768 print('\nYour python version %s is unsupported, please upgrade.\n' %
5769 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005770 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005771
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005772 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005773 dispatcher = subcommand.CommandDispatcher(__name__)
5774 try:
5775 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005776 except auth.AuthenticationError as e:
5777 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005778 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005779 if e.code != 500:
5780 raise
5781 DieWithError(
5782 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5783 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005784 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005785
5786
5787if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005788 # These affect sys.stdout so do it outside of main() to simplify mocks in
5789 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005790 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005791 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005792 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005793 sys.exit(main(sys.argv[1:]))