blob: 1a76bbef07d16a62887d7f5146ac4db3f4038ee5 [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
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000063import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040065import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000066import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000067import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000068import watchlists
69
tandrii7400cf02016-06-21 08:48:07 -070070__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000071
tandrii9d2c7a32016-06-22 03:42:45 -070072COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070073DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080074POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000075DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000076REFS_THAT_ALIAS_TO_OTHER_REFS = {
77 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
78 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
79}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
thestig@chromium.org44202a22014-03-11 19:22:18 +000081# Valid extensions for files we want to lint.
82DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
83DEFAULT_LINT_IGNORE_REGEX = r"$^"
84
Aiden Bennerc08566e2018-10-03 17:52:42 +000085# File name for yapf style config files.
86YAPF_CONFIG_FILENAME = '.style.yapf'
87
borenet6c0efe62016-10-19 08:13:29 -070088# Buildbucket master name prefix.
89MASTER_PREFIX = 'master.'
90
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000091# Shortcut since it quickly becomes redundant.
92Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000093
maruel@chromium.orgddd59412011-11-30 14:20:38 +000094# Initialized in main()
95settings = None
96
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010097# Used by tests/git_cl_test.py to add extra logging.
98# Inside the weirdly failing test, add this:
99# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700100# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +0100101_IS_BEING_TESTED = False
102
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000103
Christopher Lamf732cd52017-01-24 12:40:11 +1100104def DieWithError(message, change_desc=None):
105 if change_desc:
106 SaveDescriptionBackup(change_desc)
107
vapiera7fbd5a2016-06-16 09:17:49 -0700108 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109 sys.exit(1)
110
111
Christopher Lamf732cd52017-01-24 12:40:11 +1100112def SaveDescriptionBackup(change_desc):
113 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +0000114 print('\nsaving CL description to %s\n' % backup_path)
Christopher Lamf732cd52017-01-24 12:40:11 +1100115 backup_file = open(backup_path, 'w')
116 backup_file.write(change_desc.description)
117 backup_file.close()
118
119
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000120def GetNoGitPagerEnv():
121 env = os.environ.copy()
122 # 'cat' is a magical git string that disables pagers on all platforms.
123 env['GIT_PAGER'] = 'cat'
124 return env
125
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000126
bsep@chromium.org627d9002016-04-29 00:00:52 +0000127def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000128 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000129 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000130 except subprocess2.CalledProcessError as e:
131 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000132 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000134 'Command "%s" failed.\n%s' % (
135 ' '.join(args), error_message or e.stdout or ''))
136 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000137
138
139def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000140 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000141 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000142
143
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000144def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000145 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700146 if suppress_stderr:
147 stderr = subprocess2.VOID
148 else:
149 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000150 try:
tandrii5d48c322016-08-18 16:19:37 -0700151 (out, _), code = subprocess2.communicate(['git'] + args,
152 env=GetNoGitPagerEnv(),
153 stdout=subprocess2.PIPE,
154 stderr=stderr)
155 return code, out
156 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900157 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700158 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000159
160
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000161def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000162 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000163 return RunGitWithCode(args, suppress_stderr=True)[1]
164
165
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000166def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000167 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000168 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000169 return (version.startswith(prefix) and
170 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000171
172
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000173def BranchExists(branch):
174 """Return True if specified branch exists."""
175 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
176 suppress_stderr=True)
177 return not code
178
179
tandrii2a16b952016-10-19 07:09:44 -0700180def time_sleep(seconds):
181 # Use this so that it can be mocked in tests without interfering with python
182 # system machinery.
tandrii2a16b952016-10-19 07:09:44 -0700183 return time.sleep(seconds)
184
185
Edward Lemur01f4a4f2018-11-03 00:40:38 +0000186def time_time():
187 # Use this so that it can be mocked in tests without interfering with python
188 # system machinery.
189 return time.time()
190
191
maruel@chromium.org90541732011-04-01 17:54:18 +0000192def ask_for_data(prompt):
193 try:
194 return raw_input(prompt)
195 except KeyboardInterrupt:
196 # Hide the exception.
197 sys.exit(1)
198
199
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100200def confirm_or_exit(prefix='', action='confirm'):
201 """Asks user to press enter to continue or press Ctrl+C to abort."""
202 if not prefix or prefix.endswith('\n'):
203 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100204 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100205 mid = ' Press'
206 elif prefix.endswith(' '):
207 mid = 'press'
208 else:
209 mid = ' press'
210 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
211
212
213def ask_for_explicit_yes(prompt):
214 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
215 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
216 while True:
217 if 'yes'.startswith(result):
218 return True
219 if 'no'.startswith(result):
220 return False
221 result = ask_for_data('Please, type yes or no: ').lower()
222
223
tandrii5d48c322016-08-18 16:19:37 -0700224def _git_branch_config_key(branch, key):
225 """Helper method to return Git config key for a branch."""
226 assert branch, 'branch name is required to set git config for it'
227 return 'branch.%s.%s' % (branch, key)
228
229
230def _git_get_branch_config_value(key, default=None, value_type=str,
231 branch=False):
232 """Returns git config value of given or current branch if any.
233
234 Returns default in all other cases.
235 """
236 assert value_type in (int, str, bool)
237 if branch is False: # Distinguishing default arg value from None.
238 branch = GetCurrentBranch()
239
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000240 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700241 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000242
tandrii5d48c322016-08-18 16:19:37 -0700243 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700244 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700245 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700246 # git config also has --int, but apparently git config suffers from integer
247 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700248 args.append(_git_branch_config_key(branch, key))
249 code, out = RunGitWithCode(args)
250 if code == 0:
251 value = out.strip()
252 if value_type == int:
253 return int(value)
254 if value_type == bool:
255 return bool(value.lower() == 'true')
256 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000257 return default
258
259
tandrii5d48c322016-08-18 16:19:37 -0700260def _git_set_branch_config_value(key, value, branch=None, **kwargs):
261 """Sets the value or unsets if it's None of a git branch config.
262
263 Valid, though not necessarily existing, branch must be provided,
264 otherwise currently checked out branch is used.
265 """
266 if not branch:
267 branch = GetCurrentBranch()
268 assert branch, 'a branch name OR currently checked out branch is required'
269 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700270 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700271 if value is None:
272 args.append('--unset')
273 elif isinstance(value, bool):
274 args.append('--bool')
275 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700276 else:
tandrii33a46ff2016-08-23 05:53:40 -0700277 # git config also has --int, but apparently git config suffers from integer
278 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700279 value = str(value)
280 args.append(_git_branch_config_key(branch, key))
281 if value is not None:
282 args.append(value)
283 RunGit(args, **kwargs)
284
285
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100286def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700287 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100288
289 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
290 """
291 # Git also stores timezone offset, but it only affects visual display,
292 # actual point in time is defined by this timestamp only.
293 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
294
295
296def _git_amend_head(message, committer_timestamp):
297 """Amends commit with new message and desired committer_timestamp.
298
299 Sets committer timezone to UTC.
300 """
301 env = os.environ.copy()
302 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
303 return RunGit(['commit', '--amend', '-m', message], env=env)
304
305
machenbach@chromium.org45453142015-09-15 08:45:22 +0000306def _get_properties_from_options(options):
307 properties = dict(x.split('=', 1) for x in options.properties)
308 for key, val in properties.iteritems():
309 try:
310 properties[key] = json.loads(val)
311 except ValueError:
312 pass # If a value couldn't be evaluated, treat it as a string.
313 return properties
314
315
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000316def _prefix_master(master):
317 """Convert user-specified master name to full master name.
318
319 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
320 name, while the developers always use shortened master name
321 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
322 function does the conversion for buildbucket migration.
323 """
borenet6c0efe62016-10-19 08:13:29 -0700324 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000325 return master
borenet6c0efe62016-10-19 08:13:29 -0700326 return '%s%s' % (MASTER_PREFIX, master)
327
328
329def _unprefix_master(bucket):
330 """Convert bucket name to shortened master name.
331
332 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
333 name, while the developers always use shortened master name
334 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
335 function does the conversion for buildbucket migration.
336 """
337 if bucket.startswith(MASTER_PREFIX):
338 return bucket[len(MASTER_PREFIX):]
339 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000340
341
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000342def _buildbucket_retry(operation_name, http, *args, **kwargs):
343 """Retries requests to buildbucket service and returns parsed json content."""
344 try_count = 0
345 while True:
346 response, content = http.request(*args, **kwargs)
347 try:
348 content_json = json.loads(content)
349 except ValueError:
350 content_json = None
351
352 # Buildbucket could return an error even if status==200.
353 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000354 error = content_json.get('error')
355 if error.get('code') == 403:
356 raise BuildbucketResponseException(
357 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000358 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000359 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000360 raise BuildbucketResponseException(msg)
361
362 if response.status == 200:
Nodir Turakulov23d75d22018-06-04 12:37:32 -0700363 if content_json is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000364 raise BuildbucketResponseException(
365 'Buildbucket returns invalid json content: %s.\n'
Nodir Turakulov9ac59792018-06-04 12:34:14 -0700366 'Please file bugs at http://crbug.com, '
367 'component "Infra>Platform>BuildBucket".' %
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000368 content)
369 return content_json
370 if response.status < 500 or try_count >= 2:
371 raise httplib2.HttpLib2Error(content)
372
373 # status >= 500 means transient failures.
374 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700375 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000376 try_count += 1
377 assert False, 'unreachable'
378
379
qyearsley1fdfcb62016-10-24 13:22:03 -0700380def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700381 """Returns a dict mapping bucket names to builders and tests,
382 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 """
qyearsleydd49f942016-10-28 11:57:22 -0700384 # If no bots are listed, we try to get a set of builders and tests based
385 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700386 if not options.bot:
387 change = changelist.GetChange(
388 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700389 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700390 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700391 change=change,
392 changed_files=change.LocalPaths(),
393 repository_root=settings.GetRoot(),
394 default_presubmit=None,
395 project=None,
396 verbose=options.verbose,
397 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700398 if masters is None:
399 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100400 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700401
qyearsley1fdfcb62016-10-24 13:22:03 -0700402 if options.bucket:
403 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700404 if options.master:
405 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
qyearsleydd49f942016-10-28 11:57:22 -0700407 # If bots are listed but no master or bucket, then we need to find out
408 # the corresponding master for each bot.
409 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
410 if error_message:
411 option_parser.error(
412 'Tryserver master cannot be found because: %s\n'
413 'Please manually specify the tryserver master, e.g. '
414 '"-m tryserver.chromium.linux".' % error_message)
415 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700416
417
qyearsley123a4682016-10-26 09:12:17 -0700418def _get_bucket_map_for_builders(builders):
419 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700420 map_url = 'https://builders-map.appspot.com/'
421 try:
qyearsley123a4682016-10-26 09:12:17 -0700422 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700423 except urllib2.URLError as e:
424 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
425 (map_url, e))
426 except ValueError as e:
427 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700428 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700429 return None, 'Failed to build master map.'
430
qyearsley123a4682016-10-26 09:12:17 -0700431 bucket_map = {}
432 for builder in builders:
Nodir Turakulovb422e682018-02-20 22:51:30 -0800433 bucket = builders_map.get(builder, {}).get('bucket')
434 if bucket:
435 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700436 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700437
438
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800439def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700440 """Sends a request to Buildbucket to trigger try jobs for a changelist.
441
442 Args:
Aaron Gablefb28d482018-04-02 13:08:06 -0700443 auth_config: AuthConfig for Buildbucket.
qyearsley1fdfcb62016-10-24 13:22:03 -0700444 changelist: Changelist that the try jobs are associated with.
445 buckets: A nested dict mapping bucket names to builders to tests.
446 options: Command-line options.
447 """
tandriide281ae2016-10-12 06:02:30 -0700448 assert changelist.GetIssue(), 'CL must be uploaded first'
449 codereview_url = changelist.GetCodereviewServer()
450 assert codereview_url, 'CL must be uploaded first'
451 patchset = patchset or changelist.GetMostRecentPatchset()
452 assert patchset, 'CL must be uploaded first'
453
454 codereview_host = urlparse.urlparse(codereview_url).hostname
Aaron Gablefb28d482018-04-02 13:08:06 -0700455 # Cache the buildbucket credentials under the codereview host key, so that
456 # users can use different credentials for different buckets.
tandriide281ae2016-10-12 06:02:30 -0700457 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000458 http = authenticator.authorize(httplib2.Http())
459 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700460
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000461 buildbucket_put_url = (
462 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000463 hostname=options.buildbucket_host))
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000464 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandriide281ae2016-10-12 06:02:30 -0700465 hostname=codereview_host,
466 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000467 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700468
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700469 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800470 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700471 if options.clobber:
472 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700473 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700474 if extra_properties:
475 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000476
477 batch_req_body = {'builds': []}
478 print_text = []
479 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700480 for bucket, builders_and_tests in sorted(buckets.iteritems()):
481 print_text.append('Bucket: %s' % bucket)
482 master = None
483 if bucket.startswith(MASTER_PREFIX):
484 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000485 for builder, tests in sorted(builders_and_tests.iteritems()):
486 print_text.append(' %s: %s' % (builder, tests))
487 parameters = {
488 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000489 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100490 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000491 'revision': options.revision,
492 }],
tandrii8c5a3532016-11-04 07:52:02 -0700493 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000494 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000495 if 'presubmit' in builder.lower():
496 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000497 if tests:
498 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700499
500 tags = [
501 'builder:%s' % builder,
502 'buildset:%s' % buildset,
503 'user_agent:git_cl_try',
504 ]
505 if master:
506 parameters['properties']['master'] = master
507 tags.append('master:%s' % master)
508
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000509 batch_req_body['builds'].append(
510 {
511 'bucket': bucket,
512 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000513 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700514 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515 }
516 )
517
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000518 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700519 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000520 http,
521 buildbucket_put_url,
522 'PUT',
523 body=json.dumps(batch_req_body),
524 headers={'Content-Type': 'application/json'}
525 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000526 print_text.append('To see results here, run: git cl try-results')
527 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700528 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000529
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000530
tandrii221ab252016-10-06 08:12:04 -0700531def fetch_try_jobs(auth_config, changelist, buildbucket_host,
532 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700533 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000534
qyearsley53f48a12016-09-01 10:45:13 -0700535 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000536 """
tandrii221ab252016-10-06 08:12:04 -0700537 assert buildbucket_host
538 assert changelist.GetIssue(), 'CL must be uploaded first'
539 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
540 patchset = patchset or changelist.GetMostRecentPatchset()
541 assert patchset, 'CL must be uploaded first'
542
543 codereview_url = changelist.GetCodereviewServer()
544 codereview_host = urlparse.urlparse(codereview_url).hostname
545 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000546 if authenticator.has_cached_credentials():
547 http = authenticator.authorize(httplib2.Http())
548 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700549 print('Warning: Some results might be missing because %s' %
550 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700551 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 http = httplib2.Http()
553
554 http.force_exception_to_status_code = True
555
Andrii Shyshkalov03da1502018-10-15 03:42:34 +0000556 buildset = 'patch/gerrit/{hostname}/{issue}/{patch}'.format(
tandrii221ab252016-10-06 08:12:04 -0700557 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700559 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 params = {'tag': 'buildset:%s' % buildset}
561
562 builds = {}
563 while True:
564 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700565 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000566 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700567 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000568 for build in content.get('builds', []):
569 builds[build['id']] = build
570 if 'next_cursor' in content:
571 params['start_cursor'] = content['next_cursor']
572 else:
573 break
574 return builds
575
576
qyearsleyeab3c042016-08-24 09:18:28 -0700577def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000578 """Prints nicely result of fetch_try_jobs."""
579 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700580 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000581 return
582
583 # Make a copy, because we'll be modifying builds dictionary.
584 builds = builds.copy()
585 builder_names_cache = {}
586
587 def get_builder(b):
588 try:
589 return builder_names_cache[b['id']]
590 except KeyError:
591 try:
592 parameters = json.loads(b['parameters_json'])
593 name = parameters['builder_name']
594 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700595 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700596 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000597 name = None
598 builder_names_cache[b['id']] = name
599 return name
600
601 def get_bucket(b):
602 bucket = b['bucket']
603 if bucket.startswith('master.'):
604 return bucket[len('master.'):]
605 return bucket
606
607 if options.print_master:
608 name_fmt = '%%-%ds %%-%ds' % (
609 max(len(str(get_bucket(b))) for b in builds.itervalues()),
610 max(len(str(get_builder(b))) for b in builds.itervalues()))
611 def get_name(b):
612 return name_fmt % (get_bucket(b), get_builder(b))
613 else:
614 name_fmt = '%%-%ds' % (
615 max(len(str(get_builder(b))) for b in builds.itervalues()))
616 def get_name(b):
617 return name_fmt % get_builder(b)
618
619 def sort_key(b):
620 return b['status'], b.get('result'), get_name(b), b.get('url')
621
622 def pop(title, f, color=None, **kwargs):
623 """Pop matching builds from `builds` dict and print them."""
624
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000625 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000626 colorize = str
627 else:
628 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
629
630 result = []
631 for b in builds.values():
632 if all(b.get(k) == v for k, v in kwargs.iteritems()):
633 builds.pop(b['id'])
634 result.append(b)
635 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700636 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700638 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000639
640 total = len(builds)
641 pop(status='COMPLETED', result='SUCCESS',
642 title='Successes:', color=Fore.GREEN,
643 f=lambda b: (get_name(b), b.get('url')))
644 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
645 title='Infra Failures:', color=Fore.MAGENTA,
646 f=lambda b: (get_name(b), b.get('url')))
647 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
648 title='Failures:', color=Fore.RED,
649 f=lambda b: (get_name(b), b.get('url')))
650 pop(status='COMPLETED', result='CANCELED',
651 title='Canceled:', color=Fore.MAGENTA,
652 f=lambda b: (get_name(b),))
653 pop(status='COMPLETED', result='FAILURE',
654 failure_reason='INVALID_BUILD_DEFINITION',
655 title='Wrong master/builder name:', color=Fore.MAGENTA,
656 f=lambda b: (get_name(b),))
657 pop(status='COMPLETED', result='FAILURE',
658 title='Other failures:',
659 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
660 pop(status='COMPLETED',
661 title='Other finished:',
662 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
663 pop(status='STARTED',
664 title='Started:', color=Fore.YELLOW,
665 f=lambda b: (get_name(b), b.get('url')))
666 pop(status='SCHEDULED',
667 title='Scheduled:',
668 f=lambda b: (get_name(b), 'id=%s' % b['id']))
669 # The last section is just in case buildbucket API changes OR there is a bug.
670 pop(title='Other:',
671 f=lambda b: (get_name(b), 'id=%s' % b['id']))
672 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700673 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000674
675
Aiden Bennerc08566e2018-10-03 17:52:42 +0000676def _ComputeDiffLineRanges(files, upstream_commit):
677 """Gets the changed line ranges for each file since upstream_commit.
678
679 Parses a git diff on provided files and returns a dict that maps a file name
680 to an ordered list of range tuples in the form (start_line, count).
681 Ranges are in the same format as a git diff.
682 """
683 # If files is empty then diff_output will be a full diff.
684 if len(files) == 0:
685 return {}
686
687 # Take diff and find the line ranges where there are changes.
688 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, files, allow_prefix=True)
689 diff_output = RunGit(diff_cmd)
690
691 pattern = r'(?:^diff --git a/(?:.*) b/(.*))|(?:^@@.*\+(.*) @@)'
692 # 2 capture groups
693 # 0 == fname of diff file
694 # 1 == 'diff_start,diff_count' or 'diff_start'
695 # will match each of
696 # diff --git a/foo.foo b/foo.py
697 # @@ -12,2 +14,3 @@
698 # @@ -12,2 +17 @@
699 # running re.findall on the above string with pattern will give
700 # [('foo.py', ''), ('', '14,3'), ('', '17')]
701
702 curr_file = None
703 line_diffs = {}
704 for match in re.findall(pattern, diff_output, flags=re.MULTILINE):
705 if match[0] != '':
706 # Will match the second filename in diff --git a/a.py b/b.py.
707 curr_file = match[0]
708 line_diffs[curr_file] = []
709 else:
710 # Matches +14,3
711 if ',' in match[1]:
712 diff_start, diff_count = match[1].split(',')
713 else:
714 # Single line changes are of the form +12 instead of +12,1.
715 diff_start = match[1]
716 diff_count = 1
717
718 diff_start = int(diff_start)
719 diff_count = int(diff_count)
720
721 # If diff_count == 0 this is a removal we can ignore.
722 line_diffs[curr_file].append((diff_start, diff_count))
723
724 return line_diffs
725
726
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000727def _FindYapfConfigFile(fpath, yapf_config_cache, top_dir=None):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000728 """Checks if a yapf file is in any parent directory of fpath until top_dir.
729
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000730 Recursively checks parent directories to find yapf file and if no yapf file
731 is found returns None. Uses yapf_config_cache as a cache for
732 previously found configs.
Aiden Bennerc08566e2018-10-03 17:52:42 +0000733 """
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000734 fpath = os.path.abspath(fpath)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000735 # Return result if we've already computed it.
736 if fpath in yapf_config_cache:
737 return yapf_config_cache[fpath]
738
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000739 parent_dir = os.path.dirname(fpath)
740 if os.path.isfile(fpath):
741 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000742 else:
Aiden Benner99b0ccb2018-11-20 19:53:31 +0000743 # Otherwise fpath is a directory
744 yapf_file = os.path.join(fpath, YAPF_CONFIG_FILENAME)
745 if os.path.isfile(yapf_file):
746 ret = yapf_file
747 elif fpath == top_dir or parent_dir == fpath:
748 # If we're at the top level directory, or if we're at root
749 # there is no provided style.
750 ret = None
751 else:
752 # Otherwise recurse on the current directory.
753 ret = _FindYapfConfigFile(parent_dir, yapf_config_cache, top_dir)
Aiden Bennerc08566e2018-10-03 17:52:42 +0000754 yapf_config_cache[fpath] = ret
755 return ret
756
757
qyearsley53f48a12016-09-01 10:45:13 -0700758def write_try_results_json(output_file, builds):
759 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
760
761 The input |builds| dict is assumed to be generated by Buildbucket.
762 Buildbucket documentation: http://goo.gl/G0s101
763 """
764
765 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800766 """Extracts some of the information from one build dict."""
767 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700768 return {
769 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700770 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800771 'builder_name': parameters.get('builder_name'),
772 'created_ts': build.get('created_ts'),
773 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700774 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800775 'result': build.get('result'),
776 'status': build.get('status'),
777 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700778 'url': build.get('url'),
779 }
780
781 converted = []
782 for _, build in sorted(builds.items()):
Aiden Bennerc08566e2018-10-03 17:52:42 +0000783 converted.append(convert_build_dict(build))
qyearsley53f48a12016-09-01 10:45:13 -0700784 write_json(output_file, converted)
785
786
Aaron Gable13101a62018-02-09 13:20:41 -0800787def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000788 """Prints statistics about the change to the user."""
789 # --no-ext-diff is broken in some versions of Git, so try to work around
790 # this by overriding the environment (but there is still a problem if the
791 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000792 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000793 if 'GIT_EXTERNAL_DIFF' in env:
794 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000795
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000796 try:
797 stdout = sys.stdout.fileno()
798 except AttributeError:
799 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000800 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800801 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000802 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000803
804
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000805class BuildbucketResponseException(Exception):
806 pass
807
808
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000809class Settings(object):
810 def __init__(self):
811 self.default_server = None
812 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000813 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 self.tree_status_url = None
815 self.viewvc_url = None
816 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000817 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000818 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000819 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000820 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000821 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000822 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000823
824 def LazyUpdateIfNeeded(self):
825 """Updates the settings from a codereview.settings file, if available."""
826 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000827 # The only value that actually changes the behavior is
828 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000829 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000830 error_ok=True
831 ).strip().lower()
832
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000834 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835 LoadCodereviewSettingsFromFile(cr_settings_file)
836 self.updated = True
837
838 def GetDefaultServerUrl(self, error_ok=False):
839 if not self.default_server:
840 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000841 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843 if error_ok:
844 return self.default_server
845 if not self.default_server:
846 error_message = ('Could not find settings file. You must configure '
847 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000848 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000849 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000850 return self.default_server
851
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000852 @staticmethod
853 def GetRelativeRoot():
854 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000855
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000856 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000857 if self.root is None:
858 self.root = os.path.abspath(self.GetRelativeRoot())
859 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000861 def GetGitMirror(self, remote='origin'):
862 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000863 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000864 if not os.path.isdir(local_url):
865 return None
866 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
867 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100868 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100869 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000870 if mirror.exists():
871 return mirror
872 return None
873
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874 def GetTreeStatusUrl(self, error_ok=False):
875 if not self.tree_status_url:
876 error_message = ('You must configure your tree status URL by running '
877 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000878 self.tree_status_url = self._GetRietveldConfig(
879 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 return self.tree_status_url
881
882 def GetViewVCUrl(self):
883 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000884 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000885 return self.viewvc_url
886
rmistry@google.com90752582014-01-14 21:04:50 +0000887 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000888 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000889
rmistry@google.com78948ed2015-07-08 23:09:57 +0000890 def GetIsSkipDependencyUpload(self, branch_name):
891 """Returns true if specified branch should skip dep uploads."""
892 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
893 error_ok=True)
894
rmistry@google.com5626a922015-02-26 14:03:30 +0000895 def GetRunPostUploadHook(self):
896 run_post_upload_hook = self._GetRietveldConfig(
897 'run-post-upload-hook', error_ok=True)
898 return run_post_upload_hook == "True"
899
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000900 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000901 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000902
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000903 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000904 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000905
ukai@chromium.orge8077812012-02-03 03:41:46 +0000906 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700907 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000908 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700909 self.is_gerrit = (
910 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000911 return self.is_gerrit
912
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000913 def GetSquashGerritUploads(self):
914 """Return true if uploads to Gerrit should be squashed by default."""
915 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700916 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
917 if self.squash_gerrit_uploads is None:
918 # Default is squash now (http://crbug.com/611892#c23).
919 self.squash_gerrit_uploads = not (
920 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
921 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000922 return self.squash_gerrit_uploads
923
tandriia60502f2016-06-20 02:01:53 -0700924 def GetSquashGerritUploadsOverride(self):
925 """Return True or False if codereview.settings should be overridden.
926
927 Returns None if no override has been defined.
928 """
929 # See also http://crbug.com/611892#c23
930 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
931 error_ok=True).strip()
932 if result == 'true':
933 return True
934 if result == 'false':
935 return False
936 return None
937
tandrii@chromium.org28253532016-04-14 13:46:56 +0000938 def GetGerritSkipEnsureAuthenticated(self):
939 """Return True if EnsureAuthenticated should not be done for Gerrit
940 uploads."""
941 if self.gerrit_skip_ensure_authenticated is None:
942 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000943 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000944 error_ok=True).strip() == 'true')
945 return self.gerrit_skip_ensure_authenticated
946
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000947 def GetGitEditor(self):
948 """Return the editor specified in the git config, or None if none is."""
949 if self.git_editor is None:
950 self.git_editor = self._GetConfig('core.editor', error_ok=True)
951 return self.git_editor or None
952
thestig@chromium.org44202a22014-03-11 19:22:18 +0000953 def GetLintRegex(self):
954 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
955 DEFAULT_LINT_REGEX)
956
957 def GetLintIgnoreRegex(self):
958 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
959 DEFAULT_LINT_IGNORE_REGEX)
960
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000961 def GetProject(self):
962 if not self.project:
963 self.project = self._GetRietveldConfig('project', error_ok=True)
964 return self.project
965
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000966 def _GetRietveldConfig(self, param, **kwargs):
967 return self._GetConfig('rietveld.' + param, **kwargs)
968
rmistry@google.com78948ed2015-07-08 23:09:57 +0000969 def _GetBranchConfig(self, branch_name, param, **kwargs):
970 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
971
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 def _GetConfig(self, param, **kwargs):
973 self.LazyUpdateIfNeeded()
974 return RunGit(['config', param], **kwargs).strip()
975
976
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100977@contextlib.contextmanager
978def _get_gerrit_project_config_file(remote_url):
979 """Context manager to fetch and store Gerrit's project.config from
980 refs/meta/config branch and store it in temp file.
981
982 Provides a temporary filename or None if there was error.
983 """
984 error, _ = RunGitWithCode([
985 'fetch', remote_url,
986 '+refs/meta/config:refs/git_cl/meta/config'])
987 if error:
988 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700989 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100990 (remote_url, error))
991 yield None
992 return
993
994 error, project_config_data = RunGitWithCode(
995 ['show', 'refs/git_cl/meta/config:project.config'])
996 if error:
997 print('WARNING: project.config file not found')
998 yield None
999 return
1000
1001 with gclient_utils.temporary_directory() as tempdir:
1002 project_config_file = os.path.join(tempdir, 'project.config')
1003 gclient_utils.FileWrite(project_config_file, project_config_data)
1004 yield project_config_file
1005
1006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007def ShortBranchName(branch):
1008 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001009 return branch.replace('refs/heads/', '', 1)
1010
1011
1012def GetCurrentBranchRef():
1013 """Returns branch ref (e.g., refs/heads/master) or None."""
1014 return RunGit(['symbolic-ref', 'HEAD'],
1015 stderr=subprocess2.VOID, error_ok=True).strip() or None
1016
1017
1018def GetCurrentBranch():
1019 """Returns current branch or None.
1020
1021 For refs/heads/* branches, returns just last part. For others, full ref.
1022 """
1023 branchref = GetCurrentBranchRef()
1024 if branchref:
1025 return ShortBranchName(branchref)
1026 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027
1028
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001029class _CQState(object):
1030 """Enum for states of CL with respect to Commit Queue."""
1031 NONE = 'none'
1032 DRY_RUN = 'dry_run'
1033 COMMIT = 'commit'
1034
1035 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1036
1037
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001038class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001039 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001040 self.issue = issue
1041 self.patchset = patchset
1042 self.hostname = hostname
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001043 assert codereview in (None, 'gerrit', 'rietveld')
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001044 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001045
1046 @property
1047 def valid(self):
1048 return self.issue is not None
1049
1050
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001051def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001052 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1053 fail_result = _ParsedIssueNumberArgument()
1054
1055 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001056 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001057 if not arg.startswith('http'):
1058 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001059
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001060 url = gclient_utils.UpgradeToHttps(arg)
1061 try:
1062 parsed_url = urlparse.urlparse(url)
1063 except ValueError:
1064 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001065
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001066 if codereview is not None:
1067 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1068 return parsed or fail_result
1069
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001070 results = {}
1071 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1072 parsed = cls.ParseIssueURL(parsed_url)
1073 if parsed is not None:
1074 results[name] = parsed
1075
1076 if not results:
1077 return fail_result
1078 if len(results) == 1:
1079 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001080
Andrii Shyshkalovf5569d22018-10-15 03:35:23 +00001081 return results['gerrit']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001082
1083
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00001084def _create_description_from_log(args):
1085 """Pulls out the commit log to use as a base for the CL description."""
1086 log_args = []
1087 if len(args) == 1 and not args[0].endswith('.'):
1088 log_args = [args[0] + '..']
1089 elif len(args) == 1 and args[0].endswith('...'):
1090 log_args = [args[0][:-1]]
1091 elif len(args) == 2:
1092 log_args = [args[0] + '..' + args[1]]
1093 else:
1094 log_args = args[:] # Hope for the best!
1095 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
1096
1097
Aaron Gablea45ee112016-11-22 15:14:38 -08001098class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001099 def __init__(self, issue, url):
1100 self.issue = issue
1101 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001102 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001103
1104 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001105 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001106 self.issue, self.url)
1107
1108
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001109_CommentSummary = collections.namedtuple(
1110 '_CommentSummary', ['date', 'message', 'sender',
1111 # TODO(tandrii): these two aren't known in Gerrit.
1112 'approval', 'disapproval'])
1113
1114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 """Changelist works with one changelist in local branch.
1117
1118 Supports two codereview backends: Rietveld or Gerrit, selected at object
1119 creation.
1120
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001121 Notes:
1122 * Not safe for concurrent multi-{thread,process} use.
1123 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001124 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001125 """
1126
1127 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1128 """Create a new ChangeList instance.
1129
1130 If issue is given, the codereview must be given too.
1131
1132 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1133 Otherwise, it's decided based on current configuration of the local branch,
1134 with default being 'rietveld' for backwards compatibility.
1135 See _load_codereview_impl for more details.
1136
1137 **kwargs will be passed directly to codereview implementation.
1138 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001140 global settings
1141 if not settings:
1142 # Happens when git_cl.py is used as a utility library.
1143 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001144
1145 if issue:
1146 assert codereview, 'codereview must be known, if issue is known'
1147
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 self.branchref = branchref
1149 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001150 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151 self.branch = ShortBranchName(self.branchref)
1152 else:
1153 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001155 self.lookedup_issue = False
1156 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 self.has_description = False
1158 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001159 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001161 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001162 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001163 self._remote = None
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001164 self._cached_remote_url = (False, None) # (is_cached, value)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001165
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001166 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001167 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001168 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001169 assert self._codereview_impl
1170 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001171
1172 def _load_codereview_impl(self, codereview=None, **kwargs):
1173 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001174 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1175 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1176 self._codereview = codereview
1177 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001178 return
1179
1180 # Automatic selection based on issue number set for a current branch.
1181 # Rietveld takes precedence over Gerrit.
1182 assert not self.issue
1183 # Whether we find issue or not, we are doing the lookup.
1184 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001185 if self.GetBranch():
1186 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1187 issue = _git_get_branch_config_value(
1188 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1189 if issue:
1190 self._codereview = codereview
1191 self._codereview_impl = cls(self, **kwargs)
1192 self.issue = int(issue)
1193 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001194
1195 # No issue is set for this branch, so decide based on repo-wide settings.
1196 return self._load_codereview_impl(
1197 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1198 **kwargs)
1199
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001200 def IsGerrit(self):
1201 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001202
1203 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001204 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001205
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001206 The return value is a string suitable for passing to git cl with the --cc
1207 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001208 """
1209 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001210 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001211 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001212 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1213 return self.cc
1214
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001215 def GetCCListWithoutDefault(self):
1216 """Return the users cc'd on this CL excluding default ones."""
1217 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001218 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001219 return self.cc
1220
Daniel Cheng7227d212017-11-17 08:12:37 -08001221 def ExtendCC(self, more_cc):
1222 """Extends the list of users to cc on this CL based on the changed files."""
1223 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224
1225 def GetBranch(self):
1226 """Returns the short branch name, e.g. 'master'."""
1227 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001228 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001229 if not branchref:
1230 return None
1231 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232 self.branch = ShortBranchName(self.branchref)
1233 return self.branch
1234
1235 def GetBranchRef(self):
1236 """Returns the full branch name, e.g. 'refs/heads/master'."""
1237 self.GetBranch() # Poke the lazy loader.
1238 return self.branchref
1239
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001240 def ClearBranch(self):
1241 """Clears cached branch data of this object."""
1242 self.branch = self.branchref = None
1243
tandrii5d48c322016-08-18 16:19:37 -07001244 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1245 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1246 kwargs['branch'] = self.GetBranch()
1247 return _git_get_branch_config_value(key, default, **kwargs)
1248
1249 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1250 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1251 assert self.GetBranch(), (
1252 'this CL must have an associated branch to %sset %s%s' %
1253 ('un' if value is None else '',
1254 key,
1255 '' if value is None else ' to %r' % value))
1256 kwargs['branch'] = self.GetBranch()
1257 return _git_set_branch_config_value(key, value, **kwargs)
1258
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001259 @staticmethod
1260 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001261 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 e.g. 'origin', 'refs/heads/master'
1263 """
1264 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001265 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1266
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001268 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001270 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1271 error_ok=True).strip()
1272 if upstream_branch:
1273 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001275 # Else, try to guess the origin remote.
1276 remote_branches = RunGit(['branch', '-r']).split()
1277 if 'origin/master' in remote_branches:
1278 # Fall back on origin/master if it exits.
1279 remote = 'origin'
1280 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001282 DieWithError(
1283 'Unable to determine default branch to diff against.\n'
1284 'Either pass complete "git diff"-style arguments, like\n'
1285 ' git cl upload origin/master\n'
1286 'or verify this branch is set up to track another \n'
1287 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288
1289 return remote, upstream_branch
1290
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001291 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001292 upstream_branch = self.GetUpstreamBranch()
1293 if not BranchExists(upstream_branch):
1294 DieWithError('The upstream for the current branch (%s) does not exist '
1295 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001296 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001297 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 def GetUpstreamBranch(self):
1300 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001303 upstream_branch = upstream_branch.replace('refs/heads/',
1304 'refs/remotes/%s/' % remote)
1305 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1306 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307 self.upstream_branch = upstream_branch
1308 return self.upstream_branch
1309
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001310 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001311 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001312 remote, branch = None, self.GetBranch()
1313 seen_branches = set()
1314 while branch not in seen_branches:
1315 seen_branches.add(branch)
1316 remote, branch = self.FetchUpstreamTuple(branch)
1317 branch = ShortBranchName(branch)
1318 if remote != '.' or branch.startswith('refs/remotes'):
1319 break
1320 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001321 remotes = RunGit(['remote'], error_ok=True).split()
1322 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001323 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001324 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001325 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001326 logging.warn('Could not determine which remote this change is '
1327 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001328 else:
1329 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001330 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 branch = 'HEAD'
1332 if branch.startswith('refs/remotes'):
1333 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001334 elif branch.startswith('refs/branch-heads/'):
1335 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001336 else:
1337 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001338 return self._remote
1339
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001340 def GitSanityChecks(self, upstream_git_obj):
1341 """Checks git repo status and ensures diff is from local commits."""
1342
sbc@chromium.org79706062015-01-14 21:18:12 +00001343 if upstream_git_obj is None:
1344 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001345 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001346 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001347 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001348 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001349 return False
1350
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001351 # Verify the commit we're diffing against is in our current branch.
1352 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1353 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1354 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001355 print('ERROR: %s is not in the current branch. You may need to rebase '
1356 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001357 return False
1358
1359 # List the commits inside the diff, and verify they are all local.
1360 commits_in_diff = RunGit(
1361 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1362 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1363 remote_branch = remote_branch.strip()
1364 if code != 0:
1365 _, remote_branch = self.GetRemoteBranch()
1366
1367 commits_in_remote = RunGit(
1368 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1369
1370 common_commits = set(commits_in_diff) & set(commits_in_remote)
1371 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001372 print('ERROR: Your diff contains %d commits already in %s.\n'
1373 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1374 'the diff. If you are using a custom git flow, you can override'
1375 ' the reference used for this check with "git config '
1376 'gitcl.remotebranch <git-ref>".' % (
1377 len(common_commits), remote_branch, upstream_git_obj),
1378 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001379 return False
1380 return True
1381
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001382 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001383 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001384
1385 Returns None if it is not set.
1386 """
tandrii5d48c322016-08-18 16:19:37 -07001387 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 def GetRemoteUrl(self):
1390 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1391
1392 Returns None if there is no remote.
1393 """
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001394 is_cached, value = self._cached_remote_url
1395 if is_cached:
1396 return value
1397
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001398 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001399 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1400
1401 # If URL is pointing to a local directory, it is probably a git cache.
1402 if os.path.isdir(url):
1403 url = RunGit(['config', 'remote.%s.url' % remote],
1404 error_ok=True,
1405 cwd=url).strip()
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00001406 self._cached_remote_url = (True, url)
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001407 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001409 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001410 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001411 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001412 self.issue = self._GitGetBranchConfigValue(
1413 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001414 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 return self.issue
1416
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 def GetIssueURL(self):
1418 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001419 issue = self.GetIssue()
1420 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001421 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001422 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001424 def GetDescription(self, pretty=False, force=False):
1425 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001427 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001428 self.has_description = True
1429 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001430 # Set width to 72 columns + 2 space indent.
1431 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001433 lines = self.description.splitlines()
1434 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 return self.description
1436
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001437 def GetDescriptionFooters(self):
1438 """Returns (non_footer_lines, footers) for the commit message.
1439
1440 Returns:
1441 non_footer_lines (list(str)) - Simple list of description lines without
1442 any footer. The lines do not contain newlines, nor does the list contain
1443 the empty line between the message and the footers.
1444 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1445 [("Change-Id", "Ideadbeef...."), ...]
1446 """
1447 raw_description = self.GetDescription()
1448 msg_lines, _, footers = git_footers.split_footers(raw_description)
1449 if footers:
1450 msg_lines = msg_lines[:len(msg_lines)-1]
1451 return msg_lines, footers
1452
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001454 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001455 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001456 self.patchset = self._GitGetBranchConfigValue(
1457 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001458 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459 return self.patchset
1460
1461 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001462 """Set this branch's patchset. If patchset=0, clears the patchset."""
1463 assert self.GetBranch()
1464 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001465 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001466 else:
1467 self.patchset = int(patchset)
1468 self._GitSetBranchConfigValue(
1469 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001470
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001471 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001472 """Set this branch's issue. If issue isn't given, clears the issue."""
1473 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001474 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001475 issue = int(issue)
1476 self._GitSetBranchConfigValue(
1477 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001478 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001479 codereview_server = self._codereview_impl.GetCodereviewServer()
1480 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001481 self._GitSetBranchConfigValue(
1482 self._codereview_impl.CodereviewServerConfigKey(),
1483 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001484 else:
tandrii5d48c322016-08-18 16:19:37 -07001485 # Reset all of these just to be clean.
1486 reset_suffixes = [
1487 'last-upload-hash',
1488 self._codereview_impl.IssueConfigKey(),
1489 self._codereview_impl.PatchsetConfigKey(),
1490 self._codereview_impl.CodereviewServerConfigKey(),
1491 ] + self._PostUnsetIssueProperties()
1492 for prop in reset_suffixes:
1493 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001494 msg = RunGit(['log', '-1', '--format=%B']).strip()
1495 if msg and git_footers.get_footer_change_id(msg):
1496 print('WARNING: The change patched into this branch has a Change-Id. '
1497 'Removing it.')
1498 RunGit(['commit', '--amend', '-m',
1499 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001500 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001501 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502
dnjba1b0f32016-09-02 12:37:42 -07001503 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001504 if not self.GitSanityChecks(upstream_branch):
1505 DieWithError('\nGit sanity check failure')
1506
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001507 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001508 if not root:
1509 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001510 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001511
1512 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001513 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001514 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001515 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001516 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001517 except subprocess2.CalledProcessError:
1518 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001519 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001520 'This branch probably doesn\'t exist anymore. To reset the\n'
1521 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001522 ' git branch --set-upstream-to origin/master %s\n'
1523 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001524 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001525
maruel@chromium.org52424302012-08-29 15:14:30 +00001526 issue = self.GetIssue()
1527 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001528 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001529 description = self.GetDescription()
1530 else:
1531 # If the change was never uploaded, use the log messages of all commits
1532 # up to the branch point, as git cl upload will prefill the description
1533 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001534 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1535 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001536
1537 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001538 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001539 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001540 name,
1541 description,
1542 absroot,
1543 files,
1544 issue,
1545 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001546 author,
1547 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001548
dsansomee2d6fd92016-09-08 00:10:47 -07001549 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001550 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001551 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001552 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001553
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001554 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1555 """Sets the description for this CL remotely.
1556
1557 You can get description_lines and footers with GetDescriptionFooters.
1558
1559 Args:
1560 description_lines (list(str)) - List of CL description lines without
1561 newline characters.
1562 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1563 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1564 `List-Of-Tokens`). It will be case-normalized so that each token is
1565 title-cased.
1566 """
1567 new_description = '\n'.join(description_lines)
1568 if footers:
1569 new_description += '\n'
1570 for k, v in footers:
1571 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1572 if not git_footers.FOOTER_PATTERN.match(foot):
1573 raise ValueError('Invalid footer %r' % foot)
1574 new_description += foot + '\n'
1575 self.UpdateDescription(new_description, force)
1576
Edward Lesmes8e282792018-04-03 18:50:29 -04001577 def RunHook(self, committing, may_prompt, verbose, change, parallel):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001578 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1579 try:
1580 return presubmit_support.DoPresubmitChecks(change, committing,
1581 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1582 default_presubmit=None, may_prompt=may_prompt,
Edward Lesmes8e282792018-04-03 18:50:29 -04001583 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit(),
1584 parallel=parallel)
vapierfd77ac72016-06-16 08:33:57 -07001585 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001586 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001587
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001588 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1589 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001590 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1591 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001592 else:
1593 # Assume url.
1594 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1595 urlparse.urlparse(issue_arg))
1596 if not parsed_issue_arg or not parsed_issue_arg.valid:
1597 DieWithError('Failed to parse issue argument "%s". '
1598 'Must be an issue number or a valid URL.' % issue_arg)
1599 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001600 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001601
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001602 def CMDUpload(self, options, git_diff_args, orig_args):
1603 """Uploads a change to codereview."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00001604 assert self.IsGerrit()
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001605 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001606 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001607 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001608 else:
1609 if self.GetBranch() is None:
1610 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1611
1612 # Default to diffing against common ancestor of upstream branch
1613 base_branch = self.GetCommonAncestorWithUpstream()
1614 git_diff_args = [base_branch, 'HEAD']
1615
Aaron Gablec4c40d12017-05-22 11:49:53 -07001616
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001617 # Fast best-effort checks to abort before running potentially
1618 # expensive hooks if uploading is likely to fail anyway. Passing these
1619 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001620 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001621 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001622
1623 # Apply watchlists on upload.
1624 change = self.GetChange(base_branch, None)
1625 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1626 files = [f.LocalPath() for f in change.AffectedFiles()]
1627 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001628 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001629
1630 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001631 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001632 # Set the reviewer list now so that presubmit checks can access it.
1633 change_description = ChangeDescription(change.FullDescriptionText())
1634 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001635 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001636 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001637 change)
1638 change.SetDescriptionText(change_description.description)
1639 hook_results = self.RunHook(committing=False,
Edward Lesmes8e282792018-04-03 18:50:29 -04001640 may_prompt=not options.force,
1641 verbose=options.verbose,
1642 change=change, parallel=options.parallel)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001643 if not hook_results.should_continue():
1644 return 1
1645 if not options.reviewers and hook_results.reviewers:
1646 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001647 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001648
Aaron Gable13101a62018-02-09 13:20:41 -08001649 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001650 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001651 if not ret:
tandrii5d48c322016-08-18 16:19:37 -07001652 _git_set_branch_config_value('last-upload-hash',
1653 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001654 # Run post upload hooks, if specified.
1655 if settings.GetRunPostUploadHook():
1656 presubmit_support.DoPostUploadExecuter(
1657 change,
1658 self,
1659 settings.GetRoot(),
1660 options.verbose,
1661 sys.stdout)
1662
1663 # Upload all dependencies if specified.
1664 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001665 print()
1666 print('--dependencies has been specified.')
1667 print('All dependent local branches will be re-uploaded.')
1668 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001669 # Remove the dependencies flag from args so that we do not end up in a
1670 # loop.
1671 orig_args.remove('--dependencies')
1672 ret = upload_branch_deps(self, orig_args)
1673 return ret
1674
Ravi Mistry31e7d562018-04-02 12:53:57 -04001675 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1676 """Sets labels on the change based on the provided flags.
1677
1678 Sets labels if issue is already uploaded and known, else returns without
1679 doing anything.
1680
1681 Args:
1682 enable_auto_submit: Sets Auto-Submit+1 on the change.
1683 use_commit_queue: Sets Commit-Queue+2 on the change.
1684 cq_dry_run: Sets Commit-Queue+1 on the change. Overrides Commit-Queue+2 if
1685 both use_commit_queue and cq_dry_run are true.
1686 """
1687 if not self.GetIssue():
1688 return
1689 try:
1690 self._codereview_impl.SetLabels(enable_auto_submit, use_commit_queue,
1691 cq_dry_run)
1692 return 0
1693 except KeyboardInterrupt:
1694 raise
1695 except:
1696 labels = []
1697 if enable_auto_submit:
1698 labels.append('Auto-Submit')
1699 if use_commit_queue or cq_dry_run:
1700 labels.append('Commit-Queue')
1701 print('WARNING: Failed to set label(s) on your change: %s\n'
1702 'Either:\n'
1703 ' * Your project does not have the above label(s),\n'
1704 ' * You don\'t have permission to set the above label(s),\n'
1705 ' * There\'s a bug in this code (see stack trace below).\n' %
1706 (', '.join(labels)))
1707 # Still raise exception so that stack trace is printed.
1708 raise
1709
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001710 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001711 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001712
1713 Issue must have been already uploaded and known.
1714 """
1715 assert new_state in _CQState.ALL_STATES
1716 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001717 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001718 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001719 return 0
1720 except KeyboardInterrupt:
1721 raise
1722 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001723 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001724 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001725 ' * Your project has no CQ,\n'
1726 ' * You don\'t have permission to change the CQ state,\n'
1727 ' * There\'s a bug in this code (see stack trace below).\n'
1728 'Consider specifying which bots to trigger manually or asking your '
1729 'project owners for permissions or contacting Chrome Infra at:\n'
1730 'https://www.chromium.org/infra\n\n' %
1731 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001732 # Still raise exception so that stack trace is printed.
1733 raise
1734
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001735 # Forward methods to codereview specific implementation.
1736
Aaron Gable636b13f2017-07-14 10:42:48 -07001737 def AddComment(self, message, publish=None):
1738 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001739
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001740 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001741 """Returns list of _CommentSummary for each comment.
1742
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001743 args:
1744 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001745 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001746 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001747
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001748 def CloseIssue(self):
1749 return self._codereview_impl.CloseIssue()
1750
1751 def GetStatus(self):
1752 return self._codereview_impl.GetStatus()
1753
1754 def GetCodereviewServer(self):
1755 return self._codereview_impl.GetCodereviewServer()
1756
tandriide281ae2016-10-12 06:02:30 -07001757 def GetIssueOwner(self):
1758 """Get owner from codereview, which may differ from this checkout."""
1759 return self._codereview_impl.GetIssueOwner()
1760
Edward Lemur707d70b2018-02-07 00:50:14 +01001761 def GetReviewers(self):
1762 return self._codereview_impl.GetReviewers()
1763
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001764 def GetMostRecentPatchset(self):
1765 return self._codereview_impl.GetMostRecentPatchset()
1766
tandriide281ae2016-10-12 06:02:30 -07001767 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001768 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001769 return self._codereview_impl.CannotTriggerTryJobReason()
1770
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001771 def GetTryJobProperties(self, patchset=None):
1772 """Returns dictionary of properties to launch try job."""
1773 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001774
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001775 def __getattr__(self, attr):
1776 # This is because lots of untested code accesses Rietveld-specific stuff
1777 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001778 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001779 # Note that child method defines __getattr__ as well, and forwards it here,
1780 # because _RietveldChangelistImpl is not cleaned up yet, and given
1781 # deprecation of Rietveld, it should probably be just removed.
1782 # Until that time, avoid infinite recursion by bypassing __getattr__
1783 # of implementation class.
1784 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001785
1786
1787class _ChangelistCodereviewBase(object):
1788 """Abstract base class encapsulating codereview specifics of a changelist."""
1789 def __init__(self, changelist):
1790 self._changelist = changelist # instance of Changelist
1791
1792 def __getattr__(self, attr):
1793 # Forward methods to changelist.
1794 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1795 # _RietveldChangelistImpl to avoid this hack?
1796 return getattr(self._changelist, attr)
1797
1798 def GetStatus(self):
1799 """Apply a rough heuristic to give a simple summary of an issue's review
1800 or CQ status, assuming adherence to a common workflow.
1801
1802 Returns None if no issue for this branch, or specific string keywords.
1803 """
1804 raise NotImplementedError()
1805
1806 def GetCodereviewServer(self):
1807 """Returns server URL without end slash, like "https://codereview.com"."""
1808 raise NotImplementedError()
1809
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001810 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001811 """Fetches and returns description from the codereview server."""
1812 raise NotImplementedError()
1813
tandrii5d48c322016-08-18 16:19:37 -07001814 @classmethod
1815 def IssueConfigKey(cls):
1816 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001817 raise NotImplementedError()
1818
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001819 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001820 def PatchsetConfigKey(cls):
1821 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001822 raise NotImplementedError()
1823
tandrii5d48c322016-08-18 16:19:37 -07001824 @classmethod
1825 def CodereviewServerConfigKey(cls):
1826 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001827 raise NotImplementedError()
1828
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001829 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001830 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001831 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001832
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001833 def GetGerritObjForPresubmit(self):
1834 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1835 return None
1836
dsansomee2d6fd92016-09-08 00:10:47 -07001837 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001838 """Update the description on codereview site."""
1839 raise NotImplementedError()
1840
Aaron Gable636b13f2017-07-14 10:42:48 -07001841 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001842 """Posts a comment to the codereview site."""
1843 raise NotImplementedError()
1844
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001845 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001846 raise NotImplementedError()
1847
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001848 def CloseIssue(self):
1849 """Closes the issue."""
1850 raise NotImplementedError()
1851
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001852 def GetMostRecentPatchset(self):
1853 """Returns the most recent patchset number from the codereview site."""
1854 raise NotImplementedError()
1855
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001856 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001857 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001858 """Fetches and applies the issue.
1859
1860 Arguments:
1861 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1862 reject: if True, reject the failed patch instead of switching to 3-way
1863 merge. Rietveld only.
1864 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1865 only.
1866 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001867 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001868 """
1869 raise NotImplementedError()
1870
1871 @staticmethod
1872 def ParseIssueURL(parsed_url):
1873 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1874 failed."""
1875 raise NotImplementedError()
1876
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001877 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001878 """Best effort check that user is authenticated with codereview server.
1879
1880 Arguments:
1881 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001882 refresh: whether to attempt to refresh credentials. Ignored if not
1883 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001884 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001885 raise NotImplementedError()
1886
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001887 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001888 """Best effort check that uploading isn't supposed to fail for predictable
1889 reasons.
1890
1891 This method should raise informative exception if uploading shouldn't
1892 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001893
1894 Arguments:
1895 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001896 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001897 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001898
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001899 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001900 """Uploads a change to codereview."""
1901 raise NotImplementedError()
1902
Ravi Mistry31e7d562018-04-02 12:53:57 -04001903 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
1904 """Sets labels on the change based on the provided flags.
1905
1906 Issue must have been already uploaded and known.
1907 """
1908 raise NotImplementedError()
1909
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001910 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001911 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001912
1913 Issue must have been already uploaded and known.
1914 """
1915 raise NotImplementedError()
1916
tandriie113dfd2016-10-11 10:20:12 -07001917 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001918 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001919 raise NotImplementedError()
1920
tandriide281ae2016-10-12 06:02:30 -07001921 def GetIssueOwner(self):
1922 raise NotImplementedError()
1923
Edward Lemur707d70b2018-02-07 00:50:14 +01001924 def GetReviewers(self):
1925 raise NotImplementedError()
1926
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001927 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001928 raise NotImplementedError()
1929
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001930
1931class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001932
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001933 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001934 super(_RietveldChangelistImpl, self).__init__(changelist)
1935 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001936 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001937 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001938
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001939 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001940 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001941 self._props = None
1942 self._rpc_server = None
1943
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001944 def GetCodereviewServer(self):
1945 if not self._rietveld_server:
1946 # If we're on a branch then get the server potentially associated
1947 # with that branch.
1948 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001949 self._rietveld_server = gclient_utils.UpgradeToHttps(
1950 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001951 if not self._rietveld_server:
1952 self._rietveld_server = settings.GetDefaultServerUrl()
1953 return self._rietveld_server
1954
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001955 def EnsureAuthenticated(self, force, refresh=False):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001956 # No checks for Rietveld because we are deprecating Rietveld.
1957 pass
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001958
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001959 def EnsureCanUploadPatchset(self, force):
1960 # No checks for Rietveld because we are deprecating Rietveld.
1961 pass
1962
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001963 def FetchDescription(self, force=False):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001964 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001965
1966 def GetMostRecentPatchset(self):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001967 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001968
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001969 def GetIssueProperties(self):
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00001970 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001971
tandriie113dfd2016-10-11 10:20:12 -07001972 def CannotTriggerTryJobReason(self):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001973 raise NotImplementedError()
tandriie113dfd2016-10-11 10:20:12 -07001974
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001975 def GetTryJobProperties(self, patchset=None):
Andrii Shyshkalov03da1502018-10-15 03:42:34 +00001976 raise NotImplementedError()
tandrii8c5a3532016-11-04 07:52:02 -07001977
tandriide281ae2016-10-12 06:02:30 -07001978 def GetIssueOwner(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001979 raise NotImplementedError()
tandriide281ae2016-10-12 06:02:30 -07001980
Edward Lemur707d70b2018-02-07 00:50:14 +01001981 def GetReviewers(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001982 raise NotImplementedError()
Edward Lemur707d70b2018-02-07 00:50:14 +01001983
Aaron Gable636b13f2017-07-14 10:42:48 -07001984 def AddComment(self, message, publish=None):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001985 raise NotImplementedError()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001986
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001987 def GetCommentsSummary(self, readable=True):
1988 raise NotImplementedError()
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001989
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001990 def GetStatus(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00001991 print(
1992 'WARNING! Rietveld is no longer supported.\n'
1993 '\n'
1994 'If you have old branches in your checkout, please archive/delete them.\n'
1995 ' $ git cl archive --help\n'
1996 '\n'
1997 'See also PSA https://groups.google.com/a/chromium.org/'
1998 'forum/#!topic/infra-dev/2DIVzM2wseo\n')
1999 return 'rietveld-not-supported'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002000
dsansomee2d6fd92016-09-08 00:10:47 -07002001 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00002002 raise NotImplementedError()
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002003
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004 def CloseIssue(self):
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00002005 raise NotImplementedError()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002007 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002008 return self.SetFlags({flag: value})
2009
2010 def SetFlags(self, flags):
2011 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002012 """
phajdan.jr68598232016-08-10 03:28:28 -07002013 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002014 try:
tandrii4b233bd2016-07-06 03:50:29 -07002015 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002016 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002017 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002018 if e.code == 404:
2019 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2020 if e.code == 403:
2021 DieWithError(
2022 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002023 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002024 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002025
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002026 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002027 """Returns an upload.RpcServer() to access this review's rietveld instance.
2028 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002029 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002030 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002031 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002032 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002033 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002034
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002035 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002036 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002037 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038
tandrii5d48c322016-08-18 16:19:37 -07002039 @classmethod
2040 def PatchsetConfigKey(cls):
2041 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042
tandrii5d48c322016-08-18 16:19:37 -07002043 @classmethod
2044 def CodereviewServerConfigKey(cls):
2045 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002046
Ravi Mistry31e7d562018-04-02 12:53:57 -04002047 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
2048 raise NotImplementedError()
2049
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002050 def SetCQState(self, new_state):
2051 props = self.GetIssueProperties()
2052 if props.get('private'):
2053 DieWithError('Cannot set-commit on private issue')
2054
2055 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002056 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002057 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002058 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002059 else:
tandrii4b233bd2016-07-06 03:50:29 -07002060 assert new_state == _CQState.DRY_RUN
2061 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002062
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002063 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002064 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002065 # PatchIssue should never be called with a dirty tree. It is up to the
2066 # caller to check this, but just in case we assert here since the
2067 # consequences of the caller not checking this could be dire.
2068 assert(not git_common.is_dirty_git_tree('apply'))
2069 assert(parsed_issue_arg.valid)
2070 self._changelist.issue = parsed_issue_arg.issue
2071 if parsed_issue_arg.hostname:
2072 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2073
skobes6468b902016-10-24 08:45:10 -07002074 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2075 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2076 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002077 try:
skobes6468b902016-10-24 08:45:10 -07002078 scm_obj.apply_patch(patchset_object)
2079 except Exception as e:
2080 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002081 return 1
2082
2083 # If we had an issue, commit the current state and register the issue.
2084 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002085 self.SetIssue(self.GetIssue())
2086 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002087 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2088 'patch from issue %(i)s at patchset '
2089 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2090 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002091 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002092 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002093 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002094 return 0
2095
2096 @staticmethod
2097 def ParseIssueURL(parsed_url):
2098 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2099 return None
wychen3c1c1722016-08-04 11:46:36 -07002100 # Rietveld patch: https://domain/<number>/#ps<patchset>
2101 match = re.match(r'/(\d+)/$', parsed_url.path)
2102 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2103 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002104 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002105 issue=int(match.group(1)),
2106 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002107 hostname=parsed_url.netloc,
2108 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002109 # Typical url: https://domain/<issue_number>[/[other]]
2110 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2111 if match:
skobes6468b902016-10-24 08:45:10 -07002112 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002113 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002114 hostname=parsed_url.netloc,
2115 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002116 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2117 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2118 if match:
skobes6468b902016-10-24 08:45:10 -07002119 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002120 issue=int(match.group(1)),
2121 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002122 hostname=parsed_url.netloc,
2123 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002124 return None
2125
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002126 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002127 """Upload the patch to Rietveld."""
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00002128 raise NotImplementedError
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002129
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002130
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002131class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002132 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002133 # auth_config is Rietveld thing, kept here to preserve interface only.
2134 super(_GerritChangelistImpl, self).__init__(changelist)
2135 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002136 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002137 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002138 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002139 # Map from change number (issue) to its detail cache.
2140 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002141
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002142 if codereview_host is not None:
2143 assert not codereview_host.startswith('https://'), codereview_host
2144 self._gerrit_host = codereview_host
2145 self._gerrit_server = 'https://%s' % codereview_host
2146
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002147 def _GetGerritHost(self):
2148 # Lazy load of configs.
2149 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002150 if self._gerrit_host and '.' not in self._gerrit_host:
2151 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2152 # This happens for internal stuff http://crbug.com/614312.
2153 parsed = urlparse.urlparse(self.GetRemoteUrl())
2154 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002155 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002156 ' Your current remote is: %s' % self.GetRemoteUrl())
2157 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2158 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002159 return self._gerrit_host
2160
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002161 def _GetGitHost(self):
2162 """Returns git host to be used when uploading change to Gerrit."""
2163 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2164
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002165 def GetCodereviewServer(self):
2166 if not self._gerrit_server:
2167 # If we're on a branch then get the server potentially associated
2168 # with that branch.
2169 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002170 self._gerrit_server = self._GitGetBranchConfigValue(
2171 self.CodereviewServerConfigKey())
2172 if self._gerrit_server:
2173 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002174 if not self._gerrit_server:
2175 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2176 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002177 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002178 parts[0] = parts[0] + '-review'
2179 self._gerrit_host = '.'.join(parts)
2180 self._gerrit_server = 'https://%s' % self._gerrit_host
2181 return self._gerrit_server
2182
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002183 def _GetGerritProject(self):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002184 """Returns Gerrit project name based on remote git URL."""
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002185 remote_url = self.GetRemoteUrl()
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002186 if remote_url is None:
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002187 logging.warn('can\'t detect Gerrit project.')
2188 return None
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002189 project = urlparse.urlparse(remote_url).path.strip('/')
2190 if project.endswith('.git'):
2191 project = project[:-len('.git')]
Andrii Shyshkalov1e828672018-08-23 22:34:37 +00002192 # *.googlesource.com hosts ensure that Git/Gerrit projects don't start with
2193 # 'a/' prefix, because 'a/' prefix is used to force authentication in
2194 # gitiles/git-over-https protocol. E.g.,
2195 # https://chromium.googlesource.com/a/v8/v8 refers to the same repo/project
2196 # as
2197 # https://chromium.googlesource.com/v8/v8
2198 if project.startswith('a/'):
2199 project = project[len('a/'):]
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002200 return project
2201
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002202 def _GerritChangeIdentifier(self):
2203 """Handy method for gerrit_util.ChangeIdentifier for a given CL.
2204
2205 Not to be confused by value of "Change-Id:" footer.
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002206 If Gerrit project can be determined, this will speed up Gerrit HTTP API RPC.
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002207 """
Andrii Shyshkalov2d0e03c2018-08-25 04:18:09 +00002208 project = self._GetGerritProject()
2209 if project:
2210 return gerrit_util.ChangeIdentifier(project, self.GetIssue())
2211 # Fall back on still unique, but less efficient change number.
2212 return str(self.GetIssue())
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002213
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002214 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002215 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002216 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002217
tandrii5d48c322016-08-18 16:19:37 -07002218 @classmethod
2219 def PatchsetConfigKey(cls):
2220 return 'gerritpatchset'
2221
2222 @classmethod
2223 def CodereviewServerConfigKey(cls):
2224 return 'gerritserver'
2225
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002226 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002227 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002228 if settings.GetGerritSkipEnsureAuthenticated():
2229 # For projects with unusual authentication schemes.
2230 # See http://crbug.com/603378.
2231 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002232
2233 # Check presence of cookies only if using cookies-based auth method.
2234 cookie_auth = gerrit_util.Authenticator.get()
2235 if not isinstance(cookie_auth, gerrit_util.CookiesAuthenticator):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002236 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002237
2238 # Lazy-loader to identify Gerrit and Git hosts.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002239 self.GetCodereviewServer()
2240 git_host = self._GetGitHost()
2241 assert self._gerrit_server and self._gerrit_host
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002242
2243 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2244 git_auth = cookie_auth.get_auth_header(git_host)
2245 if gerrit_auth and git_auth:
2246 if gerrit_auth == git_auth:
2247 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002248 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002249 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002250 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002251 ' %s\n'
2252 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002253 ' Consider running the following command:\n'
2254 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002255 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002256 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002257 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002258 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002259 cookie_auth.get_new_password_message(git_host)))
2260 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002261 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002262 return
2263 else:
2264 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002265 ([] if gerrit_auth else [self._gerrit_host]) +
2266 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002267 DieWithError('Credentials for the following hosts are required:\n'
2268 ' %s\n'
2269 'These are read from %s (or legacy %s)\n'
2270 '%s' % (
2271 '\n '.join(missing),
2272 cookie_auth.get_gitcookies_path(),
2273 cookie_auth.get_netrc_path(),
2274 cookie_auth.get_new_password_message(git_host)))
2275
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002276 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002277 if not self.GetIssue():
2278 return
2279
2280 # Warm change details cache now to avoid RPCs later, reducing latency for
2281 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002282 self._GetChangeDetail(
Andrii Shyshkalovc4a73562018-09-25 18:40:17 +00002283 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT', 'LABELS'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002284
2285 status = self._GetChangeDetail()['status']
2286 if status in ('MERGED', 'ABANDONED'):
2287 DieWithError('Change %s has been %s, new uploads are not allowed' %
2288 (self.GetIssueURL(),
2289 'submitted' if status == 'MERGED' else 'abandoned'))
2290
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002291 # TODO(vadimsh): For some reason the chunk of code below was skipped if
2292 # 'is_gce' is True. I'm just refactoring it to be 'skip if not cookies'.
2293 # Apparently this check is not very important? Otherwise get_auth_email
2294 # could have been added to other implementations of Authenticator.
2295 cookies_auth = gerrit_util.Authenticator.get()
2296 if not isinstance(cookies_auth, gerrit_util.CookiesAuthenticator):
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002297 return
Vadim Shtayurab250ec12018-10-04 00:21:08 +00002298
2299 cookies_user = cookies_auth.get_auth_email(self._GetGerritHost())
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002300 if self.GetIssueOwner() == cookies_user:
2301 return
2302 logging.debug('change %s owner is %s, cookies user is %s',
2303 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002304 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002305 # so ask what Gerrit thinks of this user.
2306 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2307 if details['email'] == self.GetIssueOwner():
2308 return
2309 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002310 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002311 'as %s.\n'
2312 'Uploading may fail due to lack of permissions.' %
2313 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2314 confirm_or_exit(action='upload')
2315
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002316 def _PostUnsetIssueProperties(self):
2317 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002318 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002319
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002320 def GetGerritObjForPresubmit(self):
2321 return presubmit_support.GerritAccessor(self._GetGerritHost())
2322
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002323 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002324 """Apply a rough heuristic to give a simple summary of an issue's review
2325 or CQ status, assuming adherence to a common workflow.
2326
2327 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002328 * 'error' - error from review tool (including deleted issues)
2329 * 'unsent' - no reviewers added
2330 * 'waiting' - waiting for review
2331 * 'reply' - waiting for uploader to reply to review
2332 * 'lgtm' - Code-Review label has been set
2333 * 'commit' - in the commit queue
2334 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002335 """
2336 if not self.GetIssue():
2337 return None
2338
2339 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002340 data = self._GetChangeDetail([
2341 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002342 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002343 return 'error'
2344
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002345 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002346 return 'closed'
2347
Aaron Gable9ab38c62017-04-06 14:36:33 -07002348 if data['labels'].get('Commit-Queue', {}).get('approved'):
2349 # The section will have an "approved" subsection if anyone has voted
2350 # the maximum value on the label.
2351 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002352
Aaron Gable9ab38c62017-04-06 14:36:33 -07002353 if data['labels'].get('Code-Review', {}).get('approved'):
2354 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002355
2356 if not data.get('reviewers', {}).get('REVIEWER', []):
2357 return 'unsent'
2358
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002359 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002360 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2361 last_message_author = messages.pop().get('author', {})
2362 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002363 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2364 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002365 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002366 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002367 if last_message_author.get('_account_id') == owner:
2368 # Most recent message was by owner.
2369 return 'waiting'
2370 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002371 # Some reply from non-owner.
2372 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002373
2374 # Somehow there are no messages even though there are reviewers.
2375 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002376
2377 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002378 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002379 patchset = data['revisions'][data['current_revision']]['_number']
2380 self.SetPatchset(patchset)
2381 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002382
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002383 def FetchDescription(self, force=False):
2384 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2385 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002386 current_rev = data['current_revision']
Dan Beamcf6df902018-11-08 01:48:37 +00002387 return data['revisions'][current_rev]['commit']['message'].encode(
2388 'utf-8', 'ignore')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002389
dsansomee2d6fd92016-09-08 00:10:47 -07002390 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002391 if gerrit_util.HasPendingChangeEdit(
2392 self._GetGerritHost(), self._GerritChangeIdentifier()):
dsansomee2d6fd92016-09-08 00:10:47 -07002393 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002394 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002395 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002396 'unpublished edit. Either publish the edit in the Gerrit web UI '
2397 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002398
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002399 gerrit_util.DeletePendingChangeEdit(
2400 self._GetGerritHost(), self._GerritChangeIdentifier())
2401 gerrit_util.SetCommitMessage(
2402 self._GetGerritHost(), self._GerritChangeIdentifier(),
2403 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002404
Aaron Gable636b13f2017-07-14 10:42:48 -07002405 def AddComment(self, message, publish=None):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002406 gerrit_util.SetReview(
2407 self._GetGerritHost(), self._GerritChangeIdentifier(),
2408 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002409
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002410 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002411 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002412 messages = self._GetChangeDetail(
2413 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2414 file_comments = gerrit_util.GetChangeComments(
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002415 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002416
2417 # Build dictionary of file comments for easy access and sorting later.
2418 # {author+date: {path: {patchset: {line: url+message}}}}
2419 comments = collections.defaultdict(
2420 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2421 for path, line_comments in file_comments.iteritems():
2422 for comment in line_comments:
2423 if comment.get('tag', '').startswith('autogenerated'):
2424 continue
2425 key = (comment['author']['email'], comment['updated'])
2426 if comment.get('side', 'REVISION') == 'PARENT':
2427 patchset = 'Base'
2428 else:
2429 patchset = 'PS%d' % comment['patch_set']
2430 line = comment.get('line', 0)
2431 url = ('https://%s/c/%s/%s/%s#%s%s' %
2432 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2433 'b' if comment.get('side') == 'PARENT' else '',
2434 str(line) if line else ''))
2435 comments[key][path][patchset][line] = (url, comment['message'])
2436
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002437 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002438 for msg in messages:
2439 # Don't bother showing autogenerated messages.
2440 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2441 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002442 # Gerrit spits out nanoseconds.
2443 assert len(msg['date'].split('.')[-1]) == 9
2444 date = datetime.datetime.strptime(msg['date'][:-3],
2445 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002446 message = msg['message']
2447 key = (msg['author']['email'], msg['date'])
2448 if key in comments:
2449 message += '\n'
2450 for path, patchsets in sorted(comments.get(key, {}).items()):
2451 if readable:
2452 message += '\n%s' % path
2453 for patchset, lines in sorted(patchsets.items()):
2454 for line, (url, content) in sorted(lines.items()):
2455 if line:
2456 line_str = 'Line %d' % line
2457 path_str = '%s:%d:' % (path, line)
2458 else:
2459 line_str = 'File comment'
2460 path_str = '%s:0:' % path
2461 if readable:
2462 message += '\n %s, %s: %s' % (patchset, line_str, url)
2463 message += '\n %s\n' % content
2464 else:
2465 message += '\n%s ' % path_str
2466 message += '\n%s\n' % content
2467
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002468 summary.append(_CommentSummary(
2469 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002470 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002471 sender=msg['author']['email'],
2472 # These could be inferred from the text messages and correlated with
2473 # Code-Review label maximum, however this is not reliable.
2474 # Leaving as is until the need arises.
2475 approval=False,
2476 disapproval=False,
2477 ))
2478 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002479
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002480 def CloseIssue(self):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002481 gerrit_util.AbandonChange(
2482 self._GetGerritHost(), self._GerritChangeIdentifier(), msg='')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002483
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002484 def SubmitIssue(self, wait_for_merge=True):
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00002485 gerrit_util.SubmitChange(
2486 self._GetGerritHost(), self._GerritChangeIdentifier(),
2487 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002488
Andrii Shyshkalovb7214602018-08-22 23:20:26 +00002489 def _GetChangeDetail(self, options=None, no_cache=False):
2490 """Returns details of associated Gerrit change and caching results.
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002491
2492 If fresh data is needed, set no_cache=True which will clear cache and
2493 thus new data will be fetched from Gerrit.
2494 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002495 options = options or []
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002496 assert self.GetIssue(), 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002497
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002498 # Optimization to avoid multiple RPCs:
2499 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2500 'CURRENT_COMMIT' not in options):
2501 options.append('CURRENT_COMMIT')
2502
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002503 # Normalize issue and options for consistent keys in cache.
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002504 cache_key = str(self.GetIssue())
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002505 options = [o.upper() for o in options]
2506
2507 # Check in cache first unless no_cache is True.
2508 if no_cache:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002509 self._detail_cache.pop(cache_key, None)
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002510 else:
2511 options_set = frozenset(options)
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002512 for cached_options_set, data in self._detail_cache.get(cache_key, []):
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002513 # Assumption: data fetched before with extra options is suitable
2514 # for return for a smaller set of options.
2515 # For example, if we cached data for
2516 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2517 # and request is for options=[CURRENT_REVISION],
2518 # THEN we can return prior cached data.
2519 if options_set.issubset(cached_options_set):
2520 return data
2521
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002522 try:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002523 data = gerrit_util.GetChangeDetail(
2524 self._GetGerritHost(), self._GerritChangeIdentifier(), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002525 except gerrit_util.GerritError as e:
2526 if e.http_status == 404:
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002527 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002528 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002529
Andrii Shyshkalov03e0ed22018-08-28 19:39:30 +00002530 self._detail_cache.setdefault(cache_key, []).append(
2531 (frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002532 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002533
Andrii Shyshkalovcc5f17e2018-08-22 23:35:59 +00002534 def _GetChangeCommit(self):
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002535 assert self.GetIssue(), 'issue must be set to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002536 try:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002537 data = gerrit_util.GetChangeCommit(
2538 self._GetGerritHost(), self._GerritChangeIdentifier())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002539 except gerrit_util.GerritError as e:
2540 if e.http_status == 404:
Andrii Shyshkalove2633162018-08-27 23:50:31 +00002541 raise GerritChangeNotExists(self.GetIssue(), self.GetCodereviewServer())
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002542 raise
agable32978d92016-11-01 12:55:02 -07002543 return data
2544
Olivier Robin75ee7252018-04-13 10:02:56 +02002545 def CMDLand(self, force, bypass_hooks, verbose, parallel):
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002546 if git_common.is_dirty_git_tree('land'):
2547 return 1
tandriid60367b2016-06-22 05:25:12 -07002548 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2549 if u'Commit-Queue' in detail.get('labels', {}):
2550 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002551 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2552 'which can test and land changes for you. '
2553 'Are you sure you wish to bypass it?\n',
2554 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002555
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002556 differs = True
tandriic4344b52016-08-29 06:04:54 -07002557 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002558 # Note: git diff outputs nothing if there is no diff.
2559 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002560 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002561 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002562 if detail['current_revision'] == last_upload:
2563 differs = False
2564 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002565 print('WARNING: Local branch contents differ from latest uploaded '
2566 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002567 if differs:
2568 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002569 confirm_or_exit(
2570 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2571 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002572 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002573 elif not bypass_hooks:
2574 hook_results = self.RunHook(
2575 committing=True,
2576 may_prompt=not force,
2577 verbose=verbose,
Olivier Robin75ee7252018-04-13 10:02:56 +02002578 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None),
2579 parallel=parallel)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002580 if not hook_results.should_continue():
2581 return 1
2582
2583 self.SubmitIssue(wait_for_merge=True)
2584 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002585 links = self._GetChangeCommit().get('web_links', [])
2586 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002587 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002588 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002589 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002590 return 0
2591
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002592 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002593 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002594 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002595 assert not directory
2596 assert parsed_issue_arg.valid
2597
2598 self._changelist.issue = parsed_issue_arg.issue
2599
2600 if parsed_issue_arg.hostname:
2601 self._gerrit_host = parsed_issue_arg.hostname
2602 self._gerrit_server = 'https://%s' % self._gerrit_host
2603
tandriic2405f52016-10-10 08:13:15 -07002604 try:
2605 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002606 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002607 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002608
2609 if not parsed_issue_arg.patchset:
2610 # Use current revision by default.
2611 revision_info = detail['revisions'][detail['current_revision']]
2612 patchset = int(revision_info['_number'])
2613 else:
2614 patchset = parsed_issue_arg.patchset
2615 for revision_info in detail['revisions'].itervalues():
2616 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2617 break
2618 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002619 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002620 (parsed_issue_arg.patchset, self.GetIssue()))
2621
Aaron Gable697a91b2018-01-19 15:20:15 -08002622 remote_url = self._changelist.GetRemoteUrl()
2623 if remote_url.endswith('.git'):
2624 remote_url = remote_url[:-len('.git')]
erikchen0d14d0d2018-08-28 18:57:09 +00002625 remote_url = remote_url.rstrip('/')
2626
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002627 fetch_info = revision_info['fetch']['http']
erikchen0d14d0d2018-08-28 18:57:09 +00002628 fetch_info['url'] = fetch_info['url'].rstrip('/')
Aaron Gable697a91b2018-01-19 15:20:15 -08002629
2630 if remote_url != fetch_info['url']:
2631 DieWithError('Trying to patch a change from %s but this repo appears '
2632 'to be %s.' % (fetch_info['url'], remote_url))
2633
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002634 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002635
Aaron Gable62619a32017-06-16 08:22:09 -07002636 if force:
2637 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2638 print('Checked out commit for change %i patchset %i locally' %
2639 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002640 elif nocommit:
2641 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2642 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002643 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002644 RunGit(['cherry-pick', 'FETCH_HEAD'])
2645 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002646 (parsed_issue_arg.issue, patchset))
2647 print('Note: this created a local commit which does not have '
2648 'the same hash as the one uploaded for review. This will make '
2649 'uploading changes based on top of this branch difficult.\n'
2650 'If you want to do that, use "git cl patch --force" instead.')
2651
Stefan Zagerd08043c2017-10-12 12:07:02 -07002652 if self.GetBranch():
2653 self.SetIssue(parsed_issue_arg.issue)
2654 self.SetPatchset(patchset)
2655 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2656 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2657 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2658 else:
2659 print('WARNING: You are in detached HEAD state.\n'
2660 'The patch has been applied to your checkout, but you will not be '
2661 'able to upload a new patch set to the gerrit issue.\n'
2662 'Try using the \'-b\' option if you would like to work on a '
2663 'branch and/or upload a new patch set.')
2664
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002665 return 0
2666
2667 @staticmethod
2668 def ParseIssueURL(parsed_url):
2669 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2670 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002671 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2672 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002673 # Short urls like https://domain/<issue_number> can be used, but don't allow
2674 # specifying the patchset (you'd 404), but we allow that here.
2675 if parsed_url.path == '/':
2676 part = parsed_url.fragment
2677 else:
2678 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002679 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002680 if match:
2681 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002682 issue=int(match.group(3)),
2683 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002684 hostname=parsed_url.netloc,
2685 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002686 return None
2687
tandrii16e0b4e2016-06-07 10:34:28 -07002688 def _GerritCommitMsgHookCheck(self, offer_removal):
2689 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2690 if not os.path.exists(hook):
2691 return
2692 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2693 # custom developer made one.
2694 data = gclient_utils.FileRead(hook)
2695 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2696 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002697 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002698 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002699 'and may interfere with it in subtle ways.\n'
2700 'We recommend you remove the commit-msg hook.')
2701 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002702 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002703 gclient_utils.rm_file_or_tree(hook)
2704 print('Gerrit commit-msg hook removed.')
2705 else:
2706 print('OK, will keep Gerrit commit-msg hook in place.')
2707
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002708 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002709 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002710 if options.squash and options.no_squash:
2711 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002712
2713 if not options.squash and not options.no_squash:
2714 # Load default for user, repo, squash=true, in this order.
2715 options.squash = settings.GetSquashGerritUploads()
2716 elif options.no_squash:
2717 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002718
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002719 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002720 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002721
Aaron Gableb56ad332017-01-06 15:24:31 -08002722 # This may be None; default fallback value is determined in logic below.
2723 title = options.title
2724
Dominic Battre7d1c4842017-10-27 09:17:28 +02002725 # Extract bug number from branch name.
2726 bug = options.bug
2727 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2728 if not bug and match:
2729 bug = match.group(1)
2730
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002731 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002732 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002733 if self.GetIssue():
2734 # Try to get the message from a previous upload.
2735 message = self.GetDescription()
2736 if not message:
2737 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002738 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002739 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002740 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002741 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002742 # When uploading a subsequent patchset, -m|--message is taken
2743 # as the patchset title if --title was not provided.
2744 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002745 else:
2746 default_title = RunGit(
2747 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002748 if options.force:
2749 title = default_title
2750 else:
2751 title = ask_for_data(
2752 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002753 change_id = self._GetChangeDetail()['change_id']
2754 while True:
2755 footer_change_ids = git_footers.get_footer_change_id(message)
2756 if footer_change_ids == [change_id]:
2757 break
2758 if not footer_change_ids:
2759 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002760 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002761 continue
2762 # There is already a valid footer but with different or several ids.
2763 # Doing this automatically is non-trivial as we don't want to lose
2764 # existing other footers, yet we want to append just 1 desired
2765 # Change-Id. Thus, just create a new footer, but let user verify the
2766 # new description.
2767 message = '%s\n\nChange-Id: %s' % (message, change_id)
2768 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002769 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002770 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002771 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002772 'Please, check the proposed correction to the description, '
2773 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2774 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2775 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002776 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002777 if not options.force:
2778 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002779 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002780 message = change_desc.description
2781 if not message:
2782 DieWithError("Description is empty. Aborting...")
2783 # Continue the while loop.
2784 # Sanity check of this code - we should end up with proper message
2785 # footer.
2786 assert [change_id] == git_footers.get_footer_change_id(message)
2787 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002788 else: # if not self.GetIssue()
2789 if options.message:
2790 message = options.message
2791 else:
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002792 message = _create_description_from_log(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002793 if options.title:
2794 message = options.title + '\n\n' + message
2795 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002796
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002797 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002798 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002799 # On first upload, patchset title is always this string, while
2800 # --title flag gets converted to first line of message.
2801 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002802 if not change_desc.description:
2803 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002804 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002805 if len(change_ids) > 1:
2806 DieWithError('too many Change-Id footers, at most 1 allowed.')
2807 if not change_ids:
2808 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002809 change_desc.set_description(git_footers.add_footer_change_id(
2810 change_desc.description,
2811 GenerateGerritChangeId(change_desc.description)))
2812 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002813 assert len(change_ids) == 1
2814 change_id = change_ids[0]
2815
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002816 if options.reviewers or options.tbrs or options.add_owners_to:
2817 change_desc.update_reviewers(options.reviewers, options.tbrs,
2818 options.add_owners_to, change)
2819
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002820 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002821 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2822 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002823 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002824 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2825 desc_tempfile.write(change_desc.description)
2826 desc_tempfile.close()
2827 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2828 '-F', desc_tempfile.name]).strip()
2829 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002830 else:
2831 change_desc = ChangeDescription(
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00002832 options.message or _create_description_from_log(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002833 if not change_desc.description:
2834 DieWithError("Description is empty. Aborting...")
2835
2836 if not git_footers.get_footer_change_id(change_desc.description):
2837 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002838 change_desc.set_description(
2839 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002840 if options.reviewers or options.tbrs or options.add_owners_to:
2841 change_desc.update_reviewers(options.reviewers, options.tbrs,
2842 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002843 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002844 # For no-squash mode, we assume the remote called "origin" is the one we
2845 # want. It is not worthwhile to support different workflows for
2846 # no-squash mode.
2847 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002848 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2849
2850 assert change_desc
Andrii Shyshkalovd9fdc1f2018-09-27 02:13:09 +00002851 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002852 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2853 ref_to_push)]).splitlines()
2854 if len(commits) > 1:
2855 print('WARNING: This will upload %d commits. Run the following command '
2856 'to see which commits will be uploaded: ' % len(commits))
2857 print('git log %s..%s' % (parent, ref_to_push))
2858 print('You can also use `git squash-branch` to squash these into a '
2859 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002860 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002861
Aaron Gable6dadfbf2017-05-09 14:27:58 -07002862 if options.reviewers or options.tbrs or options.add_owners_to:
2863 change_desc.update_reviewers(options.reviewers, options.tbrs,
2864 options.add_owners_to, change)
2865
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002866 reviewers = sorted(change_desc.get_reviewers())
2867 # Add cc's from the CC_LIST and --cc flag (if any).
2868 if not options.private and not options.no_autocc:
2869 cc = self.GetCCList().split(',')
2870 else:
2871 cc = []
2872 if options.cc:
2873 cc.extend(options.cc)
2874 cc = filter(None, [email.strip() for email in cc])
2875 if change_desc.get_cced():
2876 cc.extend(change_desc.get_cced())
Andrii Shyshkalov0da5e8f2018-10-30 17:29:18 +00002877 if self._GetGerritHost() == 'chromium-review.googlesource.com':
2878 valid_accounts = set(reviewers + cc)
2879 # TODO(crbug/877717): relax this for all hosts.
2880 else:
2881 valid_accounts = gerrit_util.ValidAccounts(
2882 self._GetGerritHost(), reviewers + cc)
Andrii Shyshkalovf170af42018-10-30 07:00:44 +00002883 logging.info('accounts %s are recognized, %s invalid',
2884 sorted(valid_accounts),
2885 set(reviewers + cc).difference(set(valid_accounts)))
Andrii Shyshkalov76988a82018-10-15 03:12:25 +00002886
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002887 # Extra options that can be specified at push time. Doc:
2888 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002889 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002890
Aaron Gable844cf292017-06-28 11:32:59 -07002891 # By default, new changes are started in WIP mode, and subsequent patchsets
2892 # don't send email. At any time, passing --send-mail will mark the change
2893 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07002894 if options.send_mail:
2895 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07002896 refspec_opts.append('notify=ALL')
Jamie Madill276da0b2018-04-27 14:41:20 -04002897 elif not self.GetIssue() and options.squash:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002898 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07002899 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002900 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07002901
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002902 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07002903 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002904
Aaron Gable9b713dd2016-12-14 16:04:21 -08002905 if title:
Nick Carter8692b182017-11-06 16:30:38 -08002906 # Punctuation and whitespace in |title| must be percent-encoded.
2907 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002908
agablec6787972016-09-09 16:13:34 -07002909 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07002910 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07002911
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002912 for r in sorted(reviewers):
2913 if r in valid_accounts:
2914 refspec_opts.append('r=%s' % r)
2915 reviewers.remove(r)
2916 else:
2917 # TODO(tandrii): this should probably be a hard failure.
2918 print('WARNING: reviewer %s doesn\'t have a Gerrit account, skipping'
2919 % r)
2920 for c in sorted(cc):
2921 # refspec option will be rejected if cc doesn't correspond to an
2922 # account, even though REST call to add such arbitrary cc may succeed.
2923 if c in valid_accounts:
2924 refspec_opts.append('cc=%s' % c)
2925 cc.remove(c)
2926
rmistry9eadede2016-09-19 11:22:43 -07002927 if options.topic:
2928 # Documentation on Gerrit topics is here:
2929 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002930 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07002931
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00002932 if not change_desc.get_reviewers(tbr_only=True):
2933 # Change is not TBR, so we can inline setting other labels, too.
2934 # TODO(crbug.com/877717): make this working for TBR, too, by figuring out
2935 # max score for CR label somehow.
2936 if options.enable_auto_submit:
2937 refspec_opts.append('l=Auto-Submit+1')
2938 if options.use_commit_queue:
2939 refspec_opts.append('l=Commit-Queue+2')
2940 elif options.cq_dry_run:
2941 refspec_opts.append('l=Commit-Queue+1')
2942
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002943 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08002944 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002945 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08002946 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08002947 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
2948
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00002949 refspec_suffix = ''
2950 if refspec_opts:
2951 refspec_suffix = '%' + ','.join(refspec_opts)
2952 assert ' ' not in refspec_suffix, (
2953 'spaces not allowed in refspec: "%s"' % refspec_suffix)
2954 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
2955
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002956 try:
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002957 before_push = time_time()
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002958 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalov81db1d52018-08-23 02:17:41 +00002959 ['git', 'push', self.GetRemoteUrl(), refspec],
Edward Lemuredcefdc2018-11-08 14:41:42 +00002960 print_stdout=True,
Edward Lemur49c8eaf2018-11-07 22:13:12 +00002961 # Flush after every line: useful for seeing progress when running as
2962 # recipe.
2963 filter_fn=lambda _: sys.stdout.flush())
2964 push_returncode = 0
Edward Lemurfec80c42018-11-01 23:14:14 +00002965 except subprocess2.CalledProcessError as e:
2966 push_returncode = e.returncode
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01002967 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002968 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002969 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02002970 'credential problems:\n'
2971 ' git cl creds-check\n',
2972 change_desc)
Edward Lemurfec80c42018-11-01 23:14:14 +00002973 finally:
2974 metrics.collector.add_repeated('sub_commands', {
2975 'command': 'git push',
Edward Lemur01f4a4f2018-11-03 00:40:38 +00002976 'execution_time': time_time() - before_push,
Edward Lemurfec80c42018-11-01 23:14:14 +00002977 'exit_code': push_returncode,
2978 'arguments': metrics_utils.extract_known_subcommand_args(refspec_opts),
2979 })
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002980
2981 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07002982 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002983 change_numbers = [m.group(1)
2984 for m in map(regex.match, push_stdout.splitlines())
2985 if m]
2986 if len(change_numbers) != 1:
2987 DieWithError(
2988 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11002989 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002990 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002991 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002992
Andrii Shyshkalov2f727912018-10-15 17:02:33 +00002993 if self.GetIssue() and (reviewers or cc):
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002994 # GetIssue() is not set in case of non-squash uploads according to tests.
2995 # TODO(agable): non-squash uploads in git cl should be removed.
2996 gerrit_util.AddReviewers(
2997 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00002998 self._GerritChangeIdentifier(),
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00002999 reviewers, cc,
3000 notify=bool(options.send_mail))
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003001
Aaron Gablefd238082017-06-07 13:42:34 -07003002 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003003 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3004 score = 1
3005 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3006 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3007 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003008 gerrit_util.SetReview(
Andrii Shyshkalov0ec9d152018-08-23 00:22:58 +00003009 self._GetGerritHost(),
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003010 self._GerritChangeIdentifier(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003011 msg='Self-approving for TBR',
3012 labels={'Code-Review': score})
Andrii Shyshkalove7a7fc42018-10-30 17:35:09 +00003013 # Labels aren't set through refspec only if tbr is set (see check above).
3014 self.SetLabels(options.enable_auto_submit, options.use_commit_queue,
3015 options.cq_dry_run)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003016 return 0
3017
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003018 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3019 change_desc):
3020 """Computes parent of the generated commit to be uploaded to Gerrit.
3021
3022 Returns revision or a ref name.
3023 """
3024 if custom_cl_base:
3025 # Try to avoid creating additional unintended CLs when uploading, unless
3026 # user wants to take this risk.
3027 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3028 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3029 local_ref_of_target_remote])
3030 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003031 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003032 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3033 'If you proceed with upload, more than 1 CL may be created by '
3034 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3035 'If you are certain that specified base `%s` has already been '
3036 'uploaded to Gerrit as another CL, you may proceed.\n' %
3037 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3038 if not force:
3039 confirm_or_exit(
3040 'Do you take responsibility for cleaning up potential mess '
3041 'resulting from proceeding with upload?',
3042 action='upload')
3043 return custom_cl_base
3044
Aaron Gablef97e33d2017-03-30 15:44:27 -07003045 if remote != '.':
3046 return self.GetCommonAncestorWithUpstream()
3047
3048 # If our upstream branch is local, we base our squashed commit on its
3049 # squashed version.
3050 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3051
Aaron Gablef97e33d2017-03-30 15:44:27 -07003052 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003053 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003054
3055 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003056 # TODO(tandrii): consider checking parent change in Gerrit and using its
3057 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3058 # the tree hash of the parent branch. The upside is less likely bogus
3059 # requests to reupload parent change just because it's uploadhash is
3060 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003061 parent = RunGit(['config',
3062 'branch.%s.gerritsquashhash' % upstream_branch_name],
3063 error_ok=True).strip()
3064 # Verify that the upstream branch has been uploaded too, otherwise
3065 # Gerrit will create additional CLs when uploading.
3066 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3067 RunGitSilent(['rev-parse', parent + ':'])):
3068 DieWithError(
3069 '\nUpload upstream branch %s first.\n'
3070 'It is likely that this branch has been rebased since its last '
3071 'upload, so you just need to upload it again.\n'
3072 '(If you uploaded it with --no-squash, then branch dependencies '
3073 'are not supported, and you should reupload with --squash.)'
3074 % upstream_branch_name,
3075 change_desc)
3076 return parent
3077
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003078 def _AddChangeIdToCommitMessage(self, options, args):
3079 """Re-commits using the current message, assumes the commit hook is in
3080 place.
3081 """
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003082 log_desc = options.message or _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003083 git_command = ['commit', '--amend', '-m', log_desc]
3084 RunGit(git_command)
Andrii Shyshkalovb07575f2018-10-16 06:16:21 +00003085 new_log_desc = _create_description_from_log(args)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003086 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003087 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003088 return new_log_desc
3089 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003090 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003091
Ravi Mistry31e7d562018-04-02 12:53:57 -04003092 def SetLabels(self, enable_auto_submit, use_commit_queue, cq_dry_run):
3093 """Sets labels on the change based on the provided flags."""
3094 labels = {}
3095 notify = None;
3096 if enable_auto_submit:
3097 labels['Auto-Submit'] = 1
3098 if use_commit_queue:
3099 labels['Commit-Queue'] = 2
3100 elif cq_dry_run:
3101 labels['Commit-Queue'] = 1
3102 notify = False
3103 if labels:
Andrii Shyshkalovd06cc782018-08-23 17:24:19 +00003104 gerrit_util.SetReview(
3105 self._GetGerritHost(),
3106 self._GerritChangeIdentifier(),
3107 labels=labels, notify=notify)
Ravi Mistry31e7d562018-04-02 12:53:57 -04003108
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003109 def SetCQState(self, new_state):
3110 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003111 vote_map = {
3112 _CQState.NONE: 0,
3113 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003114 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003115 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003116 labels = {'Commit-Queue': vote_map[new_state]}
3117 notify = False if new_state == _CQState.DRY_RUN else None
Andrii Shyshkalov889677c2018-08-28 20:43:06 +00003118 gerrit_util.SetReview(
3119 self._GetGerritHost(), self._GerritChangeIdentifier(),
3120 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003121
tandriie113dfd2016-10-11 10:20:12 -07003122 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003123 try:
3124 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003125 except GerritChangeNotExists:
3126 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003127
3128 if data['status'] in ('ABANDONED', 'MERGED'):
3129 return 'CL %s is closed' % self.GetIssue()
3130
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003131 def GetTryJobProperties(self, patchset=None):
3132 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003133 data = self._GetChangeDetail(['ALL_REVISIONS'])
3134 patchset = int(patchset or self.GetPatchset())
3135 assert patchset
3136 revision_data = None # Pylint wants it to be defined.
3137 for revision_data in data['revisions'].itervalues():
3138 if int(revision_data['_number']) == patchset:
3139 break
3140 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003141 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003142 (patchset, self.GetIssue()))
3143 return {
3144 'patch_issue': self.GetIssue(),
3145 'patch_set': patchset or self.GetPatchset(),
3146 'patch_project': data['project'],
3147 'patch_storage': 'gerrit',
3148 'patch_ref': revision_data['fetch']['http']['ref'],
3149 'patch_repository_url': revision_data['fetch']['http']['url'],
3150 'patch_gerrit_url': self.GetCodereviewServer(),
3151 }
tandriie113dfd2016-10-11 10:20:12 -07003152
tandriide281ae2016-10-12 06:02:30 -07003153 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003154 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003155
Edward Lemur707d70b2018-02-07 00:50:14 +01003156 def GetReviewers(self):
3157 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
Mohamed Heikal171c0742018-11-09 20:38:51 +00003158 return [r['email'] for r in details['reviewers'].get('REVIEWER', [])]
Edward Lemur707d70b2018-02-07 00:50:14 +01003159
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003160
3161_CODEREVIEW_IMPLEMENTATIONS = {
3162 'rietveld': _RietveldChangelistImpl,
3163 'gerrit': _GerritChangelistImpl,
3164}
3165
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003166
iannuccie53c9352016-08-17 14:40:40 -07003167def _add_codereview_issue_select_options(parser, extra=""):
3168 _add_codereview_select_options(parser)
3169
3170 text = ('Operate on this issue number instead of the current branch\'s '
3171 'implicit issue.')
3172 if extra:
3173 text += ' '+extra
3174 parser.add_option('-i', '--issue', type=int, help=text)
3175
3176
3177def _process_codereview_issue_select_options(parser, options):
3178 _process_codereview_select_options(parser, options)
3179 if options.issue is not None and not options.forced_codereview:
3180 parser.error('--issue must be specified with either --rietveld or --gerrit')
3181
3182
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003183def _add_codereview_select_options(parser):
3184 """Appends --gerrit and --rietveld options to force specific codereview."""
3185 parser.codereview_group = optparse.OptionGroup(
3186 parser, 'EXPERIMENTAL! Codereview override options')
3187 parser.add_option_group(parser.codereview_group)
3188 parser.codereview_group.add_option(
3189 '--gerrit', action='store_true',
3190 help='Force the use of Gerrit for codereview')
3191 parser.codereview_group.add_option(
3192 '--rietveld', action='store_true',
3193 help='Force the use of Rietveld for codereview')
3194
3195
3196def _process_codereview_select_options(parser, options):
Andrii Shyshkalovfeec80e2018-10-16 01:00:47 +00003197 if options.rietveld:
3198 parser.error('--rietveld is no longer supported')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003199 options.forced_codereview = None
3200 if options.gerrit:
3201 options.forced_codereview = 'gerrit'
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003202
3203
tandriif9aefb72016-07-01 09:06:51 -07003204def _get_bug_line_values(default_project, bugs):
3205 """Given default_project and comma separated list of bugs, yields bug line
3206 values.
3207
3208 Each bug can be either:
3209 * a number, which is combined with default_project
3210 * string, which is left as is.
3211
3212 This function may produce more than one line, because bugdroid expects one
3213 project per line.
3214
3215 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3216 ['v8:123', 'chromium:789']
3217 """
3218 default_bugs = []
3219 others = []
3220 for bug in bugs.split(','):
3221 bug = bug.strip()
3222 if bug:
3223 try:
3224 default_bugs.append(int(bug))
3225 except ValueError:
3226 others.append(bug)
3227
3228 if default_bugs:
3229 default_bugs = ','.join(map(str, default_bugs))
3230 if default_project:
3231 yield '%s:%s' % (default_project, default_bugs)
3232 else:
3233 yield default_bugs
3234 for other in sorted(others):
3235 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3236 yield other
3237
3238
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003239class ChangeDescription(object):
3240 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003241 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003242 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003243 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003244 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003245 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3246 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3247 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3248 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003249
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003250 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003251 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003252
agable@chromium.org42c20792013-09-12 17:34:49 +00003253 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003254 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003255 return '\n'.join(self._description_lines)
3256
3257 def set_description(self, desc):
3258 if isinstance(desc, basestring):
3259 lines = desc.splitlines()
3260 else:
3261 lines = [line.rstrip() for line in desc]
3262 while lines and not lines[0]:
3263 lines.pop(0)
3264 while lines and not lines[-1]:
3265 lines.pop(-1)
3266 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003267
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003268 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3269 """Rewrites the R=/TBR= line(s) as a single line each.
3270
3271 Args:
3272 reviewers (list(str)) - list of additional emails to use for reviewers.
3273 tbrs (list(str)) - list of additional emails to use for TBRs.
3274 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3275 the change that are missing OWNER coverage. If this is not None, you
3276 must also pass a value for `change`.
3277 change (Change) - The Change that should be used for OWNERS lookups.
3278 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003279 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003280 assert isinstance(tbrs, list), tbrs
3281
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003282 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003283 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003284
3285 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003286 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003287
3288 reviewers = set(reviewers)
3289 tbrs = set(tbrs)
3290 LOOKUP = {
3291 'TBR': tbrs,
3292 'R': reviewers,
3293 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003294
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003295 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003296 regexp = re.compile(self.R_LINE)
3297 matches = [regexp.match(line) for line in self._description_lines]
3298 new_desc = [l for i, l in enumerate(self._description_lines)
3299 if not matches[i]]
3300 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003301
agable@chromium.org42c20792013-09-12 17:34:49 +00003302 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003303
3304 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003305 for match in matches:
3306 if not match:
3307 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003308 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3309
3310 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003311 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003312 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003313 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003314 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003315 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003316 LOOKUP[add_owners_to].update(
3317 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003318
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003319 # If any folks ended up in both groups, remove them from tbrs.
3320 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003321
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003322 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3323 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003324
3325 # Put the new lines in the description where the old first R= line was.
3326 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3327 if 0 <= line_loc < len(self._description_lines):
3328 if new_tbr_line:
3329 self._description_lines.insert(line_loc, new_tbr_line)
3330 if new_r_line:
3331 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003332 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003333 if new_r_line:
3334 self.append_footer(new_r_line)
3335 if new_tbr_line:
3336 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003337
Aaron Gable3a16ed12017-03-23 10:51:55 -07003338 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003339 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003340 self.set_description([
3341 '# Enter a description of the change.',
3342 '# This will be displayed on the codereview site.',
3343 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003344 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003345 '--------------------',
3346 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003347
agable@chromium.org42c20792013-09-12 17:34:49 +00003348 regexp = re.compile(self.BUG_LINE)
3349 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003350 prefix = settings.GetBugPrefix()
3351 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003352 if git_footer:
3353 self.append_footer('Bug: %s' % ', '.join(values))
3354 else:
3355 for value in values:
3356 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003357
agable@chromium.org42c20792013-09-12 17:34:49 +00003358 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003359 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003360 if not content:
3361 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003362 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003363
Bruce Dawson2377b012018-01-11 16:46:49 -08003364 # Strip off comments and default inserted "Bug:" line.
3365 clean_lines = [line.rstrip() for line in lines if not
3366 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003367 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003368 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003369 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003370
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003371 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003372 """Adds a footer line to the description.
3373
3374 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3375 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3376 that Gerrit footers are always at the end.
3377 """
3378 parsed_footer_line = git_footers.parse_footer(line)
3379 if parsed_footer_line:
3380 # Line is a gerrit footer in the form: Footer-Key: any value.
3381 # Thus, must be appended observing Gerrit footer rules.
3382 self.set_description(
3383 git_footers.add_footer(self.description,
3384 key=parsed_footer_line[0],
3385 value=parsed_footer_line[1]))
3386 return
3387
3388 if not self._description_lines:
3389 self._description_lines.append(line)
3390 return
3391
3392 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3393 if gerrit_footers:
3394 # git_footers.split_footers ensures that there is an empty line before
3395 # actual (gerrit) footers, if any. We have to keep it that way.
3396 assert top_lines and top_lines[-1] == ''
3397 top_lines, separator = top_lines[:-1], top_lines[-1:]
3398 else:
3399 separator = [] # No need for separator if there are no gerrit_footers.
3400
3401 prev_line = top_lines[-1] if top_lines else ''
3402 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3403 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3404 top_lines.append('')
3405 top_lines.append(line)
3406 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003407
tandrii99a72f22016-08-17 14:33:24 -07003408 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003409 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003410 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003411 reviewers = [match.group(2).strip()
3412 for match in matches
3413 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003414 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003415
bradnelsond975b302016-10-23 12:20:23 -07003416 def get_cced(self):
3417 """Retrieves the list of reviewers."""
3418 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3419 cced = [match.group(2).strip() for match in matches if match]
3420 return cleanup_list(cced)
3421
Nodir Turakulov23b82142017-11-16 11:04:25 -08003422 def get_hash_tags(self):
3423 """Extracts and sanitizes a list of Gerrit hashtags."""
3424 subject = (self._description_lines or ('',))[0]
3425 subject = re.sub(
3426 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3427
3428 tags = []
3429 start = 0
3430 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3431 while True:
3432 m = bracket_exp.match(subject, start)
3433 if not m:
3434 break
3435 tags.append(self.sanitize_hash_tag(m.group(1)))
3436 start = m.end()
3437
3438 if not tags:
3439 # Try "Tag: " prefix.
3440 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3441 if m:
3442 tags.append(self.sanitize_hash_tag(m.group(1)))
3443 return tags
3444
3445 @classmethod
3446 def sanitize_hash_tag(cls, tag):
3447 """Returns a sanitized Gerrit hash tag.
3448
3449 A sanitized hashtag can be used as a git push refspec parameter value.
3450 """
3451 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3452
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003453 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3454 """Updates this commit description given the parent.
3455
3456 This is essentially what Gnumbd used to do.
3457 Consult https://goo.gl/WMmpDe for more details.
3458 """
3459 assert parent_msg # No, orphan branch creation isn't supported.
3460 assert parent_hash
3461 assert dest_ref
3462 parent_footer_map = git_footers.parse_footers(parent_msg)
3463 # This will also happily parse svn-position, which GnumbD is no longer
3464 # supporting. While we'd generate correct footers, the verifier plugin
3465 # installed in Gerrit will block such commit (ie git push below will fail).
3466 parent_position = git_footers.get_position(parent_footer_map)
3467
3468 # Cherry-picks may have last line obscuring their prior footers,
3469 # from git_footers perspective. This is also what Gnumbd did.
3470 cp_line = None
3471 if (self._description_lines and
3472 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3473 cp_line = self._description_lines.pop()
3474
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003475 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003476
3477 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3478 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003479 for i, line in enumerate(footer_lines):
3480 k, v = git_footers.parse_footer(line) or (None, None)
3481 if k and k.startswith('Cr-'):
3482 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003483
3484 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003485 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003486 if parent_position[0] == dest_ref:
3487 # Same branch as parent.
3488 number = int(parent_position[1]) + 1
3489 else:
3490 number = 1 # New branch, and extra lineage.
3491 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3492 int(parent_position[1])))
3493
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003494 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3495 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003496
3497 self._description_lines = top_lines
3498 if cp_line:
3499 self._description_lines.append(cp_line)
3500 if self._description_lines[-1] != '':
3501 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003502 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003503
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003504
Aaron Gablea1bab272017-04-11 16:38:18 -07003505def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003506 """Retrieves the reviewers that approved a CL from the issue properties with
3507 messages.
3508
3509 Note that the list may contain reviewers that are not committer, thus are not
3510 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003511
3512 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003513 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003514 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003515 return sorted(
3516 set(
3517 message['sender']
3518 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003519 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003520 )
3521 )
3522
3523
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003524def FindCodereviewSettingsFile(filename='codereview.settings'):
3525 """Finds the given file starting in the cwd and going up.
3526
3527 Only looks up to the top of the repository unless an
3528 'inherit-review-settings-ok' file exists in the root of the repository.
3529 """
3530 inherit_ok_file = 'inherit-review-settings-ok'
3531 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003532 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003533 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3534 root = '/'
3535 while True:
3536 if filename in os.listdir(cwd):
3537 if os.path.isfile(os.path.join(cwd, filename)):
3538 return open(os.path.join(cwd, filename))
3539 if cwd == root:
3540 break
3541 cwd = os.path.dirname(cwd)
3542
3543
3544def LoadCodereviewSettingsFromFile(fileobj):
3545 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003546 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003547
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003548 def SetProperty(name, setting, unset_error_ok=False):
3549 fullname = 'rietveld.' + name
3550 if setting in keyvals:
3551 RunGit(['config', fullname, keyvals[setting]])
3552 else:
3553 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3554
tandrii48df5812016-10-17 03:55:37 -07003555 if not keyvals.get('GERRIT_HOST', False):
3556 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003557 # Only server setting is required. Other settings can be absent.
3558 # In that case, we ignore errors raised during option deletion attempt.
3559 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003560 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003561 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3562 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003563 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003564 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3565 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003566 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003567 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3568 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003569
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003570 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003571 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003572
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003573 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003574 RunGit(['config', 'gerrit.squash-uploads',
3575 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003576
tandrii@chromium.org28253532016-04-14 13:46:56 +00003577 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003578 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003579 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3580
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003581 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003582 # should be of the form
3583 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3584 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003585 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3586 keyvals['ORIGIN_URL_CONFIG']])
3587
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003588
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003589def urlretrieve(source, destination):
3590 """urllib is broken for SSL connections via a proxy therefore we
3591 can't use urllib.urlretrieve()."""
3592 with open(destination, 'w') as f:
3593 f.write(urllib2.urlopen(source).read())
3594
3595
ukai@chromium.org712d6102013-11-27 00:52:58 +00003596def hasSheBang(fname):
3597 """Checks fname is a #! script."""
3598 with open(fname) as f:
3599 return f.read(2).startswith('#!')
3600
3601
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003602# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3603def DownloadHooks(*args, **kwargs):
3604 pass
3605
3606
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003607def DownloadGerritHook(force):
3608 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003609
3610 Args:
3611 force: True to update hooks. False to install hooks if not present.
3612 """
3613 if not settings.GetIsGerrit():
3614 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003615 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003616 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3617 if not os.access(dst, os.X_OK):
3618 if os.path.exists(dst):
3619 if not force:
3620 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003621 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003622 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003623 if not hasSheBang(dst):
3624 DieWithError('Not a script: %s\n'
3625 'You need to download from\n%s\n'
3626 'into .git/hooks/commit-msg and '
3627 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003628 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3629 except Exception:
3630 if os.path.exists(dst):
3631 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003632 DieWithError('\nFailed to download hooks.\n'
3633 'You need to download from\n%s\n'
3634 'into .git/hooks/commit-msg and '
3635 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003636
3637
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003638def GetRietveldCodereviewSettingsInteractively():
3639 """Prompt the user for settings."""
3640 server = settings.GetDefaultServerUrl(error_ok=True)
3641 prompt = 'Rietveld server (host[:port])'
3642 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3643 newserver = ask_for_data(prompt + ':')
3644 if not server and not newserver:
3645 newserver = DEFAULT_SERVER
3646 if newserver:
3647 newserver = gclient_utils.UpgradeToHttps(newserver)
3648 if newserver != server:
3649 RunGit(['config', 'rietveld.server', newserver])
3650
3651 def SetProperty(initial, caption, name, is_url):
3652 prompt = caption
3653 if initial:
3654 prompt += ' ("x" to clear) [%s]' % initial
3655 new_val = ask_for_data(prompt + ':')
3656 if new_val == 'x':
3657 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3658 elif new_val:
3659 if is_url:
3660 new_val = gclient_utils.UpgradeToHttps(new_val)
3661 if new_val != initial:
3662 RunGit(['config', 'rietveld.' + name, new_val])
3663
3664 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3665 SetProperty(settings.GetDefaultPrivateFlag(),
3666 'Private flag (rietveld only)', 'private', False)
3667 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3668 'tree-status-url', False)
3669 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3670 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3671 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3672 'run-post-upload-hook', False)
3673
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003674
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003675class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003676 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003677
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003678 _GOOGLESOURCE = 'googlesource.com'
3679
3680 def __init__(self):
3681 # Cached list of [host, identity, source], where source is either
3682 # .gitcookies or .netrc.
3683 self._all_hosts = None
3684
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003685 def ensure_configured_gitcookies(self):
3686 """Runs checks and suggests fixes to make git use .gitcookies from default
3687 path."""
3688 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3689 configured_path = RunGitSilent(
3690 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003691 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003692 if configured_path:
3693 self._ensure_default_gitcookies_path(configured_path, default)
3694 else:
3695 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003696
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003697 @staticmethod
3698 def _ensure_default_gitcookies_path(configured_path, default_path):
3699 assert configured_path
3700 if configured_path == default_path:
3701 print('git is already configured to use your .gitcookies from %s' %
3702 configured_path)
3703 return
3704
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003705 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003706 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3707 (configured_path, default_path))
3708
3709 if not os.path.exists(configured_path):
3710 print('However, your configured .gitcookies file is missing.')
3711 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3712 action='reconfigure')
3713 RunGit(['config', '--global', 'http.cookiefile', default_path])
3714 return
3715
3716 if os.path.exists(default_path):
3717 print('WARNING: default .gitcookies file already exists %s' %
3718 default_path)
3719 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3720 default_path)
3721
3722 confirm_or_exit('Move existing .gitcookies to default location?',
3723 action='move')
3724 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003725 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003726 print('Moved and reconfigured git to use .gitcookies from %s' %
3727 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003728
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003729 @staticmethod
3730 def _configure_gitcookies_path(default_path):
3731 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3732 if os.path.exists(netrc_path):
3733 print('You seem to be using outdated .netrc for git credentials: %s' %
3734 netrc_path)
3735 print('This tool will guide you through setting up recommended '
3736 '.gitcookies store for git credentials.\n'
3737 '\n'
3738 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3739 ' git config --global --unset http.cookiefile\n'
3740 ' mv %s %s.backup\n\n' % (default_path, default_path))
3741 confirm_or_exit(action='setup .gitcookies')
3742 RunGit(['config', '--global', 'http.cookiefile', default_path])
3743 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003744
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003745 def get_hosts_with_creds(self, include_netrc=False):
3746 if self._all_hosts is None:
3747 a = gerrit_util.CookiesAuthenticator()
3748 self._all_hosts = [
3749 (h, u, s)
3750 for h, u, s in itertools.chain(
3751 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3752 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3753 )
3754 if h.endswith(self._GOOGLESOURCE)
3755 ]
3756
3757 if include_netrc:
3758 return self._all_hosts
3759 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3760
3761 def print_current_creds(self, include_netrc=False):
3762 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3763 if not hosts:
3764 print('No Git/Gerrit credentials found')
3765 return
3766 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3767 header = [('Host', 'User', 'Which file'),
3768 ['=' * l for l in lengths]]
3769 for row in (header + hosts):
3770 print('\t'.join((('%%+%ds' % l) % s)
3771 for l, s in zip(lengths, row)))
3772
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003773 @staticmethod
3774 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003775 """Parses identity "git-<username>.domain" into <username> and domain."""
3776 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003777 # distinguishable from sub-domains. But we do know typical domains:
3778 if identity.endswith('.chromium.org'):
3779 domain = 'chromium.org'
3780 username = identity[:-len('.chromium.org')]
3781 else:
3782 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003783 if username.startswith('git-'):
3784 username = username[len('git-'):]
3785 return username, domain
3786
3787 def _get_usernames_of_domain(self, domain):
3788 """Returns list of usernames referenced by .gitcookies in a given domain."""
3789 identities_by_domain = {}
3790 for _, identity, _ in self.get_hosts_with_creds():
3791 username, domain = self._parse_identity(identity)
3792 identities_by_domain.setdefault(domain, []).append(username)
3793 return identities_by_domain.get(domain)
3794
3795 def _canonical_git_googlesource_host(self, host):
3796 """Normalizes Gerrit hosts (with '-review') to Git host."""
3797 assert host.endswith(self._GOOGLESOURCE)
3798 # Prefix doesn't include '.' at the end.
3799 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3800 if prefix.endswith('-review'):
3801 prefix = prefix[:-len('-review')]
3802 return prefix + '.' + self._GOOGLESOURCE
3803
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003804 def _canonical_gerrit_googlesource_host(self, host):
3805 git_host = self._canonical_git_googlesource_host(host)
3806 prefix = git_host.split('.', 1)[0]
3807 return prefix + '-review.' + self._GOOGLESOURCE
3808
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003809 def _get_counterpart_host(self, host):
3810 assert host.endswith(self._GOOGLESOURCE)
3811 git = self._canonical_git_googlesource_host(host)
3812 gerrit = self._canonical_gerrit_googlesource_host(git)
3813 return git if gerrit == host else gerrit
3814
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003815 def has_generic_host(self):
3816 """Returns whether generic .googlesource.com has been configured.
3817
3818 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3819 """
3820 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3821 if host == '.' + self._GOOGLESOURCE:
3822 return True
3823 return False
3824
3825 def _get_git_gerrit_identity_pairs(self):
3826 """Returns map from canonic host to pair of identities (Git, Gerrit).
3827
3828 One of identities might be None, meaning not configured.
3829 """
3830 host_to_identity_pairs = {}
3831 for host, identity, _ in self.get_hosts_with_creds():
3832 canonical = self._canonical_git_googlesource_host(host)
3833 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3834 idx = 0 if canonical == host else 1
3835 pair[idx] = identity
3836 return host_to_identity_pairs
3837
3838 def get_partially_configured_hosts(self):
3839 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003840 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3841 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3842 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003843
3844 def get_conflicting_hosts(self):
3845 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003846 host
3847 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003848 if None not in (i1, i2) and i1 != i2)
3849
3850 def get_duplicated_hosts(self):
3851 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3852 return set(host for host, count in counters.iteritems() if count > 1)
3853
3854 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3855 'chromium.googlesource.com': 'chromium.org',
3856 'chrome-internal.googlesource.com': 'google.com',
3857 }
3858
3859 def get_hosts_with_wrong_identities(self):
3860 """Finds hosts which **likely** reference wrong identities.
3861
3862 Note: skips hosts which have conflicting identities for Git and Gerrit.
3863 """
3864 hosts = set()
3865 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3866 pair = self._get_git_gerrit_identity_pairs().get(host)
3867 if pair and pair[0] == pair[1]:
3868 _, domain = self._parse_identity(pair[0])
3869 if domain != expected:
3870 hosts.add(host)
3871 return hosts
3872
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003873 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003874 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003875 hosts = sorted(hosts)
3876 assert hosts
3877 if extra_column_func is None:
3878 extras = [''] * len(hosts)
3879 else:
3880 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003881 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3882 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003883 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003884 lines.append(tmpl % he)
3885 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003886
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003887 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003888 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003889 yield ('.googlesource.com wildcard record detected',
3890 ['Chrome Infrastructure team recommends to list full host names '
3891 'explicitly.'],
3892 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003893
3894 dups = self.get_duplicated_hosts()
3895 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003896 yield ('The following hosts were defined twice',
3897 self._format_hosts(dups),
3898 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003899
3900 partial = self.get_partially_configured_hosts()
3901 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003902 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3903 'These hosts are missing',
3904 self._format_hosts(partial, lambda host: 'but %s defined' %
3905 self._get_counterpart_host(host)),
3906 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003907
3908 conflicting = self.get_conflicting_hosts()
3909 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003910 yield ('The following Git hosts have differing credentials from their '
3911 'Gerrit counterparts',
3912 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3913 tuple(self._get_git_gerrit_identity_pairs()[host])),
3914 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003915
3916 wrong = self.get_hosts_with_wrong_identities()
3917 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003918 yield ('These hosts likely use wrong identity',
3919 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3920 (self._get_git_gerrit_identity_pairs()[host][0],
3921 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3922 wrong)
3923
3924 def find_and_report_problems(self):
3925 """Returns True if there was at least one problem, else False."""
3926 found = False
3927 bad_hosts = set()
3928 for title, sublines, hosts in self._find_problems():
3929 if not found:
3930 found = True
3931 print('\n\n.gitcookies problem report:\n')
3932 bad_hosts.update(hosts or [])
3933 print(' %s%s' % (title , (':' if sublines else '')))
3934 if sublines:
3935 print()
3936 print(' %s' % '\n '.join(sublines))
3937 print()
3938
3939 if bad_hosts:
3940 assert found
3941 print(' You can manually remove corresponding lines in your %s file and '
3942 'visit the following URLs with correct account to generate '
3943 'correct credential lines:\n' %
3944 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
3945 print(' %s' % '\n '.join(sorted(set(
3946 gerrit_util.CookiesAuthenticator().get_new_password_url(
3947 self._canonical_git_googlesource_host(host))
3948 for host in bad_hosts
3949 ))))
3950 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003951
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003952
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003953@metrics.collector.collect_metrics('git cl creds-check')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003954def CMDcreds_check(parser, args):
3955 """Checks credentials and suggests changes."""
3956 _, _ = parser.parse_args(args)
3957
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003958 # Code below checks .gitcookies. Abort if using something else.
3959 authn = gerrit_util.Authenticator.get()
3960 if not isinstance(authn, gerrit_util.CookiesAuthenticator):
3961 if isinstance(authn, gerrit_util.GceAuthenticator):
3962 DieWithError(
3963 'This command is not designed for GCE, are you on a bot?\n'
3964 'If you need to run this on GCE, export SKIP_GCE_AUTH_FOR_GIT=1 '
3965 'in your env.')
Aaron Gabled10ca0e2017-09-11 11:24:10 -07003966 DieWithError(
Vadim Shtayurab250ec12018-10-04 00:21:08 +00003967 'This command is not designed for bot environment. It checks '
3968 '~/.gitcookies file not generally used on bots.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003969
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003970 checker = _GitCookiesChecker()
3971 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003972
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003973 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003974 checker.print_current_creds(include_netrc=True)
3975
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003976 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003977 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003978 return 0
3979 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003980
3981
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003982@subcommand.usage('[repo root containing codereview.settings]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00003983@metrics.collector.collect_metrics('git cl config')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003984def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003985 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003986
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003987 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07003988 # TODO(tandrii): remove this once we switch to Gerrit.
3989 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003990 parser.add_option('--activate-update', action='store_true',
3991 help='activate auto-updating [rietveld] section in '
3992 '.git/config')
3993 parser.add_option('--deactivate-update', action='store_true',
3994 help='deactivate auto-updating [rietveld] section in '
3995 '.git/config')
3996 options, args = parser.parse_args(args)
3997
3998 if options.deactivate_update:
3999 RunGit(['config', 'rietveld.autoupdate', 'false'])
4000 return
4001
4002 if options.activate_update:
4003 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4004 return
4005
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004006 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004007 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004008 return 0
4009
4010 url = args[0]
4011 if not url.endswith('codereview.settings'):
4012 url = os.path.join(url, 'codereview.settings')
4013
4014 # Load code review settings and download hooks (if available).
4015 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4016 return 0
4017
4018
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004019@metrics.collector.collect_metrics('git cl baseurl')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004020def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004021 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004022 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4023 branch = ShortBranchName(branchref)
4024 _, args = parser.parse_args(args)
4025 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004026 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004027 return RunGit(['config', 'branch.%s.base-url' % branch],
4028 error_ok=False).strip()
4029 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004030 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004031 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4032 error_ok=False).strip()
4033
4034
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004035def color_for_status(status):
4036 """Maps a Changelist status to color, for CMDstatus and other tools."""
4037 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004038 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004039 'waiting': Fore.BLUE,
4040 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004041 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004042 'lgtm': Fore.GREEN,
4043 'commit': Fore.MAGENTA,
4044 'closed': Fore.CYAN,
4045 'error': Fore.WHITE,
4046 }.get(status, Fore.WHITE)
4047
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004048
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004049def get_cl_statuses(changes, fine_grained, max_processes=None):
4050 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004051
4052 If fine_grained is true, this will fetch CL statuses from the server.
4053 Otherwise, simply indicate if there's a matching url for the given branches.
4054
4055 If max_processes is specified, it is used as the maximum number of processes
4056 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4057 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004058
4059 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004060 """
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004061 if not changes:
4062 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004063
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004064 if not fine_grained:
4065 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004066 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004067 for cl in changes:
4068 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004069 return
4070
4071 # First, sort out authentication issues.
4072 logging.debug('ensuring credentials exist')
4073 for cl in changes:
4074 cl.EnsureAuthenticated(force=False, refresh=True)
4075
4076 def fetch(cl):
4077 try:
4078 return (cl, cl.GetStatus())
4079 except:
4080 # See http://crbug.com/629863.
Andrii Shyshkalov98824232018-04-19 11:37:15 -07004081 logging.exception('failed to fetch status for cl %s:', cl.GetIssue())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004082 raise
4083
4084 threads_count = len(changes)
4085 if max_processes:
4086 threads_count = max(1, min(threads_count, max_processes))
4087 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4088
4089 pool = ThreadPool(threads_count)
4090 fetched_cls = set()
4091 try:
4092 it = pool.imap_unordered(fetch, changes).__iter__()
4093 while True:
4094 try:
4095 cl, status = it.next(timeout=5)
4096 except multiprocessing.TimeoutError:
4097 break
4098 fetched_cls.add(cl)
4099 yield cl, status
4100 finally:
4101 pool.close()
4102
4103 # Add any branches that failed to fetch.
4104 for cl in set(changes) - fetched_cls:
4105 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004106
rmistry@google.com2dd99862015-06-22 12:22:18 +00004107
4108def upload_branch_deps(cl, args):
4109 """Uploads CLs of local branches that are dependents of the current branch.
4110
4111 If the local branch dependency tree looks like:
4112 test1 -> test2.1 -> test3.1
4113 -> test3.2
4114 -> test2.2 -> test3.3
4115
4116 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4117 run on the dependent branches in this order:
4118 test2.1, test3.1, test3.2, test2.2, test3.3
4119
4120 Note: This function does not rebase your local dependent branches. Use it when
4121 you make a change to the parent branch that will not conflict with its
4122 dependent branches, and you would like their dependencies updated in
4123 Rietveld.
4124 """
4125 if git_common.is_dirty_git_tree('upload-branch-deps'):
4126 return 1
4127
4128 root_branch = cl.GetBranch()
4129 if root_branch is None:
4130 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4131 'Get on a branch!')
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004132 if not cl.GetIssue():
rmistry@google.com2dd99862015-06-22 12:22:18 +00004133 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4134 'patchset dependencies without an uploaded CL.')
4135
4136 branches = RunGit(['for-each-ref',
4137 '--format=%(refname:short) %(upstream:short)',
4138 'refs/heads'])
4139 if not branches:
4140 print('No local branches found.')
4141 return 0
4142
4143 # Create a dictionary of all local branches to the branches that are dependent
4144 # on it.
4145 tracked_to_dependents = collections.defaultdict(list)
4146 for b in branches.splitlines():
4147 tokens = b.split()
4148 if len(tokens) == 2:
4149 branch_name, tracked = tokens
4150 tracked_to_dependents[tracked].append(branch_name)
4151
vapiera7fbd5a2016-06-16 09:17:49 -07004152 print()
4153 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004154 dependents = []
4155 def traverse_dependents_preorder(branch, padding=''):
4156 dependents_to_process = tracked_to_dependents.get(branch, [])
4157 padding += ' '
4158 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004159 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004160 dependents.append(dependent)
4161 traverse_dependents_preorder(dependent, padding)
4162 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004163 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004164
4165 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004166 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004167 return 0
4168
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004169 confirm_or_exit('This command will checkout all dependent branches and run '
4170 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004171
rmistry@google.com2dd99862015-06-22 12:22:18 +00004172 # Record all dependents that failed to upload.
4173 failures = {}
4174 # Go through all dependents, checkout the branch and upload.
4175 try:
4176 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004177 print()
4178 print('--------------------------------------')
4179 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004180 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004181 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004182 try:
4183 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004184 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004185 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004186 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004187 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004188 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004189 finally:
4190 # Swap back to the original root branch.
4191 RunGit(['checkout', '-q', root_branch])
4192
vapiera7fbd5a2016-06-16 09:17:49 -07004193 print()
4194 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004195 for dependent_branch in dependents:
4196 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004197 print(' %s : %s' % (dependent_branch, upload_status))
4198 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004199
4200 return 0
4201
4202
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004203@metrics.collector.collect_metrics('git cl archive')
kmarshall3bff56b2016-06-06 18:31:47 -07004204def CMDarchive(parser, args):
4205 """Archives and deletes branches associated with closed changelists."""
4206 parser.add_option(
4207 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004208 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004209 parser.add_option(
4210 '-f', '--force', action='store_true',
4211 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004212 parser.add_option(
4213 '-d', '--dry-run', action='store_true',
4214 help='Skip the branch tagging and removal steps.')
4215 parser.add_option(
4216 '-t', '--notags', action='store_true',
4217 help='Do not tag archived branches. '
4218 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004219
4220 auth.add_auth_options(parser)
4221 options, args = parser.parse_args(args)
4222 if args:
4223 parser.error('Unsupported args: %s' % ' '.join(args))
4224 auth_config = auth.extract_auth_config_from_options(options)
4225
4226 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4227 if not branches:
4228 return 0
4229
vapiera7fbd5a2016-06-16 09:17:49 -07004230 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004231 changes = [Changelist(branchref=b, auth_config=auth_config)
4232 for b in branches.splitlines()]
4233 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4234 statuses = get_cl_statuses(changes,
4235 fine_grained=True,
4236 max_processes=options.maxjobs)
4237 proposal = [(cl.GetBranch(),
4238 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4239 for cl, status in statuses
Andrii Shyshkalov51bdf8c2018-10-18 01:07:58 +00004240 if status in ('closed', 'rietveld-not-supported')]
kmarshall3bff56b2016-06-06 18:31:47 -07004241 proposal.sort()
4242
4243 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004244 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004245 return 0
4246
4247 current_branch = GetCurrentBranch()
4248
vapiera7fbd5a2016-06-16 09:17:49 -07004249 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004250 if options.notags:
4251 for next_item in proposal:
4252 print(' ' + next_item[0])
4253 else:
4254 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4255 for next_item in proposal:
4256 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004257
kmarshall9249e012016-08-23 12:02:16 -07004258 # Quit now on precondition failure or if instructed by the user, either
4259 # via an interactive prompt or by command line flags.
4260 if options.dry_run:
4261 print('\nNo changes were made (dry run).\n')
4262 return 0
4263 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004264 print('You are currently on a branch \'%s\' which is associated with a '
4265 'closed codereview issue, so archive cannot proceed. Please '
4266 'checkout another branch and run this command again.' %
4267 current_branch)
4268 return 1
kmarshall9249e012016-08-23 12:02:16 -07004269 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004270 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4271 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004272 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004273 return 1
4274
4275 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004276 if not options.notags:
4277 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004278 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004279
vapiera7fbd5a2016-06-16 09:17:49 -07004280 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004281
4282 return 0
4283
4284
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004285@metrics.collector.collect_metrics('git cl status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004286def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004287 """Show status of changelists.
4288
4289 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004290 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004291 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004292 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004293 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004294 - Magenta in the commit queue
4295 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004296 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004297
4298 Also see 'git cl comments'.
4299 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004300 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004301 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004302 parser.add_option('-f', '--fast', action='store_true',
4303 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004304 parser.add_option(
4305 '-j', '--maxjobs', action='store', type=int,
4306 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004307
4308 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004309 _add_codereview_issue_select_options(
4310 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004311 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004312 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004313 if args:
4314 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004315 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004316
iannuccie53c9352016-08-17 14:40:40 -07004317 if options.issue is not None and not options.field:
4318 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004319
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004320 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004321 cl = Changelist(auth_config=auth_config, issue=options.issue,
4322 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004323 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004324 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004325 elif options.field == 'id':
4326 issueid = cl.GetIssue()
4327 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004328 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004329 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004330 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004331 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004332 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004333 elif options.field == 'status':
4334 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004335 elif options.field == 'url':
4336 url = cl.GetIssueURL()
4337 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004338 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004339 return 0
4340
4341 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4342 if not branches:
4343 print('No local branch found.')
4344 return 0
4345
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004346 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004347 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004348 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004349 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004350 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004351 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004352 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004353
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004354 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004355 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4356 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4357 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004358 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004359 c, status = output.next()
4360 branch_statuses[c.GetBranch()] = status
4361 status = branch_statuses.pop(branch)
4362 url = cl.GetIssueURL()
4363 if url and (not status or status == 'error'):
4364 # The issue probably doesn't exist anymore.
4365 url += ' (broken)'
4366
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004367 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004368 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004369 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004370 color = ''
4371 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004372 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004373 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004374 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004375 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004376
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004377
4378 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004379 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004380 print('Current branch: %s' % branch)
4381 for cl in changes:
4382 if cl.GetBranch() == branch:
4383 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004384 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004385 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004386 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004387 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004388 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004389 print('Issue description:')
4390 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391 return 0
4392
4393
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004394def colorize_CMDstatus_doc():
4395 """To be called once in main() to add colors to git cl status help."""
4396 colors = [i for i in dir(Fore) if i[0].isupper()]
4397
4398 def colorize_line(line):
4399 for color in colors:
4400 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004401 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004402 indent = len(line) - len(line.lstrip(' ')) + 1
4403 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4404 return line
4405
4406 lines = CMDstatus.__doc__.splitlines()
4407 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4408
4409
phajdan.jre328cf92016-08-22 04:12:17 -07004410def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004411 if path == '-':
4412 json.dump(contents, sys.stdout)
4413 else:
4414 with open(path, 'w') as f:
4415 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004416
4417
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004418@subcommand.usage('[issue_number]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004419@metrics.collector.collect_metrics('git cl issue')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004420def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004421 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004422
4423 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004424 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004425 parser.add_option('-r', '--reverse', action='store_true',
4426 help='Lookup the branch(es) for the specified issues. If '
4427 'no issues are specified, all branches with mapped '
4428 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004429 parser.add_option('--json',
4430 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004431 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004432 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004433 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004434
dnj@chromium.org406c4402015-03-03 17:22:28 +00004435 if options.reverse:
4436 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004437 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004438 # Reverse issue lookup.
4439 issue_branch_map = {}
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004440
4441 git_config = {}
4442 for config in RunGit(['config', '--get-regexp',
4443 r'branch\..*issue']).splitlines():
4444 name, _space, val = config.partition(' ')
4445 git_config[name] = val
4446
dnj@chromium.org406c4402015-03-03 17:22:28 +00004447 for branch in branches:
Daniel Bratellb56a43a2018-09-06 15:49:03 +00004448 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
4449 config_key = _git_branch_config_key(ShortBranchName(branch),
4450 cls.IssueConfigKey())
4451 issue = git_config.get(config_key)
4452 if issue:
4453 issue_branch_map.setdefault(int(issue), []).append(branch)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004454 if not args:
4455 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004456 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004457 for issue in args:
4458 if not issue:
4459 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004460 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004461 print('Branch for issue number %s: %s' % (
4462 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004463 if options.json:
4464 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004465 return 0
4466
4467 if len(args) > 0:
4468 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4469 if not issue.valid:
4470 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4471 'or no argument to list it.\n'
4472 'Maybe you want to run git cl status?')
4473 cl = Changelist(codereview=issue.codereview)
4474 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004475 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004476 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004477 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4478 if options.json:
4479 write_json(options.json, {
4480 'issue': cl.GetIssue(),
4481 'issue_url': cl.GetIssueURL(),
4482 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004483 return 0
4484
4485
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004486@metrics.collector.collect_metrics('git cl comments')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004487def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004488 """Shows or posts review comments for any changelist."""
4489 parser.add_option('-a', '--add-comment', dest='comment',
4490 help='comment to add to an issue')
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004491 parser.add_option('-p', '--publish', action='store_true',
4492 help='marks CL as ready and sends comment to reviewers')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004493 parser.add_option('-i', '--issue', dest='issue',
4494 help='review issue id (defaults to current issue). '
4495 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004496 parser.add_option('-m', '--machine-readable', dest='readable',
4497 action='store_false', default=True,
4498 help='output comments in a format compatible with '
4499 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004500 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004501 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004502 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004503 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004504 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004505 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004506 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004507
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004508 issue = None
4509 if options.issue:
4510 try:
4511 issue = int(options.issue)
4512 except ValueError:
4513 DieWithError('A review issue id is expected to be a number')
4514
Andrii Shyshkalov642641d2018-10-16 05:54:41 +00004515 cl = Changelist(issue=issue, codereview='gerrit', auth_config=auth_config)
4516
4517 if not cl.IsGerrit():
4518 parser.error('rietveld is not supported')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004519
4520 if options.comment:
Sergiy Byelozyorovcb629a42018-10-28 19:20:39 +00004521 cl.AddComment(options.comment, options.publish)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004522 return 0
4523
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004524 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4525 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004526 for comment in summary:
4527 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004528 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004529 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004530 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004531 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004532 color = Fore.MAGENTA
4533 else:
4534 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004535 print('\n%s%s %s%s\n%s' % (
4536 color,
4537 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4538 comment.sender,
4539 Fore.RESET,
4540 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4541
smut@google.comc85ac942015-09-15 16:34:43 +00004542 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004543 def pre_serialize(c):
4544 dct = c.__dict__.copy()
4545 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4546 return dct
Leszek Swirski45b20c42018-09-17 17:05:26 +00004547 write_json(options.json_file, map(pre_serialize, summary))
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004548 return 0
4549
4550
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004551@subcommand.usage('[codereview url or issue id]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004552@metrics.collector.collect_metrics('git cl description')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004553def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004554 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004555 parser.add_option('-d', '--display', action='store_true',
4556 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004557 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004558 help='New description to set for this issue (- for stdin, '
4559 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004560 parser.add_option('-f', '--force', action='store_true',
4561 help='Delete any unpublished Gerrit edits for this issue '
4562 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004563
4564 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004565 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004566 options, args = parser.parse_args(args)
4567 _process_codereview_select_options(parser, options)
4568
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004569 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004570 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004571 target_issue_arg = ParseIssueNumberArgument(args[0],
4572 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004573 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004574 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004575
martiniss6eda05f2016-06-30 10:18:35 -07004576 kwargs = {
Andrii Shyshkalovdd672fb2018-10-16 06:09:51 +00004577 'auth_config': auth.extract_auth_config_from_options(options),
4578 'codereview': options.forced_codereview,
martiniss6eda05f2016-06-30 10:18:35 -07004579 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004580 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004581 if target_issue_arg:
4582 kwargs['issue'] = target_issue_arg.issue
4583 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004584 if target_issue_arg.codereview and not options.forced_codereview:
4585 detected_codereview_from_url = True
4586 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004587
4588 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004589 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004590 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004591 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004592
4593 if detected_codereview_from_url:
4594 logging.info('canonical issue/change URL: %s (type: %s)\n',
4595 cl.GetIssueURL(), target_issue_arg.codereview)
4596
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004597 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004598
smut@google.com34fb6b12015-07-13 20:03:26 +00004599 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004600 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004601 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004602
4603 if options.new_description:
4604 text = options.new_description
4605 if text == '-':
4606 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004607 elif text == '+':
4608 base_branch = cl.GetCommonAncestorWithUpstream()
4609 change = cl.GetChange(base_branch, None, local_description=True)
4610 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004611
4612 description.set_description(text)
4613 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004614 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004615
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004616 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004617 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004618 return 0
4619
4620
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004621@metrics.collector.collect_metrics('git cl lint')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004622def CMDlint(parser, args):
4623 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004624 parser.add_option('--filter', action='append', metavar='-x,+y',
4625 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004626 auth.add_auth_options(parser)
4627 options, args = parser.parse_args(args)
4628 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004629
4630 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004631 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004632 try:
4633 import cpplint
4634 import cpplint_chromium
4635 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004636 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004637 return 1
4638
4639 # Change the current working directory before calling lint so that it
4640 # shows the correct base.
4641 previous_cwd = os.getcwd()
4642 os.chdir(settings.GetRoot())
4643 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004644 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004645 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4646 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004647 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004648 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004649 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004650
4651 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004652 command = args + files
4653 if options.filter:
4654 command = ['--filter=' + ','.join(options.filter)] + command
4655 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004656
4657 white_regex = re.compile(settings.GetLintRegex())
4658 black_regex = re.compile(settings.GetLintIgnoreRegex())
4659 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4660 for filename in filenames:
4661 if white_regex.match(filename):
4662 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004663 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004664 else:
4665 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4666 extra_check_functions)
4667 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004668 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004669 finally:
4670 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004671 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004672 if cpplint._cpplint_state.error_count != 0:
4673 return 1
4674 return 0
4675
4676
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004677@metrics.collector.collect_metrics('git cl presubmit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004678def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004679 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004680 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004681 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004682 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004683 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004684 parser.add_option('--all', action='store_true',
4685 help='Run checks against all files, not just modified ones')
Edward Lesmes8e282792018-04-03 18:50:29 -04004686 parser.add_option('--parallel', action='store_true',
4687 help='Run all tests specified by input_api.RunTests in all '
4688 'PRESUBMIT files in parallel.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004689 auth.add_auth_options(parser)
4690 options, args = parser.parse_args(args)
4691 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004692
sbc@chromium.org71437c02015-04-09 19:29:40 +00004693 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004694 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004695 return 1
4696
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004697 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004698 if args:
4699 base_branch = args[0]
4700 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004701 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004702 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004703
Aaron Gable8076c282017-11-29 14:39:41 -08004704 if options.all:
4705 base_change = cl.GetChange(base_branch, None)
4706 files = [('M', f) for f in base_change.AllFiles()]
4707 change = presubmit_support.GitChange(
4708 base_change.Name(),
4709 base_change.FullDescriptionText(),
4710 base_change.RepositoryRoot(),
4711 files,
4712 base_change.issue,
4713 base_change.patchset,
4714 base_change.author_email,
4715 base_change._upstream)
4716 else:
4717 change = cl.GetChange(base_branch, None)
4718
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004719 cl.RunHook(
4720 committing=not options.upload,
4721 may_prompt=False,
4722 verbose=options.verbose,
Edward Lesmes8e282792018-04-03 18:50:29 -04004723 change=change,
4724 parallel=options.parallel)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004725 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004726
4727
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004728def GenerateGerritChangeId(message):
4729 """Returns Ixxxxxx...xxx change id.
4730
4731 Works the same way as
4732 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4733 but can be called on demand on all platforms.
4734
4735 The basic idea is to generate git hash of a state of the tree, original commit
4736 message, author/committer info and timestamps.
4737 """
4738 lines = []
4739 tree_hash = RunGitSilent(['write-tree'])
4740 lines.append('tree %s' % tree_hash.strip())
4741 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4742 if code == 0:
4743 lines.append('parent %s' % parent.strip())
4744 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4745 lines.append('author %s' % author.strip())
4746 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4747 lines.append('committer %s' % committer.strip())
4748 lines.append('')
4749 # Note: Gerrit's commit-hook actually cleans message of some lines and
4750 # whitespace. This code is not doing this, but it clearly won't decrease
4751 # entropy.
4752 lines.append(message)
4753 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4754 stdin='\n'.join(lines))
4755 return 'I%s' % change_hash.strip()
4756
4757
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004758def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004759 """Computes the remote branch ref to use for the CL.
4760
4761 Args:
4762 remote (str): The git remote for the CL.
4763 remote_branch (str): The git remote branch for the CL.
4764 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004765 """
4766 if not (remote and remote_branch):
4767 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004768
wittman@chromium.org455dc922015-01-26 20:15:50 +00004769 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004770 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004771 # refs, which are then translated into the remote full symbolic refs
4772 # below.
4773 if '/' not in target_branch:
4774 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4775 else:
4776 prefix_replacements = (
4777 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4778 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4779 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4780 )
4781 match = None
4782 for regex, replacement in prefix_replacements:
4783 match = re.search(regex, target_branch)
4784 if match:
4785 remote_branch = target_branch.replace(match.group(0), replacement)
4786 break
4787 if not match:
4788 # This is a branch path but not one we recognize; use as-is.
4789 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004790 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4791 # Handle the refs that need to land in different refs.
4792 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004793
wittman@chromium.org455dc922015-01-26 20:15:50 +00004794 # Create the true path to the remote branch.
4795 # Does the following translation:
4796 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4797 # * refs/remotes/origin/master -> refs/heads/master
4798 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4799 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4800 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4801 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4802 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4803 'refs/heads/')
4804 elif remote_branch.startswith('refs/remotes/branch-heads'):
4805 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004806
wittman@chromium.org455dc922015-01-26 20:15:50 +00004807 return remote_branch
4808
4809
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004810def cleanup_list(l):
4811 """Fixes a list so that comma separated items are put as individual items.
4812
4813 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4814 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4815 """
4816 items = sum((i.split(',') for i in l), [])
4817 stripped_items = (i.strip() for i in items)
4818 return sorted(filter(None, stripped_items))
4819
4820
Aaron Gable4db38df2017-11-03 14:59:07 -07004821@subcommand.usage('[flags]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004822@metrics.collector.collect_metrics('git cl upload')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004823def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004824 """Uploads the current changelist to codereview.
4825
4826 Can skip dependency patchset uploads for a branch by running:
4827 git config branch.branch_name.skip-deps-uploads True
4828 To unset run:
4829 git config --unset branch.branch_name.skip-deps-uploads
4830 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004831
4832 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4833 a bug number, this bug number is automatically populated in the CL
4834 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004835
4836 If subject contains text in square brackets or has "<text>: " prefix, such
4837 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4838 [git-cl] add support for hashtags
4839 Foo bar: implement foo
4840 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004841 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004842 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4843 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004844 parser.add_option('--bypass-watchlists', action='store_true',
4845 dest='bypass_watchlists',
4846 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004847 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004848 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004849 parser.add_option('--message', '-m', dest='message',
4850 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004851 parser.add_option('-b', '--bug',
4852 help='pre-populate the bug number(s) for this issue. '
4853 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004854 parser.add_option('--message-file', dest='message_file',
4855 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004856 parser.add_option('--title', '-t', dest='title',
4857 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004858 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004859 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004860 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004861 parser.add_option('--tbrs',
4862 action='append', default=[],
4863 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004864 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004865 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004866 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004867 parser.add_option('--hashtag', dest='hashtags',
4868 action='append', default=[],
4869 help=('Gerrit hashtag for new CL; '
4870 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004871 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004872 help='send email to reviewer(s) and cc(s) immediately')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004873 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004874 help='tell the commit queue to commit this patchset; '
4875 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004876 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004877 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004878 metavar='TARGET',
4879 help='Apply CL to remote ref TARGET. ' +
4880 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004881 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004882 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004883 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004884 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004885 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004886 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004887 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4888 const='TBR', help='add a set of OWNERS to TBR')
4889 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4890 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004891 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4892 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004893 help='Send the patchset to do a CQ dry run right after '
4894 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004895 parser.add_option('--dependencies', action='store_true',
4896 help='Uploads CLs of all the local branches that depend on '
4897 'the current branch')
Ravi Mistry31e7d562018-04-02 12:53:57 -04004898 parser.add_option('-a', '--enable-auto-submit', action='store_true',
4899 help='Sends your change to the CQ after an approval. Only '
4900 'works on repos that have the Auto-Submit label '
4901 'enabled')
Edward Lesmes8e282792018-04-03 18:50:29 -04004902 parser.add_option('--parallel', action='store_true',
4903 help='Run all tests specified by input_api.RunTests in all '
4904 'PRESUBMIT files in parallel.')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004905
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004906 parser.add_option('--no-autocc', action='store_true',
4907 help='Disables automatic addition of CC emails')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004908 parser.add_option('--private', action='store_true',
Sergiy Byelozyorov1aa405f2018-09-18 17:38:43 +00004909 help='Set the review private. This implies --no-autocc.')
4910
rmistry@google.com2dd99862015-06-22 12:22:18 +00004911 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004912 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004913 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004914 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004915 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004916 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004917
sbc@chromium.org71437c02015-04-09 19:29:40 +00004918 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004919 return 1
4920
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004921 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004922 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004923 options.cc = cleanup_list(options.cc)
4924
tandriib80458a2016-06-23 12:20:07 -07004925 if options.message_file:
4926 if options.message:
4927 parser.error('only one of --message and --message-file allowed.')
4928 options.message = gclient_utils.FileRead(options.message_file)
4929 options.message_file = None
4930
tandrii4d0545a2016-07-06 03:56:49 -07004931 if options.cq_dry_run and options.use_commit_queue:
4932 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4933
Aaron Gableedbc4132017-09-11 13:22:28 -07004934 if options.use_commit_queue:
4935 options.send_mail = True
4936
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004937 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4938 settings.GetIsGerrit()
4939
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004940 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
Andrii Shyshkalov9f274432018-10-15 16:40:23 +00004941 if not cl.IsGerrit():
4942 # Error out with instructions for repos not yet configured for Gerrit.
4943 print('=====================================')
4944 print('NOTICE: Rietveld is no longer supported. '
4945 'You can upload changes to Gerrit with')
4946 print(' git cl upload --gerrit')
4947 print('or set Gerrit to be your default code review tool with')
4948 print(' git config gerrit.host true')
4949 print('=====================================')
4950 return 1
4951
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004952 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004953
4954
Francois Dorayd42c6812017-05-30 15:10:20 -04004955@subcommand.usage('--description=<description file>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004956@metrics.collector.collect_metrics('git cl split')
Francois Dorayd42c6812017-05-30 15:10:20 -04004957def CMDsplit(parser, args):
4958 """Splits a branch into smaller branches and uploads CLs.
4959
4960 Creates a branch and uploads a CL for each group of files modified in the
4961 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004962 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04004963 the shared OWNERS file.
4964 """
4965 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05004966 help="A text file containing a CL description in which "
4967 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004968 parser.add_option("-c", "--comment", dest="comment_file",
4969 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11004970 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
4971 default=False,
4972 help="List the files and reviewers for each CL that would "
4973 "be created, but don't create branches or CLs.")
Stephen Martiniscb326682018-08-29 21:06:30 +00004974 parser.add_option("--cq-dry-run", action='store_true',
4975 help="If set, will do a cq dry run for each uploaded CL. "
4976 "Please be careful when doing this; more than ~10 CLs "
4977 "has the potential to overload our build "
4978 "infrastructure. Try to upload these not during high "
4979 "load times (usually 11-3 Mountain View time). Email "
4980 "infra-dev@chromium.org with any questions.")
Francois Dorayd42c6812017-05-30 15:10:20 -04004981 options, _ = parser.parse_args(args)
4982
4983 if not options.description_file:
4984 parser.error('No --description flag specified.')
4985
4986 def WrappedCMDupload(args):
4987 return CMDupload(OptionParser(), args)
4988
4989 return split_cl.SplitCl(options.description_file, options.comment_file,
Stephen Martiniscb326682018-08-29 21:06:30 +00004990 Changelist, WrappedCMDupload, options.dry_run,
4991 options.cq_dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04004992
4993
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004994@subcommand.usage('DEPRECATED')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00004995@metrics.collector.collect_metrics('git cl commit')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004996def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004997 """DEPRECATED: Used to commit the current changelist via git-svn."""
4998 message = ('git-cl no longer supports committing to SVN repositories via '
4999 'git-svn. You probably want to use `git cl land` instead.')
5000 print(message)
5001 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005002
5003
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005004# Two special branches used by git cl land.
5005MERGE_BRANCH = 'git-cl-commit'
5006CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5007
5008
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005009@subcommand.usage('[upstream branch to apply against]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005010@metrics.collector.collect_metrics('git cl land')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005011def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005012 """Commits the current changelist via git.
5013
5014 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5015 upstream and closes the issue automatically and atomically.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005016 """
5017 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5018 help='bypass upload presubmit hook')
Aaron Gablef7543cd2017-07-20 14:26:31 -07005019 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005020 help="force yes to questions (don't prompt)")
Edward Lesmes67b3faa2018-04-13 17:50:52 -04005021 parser.add_option('--parallel', action='store_true',
5022 help='Run all tests specified by input_api.RunTests in all '
5023 'PRESUBMIT files in parallel.')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005024 auth.add_auth_options(parser)
5025 (options, args) = parser.parse_args(args)
5026 auth_config = auth.extract_auth_config_from_options(options)
5027
5028 cl = Changelist(auth_config=auth_config)
5029
Robert Iannucci2e73d432018-03-14 01:10:47 -07005030 if not cl.IsGerrit():
5031 parser.error('rietveld is not supported')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005032
Robert Iannucci2e73d432018-03-14 01:10:47 -07005033 if not cl.GetIssue():
5034 DieWithError('You must upload the change first to Gerrit.\n'
5035 ' If you would rather have `git cl land` upload '
5036 'automatically for you, see http://crbug.com/642759')
5037 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
Olivier Robin75ee7252018-04-13 10:02:56 +02005038 options.verbose, options.parallel)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005039
5040
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005041@subcommand.usage('<patch url or issue id or issue url>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005042@metrics.collector.collect_metrics('git cl patch')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005043def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005044 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005045 parser.add_option('-b', dest='newbranch',
5046 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005047 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005048 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005049 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005050 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005051 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005052 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005053 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005054 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005055 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005056 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005057
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005058
5059 group = optparse.OptionGroup(
5060 parser,
5061 'Options for continuing work on the current issue uploaded from a '
5062 'different clone (e.g. different machine). Must be used independently '
5063 'from the other options. No issue number should be specified, and the '
5064 'branch must have an issue number associated with it')
5065 group.add_option('--reapply', action='store_true', dest='reapply',
5066 help='Reset the branch and reapply the issue.\n'
5067 'CAUTION: This will undo any local changes in this '
5068 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005069
5070 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005071 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005072 parser.add_option_group(group)
5073
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005074 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005075 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005076 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005077 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005078 auth_config = auth.extract_auth_config_from_options(options)
5079
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005080 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005081 if options.newbranch:
5082 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005083 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005084 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005085
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005086 cl = Changelist(auth_config=auth_config,
5087 codereview=options.forced_codereview)
5088 if not cl.GetIssue():
5089 parser.error('current branch must have an associated issue')
5090
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005091 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005092 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005093 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005094
5095 RunGit(['reset', '--hard', upstream])
5096 if options.pull:
5097 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005098
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005099 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5100 options.directory)
5101
5102 if len(args) != 1 or not args[0]:
5103 parser.error('Must specify issue number or url')
5104
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005105 target_issue_arg = ParseIssueNumberArgument(args[0],
5106 options.forced_codereview)
5107 if not target_issue_arg.valid:
5108 parser.error('invalid codereview url or CL id')
5109
5110 cl_kwargs = {
5111 'auth_config': auth_config,
5112 'codereview_host': target_issue_arg.hostname,
5113 'codereview': options.forced_codereview,
5114 }
5115 detected_codereview_from_url = False
5116 if target_issue_arg.codereview and not options.forced_codereview:
5117 detected_codereview_from_url = True
5118 cl_kwargs['codereview'] = target_issue_arg.codereview
5119 cl_kwargs['issue'] = target_issue_arg.issue
5120
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005121 # We don't want uncommitted changes mixed up with the patch.
5122 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005123 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005124
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005125 if options.newbranch:
5126 if options.force:
5127 RunGit(['branch', '-D', options.newbranch],
5128 stderr=subprocess2.PIPE, error_ok=True)
5129 RunGit(['new-branch', options.newbranch])
5130
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005131 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005132
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005133 if cl.IsGerrit():
5134 if options.reject:
5135 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005136 if options.directory:
5137 parser.error('--directory is not supported with Gerrit codereview.')
5138
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005139 if detected_codereview_from_url:
5140 print('canonical issue/change URL: %s (type: %s)\n' %
5141 (cl.GetIssueURL(), target_issue_arg.codereview))
5142
5143 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005144 options.nocommit, options.directory,
5145 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005146
5147
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005148def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005149 """Fetches the tree status and returns either 'open', 'closed',
5150 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005151 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005152 if url:
5153 status = urllib2.urlopen(url).read().lower()
5154 if status.find('closed') != -1 or status == '0':
5155 return 'closed'
5156 elif status.find('open') != -1 or status == '1':
5157 return 'open'
5158 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005159 return 'unset'
5160
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005161
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005162def GetTreeStatusReason():
5163 """Fetches the tree status from a json url and returns the message
5164 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005165 url = settings.GetTreeStatusUrl()
5166 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005167 connection = urllib2.urlopen(json_url)
5168 status = json.loads(connection.read())
5169 connection.close()
5170 return status['message']
5171
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005172
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005173@metrics.collector.collect_metrics('git cl tree')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005174def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005175 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005176 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005177 status = GetTreeStatus()
5178 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005179 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005180 return 2
5181
vapiera7fbd5a2016-06-16 09:17:49 -07005182 print('The tree is %s' % status)
5183 print()
5184 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005185 if status != 'open':
5186 return 1
5187 return 0
5188
5189
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005190@metrics.collector.collect_metrics('git cl try')
maruel@chromium.org15192402012-09-06 12:38:29 +00005191def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005192 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005193 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005194 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005195 '-b', '--bot', action='append',
5196 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5197 'times to specify multiple builders. ex: '
5198 '"-b win_rel -b win_layout". See '
5199 'the try server waterfall for the builders name and the tests '
5200 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005201 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005202 '-B', '--bucket', default='',
5203 help=('Buildbucket bucket to send the try requests.'))
5204 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005205 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005206 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005207 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005208 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005209 help='Revision to use for the try job; default: the revision will '
5210 'be determined by the try recipe that builder runs, which usually '
5211 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005212 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005213 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005214 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005215 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005216 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005217 '--category', default='git_cl_try', help='Specify custom build category.')
5218 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005219 '--project',
5220 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005221 'in recipe to determine to which repository or directory to '
5222 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005223 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005224 '-p', '--property', dest='properties', action='append', default=[],
5225 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005226 'key2=value2 etc. The value will be treated as '
5227 'json if decodable, or as string otherwise. '
5228 'NOTE: using this may make your try job not usable for CQ, '
5229 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005230 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005231 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5232 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005233 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005234 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005235 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005236 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005237 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005238 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005239
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005240 if options.master and options.master.startswith('luci.'):
5241 parser.error(
5242 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005243 # Make sure that all properties are prop=value pairs.
5244 bad_params = [x for x in options.properties if '=' not in x]
5245 if bad_params:
5246 parser.error('Got properties with missing "=": %s' % bad_params)
5247
maruel@chromium.org15192402012-09-06 12:38:29 +00005248 if args:
5249 parser.error('Unknown arguments: %s' % args)
5250
Koji Ishii31c14782018-01-08 17:17:33 +09005251 cl = Changelist(auth_config=auth_config, issue=options.issue,
5252 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005253 if not cl.GetIssue():
5254 parser.error('Need to upload first')
5255
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005256 if cl.IsGerrit():
5257 # HACK: warm up Gerrit change detail cache to save on RPCs.
5258 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5259
tandriie113dfd2016-10-11 10:20:12 -07005260 error_message = cl.CannotTriggerTryJobReason()
5261 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005262 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005263
borenet6c0efe62016-10-19 08:13:29 -07005264 if options.bucket and options.master:
5265 parser.error('Only one of --bucket and --master may be used.')
5266
qyearsley1fdfcb62016-10-24 13:22:03 -07005267 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005268
qyearsleydd49f942016-10-28 11:57:22 -07005269 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5270 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005271 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005272 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005273 print('git cl try with no bots now defaults to CQ dry run.')
5274 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5275 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005276
borenet6c0efe62016-10-19 08:13:29 -07005277 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005278 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005279 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005280 'of bot requires an initial job from a parent (usually a builder). '
5281 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005282 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005283 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005284
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005285 patchset = cl.GetMostRecentPatchset()
tandrii568043b2016-10-11 07:49:18 -07005286 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005287 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005288 except BuildbucketResponseException as ex:
5289 print('ERROR: %s' % ex)
5290 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005291 return 0
5292
5293
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005294@metrics.collector.collect_metrics('git cl try-results')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005295def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005296 """Prints info about try jobs associated with current CL."""
5297 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005298 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005299 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005300 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005301 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005302 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005303 '--color', action='store_true', default=setup_color.IS_TTY,
5304 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005305 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005306 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5307 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005308 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005309 '--json', help=('Path of JSON output file to write try job results to,'
5310 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005311 parser.add_option_group(group)
5312 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005313 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005314 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005315 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005316 if args:
5317 parser.error('Unrecognized args: %s' % ' '.join(args))
5318
5319 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005320 cl = Changelist(
5321 issue=options.issue, codereview=options.forced_codereview,
5322 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005323 if not cl.GetIssue():
5324 parser.error('Need to upload first')
5325
tandrii221ab252016-10-06 08:12:04 -07005326 patchset = options.patchset
5327 if not patchset:
5328 patchset = cl.GetMostRecentPatchset()
5329 if not patchset:
5330 parser.error('Codereview doesn\'t know about issue %s. '
5331 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005332 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005333 cl.GetIssue())
5334
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005335 try:
tandrii221ab252016-10-06 08:12:04 -07005336 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005337 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005338 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005339 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005340 if options.json:
5341 write_try_results_json(options.json, jobs)
5342 else:
5343 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005344 return 0
5345
5346
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005347@subcommand.usage('[new upstream branch]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005348@metrics.collector.collect_metrics('git cl upstream')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005349def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005350 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005351 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005352 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005353 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005355 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005356 if args:
5357 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005358 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005359 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005360 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005361 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005362
5363 # Clear configured merge-base, if there is one.
5364 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005365 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005366 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005367 return 0
5368
5369
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005370@metrics.collector.collect_metrics('git cl web')
thestig@chromium.org00858c82013-12-02 23:08:03 +00005371def CMDweb(parser, args):
5372 """Opens the current CL in the web browser."""
5373 _, args = parser.parse_args(args)
5374 if args:
5375 parser.error('Unrecognized args: %s' % ' '.join(args))
5376
5377 issue_url = Changelist().GetIssueURL()
5378 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005379 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005380 return 1
5381
Sergiy Byelozyorov2b718322018-10-24 17:43:31 +00005382 # Redirect I/O before invoking browser to hide its output. For example, this
5383 # allows to hide "Created new window in existing browser session." message
5384 # from Chrome. Based on https://stackoverflow.com/a/2323563.
5385 saved_stdout = os.dup(1)
5386 os.close(1)
5387 os.open(os.devnull, os.O_RDWR)
5388 try:
5389 webbrowser.open(issue_url)
5390 finally:
5391 os.dup2(saved_stdout, 1)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005392 return 0
5393
5394
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005395@metrics.collector.collect_metrics('git cl set-commit')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005396def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005397 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005398 parser.add_option('-d', '--dry-run', action='store_true',
5399 help='trigger in dry run mode')
5400 parser.add_option('-c', '--clear', action='store_true',
5401 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005402 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005403 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005404 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005405 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005406 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005407 if args:
5408 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005409 if options.dry_run and options.clear:
5410 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5411
iannuccie53c9352016-08-17 14:40:40 -07005412 cl = Changelist(auth_config=auth_config, issue=options.issue,
5413 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005414 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005415 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005416 elif options.dry_run:
5417 state = _CQState.DRY_RUN
5418 else:
5419 state = _CQState.COMMIT
5420 if not cl.GetIssue():
5421 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005422 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005423 return 0
5424
5425
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005426@metrics.collector.collect_metrics('git cl set-close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005427def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005428 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005429 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005430 auth.add_auth_options(parser)
5431 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005432 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005433 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005434 if args:
5435 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005436 cl = Changelist(auth_config=auth_config, issue=options.issue,
5437 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005438 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005439 if not cl.GetIssue():
5440 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005441 cl.CloseIssue()
5442 return 0
5443
5444
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005445@metrics.collector.collect_metrics('git cl diff')
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005446def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005447 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005448 parser.add_option(
5449 '--stat',
5450 action='store_true',
5451 dest='stat',
5452 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005453 auth.add_auth_options(parser)
5454 options, args = parser.parse_args(args)
5455 auth_config = auth.extract_auth_config_from_options(options)
5456 if args:
5457 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005458
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005459 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005460 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005461 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005462 if not issue:
5463 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005464
Aaron Gablea718c3e2017-08-28 17:47:28 -07005465 base = cl._GitGetBranchConfigValue('last-upload-hash')
5466 if not base:
5467 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5468 if not base:
5469 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5470 revision_info = detail['revisions'][detail['current_revision']]
5471 fetch_info = revision_info['fetch']['http']
5472 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5473 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005474
Aaron Gablea718c3e2017-08-28 17:47:28 -07005475 cmd = ['git', 'diff']
5476 if options.stat:
5477 cmd.append('--stat')
5478 cmd.append(base)
5479 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005480
5481 return 0
5482
5483
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005484@metrics.collector.collect_metrics('git cl owners')
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005485def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005486 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005487 parser.add_option(
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005488 '--ignore-current',
5489 action='store_true',
5490 help='Ignore the CL\'s current reviewers and start from scratch.')
5491 parser.add_option(
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005492 '--no-color',
5493 action='store_true',
5494 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005495 parser.add_option(
5496 '--batch',
5497 action='store_true',
5498 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005499 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005500 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005501 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005502
5503 author = RunGit(['config', 'user.email']).strip() or None
5504
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005505 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005506
5507 if args:
5508 if len(args) > 1:
5509 parser.error('Unknown args')
5510 base_branch = args[0]
5511 else:
5512 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005513 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005514
5515 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005516 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5517
5518 if options.batch:
5519 db = owners.Database(change.RepositoryRoot(), file, os.path)
5520 print('\n'.join(db.reviewers_for(affected_files, author)))
5521 return 0
5522
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005523 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005524 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005525 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005526 author,
Sidney San Martín8e6f58c2018-06-08 01:02:56 +00005527 [] if options.ignore_current else cl.GetReviewers(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005528 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005529 disable_color=options.no_color,
5530 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005531
5532
Aiden Bennerc08566e2018-10-03 17:52:42 +00005533def BuildGitDiffCmd(diff_type, upstream_commit, args, allow_prefix=False):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005534 """Generates a diff command."""
5535 # Generate diff for the current branch's changes.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005536 diff_cmd = ['-c', 'core.quotePath=false', 'diff', '--no-ext-diff']
5537
5538 if not allow_prefix:
5539 diff_cmd += ['--no-prefix']
5540
5541 diff_cmd += [diff_type, upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005542
5543 if args:
5544 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005545 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005546 diff_cmd.append(arg)
5547 else:
5548 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005549
5550 return diff_cmd
5551
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005552
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005553def MatchingFileType(file_name, extensions):
5554 """Returns true if the file name ends with one of the given extensions."""
5555 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005556
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005557
enne@chromium.org555cfe42014-01-29 18:21:39 +00005558@subcommand.usage('[files or directories to diff]')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005559@metrics.collector.collect_metrics('git cl format')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005560def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005561 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005562 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005563 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005564 parser.add_option('--full', action='store_true',
5565 help='Reformat the full content of all touched files')
5566 parser.add_option('--dry-run', action='store_true',
5567 help='Don\'t modify any file on disk.')
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005568 parser.add_option(
5569 '--python',
5570 action='store_true',
5571 default=None,
5572 help='Enables python formatting on all python files.')
5573 parser.add_option(
5574 '--no-python',
5575 action='store_true',
5576 dest='python',
5577 help='Disables python formatting on all python files. '
5578 'Takes precedence over --python. '
5579 'If neither --python or --no-python are set, python '
5580 'files that have a .style.yapf file in an ancestor '
5581 'directory will be formatted.')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005582 parser.add_option('--js', action='store_true',
5583 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005584 parser.add_option('--diff', action='store_true',
5585 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005586 parser.add_option('--presubmit', action='store_true',
5587 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005588 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005589
Daniel Chengc55eecf2016-12-30 03:11:02 -08005590 # Normalize any remaining args against the current path, so paths relative to
5591 # the current directory are still resolved as expected.
5592 args = [os.path.join(os.getcwd(), arg) for arg in args]
5593
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005594 # git diff generates paths against the root of the repository. Change
5595 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005596 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005597 if rel_base_path:
5598 os.chdir(rel_base_path)
5599
digit@chromium.org29e47272013-05-17 17:01:46 +00005600 # Grab the merge-base commit, i.e. the upstream commit of the current
5601 # branch when it was created or the last time it was rebased. This is
5602 # to cover the case where the user may have called "git fetch origin",
5603 # moving the origin branch to a newer commit, but hasn't rebased yet.
5604 upstream_commit = None
5605 cl = Changelist()
5606 upstream_branch = cl.GetUpstreamBranch()
5607 if upstream_branch:
5608 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5609 upstream_commit = upstream_commit.strip()
5610
5611 if not upstream_commit:
5612 DieWithError('Could not find base commit for this branch. '
5613 'Are you in detached state?')
5614
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005615 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5616 diff_output = RunGit(changed_files_cmd)
5617 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005618 # Filter out files deleted by this CL
5619 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005620
Christopher Lamc5ba6922017-01-24 11:19:14 +11005621 if opts.js:
Deepanjan Roy605dd312018-07-02 17:48:54 +00005622 CLANG_EXTS.extend(['.js', '.ts'])
Christopher Lamc5ba6922017-01-24 11:19:14 +11005623
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005624 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5625 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5626 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005627 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005628
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005629 top_dir = os.path.normpath(
5630 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5631
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005632 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5633 # formatted. This is used to block during the presubmit.
5634 return_value = 0
5635
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005636 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005637 # Locate the clang-format binary in the checkout
5638 try:
5639 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005640 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005641 DieWithError(e)
5642
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005643 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005644 cmd = [clang_format_tool]
5645 if not opts.dry_run and not opts.diff:
5646 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005647 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005648 if opts.diff:
5649 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005650 else:
5651 env = os.environ.copy()
5652 env['PATH'] = str(os.path.dirname(clang_format_tool))
5653 try:
5654 script = clang_format.FindClangFormatScriptInChromiumTree(
5655 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005656 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005657 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005658
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005659 cmd = [sys.executable, script, '-p0']
5660 if not opts.dry_run and not opts.diff:
5661 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005662
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005663 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5664 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005665
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005666 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5667 if opts.diff:
5668 sys.stdout.write(stdout)
5669 if opts.dry_run and len(stdout) > 0:
5670 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005671
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005672 # Similar code to above, but using yapf on .py files rather than clang-format
5673 # on C/C++ files
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005674 py_explicitly_disabled = opts.python is not None and not opts.python
5675 if python_diff_files and not py_explicitly_disabled:
Aiden Bennere47ac152018-11-20 16:44:03 +00005676 depot_tools_path = os.path.dirname(os.path.abspath(__file__))
5677 yapf_tool = os.path.join(depot_tools_path, 'yapf')
5678 if sys.platform.startswith('win'):
5679 yapf_tool += '.bat'
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005680
Aiden Bennerc08566e2018-10-03 17:52:42 +00005681 # If we couldn't find a yapf file we'll default to the chromium style
5682 # specified in depot_tools.
Aiden Bennerc08566e2018-10-03 17:52:42 +00005683 chromium_default_yapf_style = os.path.join(depot_tools_path,
5684 YAPF_CONFIG_FILENAME)
Aiden Bennerc08566e2018-10-03 17:52:42 +00005685 # Used for caching.
5686 yapf_configs = {}
5687 for f in python_diff_files:
5688 # Find the yapf style config for the current file, defaults to depot
5689 # tools default.
Aiden Benner99b0ccb2018-11-20 19:53:31 +00005690 _FindYapfConfigFile(f, yapf_configs, top_dir)
5691
5692 # Turn on python formatting by default if a yapf config is specified.
5693 # This breaks in the case of this repo though since the specified
5694 # style file is also the global default.
5695 if opts.python is None:
5696 filtered_py_files = []
5697 for f in python_diff_files:
5698 if _FindYapfConfigFile(f, yapf_configs, top_dir) is not None:
5699 filtered_py_files.append(f)
5700 else:
5701 filtered_py_files = python_diff_files
5702
5703 # Note: yapf still seems to fix indentation of the entire file
5704 # even if line ranges are specified.
5705 # See https://github.com/google/yapf/issues/499
5706 if not opts.full and filtered_py_files:
5707 py_line_diffs = _ComputeDiffLineRanges(filtered_py_files, upstream_commit)
5708
5709 for f in filtered_py_files:
5710 yapf_config = _FindYapfConfigFile(f, yapf_configs, top_dir)
5711 if yapf_config is None:
5712 yapf_config = chromium_default_yapf_style
Aiden Bennerc08566e2018-10-03 17:52:42 +00005713
5714 cmd = [yapf_tool, '--style', yapf_config, f]
5715
5716 has_formattable_lines = False
5717 if not opts.full:
5718 # Only run yapf over changed line ranges.
5719 for diff_start, diff_len in py_line_diffs[f]:
5720 diff_end = diff_start + diff_len - 1
5721 # Yapf errors out if diff_end < diff_start but this
5722 # is a valid line range diff for a removal.
5723 if diff_end >= diff_start:
5724 has_formattable_lines = True
5725 cmd += ['-l', '{}-{}'.format(diff_start, diff_end)]
5726 # If all line diffs were removals we have nothing to format.
5727 if not has_formattable_lines:
5728 continue
5729
5730 if opts.diff or opts.dry_run:
5731 cmd += ['--diff']
5732 # Will return non-zero exit code if non-empty diff.
5733 stdout = RunCommand(cmd, error_ok=True, cwd=top_dir)
5734 if opts.diff:
5735 sys.stdout.write(stdout)
5736 elif len(stdout) > 0:
5737 return_value = 2
5738 else:
5739 cmd += ['-i']
5740 RunCommand(cmd, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005741
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005742 # Dart's formatter does not have the nice property of only operating on
5743 # modified chunks, so hard code full.
5744 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005745 try:
5746 command = [dart_format.FindDartFmtToolInChromiumTree()]
5747 if not opts.dry_run and not opts.diff:
5748 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005749 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005750
ppi@chromium.org6593d932016-03-03 15:41:15 +00005751 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005752 if opts.dry_run and stdout:
5753 return_value = 2
5754 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005755 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5756 'found in this checkout. Files in other languages are still '
5757 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005758
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005759 # Format GN build files. Always run on full build files for canonical form.
5760 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005761 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07005762 if opts.dry_run or opts.diff:
5763 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005764 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005765 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5766 shell=sys.platform == 'win32',
5767 cwd=top_dir)
5768 if opts.dry_run and gn_ret == 2:
5769 return_value = 2 # Not formatted.
5770 elif opts.diff and gn_ret == 2:
5771 # TODO this should compute and print the actual diff.
5772 print("This change has GN build file diff for " + gn_diff_file)
5773 elif gn_ret != 0:
5774 # For non-dry run cases (and non-2 return values for dry-run), a
5775 # nonzero error code indicates a failure, probably because the file
5776 # doesn't parse.
5777 DieWithError("gn format failed on " + gn_diff_file +
5778 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005779
Ilya Shermane081cbe2017-08-15 17:51:04 -07005780 # Skip the metrics formatting from the global presubmit hook. These files have
5781 # a separate presubmit hook that issues an error if the files need formatting,
5782 # whereas the top-level presubmit script merely issues a warning. Formatting
5783 # these files is somewhat slow, so it's important not to duplicate the work.
5784 if not opts.presubmit:
5785 for xml_dir in GetDirtyMetricsDirs(diff_files):
5786 tool_dir = os.path.join(top_dir, xml_dir)
5787 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
5788 if opts.dry_run or opts.diff:
5789 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07005790 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07005791 if opts.diff:
5792 sys.stdout.write(stdout)
5793 if opts.dry_run and stdout:
5794 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05005795
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005796 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005797
Steven Holte2e664bf2017-04-21 13:10:47 -07005798def GetDirtyMetricsDirs(diff_files):
5799 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
5800 metrics_xml_dirs = [
5801 os.path.join('tools', 'metrics', 'actions'),
5802 os.path.join('tools', 'metrics', 'histograms'),
5803 os.path.join('tools', 'metrics', 'rappor'),
5804 os.path.join('tools', 'metrics', 'ukm')]
5805 for xml_dir in metrics_xml_dirs:
5806 if any(file.startswith(xml_dir) for file in xml_diff_files):
5807 yield xml_dir
5808
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005809
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005810@subcommand.usage('<codereview url or issue id>')
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005811@metrics.collector.collect_metrics('git cl checkout')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005812def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005813 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005814 _, args = parser.parse_args(args)
5815
5816 if len(args) != 1:
5817 parser.print_help()
5818 return 1
5819
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005820 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005821 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02005822 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005823
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005824 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005825
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005826 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005827 output = RunGit(['config', '--local', '--get-regexp',
5828 r'branch\..*\.%s' % issueprefix],
5829 error_ok=True)
5830 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005831 if issue == target_issue:
5832 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005833
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005834 branches = []
5835 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005836 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005837 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005838 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005839 return 1
5840 if len(branches) == 1:
5841 RunGit(['checkout', branches[0]])
5842 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005843 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005844 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005845 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005846 which = raw_input('Choose by index: ')
5847 try:
5848 RunGit(['checkout', branches[int(which)]])
5849 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005850 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005851 return 1
5852
5853 return 0
5854
5855
maruel@chromium.org29404b52014-09-08 22:58:00 +00005856def CMDlol(parser, args):
5857 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005858 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005859 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5860 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5861 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005862 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005863 return 0
5864
5865
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005866class OptionParser(optparse.OptionParser):
5867 """Creates the option parse and add --verbose support."""
5868 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005869 optparse.OptionParser.__init__(
5870 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005871 self.add_option(
5872 '-v', '--verbose', action='count', default=0,
5873 help='Use 2 times for more debugging info')
5874
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005875 def parse_args(self, args=None, _values=None):
Andrii Shyshkalov46f20cd2018-10-30 06:42:54 +00005876 try:
5877 return self._parse_args(args)
5878 finally:
5879 # Regardless of success or failure of args parsing, we want to report
5880 # metrics, but only after logging has been initialized (if parsing
5881 # succeeded).
5882 global settings
5883 settings = Settings()
5884
5885 if not metrics.DISABLE_METRICS_COLLECTION:
5886 # GetViewVCUrl ultimately calls logging method.
5887 project_url = settings.GetViewVCUrl().strip('/+')
5888 if project_url in metrics_utils.KNOWN_PROJECT_URLS:
5889 metrics.collector.add('project_urls', [project_url])
5890
5891 def _parse_args(self, args=None):
Edward Lemur5ba1e9c2018-07-23 18:19:02 +00005892 # Create an optparse.Values object that will store only the actual passed
5893 # options, without the defaults.
5894 actual_options = optparse.Values()
5895 _, args = optparse.OptionParser.parse_args(self, args, actual_options)
5896 # Create an optparse.Values object with the default options.
5897 options = optparse.Values(self.get_default_values().__dict__)
5898 # Update it with the options passed by the user.
5899 options._update_careful(actual_options.__dict__)
5900 # Store the options passed by the user in an _actual_options attribute.
5901 # We store only the keys, and not the values, since the values can contain
5902 # arbitrary information, which might be PII.
5903 metrics.collector.add('arguments', actual_options.__dict__.keys())
5904
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005905 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01005906 logging.basicConfig(
5907 level=levels[min(options.verbose, len(levels) - 1)],
5908 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
5909 '%(filename)s] %(message)s')
Edward Lemur83bd7f42018-10-10 00:14:21 +00005910
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005911 return options, args
5912
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005913
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005914def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005915 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005916 print('\nYour python version %s is unsupported, please upgrade.\n' %
5917 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005918 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005919
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005920 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005921 dispatcher = subcommand.CommandDispatcher(__name__)
5922 try:
5923 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005924 except auth.AuthenticationError as e:
5925 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005926 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005927 if e.code != 500:
5928 raise
5929 DieWithError(
5930 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5931 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005932 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005933
5934
5935if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005936 # These affect sys.stdout so do it outside of main() to simplify mocks in
5937 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005938 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005939 setup_color.init()
Edward Lemur6f812e12018-07-31 22:45:57 +00005940 with metrics.collector.print_notice_and_exit():
sbc@chromium.org013731e2015-02-26 18:28:43 +00005941 sys.exit(main(sys.argv[1:]))