blob: 5e5993a2eef3cb1dcccc6a9cefe79845b11a0861 [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02002# Copyright (c) 2013 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00008"""A git-command for integrating reviews on Gerrit."""
maruel@chromium.org725f1c32011-04-01 20:24:54 +00009
vapiera7fbd5a2016-06-16 09:17:49 -070010from __future__ import print_function
11
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000012from distutils.version import LooseVersion
calamity@chromium.orgffde55c2015-03-12 00:44:17 +000013from multiprocessing.pool import ThreadPool
thakis@chromium.org3421c992014-11-02 02:20:32 +000014import base64
rmistry@google.com2dd99862015-06-22 12:22:18 +000015import collections
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +010016import contextlib
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +010017import datetime
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +010018import fnmatch
sheyang@google.com6ebaf782015-05-12 19:17:54 +000019import httplib
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +010020import itertools
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000021import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000023import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import optparse
25import os
26import re
Andrii Shyshkalov353637c2017-03-14 16:52:18 +010027import shutil
ukai@chromium.org78c4b982012-02-14 02:20:26 +000028import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029import sys
Aaron Gable9a03ae02017-11-03 11:31:07 -070030import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031import textwrap
Edward Lemurfec80c42018-11-01 23:14:14 +000032import time
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000033import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000035import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000036import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000037import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000038import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000039
40try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080041 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042except ImportError:
43 pass
44
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000045from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000046from third_party import httplib2
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000047import auth
skobes6468b902016-10-24 08:45:10 -070048import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000049import clang_format
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000050import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000051import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000052import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000053import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000054import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000055import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000056import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000057import git_footers
Edward Lemur5ba1e9c2018-07-23 18:19:02 +000058import metrics
Edward Lesmes93277a72018-10-18 22:04:26 +000059import metrics_utils
piman@chromium.org336f9122014-09-04 02:16:55 +000060import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000061import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import presubmit_support
63import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040064import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000065import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000067import watchlists
68
tandrii7400cf02016-06-21 08:48:07 -070069__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000070
Edward Lemur0f58ae42019-04-30 17:24:12 +000071# Traces for git push will be stored in a traces directory inside the
72# depot_tools checkout.
73DEPOT_TOOLS = os.path.dirname(os.path.abspath(__file__))
74TRACES_DIR = os.path.join(DEPOT_TOOLS, 'traces')
75
76# When collecting traces, Git hashes will be reduced to 6 characters to reduce
77# the size after compression.
78GIT_HASH_RE = re.compile(r'\b([a-f0-9]{6})[a-f0-9]{34}\b', flags=re.I)
79# Used to redact the cookies from the gitcookies file.
80GITCOOKIES_REDACT_RE = re.compile(r'1/.*')
81
Edward Lemur1b52d872019-05-09 21:12:12 +000082# The maximum number of traces we will keep. Multiplied by 3 since we store
83# 3 files per trace.
84MAX_TRACES = 3 * 10
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 Lemur1b52d872019-05-09 21:12:12 +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 Lemur1b52d872019-05-09 21:12:12 +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 Lemur1b52d872019-05-09 21:12:12 +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 Lemurdc8e23d2019-05-07 00:45:48 +00002539
Edward Lemur1b52d872019-05-09 21:12:12 +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'
Michael Mosse7f0b4c2019-05-08 04:36:24 +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 Lemur1b52d872019-05-09 21:12:12 +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 if os.path.isfile(packet_traces):
2561 contents = gclient_utils.FileRead(packet_traces)
2562 gclient_utils.FileWrite(
2563 packet_traces, GIT_HASH_RE.sub(r'\1', contents))
2564 shutil.make_archive(traces_zip, 'zip', traces_dir)
2565
2566 # Collect and compress the git config and gitcookies.
2567 git_config = RunGit(['config', '-l'])
2568 gclient_utils.FileWrite(
2569 os.path.join(git_info_dir, 'git-config'),
2570 git_config)
2571
2572 cookie_auth = gerrit_util.Authenticator.get()
2573 if isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
2574 gitcookies_path = cookie_auth.get_gitcookies_path()
2575 if os.path.isfile(gitcookies_path):
2576 gitcookies = gclient_utils.FileRead(gitcookies_path)
2577 gclient_utils.FileWrite(
2578 os.path.join(git_info_dir, 'gitcookies'),
2579 GITCOOKIES_REDACT_RE.sub('REDACTED', gitcookies))
2580 shutil.make_archive(git_info_zip, 'zip', git_info_dir)
2581
2582 print(TRACES_MESSAGE % {'trace_name': trace_name})
2583
2584 gclient_utils.rmtree(git_info_dir)
2585
2586 def _RunGitPushWithTraces(
2587 self, change_desc, refspec, refspec_opts, git_push_metadata):
2588 """Run git push and collect the traces resulting from the execution."""
2589 # Create a temporary directory to store traces in. Traces will be compressed
2590 # and stored in a 'traces' dir inside depot_tools.
2591 traces_dir = tempfile.mkdtemp()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002592
2593 env = os.environ.copy()
2594 env['GIT_REDACT_COOKIES'] = 'o,SSO,GSSO_Uberproxy'
2595 env['GIT_TR2_EVENT'] = os.path.join(traces_dir, 'tr2-event')
2596 env['GIT_TRACE_CURL'] = os.path.join(traces_dir, 'trace-curl')
2597 env['GIT_TRACE_CURL_NO_DATA'] = '1'
2598 env['GIT_TRACE_PACKET'] = os.path.join(traces_dir, 'trace-packet')
2599
2600 try:
2601 push_returncode = 0
Edward Lemur1b52d872019-05-09 21:12:12 +00002602 remote_url = self.GetRemoteUrl()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002603 before_push = time_time()
2604 push_stdout = gclient_utils.CheckCallAndFilter(
Edward Lemur1b52d872019-05-09 21:12:12 +00002605 ['git', 'push', remote_url, refspec],
Edward Lemur0f58ae42019-04-30 17:24:12 +00002606 env=env,
2607 print_stdout=True,
2608 # Flush after every line: useful for seeing progress when running as
2609 # recipe.
2610 filter_fn=lambda _: sys.stdout.flush())
2611 except subprocess2.CalledProcessError as e:
2612 push_returncode = e.returncode
2613 DieWithError('Failed to create a change. Please examine output above '
2614 'for the reason of the failure.\n'
2615 'Hint: run command below to diagnose common Git/Gerrit '
2616 'credential problems:\n'
Edward Lemur1b52d872019-05-09 21:12:12 +00002617 ' git cl creds-check',
Edward Lemur0f58ae42019-04-30 17:24:12 +00002618 change_desc)
2619 finally:
2620 execution_time = time_time() - before_push
2621 metrics.collector.add_repeated('sub_commands', {
2622 'command': 'git push',
2623 'execution_time': execution_time,
2624 'exit_code': push_returncode,
2625 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2626 })
2627
Edward Lemur1b52d872019-05-09 21:12:12 +00002628 git_push_metadata['execution_time'] = execution_time
2629 git_push_metadata['exit_code'] = push_returncode
2630 self._WriteGitPushTraces(traces_dir, git_push_metadata)
Edward Lemur0f58ae42019-04-30 17:24:12 +00002631
Edward Lemur1b52d872019-05-09 21:12:12 +00002632 self._CleanUpOldTraces()
Edward Lemur0f58ae42019-04-30 17:24:12 +00002633 gclient_utils.rmtree(traces_dir)
2634
2635 return push_stdout
2636
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002637 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002638 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002639 if options.squash and options.no_squash:
2640 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002641
2642 if not options.squash and not options.no_squash:
2643 # Load default for user, repo, squash=true, in this order.
2644 options.squash = settings.GetSquashGerritUploads()
2645 elif options.no_squash:
2646 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002647
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002648 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002649 branch = GetTargetRef(remote, remote_branch, options.target_branch)
Aaron Gableb56ad332017-01-06 15:24:31 -08002650 # This may be None; default fallback value is determined in logic below.
2651 title = options.title
2652
Dominic Battre7d1c4842017-10-27 09:17:28 +02002653 # Extract bug number from branch name.
2654 bug = options.bug
2655 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2656 if not bug and match:
2657 bug = match.group(1)
2658
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002659 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002660 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002661 if self.GetIssue():
2662 # Try to get the message from a previous upload.
2663 message = self.GetDescription()
2664 if not message:
2665 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002666 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002667 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002668 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002669 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002670 # When uploading a subsequent patchset, -m|--message is taken
2671 # as the patchset title if --title was not provided.
2672 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002673 else:
2674 default_title = RunGit(
2675 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002676 if options.force:
2677 title = default_title
2678 else:
2679 title = ask_for_data(
2680 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002681 change_id = self._GetChangeDetail()['change_id']
2682 while True:
2683 footer_change_ids = git_footers.get_footer_change_id(message)
2684 if footer_change_ids == [change_id]:
2685 break
2686 if not footer_change_ids:
2687 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002688 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002689 continue
2690 # There is already a valid footer but with different or several ids.
2691 # Doing this automatically is non-trivial as we don't want to lose
2692 # existing other footers, yet we want to append just 1 desired
2693 # Change-Id. Thus, just create a new footer, but let user verify the
2694 # new description.
2695 message = '%s\n\nChange-Id: %s' % (message, change_id)
2696 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002697 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002698 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002699 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002700 'Please, check the proposed correction to the description, '
2701 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2702 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2703 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002704 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002705 if not options.force:
2706 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002707 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002708 message = change_desc.description
2709 if not message:
2710 DieWithError("Description is empty. Aborting...")
2711 # Continue the while loop.
2712 # Sanity check of this code - we should end up with proper message
2713 # footer.
2714 assert [change_id] == git_footers.get_footer_change_id(message)
2715 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002716 else: # if not self.GetIssue()
2717 if options.message:
2718 message = options.message
2719 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002720 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002721 if options.title:
2722 message = options.title + '\n\n' + message
2723 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002724
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002725 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002726 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002727 # On first upload, patchset title is always this string, while
2728 # --title flag gets converted to first line of message.
2729 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002730 if not change_desc.description:
2731 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002732 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002733 if len(change_ids) > 1:
2734 DieWithError('too many Change-Id footers, at most 1 allowed.')
2735 if not change_ids:
2736 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002737 change_desc.set_description(git_footers.add_footer_change_id(
2738 change_desc.description,
2739 GenerateGerritChangeId(change_desc.description)))
2740 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002741 assert len(change_ids) == 1
2742 change_id = change_ids[0]
2743
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002744 if options.reviewers or options.tbrs or options.add_owners_to:
2745 change_desc.update_reviewers(options.reviewers, options.tbrs,
2746 options.add_owners_to, change)
2747
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002748 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002749 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2750 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002751 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002752 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2753 desc_tempfile.write(change_desc.description)
2754 desc_tempfile.close()
2755 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2756 '-F', desc_tempfile.name]).strip()
2757 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002758 else:
2759 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002760 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002761 if not change_desc.description:
2762 DieWithError("Description is empty. Aborting...")
2763
2764 if not git_footers.get_footer_change_id(change_desc.description):
2765 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002766 change_desc.set_description(
2767 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002768 if options.reviewers or options.tbrs or options.add_owners_to:
2769 change_desc.update_reviewers(options.reviewers, options.tbrs,
2770 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002771 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002772 # For no-squash mode, we assume the remote called "origin" is the one we
2773 # want. It is not worthwhile to support different workflows for
2774 # no-squash mode.
2775 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002776 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2777
2778 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002779 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002780 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2781 ref_to_push)]).splitlines()
2782 if len(commits) > 1:
2783 print('WARNING: This will upload %d commits. Run the following command '
2784 'to see which commits will be uploaded: ' % len(commits))
2785 print('git log %s..%s' % (parent, ref_to_push))
2786 print('You can also use `git squash-branch` to squash these into a '
2787 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002788 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002789
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002790 if options.reviewers or options.tbrs or options.add_owners_to:
2791 change_desc.update_reviewers(options.reviewers, options.tbrs,
2792 options.add_owners_to, change)
2793
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002794 reviewers = sorted(change_desc.get_reviewers())
2795 # Add cc's from the CC_LIST and --cc flag (if any).
2796 if not options.private and not options.no_autocc:
2797 cc = self.GetCCList().split(',')
2798 else:
2799 cc = []
2800 if options.cc:
2801 cc.extend(options.cc)
2802 cc = filter(None, [email.strip() for email in cc])
2803 if change_desc.get_cced():
2804 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002805 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2806 valid_accounts = set(reviewers + cc)
2807 # TODO(crbug/877717): relax this for all hosts.
2808 else:
2809 valid_accounts = gerrit_util.ValidAccounts(
2810 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002811 logging.info('accounts %s are recognized, %s invalid',
2812 sorted(valid_accounts),
2813 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002814
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002815 # Extra options that can be specified at push time. Doc:
2816 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002817 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002818
Aaron Gable844cf292017-06-28 11:32:59 -07002819 # By default, new changes are started in WIP mode, and subsequent patchsets
2820 # don't send email. At any time, passing --send-mail will mark the change
2821 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002822 if options.send_mail:
2823 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002824 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002825 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002826 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002827 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002828 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002829
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002830 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002831 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002832
Aaron Gable9b713dd2016-12-14 16:04:21 -08002833 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002834 # Punctuation and whitespace in |title| must be percent-encoded.
2835 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002836
agablec6787972016-09-09 16:13:34 -07002837 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002838 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002839
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002840 for r in sorted(reviewers):
2841 if r in valid_accounts:
2842 refspec_opts.append('r=%s' % r)
2843 reviewers.remove(r)
2844 else:
2845 # TODO(tandrii): this should probably be a hard failure.
2846 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2847 % r)
2848 for c in sorted(cc):
2849 # refspec option will be rejected if cc doesn't correspond to an
2850 # account, even though REST call to add such arbitrary cc may succeed.
2851 if c in valid_accounts:
2852 refspec_opts.append('cc=%s' % c)
2853 cc.remove(c)
2854
rmistry9eadede2016-09-19 11:22:43 -07002855 if options.topic:
2856 # Documentation on Gerrit topics is here:
2857 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002858 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002859
Edward Lemur687ca902018-12-05 02:30:30 +00002860 if options.enable_auto_submit:
2861 refspec_opts.append('l=Auto-Submit+1')
2862 if options.use_commit_queue:
2863 refspec_opts.append('l=Commit-Queue+2')
2864 elif options.cq_dry_run:
2865 refspec_opts.append('l=Commit-Queue+1')
2866
2867 if change_desc.get_reviewers(tbr_only=True):
2868 score = gerrit_util.GetCodeReviewTbrScore(
2869 self._GetGerritHost(),
2870 self._GetGerritProject())
2871 refspec_opts.append('l=Code-Review+%s' % score)
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002872
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002873 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002874 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002875 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002876 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002877 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2878
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002879 refspec_suffix = ''
2880 if refspec_opts:
2881 refspec_suffix = '%' + ','.join(refspec_opts)
2882 assert ' ' not in refspec_suffix, (
2883 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2884 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2885
Edward Lemur1b52d872019-05-09 21:12:12 +00002886 git_push_metadata = {
2887 'gerrit_host': self._GetGerritHost(),
2888 'title': title or '<untitled>',
2889 'change_id': change_id,
2890 'description': change_desc.description,
2891 }
2892 push_stdout = self._RunGitPushWithTraces(
2893 change_desc, refspec, refspec_opts, git_push_metadata)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002894
2895 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002896 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002897 change_numbers = [m.group(1)
2898 for m in map(regex.match, push_stdout.splitlines())
2899 if m]
2900 if len(change_numbers) != 1:
2901 DieWithError(
2902 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002903 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002904 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002905 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002906
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002907 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002908 # GetIssue() is not set in case of non-squash uploads according to tests.
2909 # TODO(agable): non-squash uploads in git cl should be removed.
2910 gerrit_util.AddReviewers(
2911 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002912 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002913 reviewers, cc,
2914 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002915
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002916 return 0
2917
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002918 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
2919 change_desc):
2920 """Computes parent of the generated commit to be uploaded to Gerrit.
2921
2922 Returns revision or a ref name.
2923 """
2924 if custom_cl_base:
2925 # Try to avoid creating additional unintended CLs when uploading, unless
2926 # user wants to take this risk.
2927 local_ref_of_target_remote = self.GetRemoteBranch()[1]
2928 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
2929 local_ref_of_target_remote])
2930 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002931 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002932 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
2933 'If you proceed with upload, more than 1 CL may be created by '
2934 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
2935 'If you are certain that specified base `%s` has already been '
2936 'uploaded to Gerrit as another CL, you may proceed.\n' %
2937 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
2938 if not force:
2939 confirm_or_exit(
2940 'Do you take responsibility for cleaning up potential mess '
2941 'resulting from proceeding with upload?',
2942 action='upload')
2943 return custom_cl_base
2944
Aaron Gablef97e33d2017-03-30 15:44:27 -07002945 if remote != '.':
2946 return self.GetCommonAncestorWithUpstream()
2947
2948 # If our upstream branch is local, we base our squashed commit on its
2949 # squashed version.
2950 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2951
Aaron Gablef97e33d2017-03-30 15:44:27 -07002952 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07002953 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07002954
2955 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002956 # TODO(tandrii): consider checking parent change in Gerrit and using its
2957 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
2958 # the tree hash of the parent branch. The upside is less likely bogus
2959 # requests to reupload parent change just because it's uploadhash is
2960 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07002961 parent = RunGit(['config',
2962 'branch.%s.gerritsquashhash' % upstream_branch_name],
2963 error_ok=True).strip()
2964 # Verify that the upstream branch has been uploaded too, otherwise
2965 # Gerrit will create additional CLs when uploading.
2966 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2967 RunGitSilent(['rev-parse', parent + ':'])):
2968 DieWithError(
2969 '\nUpload upstream branch %s first.\n'
2970 'It is likely that this branch has been rebased since its last '
2971 'upload, so you just need to upload it again.\n'
2972 '(If you uploaded it with --no-squash, then branch dependencies '
2973 'are not supported, and you should reupload with --squash.)'
2974 % upstream_branch_name,
2975 change_desc)
2976 return parent
2977
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002978 def _AddChangeIdToCommitMessage(self, options, args):
2979 """Re-commits using the current message, assumes the commit hook is in
2980 place.
2981 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002982 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002983 git_command = ['commit', '--amend', '-m', log_desc]
2984 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002985 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002986 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002987 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002988 return new_log_desc
2989 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002990 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002991
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002992 def SetCQState(self, new_state):
2993 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002994 vote_map = {
2995 _CQState.NONE: 0,
2996 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002997 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002998 }
Aaron Gablefc62f762017-07-17 11:12:07 -07002999 labels = {'Commit-Queue': vote_map[new_state]}
3000 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003001 gerrit_util.SetReview(
3002 self._GetGerritHost(), self._GerritChangeIdentifier(),
3003 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003004
tandriie113dfd2016-10-11 10:20:12 -07003005 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003006 try:
3007 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003008 except GerritChangeNotExists:
3009 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003010
3011 if data['status'] in ('ABANDONED', 'MERGED'):
3012 return 'CL %s is closed' % self.GetIssue()
3013
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003014 def GetTryJobProperties(self, patchset=None):
3015 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003016 data = self._GetChangeDetail(['ALL_REVISIONS'])
3017 patchset = int(patchset or self.GetPatchset())
3018 assert patchset
3019 revision_data = None # Pylint wants it to be defined.
3020 for revision_data in data['revisions'].itervalues():
3021 if int(revision_data['_number']) == patchset:
3022 break
3023 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003024 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003025 (patchset, self.GetIssue()))
3026 return {
3027 'patch_issue': self.GetIssue(),
3028 'patch_set': patchset or self.GetPatchset(),
3029 'patch_project': data['project'],
3030 'patch_storage': 'gerrit',
3031 'patch_ref': revision_data['fetch']['http']['ref'],
3032 'patch_repository_url': revision_data['fetch']['http']['url'],
3033 'patch_gerrit_url': self.GetCodereviewServer(),
3034 }
tandriie113dfd2016-10-11 10:20:12 -07003035
tandriide281ae2016-10-12 06:02:30 -07003036 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003037 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003038
Edward Lemur707d70b2018-02-07 00:50:14 +01003039 def GetReviewers(self):
3040 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003041 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003042
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003043
3044_CODEREVIEW_IMPLEMENTATIONS = {
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003045 'gerrit': _GerritChangelistImpl,
3046}
3047
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003048
iannuccie53c9352016-08-17 14:40:40 -07003049def _add_codereview_issue_select_options(parser, extra=""):
3050 _add_codereview_select_options(parser)
3051
3052 text = ('Operate on this issue number instead of the current branch\'s '
3053 'implicit issue.')
3054 if extra:
3055 text += ' '+extra
3056 parser.add_option('-i', '--issue', type=int, help=text)
3057
3058
3059def _process_codereview_issue_select_options(parser, options):
3060 _process_codereview_select_options(parser, options)
3061 if options.issue is not None and not options.forced_codereview:
3062 parser.error('--issue must be specified with either --rietveld or --gerrit')
3063
3064
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003065def _add_codereview_select_options(parser):
3066 """Appends --gerrit and --rietveld options to force specific codereview."""
3067 parser.codereview_group = optparse.OptionGroup(
3068 parser, 'EXPERIMENTAL! Codereview override options')
3069 parser.add_option_group(parser.codereview_group)
3070 parser.codereview_group.add_option(
3071 '--gerrit', action='store_true',
3072 help='Force the use of Gerrit for codereview')
3073 parser.codereview_group.add_option(
3074 '--rietveld', action='store_true',
3075 help='Force the use of Rietveld for codereview')
3076
3077
3078def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003079 if options.rietveld:
3080 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003081 options.forced_codereview = None
3082 if options.gerrit:
3083 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003084
3085
tandriif9aefb72016-07-01 09:06:51 -07003086def _get_bug_line_values(default_project, bugs):
3087 """Given default_project and comma separated list of bugs, yields bug line
3088 values.
3089
3090 Each bug can be either:
3091 * a number, which is combined with default_project
3092 * string, which is left as is.
3093
3094 This function may produce more than one line, because bugdroid expects one
3095 project per line.
3096
3097 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3098 ['v8:123', 'chromium:789']
3099 """
3100 default_bugs = []
3101 others = []
3102 for bug in bugs.split(','):
3103 bug = bug.strip()
3104 if bug:
3105 try:
3106 default_bugs.append(int(bug))
3107 except ValueError:
3108 others.append(bug)
3109
3110 if default_bugs:
3111 default_bugs = ','.join(map(str, default_bugs))
3112 if default_project:
3113 yield '%s:%s' % (default_project, default_bugs)
3114 else:
3115 yield default_bugs
3116 for other in sorted(others):
3117 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3118 yield other
3119
3120
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003121class ChangeDescription(object):
3122 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003123 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003124 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003125 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003126 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003127 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3128 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3129 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3130 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003131
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003132 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003133 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003134
agable@chromium.org42c20792013-09-12 17:34:49 +00003135 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003136 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003137 return '\n'.join(self._description_lines)
3138
3139 def set_description(self, desc):
3140 if isinstance(desc, basestring):
3141 lines = desc.splitlines()
3142 else:
3143 lines = [line.rstrip() for line in desc]
3144 while lines and not lines[0]:
3145 lines.pop(0)
3146 while lines and not lines[-1]:
3147 lines.pop(-1)
3148 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003149
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003150 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3151 """Rewrites the R=/TBR= line(s) as a single line each.
3152
3153 Args:
3154 reviewers (list(str)) - list of additional emails to use for reviewers.
3155 tbrs (list(str)) - list of additional emails to use for TBRs.
3156 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3157 the change that are missing OWNER coverage. If this is not None, you
3158 must also pass a value for `change`.
3159 change (Change) - The Change that should be used for OWNERS lookups.
3160 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003161 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003162 assert isinstance(tbrs, list), tbrs
3163
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003164 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003165 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003166
3167 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003168 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003169
3170 reviewers = set(reviewers)
3171 tbrs = set(tbrs)
3172 LOOKUP = {
3173 'TBR': tbrs,
3174 'R': reviewers,
3175 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003176
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003177 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003178 regexp = re.compile(self.R_LINE)
3179 matches = [regexp.match(line) for line in self._description_lines]
3180 new_desc = [l for i, l in enumerate(self._description_lines)
3181 if not matches[i]]
3182 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003183
agable@chromium.org42c20792013-09-12 17:34:49 +00003184 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003185
3186 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003187 for match in matches:
3188 if not match:
3189 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003190 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3191
3192 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003193 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003194 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003195 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003196 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003197 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003198 LOOKUP[add_owners_to].update(
3199 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003200
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003201 # If any folks ended up in both groups, remove them from tbrs.
3202 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003203
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003204 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3205 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003206
3207 # Put the new lines in the description where the old first R= line was.
3208 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3209 if 0 <= line_loc < len(self._description_lines):
3210 if new_tbr_line:
3211 self._description_lines.insert(line_loc, new_tbr_line)
3212 if new_r_line:
3213 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003214 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003215 if new_r_line:
3216 self.append_footer(new_r_line)
3217 if new_tbr_line:
3218 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003219
Aaron Gable3a16ed12017-03-23 10:51:55 -07003220 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003221 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003222 self.set_description([
3223 '# Enter a description of the change.',
3224 '# This will be displayed on the codereview site.',
3225 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003226 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003227 '--------------------',
3228 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003229
agable@chromium.org42c20792013-09-12 17:34:49 +00003230 regexp = re.compile(self.BUG_LINE)
Jonas Termansend0f79112019-03-22 15:28:26 +00003231 prefix = settings.GetBugPrefix()
agable@chromium.org42c20792013-09-12 17:34:49 +00003232 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003233 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003234 if git_footer:
3235 self.append_footer('Bug: %s' % ', '.join(values))
3236 else:
3237 for value in values:
3238 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003239
agable@chromium.org42c20792013-09-12 17:34:49 +00003240 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003241 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003242 if not content:
3243 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003244 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003245
Bruce Dawson2377b012018-01-11 16:46:49 -08003246 # Strip off comments and default inserted "Bug:" line.
3247 clean_lines = [line.rstrip() for line in lines if not
Jonas Termansend0f79112019-03-22 15:28:26 +00003248 (line.startswith('#') or
3249 line.rstrip() == "Bug:" or
3250 line.rstrip() == "Bug: " + prefix)]
agable@chromium.org42c20792013-09-12 17:34:49 +00003251 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003252 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003253 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003254
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003255 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003256 """Adds a footer line to the description.
3257
3258 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3259 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3260 that Gerrit footers are always at the end.
3261 """
3262 parsed_footer_line = git_footers.parse_footer(line)
3263 if parsed_footer_line:
3264 # Line is a gerrit footer in the form: Footer-Key: any value.
3265 # Thus, must be appended observing Gerrit footer rules.
3266 self.set_description(
3267 git_footers.add_footer(self.description,
3268 key=parsed_footer_line[0],
3269 value=parsed_footer_line[1]))
3270 return
3271
3272 if not self._description_lines:
3273 self._description_lines.append(line)
3274 return
3275
3276 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3277 if gerrit_footers:
3278 # git_footers.split_footers ensures that there is an empty line before
3279 # actual (gerrit) footers, if any. We have to keep it that way.
3280 assert top_lines and top_lines[-1] == ''
3281 top_lines, separator = top_lines[:-1], top_lines[-1:]
3282 else:
3283 separator = [] # No need for separator if there are no gerrit_footers.
3284
3285 prev_line = top_lines[-1] if top_lines else ''
3286 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3287 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3288 top_lines.append('')
3289 top_lines.append(line)
3290 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003291
tandrii99a72f22016-08-17 14:33:24 -07003292 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003293 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003294 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003295 reviewers = [match.group(2).strip()
3296 for match in matches
3297 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003298 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003299
bradnelsond975b302016-10-23 12:20:23 -07003300 def get_cced(self):
3301 """Retrieves the list of reviewers."""
3302 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3303 cced = [match.group(2).strip() for match in matches if match]
3304 return cleanup_list(cced)
3305
Nodir Turakulov23b82142017-11-16 11:04:25 -08003306 def get_hash_tags(self):
3307 """Extracts and sanitizes a list of Gerrit hashtags."""
3308 subject = (self._description_lines or ('',))[0]
3309 subject = re.sub(
3310 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3311
3312 tags = []
3313 start = 0
3314 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3315 while True:
3316 m = bracket_exp.match(subject, start)
3317 if not m:
3318 break
3319 tags.append(self.sanitize_hash_tag(m.group(1)))
3320 start = m.end()
3321
3322 if not tags:
3323 # Try "Tag: " prefix.
3324 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3325 if m:
3326 tags.append(self.sanitize_hash_tag(m.group(1)))
3327 return tags
3328
3329 @classmethod
3330 def sanitize_hash_tag(cls, tag):
3331 """Returns a sanitized Gerrit hash tag.
3332
3333 A sanitized hashtag can be used as a git push refspec parameter value.
3334 """
3335 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3336
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003337 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3338 """Updates this commit description given the parent.
3339
3340 This is essentially what Gnumbd used to do.
3341 Consult https://goo.gl/WMmpDe for more details.
3342 """
3343 assert parent_msg # No, orphan branch creation isn't supported.
3344 assert parent_hash
3345 assert dest_ref
3346 parent_footer_map = git_footers.parse_footers(parent_msg)
3347 # This will also happily parse svn-position, which GnumbD is no longer
3348 # supporting. While we'd generate correct footers, the verifier plugin
3349 # installed in Gerrit will block such commit (ie git push below will fail).
3350 parent_position = git_footers.get_position(parent_footer_map)
3351
3352 # Cherry-picks may have last line obscuring their prior footers,
3353 # from git_footers perspective. This is also what Gnumbd did.
3354 cp_line = None
3355 if (self._description_lines and
3356 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3357 cp_line = self._description_lines.pop()
3358
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003359 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003360
3361 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3362 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003363 for i, line in enumerate(footer_lines):
3364 k, v = git_footers.parse_footer(line) or (None, None)
3365 if k and k.startswith('Cr-'):
3366 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003367
3368 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003369 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003370 if parent_position[0] == dest_ref:
3371 # Same branch as parent.
3372 number = int(parent_position[1]) + 1
3373 else:
3374 number = 1 # New branch, and extra lineage.
3375 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3376 int(parent_position[1])))
3377
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003378 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3379 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003380
3381 self._description_lines = top_lines
3382 if cp_line:
3383 self._description_lines.append(cp_line)
3384 if self._description_lines[-1] != '':
3385 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003386 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003387
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003388
Aaron Gablea1bab272017-04-11 16:38:18 -07003389def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003390 """Retrieves the reviewers that approved a CL from the issue properties with
3391 messages.
3392
3393 Note that the list may contain reviewers that are not committer, thus are not
3394 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003395
3396 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003397 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003398 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003399 return sorted(
3400 set(
3401 message['sender']
3402 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003403 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003404 )
3405 )
3406
3407
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003408def FindCodereviewSettingsFile(filename='codereview.settings'):
3409 """Finds the given file starting in the cwd and going up.
3410
3411 Only looks up to the top of the repository unless an
3412 'inherit-review-settings-ok' file exists in the root of the repository.
3413 """
3414 inherit_ok_file = 'inherit-review-settings-ok'
3415 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003416 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003417 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3418 root = '/'
3419 while True:
3420 if filename in os.listdir(cwd):
3421 if os.path.isfile(os.path.join(cwd, filename)):
3422 return open(os.path.join(cwd, filename))
3423 if cwd == root:
3424 break
3425 cwd = os.path.dirname(cwd)
3426
3427
3428def LoadCodereviewSettingsFromFile(fileobj):
3429 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003430 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003431
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003432 def SetProperty(name, setting, unset_error_ok=False):
3433 fullname = 'rietveld.' + name
3434 if setting in keyvals:
3435 RunGit(['config', fullname, keyvals[setting]])
3436 else:
3437 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3438
tandrii48df5812016-10-17 03:55:37 -07003439 if not keyvals.get('GERRIT_HOST', False):
3440 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003441 # Only server setting is required. Other settings can be absent.
3442 # In that case, we ignore errors raised during option deletion attempt.
3443 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
3444 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3445 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003446 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003447 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3448 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003449 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3450 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003451
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003452 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003453 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003454
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003455 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003456 RunGit(['config', 'gerrit.squash-uploads',
3457 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003458
tandrii@chromium.org28253532016-04-14 13:46:56 +00003459 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003460 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003461 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3462
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003463 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003464 # should be of the form
3465 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3466 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003467 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3468 keyvals['ORIGIN_URL_CONFIG']])
3469
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003470
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003471def urlretrieve(source, destination):
3472 """urllib is broken for SSL connections via a proxy therefore we
3473 can't use urllib.urlretrieve()."""
3474 with open(destination, 'w') as f:
3475 f.write(urllib2.urlopen(source).read())
3476
3477
ukai@chromium.org712d6102013-11-27 00:52:58 +00003478def hasSheBang(fname):
3479 """Checks fname is a #! script."""
3480 with open(fname) as f:
3481 return f.read(2).startswith('#!')
3482
3483
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003484# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3485def DownloadHooks(*args, **kwargs):
3486 pass
3487
3488
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003489def DownloadGerritHook(force):
3490 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003491
3492 Args:
3493 force: True to update hooks. False to install hooks if not present.
3494 """
3495 if not settings.GetIsGerrit():
3496 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003497 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003498 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3499 if not os.access(dst, os.X_OK):
3500 if os.path.exists(dst):
3501 if not force:
3502 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003503 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003504 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003505 if not hasSheBang(dst):
3506 DieWithError('Not a script: %s\n'
3507 'You need to download from\n%s\n'
3508 'into .git/hooks/commit-msg and '
3509 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003510 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3511 except Exception:
3512 if os.path.exists(dst):
3513 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003514 DieWithError('\nFailed to download hooks.\n'
3515 'You need to download from\n%s\n'
3516 'into .git/hooks/commit-msg and '
3517 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003518
3519
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003520class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003521 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003522
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003523 _GOOGLESOURCE = 'googlesource.com'
3524
3525 def __init__(self):
3526 # Cached list of [host, identity, source], where source is either
3527 # .gitcookies or .netrc.
3528 self._all_hosts = None
3529
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003530 def ensure_configured_gitcookies(self):
3531 """Runs checks and suggests fixes to make git use .gitcookies from default
3532 path."""
3533 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3534 configured_path = RunGitSilent(
3535 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003536 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003537 if configured_path:
3538 self._ensure_default_gitcookies_path(configured_path, default)
3539 else:
3540 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003541
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003542 @staticmethod
3543 def _ensure_default_gitcookies_path(configured_path, default_path):
3544 assert configured_path
3545 if configured_path == default_path:
3546 print('git is already configured to use your .gitcookies from %s' %
3547 configured_path)
3548 return
3549
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003550 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003551 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3552 (configured_path, default_path))
3553
3554 if not os.path.exists(configured_path):
3555 print('However, your configured .gitcookies file is missing.')
3556 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3557 action='reconfigure')
3558 RunGit(['config', '--global', 'http.cookiefile', default_path])
3559 return
3560
3561 if os.path.exists(default_path):
3562 print('WARNING: default .gitcookies file already exists %s' %
3563 default_path)
3564 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3565 default_path)
3566
3567 confirm_or_exit('Move existing .gitcookies to default location?',
3568 action='move')
3569 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003570 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003571 print('Moved and reconfigured git to use .gitcookies from %s' %
3572 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003573
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003574 @staticmethod
3575 def _configure_gitcookies_path(default_path):
3576 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3577 if os.path.exists(netrc_path):
3578 print('You seem to be using outdated .netrc for git credentials: %s' %
3579 netrc_path)
3580 print('This tool will guide you through setting up recommended '
3581 '.gitcookies store for git credentials.\n'
3582 '\n'
3583 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3584 ' git config --global --unset http.cookiefile\n'
3585 ' mv %s %s.backup\n\n' % (default_path, default_path))
3586 confirm_or_exit(action='setup .gitcookies')
3587 RunGit(['config', '--global', 'http.cookiefile', default_path])
3588 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003589
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003590 def get_hosts_with_creds(self, include_netrc=False):
3591 if self._all_hosts is None:
3592 a = gerrit_util.CookiesAuthenticator()
3593 self._all_hosts = [
3594 (h, u, s)
3595 for h, u, s in itertools.chain(
3596 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3597 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3598 )
3599 if h.endswith(self._GOOGLESOURCE)
3600 ]
3601
3602 if include_netrc:
3603 return self._all_hosts
3604 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3605
3606 def print_current_creds(self, include_netrc=False):
3607 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3608 if not hosts:
3609 print('No Git/Gerrit credentials found')
3610 return
3611 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3612 header = [('Host', 'User', 'Which file'),
3613 ['=' * l for l in lengths]]
3614 for row in (header + hosts):
3615 print('\t'.join((('%%+%ds' % l) % s)
3616 for l, s in zip(lengths, row)))
3617
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003618 @staticmethod
3619 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003620 """Parses identity "git-<username>.domain" into <username> and domain."""
3621 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003622 # distinguishable from sub-domains. But we do know typical domains:
3623 if identity.endswith('.chromium.org'):
3624 domain = 'chromium.org'
3625 username = identity[:-len('.chromium.org')]
3626 else:
3627 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003628 if username.startswith('git-'):
3629 username = username[len('git-'):]
3630 return username, domain
3631
3632 def _get_usernames_of_domain(self, domain):
3633 """Returns list of usernames referenced by .gitcookies in a given domain."""
3634 identities_by_domain = {}
3635 for _, identity, _ in self.get_hosts_with_creds():
3636 username, domain = self._parse_identity(identity)
3637 identities_by_domain.setdefault(domain, []).append(username)
3638 return identities_by_domain.get(domain)
3639
3640 def _canonical_git_googlesource_host(self, host):
3641 """Normalizes Gerrit hosts (with '-review') to Git host."""
3642 assert host.endswith(self._GOOGLESOURCE)
3643 # Prefix doesn't include '.' at the end.
3644 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3645 if prefix.endswith('-review'):
3646 prefix = prefix[:-len('-review')]
3647 return prefix + '.' + self._GOOGLESOURCE
3648
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003649 def _canonical_gerrit_googlesource_host(self, host):
3650 git_host = self._canonical_git_googlesource_host(host)
3651 prefix = git_host.split('.', 1)[0]
3652 return prefix + '-review.' + self._GOOGLESOURCE
3653
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003654 def _get_counterpart_host(self, host):
3655 assert host.endswith(self._GOOGLESOURCE)
3656 git = self._canonical_git_googlesource_host(host)
3657 gerrit = self._canonical_gerrit_googlesource_host(git)
3658 return git if gerrit == host else gerrit
3659
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003660 def has_generic_host(self):
3661 """Returns whether generic .googlesource.com has been configured.
3662
3663 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3664 """
3665 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3666 if host == '.' + self._GOOGLESOURCE:
3667 return True
3668 return False
3669
3670 def _get_git_gerrit_identity_pairs(self):
3671 """Returns map from canonic host to pair of identities (Git, Gerrit).
3672
3673 One of identities might be None, meaning not configured.
3674 """
3675 host_to_identity_pairs = {}
3676 for host, identity, _ in self.get_hosts_with_creds():
3677 canonical = self._canonical_git_googlesource_host(host)
3678 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3679 idx = 0 if canonical == host else 1
3680 pair[idx] = identity
3681 return host_to_identity_pairs
3682
3683 def get_partially_configured_hosts(self):
3684 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003685 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3686 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3687 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003688
3689 def get_conflicting_hosts(self):
3690 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003691 host
3692 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003693 if None not in (i1, i2) and i1 != i2)
3694
3695 def get_duplicated_hosts(self):
3696 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3697 return set(host for host, count in counters.iteritems() if count > 1)
3698
3699 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3700 'chromium.googlesource.com': 'chromium.org',
3701 'chrome-internal.googlesource.com': 'google.com',
3702 }
3703
3704 def get_hosts_with_wrong_identities(self):
3705 """Finds hosts which **likely** reference wrong identities.
3706
3707 Note: skips hosts which have conflicting identities for Git and Gerrit.
3708 """
3709 hosts = set()
3710 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3711 pair = self._get_git_gerrit_identity_pairs().get(host)
3712 if pair and pair[0] == pair[1]:
3713 _, domain = self._parse_identity(pair[0])
3714 if domain != expected:
3715 hosts.add(host)
3716 return hosts
3717
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003718 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003719 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003720 hosts = sorted(hosts)
3721 assert hosts
3722 if extra_column_func is None:
3723 extras = [''] * len(hosts)
3724 else:
3725 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003726 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3727 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003728 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003729 lines.append(tmpl % he)
3730 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003731
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003732 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003733 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003734 yield ('.googlesource.com wildcard record detected',
3735 ['Chrome Infrastructure team recommends to list full host names '
3736 'explicitly.'],
3737 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003738
3739 dups = self.get_duplicated_hosts()
3740 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003741 yield ('The following hosts were defined twice',
3742 self._format_hosts(dups),
3743 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003744
3745 partial = self.get_partially_configured_hosts()
3746 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003747 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3748 'These hosts are missing',
3749 self._format_hosts(partial, lambda host: 'but %s defined' %
3750 self._get_counterpart_host(host)),
3751 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003752
3753 conflicting = self.get_conflicting_hosts()
3754 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003755 yield ('The following Git hosts have differing credentials from their '
3756 'Gerrit counterparts',
3757 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3758 tuple(self._get_git_gerrit_identity_pairs()[host])),
3759 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003760
3761 wrong = self.get_hosts_with_wrong_identities()
3762 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003763 yield ('These hosts likely use wrong identity',
3764 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3765 (self._get_git_gerrit_identity_pairs()[host][0],
3766 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3767 wrong)
3768
3769 def find_and_report_problems(self):
3770 """Returns True if there was at least one problem, else False."""
3771 found = False
3772 bad_hosts = set()
3773 for title, sublines, hosts in self._find_problems():
3774 if not found:
3775 found = True
3776 print('\n\n.gitcookies problem report:\n')
3777 bad_hosts.update(hosts or [])
3778 print(' %s%s' % (title , (':' if sublines else '')))
3779 if sublines:
3780 print()
3781 print(' %s' % '\n '.join(sublines))
3782 print()
3783
3784 if bad_hosts:
3785 assert found
3786 print(' You can manually remove corresponding lines in your %s file and '
3787 'visit the following URLs with correct account to generate '
3788 'correct credential lines:\n' %
3789 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3790 print(' %s' % '\n '.join(sorted(set(
3791 gerrit_util.CookiesAuthenticator().get_new_password_url(
3792 self._canonical_git_googlesource_host(host))
3793 for host in bad_hosts
3794 ))))
3795 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003796
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003797
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003798@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003799def CMDcreds_check(parser, args):
3800 """Checks credentials and suggests changes."""
3801 _, _ = parser.parse_args(args)
3802
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003803 # Code below checks .gitcookies. Abort if using something else.
3804 authn = gerrit_util.Authenticator.get()
3805 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3806 if isinstance(authn, gerrit_util.GceAuthenticator):
3807 DieWithError(
3808 'This command is not designed for GCE, are you on a bot?\n'
3809 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3810 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003811 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003812 'This command is not designed for bot environment. It checks '
3813 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003814
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003815 checker = _GitCookiesChecker()
3816 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003817
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003818 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003819 checker.print_current_creds(include_netrc=True)
3820
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003821 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003822 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003823 return 0
3824 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003825
3826
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003827@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003828def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003829 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003830 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3831 branch = ShortBranchName(branchref)
3832 _, args = parser.parse_args(args)
3833 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003834 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003835 return RunGit(['config', 'branch.%s.base-url' % branch],
3836 error_ok=False).strip()
3837 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003838 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003839 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3840 error_ok=False).strip()
3841
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003842def color_for_status(status):
3843 """Maps a Changelist status to color, for CMDstatus and other tools."""
3844 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07003845 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003846 'waiting': Fore.BLUE,
3847 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07003848 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003849 'lgtm': Fore.GREEN,
3850 'commit': Fore.MAGENTA,
3851 'closed': Fore.CYAN,
3852 'error': Fore.WHITE,
3853 }.get(status, Fore.WHITE)
3854
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003855
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003856def get_cl_statuses(changes, fine_grained, max_processes=None):
3857 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003858
3859 If fine_grained is true, this will fetch CL statuses from the server.
3860 Otherwise, simply indicate if there's a matching url for the given branches.
3861
3862 If max_processes is specified, it is used as the maximum number of processes
3863 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3864 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003865
3866 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003867 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003868 if not changes:
3869 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003870
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003871 if not fine_grained:
3872 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07003873 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003874 for cl in changes:
3875 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003876 return
3877
3878 # First, sort out authentication issues.
3879 logging.debug('ensuring credentials exist')
3880 for cl in changes:
3881 cl.EnsureAuthenticated(force=False, refresh=True)
3882
3883 def fetch(cl):
3884 try:
3885 return (cl, cl.GetStatus())
3886 except:
3887 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07003888 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01003889 raise
3890
3891 threads_count = len(changes)
3892 if max_processes:
3893 threads_count = max(1, min(threads_count, max_processes))
3894 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
3895
3896 pool = ThreadPool(threads_count)
3897 fetched_cls = set()
3898 try:
3899 it = pool.imap_unordered(fetch, changes).__iter__()
3900 while True:
3901 try:
3902 cl, status = it.next(timeout=5)
3903 except multiprocessing.TimeoutError:
3904 break
3905 fetched_cls.add(cl)
3906 yield cl, status
3907 finally:
3908 pool.close()
3909
3910 # Add any branches that failed to fetch.
3911 for cl in set(changes) - fetched_cls:
3912 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003913
rmistry@google.com2dd99862015-06-22 12:22:18 +00003914
3915def upload_branch_deps(cl, args):
3916 """Uploads CLs of local branches that are dependents of the current branch.
3917
3918 If the local branch dependency tree looks like:
3919 test1 -> test2.1 -> test3.1
3920 -> test3.2
3921 -> test2.2 -> test3.3
3922
3923 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3924 run on the dependent branches in this order:
3925 test2.1, test3.1, test3.2, test2.2, test3.3
3926
3927 Note: This function does not rebase your local dependent branches. Use it when
3928 you make a change to the parent branch that will not conflict with its
3929 dependent branches, and you would like their dependencies updated in
3930 Rietveld.
3931 """
3932 if git_common.is_dirty_git_tree('upload-branch-deps'):
3933 return 1
3934
3935 root_branch = cl.GetBranch()
3936 if root_branch is None:
3937 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3938 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00003939 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00003940 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3941 'patchset dependencies without an uploaded CL.')
3942
3943 branches = RunGit(['for-each-ref',
3944 '--format=%(refname:short) %(upstream:short)',
3945 'refs/heads'])
3946 if not branches:
3947 print('No local branches found.')
3948 return 0
3949
3950 # Create a dictionary of all local branches to the branches that are dependent
3951 # on it.
3952 tracked_to_dependents = collections.defaultdict(list)
3953 for b in branches.splitlines():
3954 tokens = b.split()
3955 if len(tokens) == 2:
3956 branch_name, tracked = tokens
3957 tracked_to_dependents[tracked].append(branch_name)
3958
vapiera7fbd5a2016-06-16 09:17:49 -07003959 print()
3960 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003961 dependents = []
3962 def traverse_dependents_preorder(branch, padding=''):
3963 dependents_to_process = tracked_to_dependents.get(branch, [])
3964 padding += ' '
3965 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003966 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003967 dependents.append(dependent)
3968 traverse_dependents_preorder(dependent, padding)
3969 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003970 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003971
3972 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003973 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003974 return 0
3975
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003976 confirm_or_exit('This command will checkout all dependent branches and run '
3977 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003978
rmistry@google.com2dd99862015-06-22 12:22:18 +00003979 # Record all dependents that failed to upload.
3980 failures = {}
3981 # Go through all dependents, checkout the branch and upload.
3982 try:
3983 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003984 print()
3985 print('--------------------------------------')
3986 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003987 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003988 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003989 try:
3990 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003991 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003992 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003993 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00003994 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003995 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003996 finally:
3997 # Swap back to the original root branch.
3998 RunGit(['checkout', '-q', root_branch])
3999
vapiera7fbd5a2016-06-16 09:17:49 -07004000 print()
4001 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004002 for dependent_branch in dependents:
4003 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004004 print(' %s : %s' % (dependent_branch, upload_status))
4005 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004006
4007 return 0
4008
4009
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004010@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004011def CMDarchive(parser, args):
4012 """Archives and deletes branches associated with closed changelists."""
4013 parser.add_option(
4014 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004015 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004016 parser.add_option(
4017 '-f', '--force', action='store_true',
4018 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004019 parser.add_option(
4020 '-d', '--dry-run', action='store_true',
4021 help='Skip the branch tagging and removal steps.')
4022 parser.add_option(
4023 '-t', '--notags', action='store_true',
4024 help='Do not tag archived branches. '
4025 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004026
4027 auth.add_auth_options(parser)
4028 options, args = parser.parse_args(args)
4029 if args:
4030 parser.error('Unsupported args: %s' % ' '.join(args))
4031 auth_config = auth.extract_auth_config_from_options(options)
4032
4033 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4034 if not branches:
4035 return 0
4036
vapiera7fbd5a2016-06-16 09:17:49 -07004037 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004038 changes = [Changelist(branchref=b, auth_config=auth_config)
4039 for b in branches.splitlines()]
4040 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4041 statuses = get_cl_statuses(changes,
4042 fine_grained=True,
4043 max_processes=options.maxjobs)
4044 proposal = [(cl.GetBranch(),
4045 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4046 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004047 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004048 proposal.sort()
4049
4050 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004051 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004052 return 0
4053
4054 current_branch = GetCurrentBranch()
4055
vapiera7fbd5a2016-06-16 09:17:49 -07004056 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004057 if options.notags:
4058 for next_item in proposal:
4059 print(' ' + next_item[0])
4060 else:
4061 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4062 for next_item in proposal:
4063 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004064
kmarshall9249e012016-08-23 12:02:16 -07004065 # Quit now on precondition failure or if instructed by the user, either
4066 # via an interactive prompt or by command line flags.
4067 if options.dry_run:
4068 print('\nNo changes were made (dry run).\n')
4069 return 0
4070 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004071 print('You are currently on a branch \'%s\' which is associated with a '
4072 'closed codereview issue, so archive cannot proceed. Please '
4073 'checkout another branch and run this command again.' %
4074 current_branch)
4075 return 1
kmarshall9249e012016-08-23 12:02:16 -07004076 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004077 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4078 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004079 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004080 return 1
4081
4082 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004083 if not options.notags:
4084 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004085 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004086
vapiera7fbd5a2016-06-16 09:17:49 -07004087 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004088
4089 return 0
4090
4091
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004092@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004093def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004094 """Show status of changelists.
4095
4096 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004097 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004098 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004099 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004100 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004101 - Magenta in the commit queue
4102 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004103 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004104
4105 Also see 'git cl comments'.
4106 """
Alan Cuttera3be9a52019-03-04 18:50:33 +00004107 parser.add_option(
4108 '--no-branch-color',
4109 action='store_true',
4110 help='Disable colorized branch names')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004111 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004112 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004113 parser.add_option('-f', '--fast', action='store_true',
4114 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004115 parser.add_option(
4116 '-j', '--maxjobs', action='store', type=int,
4117 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004118
4119 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004120 _add_codereview_issue_select_options(
4121 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004122 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004123 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004124 if args:
4125 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004126 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004127
iannuccie53c9352016-08-17 14:40:40 -07004128 if options.issue is not None and not options.field:
4129 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004130
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004131 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004132 cl = Changelist(auth_config=auth_config, issue=options.issue,
4133 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004134 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004135 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004136 elif options.field == 'id':
4137 issueid = cl.GetIssue()
4138 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004139 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004140 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004141 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004142 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004143 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004144 elif options.field == 'status':
4145 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004146 elif options.field == 'url':
4147 url = cl.GetIssueURL()
4148 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004149 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004150 return 0
4151
4152 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4153 if not branches:
4154 print('No local branch found.')
4155 return 0
4156
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004157 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004158 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004159 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004160 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004161 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004162 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004163 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004164
Daniel McArdlea23bf592019-02-12 00:25:12 +00004165 current_branch = GetCurrentBranch()
4166
4167 def FormatBranchName(branch, colorize=False):
4168 """Simulates 'git branch' behavior. Colorizes and prefixes branch name with
4169 an asterisk when it is the current branch."""
4170
4171 asterisk = ""
4172 color = Fore.RESET
4173 if branch == current_branch:
4174 asterisk = "* "
4175 color = Fore.GREEN
4176 branch_name = ShortBranchName(branch)
4177
4178 if colorize:
4179 return asterisk + color + branch_name + Fore.RESET
Daniel McArdle452a49f2019-02-14 17:28:31 +00004180 return asterisk + branch_name
4181
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004182 branch_statuses = {}
Daniel McArdlea23bf592019-02-12 00:25:12 +00004183
4184 alignment = max(5, max(len(FormatBranchName(c.GetBranch())) for c in changes))
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004185 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4186 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004187 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004188 c, status = output.next()
4189 branch_statuses[c.GetBranch()] = status
4190 status = branch_statuses.pop(branch)
4191 url = cl.GetIssueURL()
4192 if url and (not status or status == 'error'):
4193 # The issue probably doesn't exist anymore.
4194 url += ' (broken)'
4195
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004196 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004197 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004198 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004199 color = ''
4200 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004201 status_str = '(%s)' % status if status else ''
Daniel McArdle452a49f2019-02-14 17:28:31 +00004202
Alan Cuttera3be9a52019-03-04 18:50:33 +00004203 branch_display = FormatBranchName(branch)
4204 padding = ' ' * (alignment - len(branch_display))
4205 if not options.no_branch_color:
4206 branch_display = FormatBranchName(branch, colorize=True)
Daniel McArdle452a49f2019-02-14 17:28:31 +00004207
Alan Cuttera3be9a52019-03-04 18:50:33 +00004208 print(' %s : %s%s %s%s' % (padding + branch_display, color, url,
4209 status_str, reset))
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004210
vapiera7fbd5a2016-06-16 09:17:49 -07004211 print()
Daniel McArdlea23bf592019-02-12 00:25:12 +00004212 print('Current branch: %s' % current_branch)
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004213 for cl in changes:
Daniel McArdlea23bf592019-02-12 00:25:12 +00004214 if cl.GetBranch() == current_branch:
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004215 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004216 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004217 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004218 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004219 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004220 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004221 print('Issue description:')
4222 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004223 return 0
4224
4225
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004226def colorize_CMDstatus_doc():
4227 """To be called once in main() to add colors to git cl status help."""
4228 colors = [i for i in dir(Fore) if i[0].isupper()]
4229
4230 def colorize_line(line):
4231 for color in colors:
4232 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004233 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004234 indent = len(line) - len(line.lstrip(' ')) + 1
4235 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4236 return line
4237
4238 lines = CMDstatus.__doc__.splitlines()
4239 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4240
4241
phajdan.jre328cf92016-08-22 04:12:17 -07004242def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004243 if path == '-':
4244 json.dump(contents, sys.stdout)
4245 else:
4246 with open(path, 'w') as f:
4247 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004248
4249
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004250@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004251@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004252def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004253 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004254
4255 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004256 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004257 parser.add_option('-r', '--reverse', action='store_true',
4258 help='Lookup the branch(es) for the specified issues. If '
4259 'no issues are specified, all branches with mapped '
4260 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004261 parser.add_option('--json',
4262 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004263 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004264 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004265 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004266
dnj@chromium.org406c4402015-03-03 17:22:28 +00004267 if options.reverse:
4268 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004269 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004270 # Reverse issue lookup.
4271 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004272
4273 git_config = {}
4274 for config in RunGit(['config', '--get-regexp',
4275 r'branch\..*issue']).splitlines():
4276 name, _space, val = config.partition(' ')
4277 git_config[name] = val
4278
dnj@chromium.org406c4402015-03-03 17:22:28 +00004279 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004280 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4281 config_key = _git_branch_config_key(ShortBranchName(branch),
4282 cls.IssueConfigKey())
4283 issue = git_config.get(config_key)
4284 if issue:
4285 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004286 if not args:
4287 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004288 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004289 for issue in args:
Lei Zhang5a368d42019-03-25 23:18:19 +00004290 try:
4291 issue_num = int(issue)
4292 except ValueError:
4293 print('ERROR cannot parse issue number: %s' % issue, file=sys.stderr)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004294 continue
Lei Zhang5a368d42019-03-25 23:18:19 +00004295 result[issue_num] = issue_branch_map.get(issue_num)
vapiera7fbd5a2016-06-16 09:17:49 -07004296 print('Branch for issue number %s: %s' % (
Lei Zhang5a368d42019-03-25 23:18:19 +00004297 issue, ', '.join(issue_branch_map.get(issue_num) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004298 if options.json:
4299 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004300 return 0
4301
4302 if len(args) > 0:
4303 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4304 if not issue.valid:
4305 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4306 'or no argument to list it.\n'
4307 'Maybe you want to run git cl status?')
4308 cl = Changelist(codereview=issue.codereview)
4309 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004310 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004311 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004312 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4313 if options.json:
4314 write_json(options.json, {
4315 'issue': cl.GetIssue(),
4316 'issue_url': cl.GetIssueURL(),
4317 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004318 return 0
4319
4320
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004321@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004322def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004323 """Shows or posts review comments for any changelist."""
4324 parser.add_option('-a', '--add-comment', dest='comment',
4325 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004326 parser.add_option('-p', '--publish', action='store_true',
4327 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004328 parser.add_option('-i', '--issue', dest='issue',
4329 help='review issue id (defaults to current issue). '
4330 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004331 parser.add_option('-m', '--machine-readable', dest='readable',
4332 action='store_false', default=True,
4333 help='output comments in a format compatible with '
4334 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004335 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004336 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004337 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004338 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004339 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004340 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004341 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004342
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004343 issue = None
4344 if options.issue:
4345 try:
4346 issue = int(options.issue)
4347 except ValueError:
4348 DieWithError('A review issue id is expected to be a number')
4349
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004350 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4351
4352 if not cl.IsGerrit():
4353 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004354
4355 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004356 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004357 return 0
4358
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004359 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4360 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004361 for comment in summary:
4362 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004363 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004364 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004365 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004366 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004367 color = Fore.MAGENTA
Quinten Yearsley0e617c02019-02-20 00:37:03 +00004368 elif comment.autogenerated:
4369 color = Fore.CYAN
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004370 else:
4371 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004372 print('\n%s%s %s%s\n%s' % (
4373 color,
4374 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4375 comment.sender,
4376 Fore.RESET,
4377 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4378
smut@google.comc85ac942015-09-15 16:34:43 +00004379 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004380 def pre_serialize(c):
4381 dct = c.__dict__.copy()
4382 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4383 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004384 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004385 return 0
4386
4387
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004388@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004389@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004390def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004391 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004392 parser.add_option('-d', '--display', action='store_true',
4393 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004394 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004395 help='New description to set for this issue (- for stdin, '
4396 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004397 parser.add_option('-f', '--force', action='store_true',
4398 help='Delete any unpublished Gerrit edits for this issue '
4399 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004400
4401 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004402 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004403 options, args = parser.parse_args(args)
4404 _process_codereview_select_options(parser, options)
4405
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004406 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004407 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004408 target_issue_arg = ParseIssueNumberArgument(args[0],
4409 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004410 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004411 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004412
martiniss6eda05f2016-06-30 10:18:35 -07004413 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004414 'auth_config': auth.extract_auth_config_from_options(options),
4415 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004416 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004417 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004418 if target_issue_arg:
4419 kwargs['issue'] = target_issue_arg.issue
4420 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004421 if target_issue_arg.codereview and not options.forced_codereview:
4422 detected_codereview_from_url = True
4423 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004424
4425 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004426 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004427 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004428 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004429
4430 if detected_codereview_from_url:
4431 logging.info('canonical issue/change URL: %s (type: %s)\n',
4432 cl.GetIssueURL(), target_issue_arg.codereview)
4433
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004434 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004435
smut@google.com34fb6b12015-07-13 20:03:26 +00004436 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004437 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004438 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004439
4440 if options.new_description:
4441 text = options.new_description
4442 if text == '-':
4443 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004444 elif text == '+':
4445 base_branch = cl.GetCommonAncestorWithUpstream()
4446 change = cl.GetChange(base_branch, None, local_description=True)
4447 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004448
4449 description.set_description(text)
4450 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004451 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004452
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004453 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004454 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004455 return 0
4456
4457
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004458@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004459def CMDlint(parser, args):
4460 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004461 parser.add_option('--filter', action='append', metavar='-x,+y',
4462 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004463 auth.add_auth_options(parser)
4464 options, args = parser.parse_args(args)
4465 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004466
4467 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004468 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004469 try:
4470 import cpplint
4471 import cpplint_chromium
4472 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004473 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004474 return 1
4475
4476 # Change the current working directory before calling lint so that it
4477 # shows the correct base.
4478 previous_cwd = os.getcwd()
4479 os.chdir(settings.GetRoot())
4480 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004481 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004482 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4483 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004484 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004485 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004486 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004487
4488 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004489 command = args + files
4490 if options.filter:
4491 command = ['--filter=' + ','.join(options.filter)] + command
4492 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004493
4494 white_regex = re.compile(settings.GetLintRegex())
4495 black_regex = re.compile(settings.GetLintIgnoreRegex())
4496 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4497 for filename in filenames:
4498 if white_regex.match(filename):
4499 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004500 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004501 else:
4502 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4503 extra_check_functions)
4504 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004505 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004506 finally:
4507 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004508 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004509 if cpplint._cpplint_state.error_count != 0:
4510 return 1
4511 return 0
4512
4513
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004514@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004515def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004516 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004517 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004518 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004519 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004520 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004521 parser.add_option('--all', action='store_true',
4522 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004523 parser.add_option('--parallel', action='store_true',
4524 help='Run all tests specified by input_api.RunTests in all '
4525 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004526 auth.add_auth_options(parser)
4527 options, args = parser.parse_args(args)
4528 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004529
sbc@chromium.org71437c02015-04-09 19:29:40 +00004530 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004531 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004532 return 1
4533
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004534 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004535 if args:
4536 base_branch = args[0]
4537 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004538 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004539 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004540
Aaron Gable8076c282017-11-29 14:39:41 -08004541 if options.all:
4542 base_change = cl.GetChange(base_branch, None)
4543 files = [('M', f) for f in base_change.AllFiles()]
4544 change = presubmit_support.GitChange(
4545 base_change.Name(),
4546 base_change.FullDescriptionText(),
4547 base_change.RepositoryRoot(),
4548 files,
4549 base_change.issue,
4550 base_change.patchset,
4551 base_change.author_email,
4552 base_change._upstream)
4553 else:
4554 change = cl.GetChange(base_branch, None)
4555
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004556 cl.RunHook(
4557 committing=not options.upload,
4558 may_prompt=False,
4559 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004560 change=change,
4561 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004562 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004563
4564
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004565def GenerateGerritChangeId(message):
4566 """Returns Ixxxxxx...xxx change id.
4567
4568 Works the same way as
4569 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4570 but can be called on demand on all platforms.
4571
4572 The basic idea is to generate git hash of a state of the tree, original commit
4573 message, author/committer info and timestamps.
4574 """
4575 lines = []
4576 tree_hash = RunGitSilent(['write-tree'])
4577 lines.append('tree %s' % tree_hash.strip())
4578 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4579 if code == 0:
4580 lines.append('parent %s' % parent.strip())
4581 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4582 lines.append('author %s' % author.strip())
4583 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4584 lines.append('committer %s' % committer.strip())
4585 lines.append('')
4586 # Note: Gerrit's commit-hook actually cleans message of some lines and
4587 # whitespace. This code is not doing this, but it clearly won't decrease
4588 # entropy.
4589 lines.append(message)
4590 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
Raul Tambreb946b232019-03-26 14:48:46 +00004591 stdin=('\n'.join(lines)).encode())
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004592 return 'I%s' % change_hash.strip()
4593
4594
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004595def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004596 """Computes the remote branch ref to use for the CL.
4597
4598 Args:
4599 remote (str): The git remote for the CL.
4600 remote_branch (str): The git remote branch for the CL.
4601 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004602 """
4603 if not (remote and remote_branch):
4604 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004605
wittman@chromium.org455dc922015-01-26 20:15:50 +00004606 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004607 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004608 # refs, which are then translated into the remote full symbolic refs
4609 # below.
4610 if '/' not in target_branch:
4611 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4612 else:
4613 prefix_replacements = (
4614 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4615 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4616 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4617 )
4618 match = None
4619 for regex, replacement in prefix_replacements:
4620 match = re.search(regex, target_branch)
4621 if match:
4622 remote_branch = target_branch.replace(match.group(0), replacement)
4623 break
4624 if not match:
4625 # This is a branch path but not one we recognize; use as-is.
4626 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004627 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4628 # Handle the refs that need to land in different refs.
4629 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004630
wittman@chromium.org455dc922015-01-26 20:15:50 +00004631 # Create the true path to the remote branch.
4632 # Does the following translation:
4633 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4634 # * refs/remotes/origin/master -> refs/heads/master
4635 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4636 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4637 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4638 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4639 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4640 'refs/heads/')
4641 elif remote_branch.startswith('refs/remotes/branch-heads'):
4642 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004643
wittman@chromium.org455dc922015-01-26 20:15:50 +00004644 return remote_branch
4645
4646
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004647def cleanup_list(l):
4648 """Fixes a list so that comma separated items are put as individual items.
4649
4650 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4651 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4652 """
4653 items = sum((i.split(',') for i in l), [])
4654 stripped_items = (i.strip() for i in items)
4655 return sorted(filter(None, stripped_items))
4656
4657
Aaron Gable4db38df2017-11-03 14:59:07 -07004658@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004659@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004660def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004661 """Uploads the current changelist to codereview.
4662
4663 Can skip dependency patchset uploads for a branch by running:
4664 git config branch.branch_name.skip-deps-uploads True
4665 To unset run:
4666 git config --unset branch.branch_name.skip-deps-uploads
4667 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004668
4669 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4670 a bug number, this bug number is automatically populated in the CL
4671 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004672
4673 If subject contains text in square brackets or has "<text>: " prefix, such
4674 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4675 [git-cl] add support for hashtags
4676 Foo bar: implement foo
4677 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004678 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004679 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4680 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004681 parser.add_option('--bypass-watchlists', action='store_true',
4682 dest='bypass_watchlists',
4683 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004684 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004685 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004686 parser.add_option('--message', '-m', dest='message',
4687 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004688 parser.add_option('-b', '--bug',
4689 help='pre-populate the bug number(s) for this issue. '
4690 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004691 parser.add_option('--message-file', dest='message_file',
4692 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004693 parser.add_option('--title', '-t', dest='title',
4694 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004695 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004696 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004697 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004698 parser.add_option('--tbrs',
4699 action='append', default=[],
4700 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004701 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004702 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004703 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004704 parser.add_option('--hashtag', dest='hashtags',
4705 action='append', default=[],
4706 help=('Gerrit hashtag for new CL; '
4707 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004708 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004709 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004710 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004711 help='tell the commit queue to commit this patchset; '
4712 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004713 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004714 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004715 metavar='TARGET',
4716 help='Apply CL to remote ref TARGET. ' +
4717 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004718 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004719 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004720 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004721 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004722 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004723 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004724 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4725 const='TBR', help='add a set of OWNERS to TBR')
4726 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4727 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004728 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4729 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004730 help='Send the patchset to do a CQ dry run right after '
4731 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004732 parser.add_option('--dependencies', action='store_true',
4733 help='Uploads CLs of all the local branches that depend on '
4734 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004735 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4736 help='Sends your change to the CQ after an approval. Only '
4737 'works on repos that have the Auto-Submit label '
4738 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004739 parser.add_option('--parallel', action='store_true',
4740 help='Run all tests specified by input_api.RunTests in all '
4741 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004742
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004743 parser.add_option('--no-autocc', action='store_true',
4744 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004745 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004746 help='Set the review private. This implies --no-autocc.')
4747
rmistry@google.com2dd99862015-06-22 12:22:18 +00004748 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004749 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004750 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004751 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004752 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004753 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004754
sbc@chromium.org71437c02015-04-09 19:29:40 +00004755 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004756 return 1
4757
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004758 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004759 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004760 options.cc = cleanup_list(options.cc)
4761
tandriib80458a2016-06-23 12:20:07 -07004762 if options.message_file:
4763 if options.message:
4764 parser.error('only one of --message and --message-file allowed.')
4765 options.message = gclient_utils.FileRead(options.message_file)
4766 options.message_file = None
4767
tandrii4d0545a2016-07-06 03:56:49 -07004768 if options.cq_dry_run and options.use_commit_queue:
4769 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4770
Aaron Gableedbc4132017-09-11 13:22:28 -07004771 if options.use_commit_queue:
4772 options.send_mail = True
4773
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004774 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4775 settings.GetIsGerrit()
4776
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004777 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004778 if not cl.IsGerrit():
4779 # Error out with instructions for repos not yet configured for Gerrit.
4780 print('=====================================')
4781 print('NOTICE: Rietveld is no longer supported. '
4782 'You can upload changes to Gerrit with')
4783 print(' git cl upload --gerrit')
4784 print('or set Gerrit to be your default code review tool with')
4785 print(' git config gerrit.host true')
4786 print('=====================================')
4787 return 1
4788
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004789 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004790
4791
Francois Dorayd42c6812017-05-30 15:10:20 -04004792@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004793@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004794def CMDsplit(parser, args):
4795 """Splits a branch into smaller branches and uploads CLs.
4796
4797 Creates a branch and uploads a CL for each group of files modified in the
4798 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004799 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004800 the shared OWNERS file.
4801 """
4802 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004803 help="A text file containing a CL description in which "
4804 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004805 parser.add_option("-c", "--comment", dest="comment_file",
4806 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004807 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4808 default=False,
4809 help="List the files and reviewers for each CL that would "
4810 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004811 parser.add_option("--cq-dry-run", action='store_true',
4812 help="If set, will do a cq dry run for each uploaded CL. "
4813 "Please be careful when doing this; more than ~10 CLs "
4814 "has the potential to overload our build "
4815 "infrastructure. Try to upload these not during high "
4816 "load times (usually 11-3 Mountain View time). Email "
4817 "infra-dev@chromium.org with any questions.")
Takuto Ikuta51eca592019-02-14 19:40:52 +00004818 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4819 default=True,
4820 help='Sends your change to the CQ after an approval. Only '
4821 'works on repos that have the Auto-Submit label '
4822 'enabled')
Francois Dorayd42c6812017-05-30 15:10:20 -04004823 options, _ = parser.parse_args(args)
4824
4825 if not options.description_file:
4826 parser.error('No --description flag specified.')
4827
4828 def WrappedCMDupload(args):
4829 return CMDupload(OptionParser(), args)
4830
4831 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004832 Changelist, WrappedCMDupload, options.dry_run,
Takuto Ikuta51eca592019-02-14 19:40:52 +00004833 options.cq_dry_run, options.enable_auto_submit)
Francois Dorayd42c6812017-05-30 15:10:20 -04004834
4835
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004836@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004837@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004838def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004839 """DEPRECATED: Used to commit the current changelist via git-svn."""
4840 message = ('git-cl no longer supports committing to SVN repositories via '
4841 'git-svn. You probably want to use `git cl land` instead.')
4842 print(message)
4843 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004844
4845
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01004846# Two special branches used by git cl land.
4847MERGE_BRANCH = 'git-cl-commit'
4848CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4849
4850
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004851@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004852@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004853def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004854 """Commits the current changelist via git.
4855
4856 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4857 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004858 """
4859 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4860 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004861 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004862 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04004863 parser.add_option('--parallel', action='store_true',
4864 help='Run all tests specified by input_api.RunTests in all '
4865 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004866 auth.add_auth_options(parser)
4867 (options, args) = parser.parse_args(args)
4868 auth_config = auth.extract_auth_config_from_options(options)
4869
4870 cl = Changelist(auth_config=auth_config)
4871
Robert Iannucci2e73d432018-03-14 01:10:47 -07004872 if not cl.IsGerrit():
4873 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004874
Robert Iannucci2e73d432018-03-14 01:10:47 -07004875 if not cl.GetIssue():
4876 DieWithError('You must upload the change first to Gerrit.\n'
4877 ' If you would rather have `git cl land` upload '
4878 'automatically for you, see http://crbug.com/642759')
4879 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02004880 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004881
4882
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004883@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004884@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004885def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004886 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004887 parser.add_option('-b', dest='newbranch',
4888 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004889 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07004890 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004891 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07004892 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004893 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004894 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004895 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004896 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004897 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004898 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004899
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004900
4901 group = optparse.OptionGroup(
4902 parser,
4903 'Options for continuing work on the current issue uploaded from a '
4904 'different clone (e.g. different machine). Must be used independently '
4905 'from the other options. No issue number should be specified, and the '
4906 'branch must have an issue number associated with it')
4907 group.add_option('--reapply', action='store_true', dest='reapply',
4908 help='Reset the branch and reapply the issue.\n'
4909 'CAUTION: This will undo any local changes in this '
4910 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004911
4912 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004913 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004914 parser.add_option_group(group)
4915
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004916 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004917 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004918 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004919 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004920 auth_config = auth.extract_auth_config_from_options(options)
4921
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004922 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004923 if options.newbranch:
4924 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004925 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004926 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004927
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004928 cl = Changelist(auth_config=auth_config,
4929 codereview=options.forced_codereview)
4930 if not cl.GetIssue():
4931 parser.error('current branch must have an associated issue')
4932
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004933 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01004934 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004935 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004936
4937 RunGit(['reset', '--hard', upstream])
4938 if options.pull:
4939 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004940
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004941 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4942 options.directory)
4943
4944 if len(args) != 1 or not args[0]:
4945 parser.error('Must specify issue number or url')
4946
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004947 target_issue_arg = ParseIssueNumberArgument(args[0],
4948 options.forced_codereview)
4949 if not target_issue_arg.valid:
4950 parser.error('invalid codereview url or CL id')
4951
4952 cl_kwargs = {
4953 'auth_config': auth_config,
4954 'codereview_host': target_issue_arg.hostname,
4955 'codereview': options.forced_codereview,
4956 }
4957 detected_codereview_from_url = False
4958 if target_issue_arg.codereview and not options.forced_codereview:
4959 detected_codereview_from_url = True
4960 cl_kwargs['codereview'] = target_issue_arg.codereview
4961 cl_kwargs['issue'] = target_issue_arg.issue
4962
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004963 # We don't want uncommitted changes mixed up with the patch.
4964 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004965 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004966
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004967 if options.newbranch:
4968 if options.force:
4969 RunGit(['branch', '-D', options.newbranch],
4970 stderr=subprocess2.PIPE, error_ok=True)
4971 RunGit(['new-branch', options.newbranch])
4972
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004973 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004974
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004975 if cl.IsGerrit():
4976 if options.reject:
4977 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004978 if options.directory:
4979 parser.error('--directory is not supported with Gerrit codereview.')
4980
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004981 if detected_codereview_from_url:
4982 print('canonical issue/change URL: %s (type: %s)\n' %
4983 (cl.GetIssueURL(), target_issue_arg.codereview))
4984
4985 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07004986 options.nocommit, options.directory,
4987 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004988
4989
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004990def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004991 """Fetches the tree status and returns either 'open', 'closed',
4992 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004993 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004994 if url:
4995 status = urllib2.urlopen(url).read().lower()
4996 if status.find('closed') != -1 or status == '0':
4997 return 'closed'
4998 elif status.find('open') != -1 or status == '1':
4999 return 'open'
5000 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005001 return 'unset'
5002
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005003
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005004def GetTreeStatusReason():
5005 """Fetches the tree status from a json url and returns the message
5006 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005007 url = settings.GetTreeStatusUrl()
5008 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005009 connection = urllib2.urlopen(json_url)
5010 status = json.loads(connection.read())
5011 connection.close()
5012 return status['message']
5013
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005014
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005015@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005016def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005017 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005018 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005019 status = GetTreeStatus()
5020 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005021 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005022 return 2
5023
vapiera7fbd5a2016-06-16 09:17:49 -07005024 print('The tree is %s' % status)
5025 print()
5026 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005027 if status != 'open':
5028 return 1
5029 return 0
5030
5031
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005032@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005033def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005034 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005035 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005036 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005037 '-b', '--bot', action='append',
5038 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5039 'times to specify multiple builders. ex: '
5040 '"-b win_rel -b win_layout". See '
5041 'the try server waterfall for the builders name and the tests '
5042 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005043 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005044 '-B', '--bucket', default='',
5045 help=('Buildbucket bucket to send the try requests.'))
5046 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005047 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005048 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005049 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005050 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005051 help='Revision to use for the try job; default: the revision will '
5052 'be determined by the try recipe that builder runs, which usually '
5053 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005054 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005055 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005056 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005057 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005058 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005059 '--category', default='git_cl_try', help='Specify custom build category.')
5060 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005061 '--project',
5062 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005063 'in recipe to determine to which repository or directory to '
5064 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005065 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005066 '-p', '--property', dest='properties', action='append', default=[],
5067 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005068 'key2=value2 etc. The value will be treated as '
5069 'json if decodable, or as string otherwise. '
5070 'NOTE: using this may make your try job not usable for CQ, '
5071 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005072 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005073 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5074 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005075 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005076 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005077 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005078 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005079 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005080 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005081
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005082 if options.master and options.master.startswith('luci.'):
5083 parser.error(
5084 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005085 # Make sure that all properties are prop=value pairs.
5086 bad_params = [x for x in options.properties if '=' not in x]
5087 if bad_params:
5088 parser.error('Got properties with missing "=": %s' % bad_params)
5089
maruel@chromium.org15192402012-09-06 12:38:29 +00005090 if args:
5091 parser.error('Unknown arguments: %s' % args)
5092
Koji Ishii31c14782018-01-08 17:17:33 +09005093 cl = Changelist(auth_config=auth_config, issue=options.issue,
5094 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005095 if not cl.GetIssue():
5096 parser.error('Need to upload first')
5097
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005098 if cl.IsGerrit():
5099 # HACK: warm up Gerrit change detail cache to save on RPCs.
5100 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5101
tandriie113dfd2016-10-11 10:20:12 -07005102 error_message = cl.CannotTriggerTryJobReason()
5103 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005104 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005105
borenet6c0efe62016-10-19 08:13:29 -07005106 if options.bucket and options.master:
5107 parser.error('Only one of --bucket and --master may be used.')
5108
qyearsley1fdfcb62016-10-24 13:22:03 -07005109 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005110
qyearsleydd49f942016-10-28 11:57:22 -07005111 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5112 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005113 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005114 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005115 print('git cl try with no bots now defaults to CQ dry run.')
5116 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5117 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005118
borenet6c0efe62016-10-19 08:13:29 -07005119 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005120 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005121 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005122 'of bot requires an initial job from a parent (usually a builder). '
5123 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005124 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005125 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005126
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005127 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005128 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005129 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005130 except BuildbucketResponseException as ex:
5131 print('ERROR: %s' % ex)
5132 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005133 return 0
5134
5135
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005136@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005137def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005138 """Prints info about try jobs associated with current CL."""
5139 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005140 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005141 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005142 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005143 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005144 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005145 '--color', action='store_true', default=setup_color.IS_TTY,
5146 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005147 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005148 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5149 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005150 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005151 '--json', help=('Path of JSON output file to write try job results to,'
5152 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005153 parser.add_option_group(group)
5154 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005155 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005156 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005157 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005158 if args:
5159 parser.error('Unrecognized args: %s' % ' '.join(args))
5160
5161 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005162 cl = Changelist(
5163 issue=options.issue, codereview=options.forced_codereview,
5164 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005165 if not cl.GetIssue():
5166 parser.error('Need to upload first')
5167
tandrii221ab252016-10-06 08:12:04 -07005168 patchset = options.patchset
5169 if not patchset:
5170 patchset = cl.GetMostRecentPatchset()
5171 if not patchset:
5172 parser.error('Codereview doesn\'t know about issue %s. '
5173 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005174 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005175 cl.GetIssue())
5176
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005177 try:
tandrii221ab252016-10-06 08:12:04 -07005178 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005179 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005180 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005181 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005182 if options.json:
5183 write_try_results_json(options.json, jobs)
5184 else:
5185 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005186 return 0
5187
5188
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005189@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005190@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005191def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005192 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005193 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005194 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005195 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005196
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005197 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005198 if args:
5199 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005200 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005201 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005202 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005203 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005204
5205 # Clear configured merge-base, if there is one.
5206 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005207 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005208 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005209 return 0
5210
5211
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005212@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005213def CMDweb(parser, args):
5214 """Opens the current CL in the web browser."""
5215 _, args = parser.parse_args(args)
5216 if args:
5217 parser.error('Unrecognized args: %s' % ' '.join(args))
5218
5219 issue_url = Changelist().GetIssueURL()
5220 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005221 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005222 return 1
5223
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005224 # Redirect I/O before invoking browser to hide its output. For example, this
5225 # allows to hide "Created new window in existing browser session." message
5226 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5227 saved_stdout = os.dup(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005228 saved_stderr = os.dup(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005229 os.close(1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005230 os.close(2)
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005231 os.open(os.devnull, os.O_RDWR)
5232 try:
5233 webbrowser.open(issue_url)
5234 finally:
5235 os.dup2(saved_stdout, 1)
Sergiy Belozorov06684032019-03-06 16:53:08 +00005236 os.dup2(saved_stderr, 2)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005237 return 0
5238
5239
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005240@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005241def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005242 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005243 parser.add_option('-d', '--dry-run', action='store_true',
5244 help='trigger in dry run mode')
5245 parser.add_option('-c', '--clear', action='store_true',
5246 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005247 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005248 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005249 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005250 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005251 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005252 if args:
5253 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005254 if options.dry_run and options.clear:
5255 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5256
iannuccie53c9352016-08-17 14:40:40 -07005257 cl = Changelist(auth_config=auth_config, issue=options.issue,
5258 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005259 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005260 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005261 elif options.dry_run:
5262 state = _CQState.DRY_RUN
5263 else:
5264 state = _CQState.COMMIT
5265 if not cl.GetIssue():
5266 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005267 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005268 return 0
5269
5270
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005271@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005272def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005273 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005274 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005275 auth.add_auth_options(parser)
5276 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005277 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005278 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005279 if args:
5280 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005281 cl = Changelist(auth_config=auth_config, issue=options.issue,
5282 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005283 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005284 if not cl.GetIssue():
5285 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005286 cl.CloseIssue()
5287 return 0
5288
5289
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005290@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005291def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005292 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005293 parser.add_option(
5294 '--stat',
5295 action='store_true',
5296 dest='stat',
5297 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005298 auth.add_auth_options(parser)
5299 options, args = parser.parse_args(args)
5300 auth_config = auth.extract_auth_config_from_options(options)
5301 if args:
5302 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005303
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005304 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005305 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005306 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005307 if not issue:
5308 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005309
Aaron Gablea718c3e2017-08-28 17:47:28 -07005310 base = cl._GitGetBranchConfigValue('last-upload-hash')
5311 if not base:
5312 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5313 if not base:
5314 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5315 revision_info = detail['revisions'][detail['current_revision']]
5316 fetch_info = revision_info['fetch']['http']
5317 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5318 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005319
Aaron Gablea718c3e2017-08-28 17:47:28 -07005320 cmd = ['git', 'diff']
5321 if options.stat:
5322 cmd.append('--stat')
5323 cmd.append(base)
5324 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005325
5326 return 0
5327
5328
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005329@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005330def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005331 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005332 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005333 '--ignore-current',
5334 action='store_true',
5335 help='Ignore the CL\'s current reviewers and start from scratch.')
5336 parser.add_option(
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005337 '--ignore-self',
5338 action='store_true',
5339 help='Do not consider CL\'s author as an owners.')
5340 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005341 '--no-color',
5342 action='store_true',
5343 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005344 parser.add_option(
5345 '--batch',
5346 action='store_true',
5347 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005348 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005349 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005350 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005351
5352 author = RunGit(['config', 'user.email']).strip() or None
5353
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005354 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005355
5356 if args:
5357 if len(args) > 1:
5358 parser.error('Unknown args')
5359 base_branch = args[0]
5360 else:
5361 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005362 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005363
5364 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005365 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5366
5367 if options.batch:
5368 db = owners.Database(change.RepositoryRoot(), file, os.path)
5369 print('\n'.join(db.reviewers_for(affected_files, author)))
5370 return 0
5371
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005372 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005373 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005374 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005375 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005376 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005377 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005378 disable_color=options.no_color,
Sylvain Defresneb1f865d2019-02-12 12:38:22 +00005379 override_files=change.OriginalOwnersFiles(),
5380 ignore_author=options.ignore_self).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005381
5382
Aiden Bennerc08566e2018-10-03 17:52:42 +00005383def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005384 """Generates a diff command."""
5385 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005386 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5387
Aiden Benner6c18a1a2018-11-23 20:18:23 +00005388 if allow_prefix:
5389 # explicitly setting --src-prefix and --dst-prefix is necessary in the
5390 # case that diff.noprefix is set in the user's git config.
5391 diff_cmd += ['--src-prefix=a/', '--dst-prefix=b/']
5392 else:
Aiden Bennerc08566e2018-10-03 17:52:42 +00005393 diff_cmd += ['--no-prefix']
5394
5395 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005396
5397 if args:
5398 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005399 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005400 diff_cmd.append(arg)
5401 else:
5402 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005403
5404 return diff_cmd
5405
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005406
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005407def MatchingFileType(file_name, extensions):
5408 """Returns true if the file name ends with one of the given extensions."""
5409 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005410
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005411
enne@chromium.org555cfe42014-01-29 18:21:39 +00005412@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005413@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005414def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005415 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005416 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005417 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005418 parser.add_option('--full', action='store_true',
5419 help='Reformat the full content of all touched files')
5420 parser.add_option('--dry-run', action='store_true',
5421 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005422 parser.add_option(
5423 '--python',
5424 action='store_true',
5425 default=None,
5426 help='Enables python formatting on all python files.')
5427 parser.add_option(
5428 '--no-python',
5429 action='store_true',
5430 dest='python',
5431 help='Disables python formatting on all python files. '
5432 'Takes precedence over --python. '
5433 'If neither --python or --no-python are set, python '
5434 'files that have a .style.yapf file in an ancestor '
5435 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005436 parser.add_option('--js', action='store_true',
5437 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005438 parser.add_option('--diff', action='store_true',
5439 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005440 parser.add_option('--presubmit', action='store_true',
5441 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005442 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005443
Daniel Chengc55eecf2016-12-30 03:11:02 -08005444 # Normalize any remaining args against the current path, so paths relative to
5445 # the current directory are still resolved as expected.
5446 args = [os.path.join(os.getcwd(), arg) for arg in args]
5447
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005448 # git diff generates paths against the root of the repository. Change
5449 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005450 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005451 if rel_base_path:
5452 os.chdir(rel_base_path)
5453
digit@chromium.org29e47272013-05-17 17:01:46 +00005454 # Grab the merge-base commit, i.e. the upstream commit of the current
5455 # branch when it was created or the last time it was rebased. This is
5456 # to cover the case where the user may have called "git fetch origin",
5457 # moving the origin branch to a newer commit, but hasn't rebased yet.
5458 upstream_commit = None
5459 cl = Changelist()
5460 upstream_branch = cl.GetUpstreamBranch()
5461 if upstream_branch:
5462 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5463 upstream_commit = upstream_commit.strip()
5464
5465 if not upstream_commit:
5466 DieWithError('Could not find base commit for this branch. '
5467 'Are you in detached state?')
5468
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005469 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5470 diff_output = RunGit(changed_files_cmd)
5471 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005472 # Filter out files deleted by this CL
5473 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005474
Christopher Lamc5ba6922017-01-24 11:19:14 +11005475 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005476 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005477
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005478 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5479 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5480 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005481 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005482
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005483 top_dir = os.path.normpath(
5484 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5485
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005486 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5487 # formatted. This is used to block during the presubmit.
5488 return_value = 0
5489
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005490 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005491 # Locate the clang-format binary in the checkout
5492 try:
5493 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005494 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005495 DieWithError(e)
5496
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005497 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005498 cmd = [clang_format_tool]
5499 if not opts.dry_run and not opts.diff:
5500 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005501 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005502 if opts.diff:
5503 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005504 else:
5505 env = os.environ.copy()
5506 env['PATH'] = str(os.path.dirname(clang_format_tool))
5507 try:
5508 script = clang_format.FindClangFormatScriptInChromiumTree(
5509 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005510 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005511 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005512
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005513 cmd = [sys.executable, script, '-p0']
5514 if not opts.dry_run and not opts.diff:
5515 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005516
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005517 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5518 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005519
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005520 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5521 if opts.diff:
5522 sys.stdout.write(stdout)
5523 if opts.dry_run and len(stdout) > 0:
5524 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005525
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005526 # Similar code to above, but using yapf on .py files rather than clang-format
5527 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005528 py_explicitly_disabled = opts.python is not None and not opts.python
5529 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005530 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5531 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5532 if sys.platform.startswith('win'):
5533 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005534
Aiden Bennerc08566e2018-10-03 17:52:42 +00005535 # If we couldn't find a yapf file we'll default to the chromium style
5536 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005537 chromium_default_yapf_style = os.path.join(depot_tools_path,
5538 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005539 # Used for caching.
5540 yapf_configs = {}
5541 for f in python_diff_files:
5542 # Find the yapf style config for the current file, defaults to depot
5543 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005544 _FindYapfConfigFile(f, yapf_configs, top_dir)
5545
5546 # Turn on python formatting by default if a yapf config is specified.
5547 # This breaks in the case of this repo though since the specified
5548 # style file is also the global default.
5549 if opts.python is None:
5550 filtered_py_files = []
5551 for f in python_diff_files:
5552 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5553 filtered_py_files.append(f)
5554 else:
5555 filtered_py_files = python_diff_files
5556
5557 # Note: yapf still seems to fix indentation of the entire file
5558 # even if line ranges are specified.
5559 # See https://github.com/google/yapf/issues/499
5560 if not opts.full and filtered_py_files:
5561 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5562
5563 for f in filtered_py_files:
5564 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5565 if yapf_config is None:
5566 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005567
5568 cmd = [yapf_tool, '--style', yapf_config, f]
5569
5570 has_formattable_lines = False
5571 if not opts.full:
5572 # Only run yapf over changed line ranges.
5573 for diff_start, diff_len in py_line_diffs[f]:
5574 diff_end = diff_start + diff_len - 1
5575 # Yapf errors out if diff_end < diff_start but this
5576 # is a valid line range diff for a removal.
5577 if diff_end >= diff_start:
5578 has_formattable_lines = True
5579 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5580 # If all line diffs were removals we have nothing to format.
5581 if not has_formattable_lines:
5582 continue
5583
5584 if opts.diff or opts.dry_run:
5585 cmd += ['--diff']
5586 # Will return non-zero exit code if non-empty diff.
5587 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5588 if opts.diff:
5589 sys.stdout.write(stdout)
5590 elif len(stdout) > 0:
5591 return_value = 2
5592 else:
5593 cmd += ['-i']
5594 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005595
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005596 # Dart's formatter does not have the nice property of only operating on
5597 # modified chunks, so hard code full.
5598 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005599 try:
5600 command = [dart_format.FindDartFmtToolInChromiumTree()]
5601 if not opts.dry_run and not opts.diff:
5602 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005603 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005604
ppi@chromium.org6593d932016-03-03 15:41:15 +00005605 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005606 if opts.dry_run and stdout:
5607 return_value = 2
5608 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005609 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5610 'found in this checkout. Files in other languages are still '
5611 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005612
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005613 # Format GN build files. Always run on full build files for canonical form.
5614 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005615 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005616 if opts.dry_run or opts.diff:
5617 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005618 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005619 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5620 shell=sys.platform == 'win32',
5621 cwd=top_dir)
5622 if opts.dry_run and gn_ret == 2:
5623 return_value = 2 # Not formatted.
5624 elif opts.diff and gn_ret == 2:
5625 # TODO this should compute and print the actual diff.
5626 print("This change has GN build file diff for " + gn_diff_file)
5627 elif gn_ret != 0:
5628 # For non-dry run cases (and non-2 return values for dry-run), a
5629 # nonzero error code indicates a failure, probably because the file
5630 # doesn't parse.
5631 DieWithError("gn format failed on " + gn_diff_file +
5632 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005633
Ilya Shermane081cbe2017-08-15 17:51:04 -07005634 # Skip the metrics formatting from the global presubmit hook. These files have
5635 # a separate presubmit hook that issues an error if the files need formatting,
5636 # whereas the top-level presubmit script merely issues a warning. Formatting
5637 # these files is somewhat slow, so it's important not to duplicate the work.
5638 if not opts.presubmit:
5639 for xml_dir in GetDirtyMetricsDirs(diff_files):
5640 tool_dir = os.path.join(top_dir, xml_dir)
5641 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5642 if opts.dry_run or opts.diff:
5643 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005644 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005645 if opts.diff:
5646 sys.stdout.write(stdout)
5647 if opts.dry_run and stdout:
5648 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005649
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005650 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005651
Steven Holte2e664bf2017-04-21 13:10:47 -07005652def GetDirtyMetricsDirs(diff_files):
5653 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5654 metrics_xml_dirs = [
5655 os.path.join('tools', 'metrics', 'actions'),
5656 os.path.join('tools', 'metrics', 'histograms'),
5657 os.path.join('tools', 'metrics', 'rappor'),
5658 os.path.join('tools', 'metrics', 'ukm')]
5659 for xml_dir in metrics_xml_dirs:
5660 if any(file.startswith(xml_dir) for file in xml_diff_files):
5661 yield xml_dir
5662
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005663
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005664@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005665@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005666def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005667 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005668 _, args = parser.parse_args(args)
5669
5670 if len(args) != 1:
5671 parser.print_help()
5672 return 1
5673
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005674 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005675 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005676 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005677
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005678 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005679
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005680 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005681 output = RunGit(['config', '--local', '--get-regexp',
5682 r'branch\..*\.%s' % issueprefix],
5683 error_ok=True)
5684 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005685 if issue == target_issue:
5686 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005687
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005688 branches = []
5689 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005690 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005691 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005692 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005693 return 1
5694 if len(branches) == 1:
5695 RunGit(['checkout', branches[0]])
5696 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005697 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005698 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005699 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005700 which = raw_input('Choose by index: ')
5701 try:
5702 RunGit(['checkout', branches[int(which)]])
5703 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005704 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005705 return 1
5706
5707 return 0
5708
5709
maruel@chromium.org29404b52014-09-08 22:58:00 +00005710def CMDlol(parser, args):
5711 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005712 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005713 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5714 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5715 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005716 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005717 return 0
5718
5719
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005720class OptionParser(optparse.OptionParser):
5721 """Creates the option parse and add --verbose support."""
5722 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005723 optparse.OptionParser.__init__(
5724 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005725 self.add_option(
5726 '-v', '--verbose', action='count', default=0,
5727 help='Use 2 times for more debugging info')
5728
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005729 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005730 try:
5731 return self._parse_args(args)
5732 finally:
5733 # Regardless of success or failure of args parsing, we want to report
5734 # metrics, but only after logging has been initialized (if parsing
5735 # succeeded).
5736 global settings
5737 settings = Settings()
5738
5739 if not metrics.DISABLE_METRICS_COLLECTION:
5740 # GetViewVCUrl ultimately calls logging method.
5741 project_url = settings.GetViewVCUrl().strip('/+')
5742 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5743 metrics.collector.add('project_urls', [project_url])
5744
5745 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005746 # Create an optparse.Values object that will store only the actual passed
5747 # options, without the defaults.
5748 actual_options = optparse.Values()
5749 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5750 # Create an optparse.Values object with the default options.
5751 options = optparse.Values(self.get_default_values().__dict__)
5752 # Update it with the options passed by the user.
5753 options._update_careful(actual_options.__dict__)
5754 # Store the options passed by the user in an _actual_options attribute.
5755 # We store only the keys, and not the values, since the values can contain
5756 # arbitrary information, which might be PII.
5757 metrics.collector.add('arguments', actual_options.__dict__.keys())
5758
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005759 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005760 logging.basicConfig(
5761 level=levels[min(options.verbose, len(levels) - 1)],
5762 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5763 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005764
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005765 return options, args
5766
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005768def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005769 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005770 print('\nYour python version %s is unsupported, please upgrade.\n' %
5771 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005772 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005773
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005774 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005775 dispatcher = subcommand.CommandDispatcher(__name__)
5776 try:
5777 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005778 except auth.AuthenticationError as e:
5779 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005780 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005781 if e.code != 500:
5782 raise
5783 DieWithError(
5784 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5785 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005786 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005787
5788
5789if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005790 # These affect sys.stdout so do it outside of main() to simplify mocks in
5791 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005792 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005793 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005794 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005795 sys.exit(main(sys.argv[1:]))