blob: cf49aaf785848ee1255b0c768a4f60271a0aaacd [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
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00008"""A git-command for integrating reviews on Rietveld and 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
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000032import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000034import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000035import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000036import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000037import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000038
39try:
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -080040 import readline # pylint: disable=import-error,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000041except ImportError:
42 pass
43
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000044from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000045from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046from third_party import upload
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
piman@chromium.org336f9122014-09-04 02:16:55 +000058import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000059import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000060import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000061import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000062import scm
Francois Dorayd42c6812017-05-30 15:10:20 -040063import split_cl
maruel@chromium.org0633fb42013-08-16 20:06:14 +000064import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000066import watchlists
67
tandrii7400cf02016-06-21 08:48:07 -070068__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000069
tandrii9d2c7a32016-06-22 03:42:45 -070070COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070071DEFAULT_SERVER = 'https://codereview.chromium.org'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -080072POSTUPSTREAM_HOOK = '.git/hooks/post-cl-land'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
rmistry@google.comc68112d2015-03-03 12:48:06 +000074REFS_THAT_ALIAS_TO_OTHER_REFS = {
75 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
76 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
77}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
thestig@chromium.org44202a22014-03-11 19:22:18 +000079# Valid extensions for files we want to lint.
80DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
81DEFAULT_LINT_IGNORE_REGEX = r"$^"
82
borenet6c0efe62016-10-19 08:13:29 -070083# Buildbucket master name prefix.
84MASTER_PREFIX = 'master.'
85
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000086# Shortcut since it quickly becomes redundant.
87Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000088
maruel@chromium.orgddd59412011-11-30 14:20:38 +000089# Initialized in main()
90settings = None
91
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010092# Used by tests/git_cl_test.py to add extra logging.
93# Inside the weirdly failing test, add this:
94# >>> self.mock(git_cl, '_IS_BEING_TESTED', True)
Quinten Yearsley0c62da92017-05-31 13:39:42 -070095# And scroll up to see the stack trace printed.
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +010096_IS_BEING_TESTED = False
97
maruel@chromium.orgddd59412011-11-30 14:20:38 +000098
Christopher Lamf732cd52017-01-24 12:40:11 +110099def DieWithError(message, change_desc=None):
100 if change_desc:
101 SaveDescriptionBackup(change_desc)
102
vapiera7fbd5a2016-06-16 09:17:49 -0700103 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000104 sys.exit(1)
105
106
Christopher Lamf732cd52017-01-24 12:40:11 +1100107def SaveDescriptionBackup(change_desc):
108 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
109 print('\nError after CL description prompt -- saving description to %s\n' %
110 backup_path)
111 backup_file = open(backup_path, 'w')
112 backup_file.write(change_desc.description)
113 backup_file.close()
114
115
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000116def GetNoGitPagerEnv():
117 env = os.environ.copy()
118 # 'cat' is a magical git string that disables pagers on all platforms.
119 env['GIT_PAGER'] = 'cat'
120 return env
121
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000122
bsep@chromium.org627d9002016-04-29 00:00:52 +0000123def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000124 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000125 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000126 except subprocess2.CalledProcessError as e:
127 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000128 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000129 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000130 'Command "%s" failed.\n%s' % (
131 ' '.join(args), error_message or e.stdout or ''))
132 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
135def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000136 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000137 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000138
139
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000140def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000141 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700142 if suppress_stderr:
143 stderr = subprocess2.VOID
144 else:
145 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000146 try:
tandrii5d48c322016-08-18 16:19:37 -0700147 (out, _), code = subprocess2.communicate(['git'] + args,
148 env=GetNoGitPagerEnv(),
149 stdout=subprocess2.PIPE,
150 stderr=stderr)
151 return code, out
152 except subprocess2.CalledProcessError as e:
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +0900153 logging.debug('Failed running %s', ['git'] + args)
tandrii5d48c322016-08-18 16:19:37 -0700154 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000155
156
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000157def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000158 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000159 return RunGitWithCode(args, suppress_stderr=True)[1]
160
161
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000162def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000163 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000164 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000165 return (version.startswith(prefix) and
166 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000167
168
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000169def BranchExists(branch):
170 """Return True if specified branch exists."""
171 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
172 suppress_stderr=True)
173 return not code
174
175
tandrii2a16b952016-10-19 07:09:44 -0700176def time_sleep(seconds):
177 # Use this so that it can be mocked in tests without interfering with python
178 # system machinery.
179 import time # Local import to discourage others from importing time globally.
180 return time.sleep(seconds)
181
182
maruel@chromium.org90541732011-04-01 17:54:18 +0000183def ask_for_data(prompt):
184 try:
185 return raw_input(prompt)
186 except KeyboardInterrupt:
187 # Hide the exception.
188 sys.exit(1)
189
190
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100191def confirm_or_exit(prefix='', action='confirm'):
192 """Asks user to press enter to continue or press Ctrl+C to abort."""
193 if not prefix or prefix.endswith('\n'):
194 mid = 'Press'
Andrii Shyshkalov353637c2017-03-14 16:52:18 +0100195 elif prefix.endswith('.') or prefix.endswith('?'):
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +0100196 mid = ' Press'
197 elif prefix.endswith(' '):
198 mid = 'press'
199 else:
200 mid = ' press'
201 ask_for_data('%s%s Enter to %s, or Ctrl+C to abort' % (prefix, mid, action))
202
203
204def ask_for_explicit_yes(prompt):
205 """Returns whether user typed 'y' or 'yes' to confirm the given prompt"""
206 result = ask_for_data(prompt + ' [Yes/No]: ').lower()
207 while True:
208 if 'yes'.startswith(result):
209 return True
210 if 'no'.startswith(result):
211 return False
212 result = ask_for_data('Please, type yes or no: ').lower()
213
214
tandrii5d48c322016-08-18 16:19:37 -0700215def _git_branch_config_key(branch, key):
216 """Helper method to return Git config key for a branch."""
217 assert branch, 'branch name is required to set git config for it'
218 return 'branch.%s.%s' % (branch, key)
219
220
221def _git_get_branch_config_value(key, default=None, value_type=str,
222 branch=False):
223 """Returns git config value of given or current branch if any.
224
225 Returns default in all other cases.
226 """
227 assert value_type in (int, str, bool)
228 if branch is False: # Distinguishing default arg value from None.
229 branch = GetCurrentBranch()
230
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000231 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700232 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000233
tandrii5d48c322016-08-18 16:19:37 -0700234 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700235 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700236 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700237 # git config also has --int, but apparently git config suffers from integer
238 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700239 args.append(_git_branch_config_key(branch, key))
240 code, out = RunGitWithCode(args)
241 if code == 0:
242 value = out.strip()
243 if value_type == int:
244 return int(value)
245 if value_type == bool:
246 return bool(value.lower() == 'true')
247 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000248 return default
249
250
tandrii5d48c322016-08-18 16:19:37 -0700251def _git_set_branch_config_value(key, value, branch=None, **kwargs):
252 """Sets the value or unsets if it's None of a git branch config.
253
254 Valid, though not necessarily existing, branch must be provided,
255 otherwise currently checked out branch is used.
256 """
257 if not branch:
258 branch = GetCurrentBranch()
259 assert branch, 'a branch name OR currently checked out branch is required'
260 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700261 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700262 if value is None:
263 args.append('--unset')
264 elif isinstance(value, bool):
265 args.append('--bool')
266 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700267 else:
tandrii33a46ff2016-08-23 05:53:40 -0700268 # git config also has --int, but apparently git config suffers from integer
269 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700270 value = str(value)
271 args.append(_git_branch_config_key(branch, key))
272 if value is not None:
273 args.append(value)
274 RunGit(args, **kwargs)
275
276
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100277def _get_committer_timestamp(commit):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700278 """Returns Unix timestamp as integer of a committer in a commit.
Andrii Shyshkalova6695812016-12-06 17:47:09 +0100279
280 Commit can be whatever git show would recognize, such as HEAD, sha1 or ref.
281 """
282 # Git also stores timezone offset, but it only affects visual display,
283 # actual point in time is defined by this timestamp only.
284 return int(RunGit(['show', '-s', '--format=%ct', commit]).strip())
285
286
287def _git_amend_head(message, committer_timestamp):
288 """Amends commit with new message and desired committer_timestamp.
289
290 Sets committer timezone to UTC.
291 """
292 env = os.environ.copy()
293 env['GIT_COMMITTER_DATE'] = '%d+0000' % committer_timestamp
294 return RunGit(['commit', '--amend', '-m', message], env=env)
295
296
machenbach@chromium.org45453142015-09-15 08:45:22 +0000297def _get_properties_from_options(options):
298 properties = dict(x.split('=', 1) for x in options.properties)
299 for key, val in properties.iteritems():
300 try:
301 properties[key] = json.loads(val)
302 except ValueError:
303 pass # If a value couldn't be evaluated, treat it as a string.
304 return properties
305
306
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000307def _prefix_master(master):
308 """Convert user-specified master name to full master name.
309
310 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
311 name, while the developers always use shortened master name
312 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
313 function does the conversion for buildbucket migration.
314 """
borenet6c0efe62016-10-19 08:13:29 -0700315 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000316 return master
borenet6c0efe62016-10-19 08:13:29 -0700317 return '%s%s' % (MASTER_PREFIX, master)
318
319
320def _unprefix_master(bucket):
321 """Convert bucket name to shortened master name.
322
323 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
324 name, while the developers always use shortened master name
325 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
326 function does the conversion for buildbucket migration.
327 """
328 if bucket.startswith(MASTER_PREFIX):
329 return bucket[len(MASTER_PREFIX):]
330 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000331
332
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000333def _buildbucket_retry(operation_name, http, *args, **kwargs):
334 """Retries requests to buildbucket service and returns parsed json content."""
335 try_count = 0
336 while True:
337 response, content = http.request(*args, **kwargs)
338 try:
339 content_json = json.loads(content)
340 except ValueError:
341 content_json = None
342
343 # Buildbucket could return an error even if status==200.
344 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000345 error = content_json.get('error')
346 if error.get('code') == 403:
347 raise BuildbucketResponseException(
348 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000349 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000350 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000351 raise BuildbucketResponseException(msg)
352
353 if response.status == 200:
354 if not content_json:
355 raise BuildbucketResponseException(
356 'Buildbucket returns invalid json content: %s.\n'
357 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
358 content)
359 return content_json
360 if response.status < 500 or try_count >= 2:
361 raise httplib2.HttpLib2Error(content)
362
363 # status >= 500 means transient failures.
364 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700365 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000366 try_count += 1
367 assert False, 'unreachable'
368
369
qyearsley1fdfcb62016-10-24 13:22:03 -0700370def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700371 """Returns a dict mapping bucket names to builders and tests,
372 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700373 """
qyearsleydd49f942016-10-28 11:57:22 -0700374 # If no bots are listed, we try to get a set of builders and tests based
375 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700376 if not options.bot:
377 change = changelist.GetChange(
378 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700379 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700380 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700381 change=change,
382 changed_files=change.LocalPaths(),
383 repository_root=settings.GetRoot(),
384 default_presubmit=None,
385 project=None,
386 verbose=options.verbose,
387 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700388 if masters is None:
389 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100390 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700391
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 if options.bucket:
393 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700394 if options.master:
395 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700396
qyearsleydd49f942016-10-28 11:57:22 -0700397 # If bots are listed but no master or bucket, then we need to find out
398 # the corresponding master for each bot.
399 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
400 if error_message:
401 option_parser.error(
402 'Tryserver master cannot be found because: %s\n'
403 'Please manually specify the tryserver master, e.g. '
404 '"-m tryserver.chromium.linux".' % error_message)
405 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
407
qyearsley123a4682016-10-26 09:12:17 -0700408def _get_bucket_map_for_builders(builders):
409 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 map_url = 'https://builders-map.appspot.com/'
411 try:
qyearsley123a4682016-10-26 09:12:17 -0700412 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700413 except urllib2.URLError as e:
414 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
415 (map_url, e))
416 except ValueError as e:
417 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700418 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 return None, 'Failed to build master map.'
420
qyearsley123a4682016-10-26 09:12:17 -0700421 bucket_map = {}
422 for builder in builders:
Nodir Turakulov44e01ff2018-01-25 11:12:30 -0800423 builder_info = builders_map.get(builder, {})
424 if isinstance(builder_info, list):
425 # This is a list of masters, legacy mode.
426 # TODO(nodir): remove this code path.
427 buckets = map(_prefix_master, builder_info)
428 else:
429 buckets = builder_info.get('buckets') or []
430 if not buckets:
431 return None, ('No matching bucket for builder %s.' % builder)
432 if len(buckets) > 1:
433 return None, ('The builder name %s exists in multiple buckets %s.' %
434 (builder, buckets))
435 bucket_map.setdefault(buckets[0], {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700436
437 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700438
439
borenet6c0efe62016-10-19 08:13:29 -0700440def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700441 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700442 """Sends a request to Buildbucket to trigger try jobs for a changelist.
443
444 Args:
445 auth_config: AuthConfig for Rietveld.
446 changelist: Changelist that the try jobs are associated with.
447 buckets: A nested dict mapping bucket names to builders to tests.
448 options: Command-line options.
449 """
tandriide281ae2016-10-12 06:02:30 -0700450 assert changelist.GetIssue(), 'CL must be uploaded first'
451 codereview_url = changelist.GetCodereviewServer()
452 assert codereview_url, 'CL must be uploaded first'
453 patchset = patchset or changelist.GetMostRecentPatchset()
454 assert patchset, 'CL must be uploaded first'
455
456 codereview_host = urlparse.urlparse(codereview_url).hostname
457 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))
tandriide281ae2016-10-12 06:02:30 -0700464 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
465 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
466 hostname=codereview_host,
467 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000468 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700469
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700470 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700471 shared_parameters_properties['category'] = category
472 if options.clobber:
473 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700474 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700475 if extra_properties:
476 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000477
478 batch_req_body = {'builds': []}
479 print_text = []
480 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700481 for bucket, builders_and_tests in sorted(buckets.iteritems()):
482 print_text.append('Bucket: %s' % bucket)
483 master = None
484 if bucket.startswith(MASTER_PREFIX):
485 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000486 for builder, tests in sorted(builders_and_tests.iteritems()):
487 print_text.append(' %s: %s' % (builder, tests))
488 parameters = {
489 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000490 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100491 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000492 'revision': options.revision,
493 }],
tandrii8c5a3532016-11-04 07:52:02 -0700494 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000495 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000496 if 'presubmit' in builder.lower():
497 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000498 if tests:
499 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700500
501 tags = [
502 'builder:%s' % builder,
503 'buildset:%s' % buildset,
504 'user_agent:git_cl_try',
505 ]
506 if master:
507 parameters['properties']['master'] = master
508 tags.append('master:%s' % master)
509
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000510 batch_req_body['builds'].append(
511 {
512 'bucket': bucket,
513 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000514 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700515 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000516 }
517 )
518
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000519 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700520 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000521 http,
522 buildbucket_put_url,
523 'PUT',
524 body=json.dumps(batch_req_body),
525 headers={'Content-Type': 'application/json'}
526 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000527 print_text.append('To see results here, run: git cl try-results')
528 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700529 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000530
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000531
tandrii221ab252016-10-06 08:12:04 -0700532def fetch_try_jobs(auth_config, changelist, buildbucket_host,
533 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700534 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535
qyearsley53f48a12016-09-01 10:45:13 -0700536 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000537 """
tandrii221ab252016-10-06 08:12:04 -0700538 assert buildbucket_host
539 assert changelist.GetIssue(), 'CL must be uploaded first'
540 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
541 patchset = patchset or changelist.GetMostRecentPatchset()
542 assert patchset, 'CL must be uploaded first'
543
544 codereview_url = changelist.GetCodereviewServer()
545 codereview_host = urlparse.urlparse(codereview_url).hostname
546 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000547 if authenticator.has_cached_credentials():
548 http = authenticator.authorize(httplib2.Http())
549 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700550 print('Warning: Some results might be missing because %s' %
551 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700552 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 http = httplib2.Http()
554
555 http.force_exception_to_status_code = True
556
tandrii221ab252016-10-06 08:12:04 -0700557 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
558 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
559 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000560 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700561 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000562 params = {'tag': 'buildset:%s' % buildset}
563
564 builds = {}
565 while True:
566 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700567 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000568 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700569 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000570 for build in content.get('builds', []):
571 builds[build['id']] = build
572 if 'next_cursor' in content:
573 params['start_cursor'] = content['next_cursor']
574 else:
575 break
576 return builds
577
578
qyearsleyeab3c042016-08-24 09:18:28 -0700579def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000580 """Prints nicely result of fetch_try_jobs."""
581 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700582 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000583 return
584
585 # Make a copy, because we'll be modifying builds dictionary.
586 builds = builds.copy()
587 builder_names_cache = {}
588
589 def get_builder(b):
590 try:
591 return builder_names_cache[b['id']]
592 except KeyError:
593 try:
594 parameters = json.loads(b['parameters_json'])
595 name = parameters['builder_name']
596 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700597 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700598 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000599 name = None
600 builder_names_cache[b['id']] = name
601 return name
602
603 def get_bucket(b):
604 bucket = b['bucket']
605 if bucket.startswith('master.'):
606 return bucket[len('master.'):]
607 return bucket
608
609 if options.print_master:
610 name_fmt = '%%-%ds %%-%ds' % (
611 max(len(str(get_bucket(b))) for b in builds.itervalues()),
612 max(len(str(get_builder(b))) for b in builds.itervalues()))
613 def get_name(b):
614 return name_fmt % (get_bucket(b), get_builder(b))
615 else:
616 name_fmt = '%%-%ds' % (
617 max(len(str(get_builder(b))) for b in builds.itervalues()))
618 def get_name(b):
619 return name_fmt % get_builder(b)
620
621 def sort_key(b):
622 return b['status'], b.get('result'), get_name(b), b.get('url')
623
624 def pop(title, f, color=None, **kwargs):
625 """Pop matching builds from `builds` dict and print them."""
626
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000627 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000628 colorize = str
629 else:
630 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
631
632 result = []
633 for b in builds.values():
634 if all(b.get(k) == v for k, v in kwargs.iteritems()):
635 builds.pop(b['id'])
636 result.append(b)
637 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700638 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000639 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700640 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000641
642 total = len(builds)
643 pop(status='COMPLETED', result='SUCCESS',
644 title='Successes:', color=Fore.GREEN,
645 f=lambda b: (get_name(b), b.get('url')))
646 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
647 title='Infra Failures:', color=Fore.MAGENTA,
648 f=lambda b: (get_name(b), b.get('url')))
649 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
650 title='Failures:', color=Fore.RED,
651 f=lambda b: (get_name(b), b.get('url')))
652 pop(status='COMPLETED', result='CANCELED',
653 title='Canceled:', color=Fore.MAGENTA,
654 f=lambda b: (get_name(b),))
655 pop(status='COMPLETED', result='FAILURE',
656 failure_reason='INVALID_BUILD_DEFINITION',
657 title='Wrong master/builder name:', color=Fore.MAGENTA,
658 f=lambda b: (get_name(b),))
659 pop(status='COMPLETED', result='FAILURE',
660 title='Other failures:',
661 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
662 pop(status='COMPLETED',
663 title='Other finished:',
664 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
665 pop(status='STARTED',
666 title='Started:', color=Fore.YELLOW,
667 f=lambda b: (get_name(b), b.get('url')))
668 pop(status='SCHEDULED',
669 title='Scheduled:',
670 f=lambda b: (get_name(b), 'id=%s' % b['id']))
671 # The last section is just in case buildbucket API changes OR there is a bug.
672 pop(title='Other:',
673 f=lambda b: (get_name(b), 'id=%s' % b['id']))
674 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700675 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000676
677
qyearsley53f48a12016-09-01 10:45:13 -0700678def write_try_results_json(output_file, builds):
679 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
680
681 The input |builds| dict is assumed to be generated by Buildbucket.
682 Buildbucket documentation: http://goo.gl/G0s101
683 """
684
685 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800686 """Extracts some of the information from one build dict."""
687 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700688 return {
689 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700690 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800691 'builder_name': parameters.get('builder_name'),
692 'created_ts': build.get('created_ts'),
693 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700694 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800695 'result': build.get('result'),
696 'status': build.get('status'),
697 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700698 'url': build.get('url'),
699 }
700
701 converted = []
702 for _, build in sorted(builds.items()):
703 converted.append(convert_build_dict(build))
704 write_json(output_file, converted)
705
706
Aaron Gable13101a62018-02-09 13:20:41 -0800707def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000708 """Prints statistics about the change to the user."""
709 # --no-ext-diff is broken in some versions of Git, so try to work around
710 # this by overriding the environment (but there is still a problem if the
711 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000712 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000713 if 'GIT_EXTERNAL_DIFF' in env:
714 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000715
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000716 try:
717 stdout = sys.stdout.fileno()
718 except AttributeError:
719 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000720 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800721 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000722 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000723
724
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000725class BuildbucketResponseException(Exception):
726 pass
727
728
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729class Settings(object):
730 def __init__(self):
731 self.default_server = None
732 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000733 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 self.tree_status_url = None
735 self.viewvc_url = None
736 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000737 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000738 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000739 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000740 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000741 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000742 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743
744 def LazyUpdateIfNeeded(self):
745 """Updates the settings from a codereview.settings file, if available."""
746 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000747 # The only value that actually changes the behavior is
748 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000749 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000750 error_ok=True
751 ).strip().lower()
752
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000754 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 LoadCodereviewSettingsFromFile(cr_settings_file)
756 self.updated = True
757
758 def GetDefaultServerUrl(self, error_ok=False):
759 if not self.default_server:
760 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000761 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000762 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763 if error_ok:
764 return self.default_server
765 if not self.default_server:
766 error_message = ('Could not find settings file. You must configure '
767 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000768 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000769 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770 return self.default_server
771
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000772 @staticmethod
773 def GetRelativeRoot():
774 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000777 if self.root is None:
778 self.root = os.path.abspath(self.GetRelativeRoot())
779 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000780
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000781 def GetGitMirror(self, remote='origin'):
782 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000783 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000784 if not os.path.isdir(local_url):
785 return None
786 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
787 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100788 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100789 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000790 if mirror.exists():
791 return mirror
792 return None
793
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 def GetTreeStatusUrl(self, error_ok=False):
795 if not self.tree_status_url:
796 error_message = ('You must configure your tree status URL by running '
797 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000798 self.tree_status_url = self._GetRietveldConfig(
799 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800 return self.tree_status_url
801
802 def GetViewVCUrl(self):
803 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000804 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805 return self.viewvc_url
806
rmistry@google.com90752582014-01-14 21:04:50 +0000807 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000808 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000809
rmistry@google.com78948ed2015-07-08 23:09:57 +0000810 def GetIsSkipDependencyUpload(self, branch_name):
811 """Returns true if specified branch should skip dep uploads."""
812 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
813 error_ok=True)
814
rmistry@google.com5626a922015-02-26 14:03:30 +0000815 def GetRunPostUploadHook(self):
816 run_post_upload_hook = self._GetRietveldConfig(
817 'run-post-upload-hook', error_ok=True)
818 return run_post_upload_hook == "True"
819
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000820 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000821 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000822
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000823 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000824 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000825
ukai@chromium.orge8077812012-02-03 03:41:46 +0000826 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700827 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000828 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700829 self.is_gerrit = (
830 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000831 return self.is_gerrit
832
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000833 def GetSquashGerritUploads(self):
834 """Return true if uploads to Gerrit should be squashed by default."""
835 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700836 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
837 if self.squash_gerrit_uploads is None:
838 # Default is squash now (http://crbug.com/611892#c23).
839 self.squash_gerrit_uploads = not (
840 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
841 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000842 return self.squash_gerrit_uploads
843
tandriia60502f2016-06-20 02:01:53 -0700844 def GetSquashGerritUploadsOverride(self):
845 """Return True or False if codereview.settings should be overridden.
846
847 Returns None if no override has been defined.
848 """
849 # See also http://crbug.com/611892#c23
850 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
851 error_ok=True).strip()
852 if result == 'true':
853 return True
854 if result == 'false':
855 return False
856 return None
857
tandrii@chromium.org28253532016-04-14 13:46:56 +0000858 def GetGerritSkipEnsureAuthenticated(self):
859 """Return True if EnsureAuthenticated should not be done for Gerrit
860 uploads."""
861 if self.gerrit_skip_ensure_authenticated is None:
862 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000863 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000864 error_ok=True).strip() == 'true')
865 return self.gerrit_skip_ensure_authenticated
866
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000867 def GetGitEditor(self):
868 """Return the editor specified in the git config, or None if none is."""
869 if self.git_editor is None:
870 self.git_editor = self._GetConfig('core.editor', error_ok=True)
871 return self.git_editor or None
872
thestig@chromium.org44202a22014-03-11 19:22:18 +0000873 def GetLintRegex(self):
874 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
875 DEFAULT_LINT_REGEX)
876
877 def GetLintIgnoreRegex(self):
878 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
879 DEFAULT_LINT_IGNORE_REGEX)
880
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000881 def GetProject(self):
882 if not self.project:
883 self.project = self._GetRietveldConfig('project', error_ok=True)
884 return self.project
885
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000886 def _GetRietveldConfig(self, param, **kwargs):
887 return self._GetConfig('rietveld.' + param, **kwargs)
888
rmistry@google.com78948ed2015-07-08 23:09:57 +0000889 def _GetBranchConfig(self, branch_name, param, **kwargs):
890 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892 def _GetConfig(self, param, **kwargs):
893 self.LazyUpdateIfNeeded()
894 return RunGit(['config', param], **kwargs).strip()
895
896
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100897@contextlib.contextmanager
898def _get_gerrit_project_config_file(remote_url):
899 """Context manager to fetch and store Gerrit's project.config from
900 refs/meta/config branch and store it in temp file.
901
902 Provides a temporary filename or None if there was error.
903 """
904 error, _ = RunGitWithCode([
905 'fetch', remote_url,
906 '+refs/meta/config:refs/git_cl/meta/config'])
907 if error:
908 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700909 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100910 (remote_url, error))
911 yield None
912 return
913
914 error, project_config_data = RunGitWithCode(
915 ['show', 'refs/git_cl/meta/config:project.config'])
916 if error:
917 print('WARNING: project.config file not found')
918 yield None
919 return
920
921 with gclient_utils.temporary_directory() as tempdir:
922 project_config_file = os.path.join(tempdir, 'project.config')
923 gclient_utils.FileWrite(project_config_file, project_config_data)
924 yield project_config_file
925
926
927def _is_git_numberer_enabled(remote_url, remote_ref):
928 """Returns True if Git Numberer is enabled on this ref."""
929 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100930 KNOWN_PROJECTS_WHITELIST = [
931 'chromium/src',
932 'external/webrtc',
933 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100934 'infra/experimental',
Edward Lemur32357d32017-09-11 20:22:45 +0200935 # For webrtc.googlesource.com/src.
936 'src',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100937 ]
938
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100939 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
940 url_parts = urlparse.urlparse(remote_url)
941 project_name = url_parts.path.lstrip('/').rstrip('git./')
942 for known in KNOWN_PROJECTS_WHITELIST:
943 if project_name.endswith(known):
944 break
945 else:
946 # Early exit to avoid extra fetches for repos that aren't using Git
947 # Numberer.
948 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100949
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100950 with _get_gerrit_project_config_file(remote_url) as project_config_file:
951 if project_config_file is None:
952 # Failed to fetch project.config, which shouldn't happen on open source
953 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100954 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100955 def get_opts(x):
956 code, out = RunGitWithCode(
957 ['config', '-f', project_config_file, '--get-all',
958 'plugin.git-numberer.validate-%s-refglob' % x])
959 if code == 0:
960 return out.strip().splitlines()
961 return []
962 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100963
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100964 logging.info('validator config enabled %s disabled %s refglobs for '
965 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000966
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100967 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100968 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100969 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100970 return True
971 return False
972
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100973 if match_refglobs(disabled):
974 return False
975 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100976
977
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978def ShortBranchName(branch):
979 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000980 return branch.replace('refs/heads/', '', 1)
981
982
983def GetCurrentBranchRef():
984 """Returns branch ref (e.g., refs/heads/master) or None."""
985 return RunGit(['symbolic-ref', 'HEAD'],
986 stderr=subprocess2.VOID, error_ok=True).strip() or None
987
988
989def GetCurrentBranch():
990 """Returns current branch or None.
991
992 For refs/heads/* branches, returns just last part. For others, full ref.
993 """
994 branchref = GetCurrentBranchRef()
995 if branchref:
996 return ShortBranchName(branchref)
997 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998
999
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001000class _CQState(object):
1001 """Enum for states of CL with respect to Commit Queue."""
1002 NONE = 'none'
1003 DRY_RUN = 'dry_run'
1004 COMMIT = 'commit'
1005
1006 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1007
1008
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001009class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001010 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001011 self.issue = issue
1012 self.patchset = patchset
1013 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001014 assert codereview in (None, 'rietveld', 'gerrit')
1015 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001016
1017 @property
1018 def valid(self):
1019 return self.issue is not None
1020
1021
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001022def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001023 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1024 fail_result = _ParsedIssueNumberArgument()
1025
1026 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001027 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001028 if not arg.startswith('http'):
1029 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001030
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001031 url = gclient_utils.UpgradeToHttps(arg)
1032 try:
1033 parsed_url = urlparse.urlparse(url)
1034 except ValueError:
1035 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001036
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001037 if codereview is not None:
1038 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1039 return parsed or fail_result
1040
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001041 results = {}
1042 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1043 parsed = cls.ParseIssueURL(parsed_url)
1044 if parsed is not None:
1045 results[name] = parsed
1046
1047 if not results:
1048 return fail_result
1049 if len(results) == 1:
1050 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001051
1052 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1053 # This is likely Gerrit.
1054 return results['gerrit']
1055 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001056 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001057
1058
Aaron Gablea45ee112016-11-22 15:14:38 -08001059class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001060 def __init__(self, issue, url):
1061 self.issue = issue
1062 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001063 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001064
1065 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001066 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001067 self.issue, self.url)
1068
1069
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001070_CommentSummary = collections.namedtuple(
1071 '_CommentSummary', ['date', 'message', 'sender',
1072 # TODO(tandrii): these two aren't known in Gerrit.
1073 'approval', 'disapproval'])
1074
1075
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001076class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001077 """Changelist works with one changelist in local branch.
1078
1079 Supports two codereview backends: Rietveld or Gerrit, selected at object
1080 creation.
1081
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001082 Notes:
1083 * Not safe for concurrent multi-{thread,process} use.
1084 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001085 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001086 """
1087
1088 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1089 """Create a new ChangeList instance.
1090
1091 If issue is given, the codereview must be given too.
1092
1093 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1094 Otherwise, it's decided based on current configuration of the local branch,
1095 with default being 'rietveld' for backwards compatibility.
1096 See _load_codereview_impl for more details.
1097
1098 **kwargs will be passed directly to codereview implementation.
1099 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001101 global settings
1102 if not settings:
1103 # Happens when git_cl.py is used as a utility library.
1104 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001105
1106 if issue:
1107 assert codereview, 'codereview must be known, if issue is known'
1108
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 self.branchref = branchref
1110 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001111 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001112 self.branch = ShortBranchName(self.branchref)
1113 else:
1114 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001116 self.lookedup_issue = False
1117 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118 self.has_description = False
1119 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001120 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001122 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001123 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001124 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001125
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001126 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001127 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001128 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001129 assert self._codereview_impl
1130 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001131
1132 def _load_codereview_impl(self, codereview=None, **kwargs):
1133 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001134 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1135 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1136 self._codereview = codereview
1137 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001138 return
1139
1140 # Automatic selection based on issue number set for a current branch.
1141 # Rietveld takes precedence over Gerrit.
1142 assert not self.issue
1143 # Whether we find issue or not, we are doing the lookup.
1144 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001145 if self.GetBranch():
1146 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1147 issue = _git_get_branch_config_value(
1148 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1149 if issue:
1150 self._codereview = codereview
1151 self._codereview_impl = cls(self, **kwargs)
1152 self.issue = int(issue)
1153 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001154
1155 # No issue is set for this branch, so decide based on repo-wide settings.
1156 return self._load_codereview_impl(
1157 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1158 **kwargs)
1159
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001160 def IsGerrit(self):
1161 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001162
1163 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001164 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001165
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001166 The return value is a string suitable for passing to git cl with the --cc
1167 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001168 """
1169 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001170 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001171 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001172 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1173 return self.cc
1174
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001175 def GetCCListWithoutDefault(self):
1176 """Return the users cc'd on this CL excluding default ones."""
1177 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001178 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001179 return self.cc
1180
Daniel Cheng7227d212017-11-17 08:12:37 -08001181 def ExtendCC(self, more_cc):
1182 """Extends the list of users to cc on this CL based on the changed files."""
1183 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184
1185 def GetBranch(self):
1186 """Returns the short branch name, e.g. 'master'."""
1187 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001188 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001189 if not branchref:
1190 return None
1191 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 self.branch = ShortBranchName(self.branchref)
1193 return self.branch
1194
1195 def GetBranchRef(self):
1196 """Returns the full branch name, e.g. 'refs/heads/master'."""
1197 self.GetBranch() # Poke the lazy loader.
1198 return self.branchref
1199
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001200 def ClearBranch(self):
1201 """Clears cached branch data of this object."""
1202 self.branch = self.branchref = None
1203
tandrii5d48c322016-08-18 16:19:37 -07001204 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1205 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1206 kwargs['branch'] = self.GetBranch()
1207 return _git_get_branch_config_value(key, default, **kwargs)
1208
1209 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1210 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1211 assert self.GetBranch(), (
1212 'this CL must have an associated branch to %sset %s%s' %
1213 ('un' if value is None else '',
1214 key,
1215 '' if value is None else ' to %r' % value))
1216 kwargs['branch'] = self.GetBranch()
1217 return _git_set_branch_config_value(key, value, **kwargs)
1218
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001219 @staticmethod
1220 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001221 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 e.g. 'origin', 'refs/heads/master'
1223 """
1224 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001225 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001227 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001228 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001230 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1231 error_ok=True).strip()
1232 if upstream_branch:
1233 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001235 # Else, try to guess the origin remote.
1236 remote_branches = RunGit(['branch', '-r']).split()
1237 if 'origin/master' in remote_branches:
1238 # Fall back on origin/master if it exits.
1239 remote = 'origin'
1240 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001242 DieWithError(
1243 'Unable to determine default branch to diff against.\n'
1244 'Either pass complete "git diff"-style arguments, like\n'
1245 ' git cl upload origin/master\n'
1246 'or verify this branch is set up to track another \n'
1247 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248
1249 return remote, upstream_branch
1250
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001251 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001252 upstream_branch = self.GetUpstreamBranch()
1253 if not BranchExists(upstream_branch):
1254 DieWithError('The upstream for the current branch (%s) does not exist '
1255 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001256 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001257 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001258
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 def GetUpstreamBranch(self):
1260 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001261 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001263 upstream_branch = upstream_branch.replace('refs/heads/',
1264 'refs/remotes/%s/' % remote)
1265 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1266 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001267 self.upstream_branch = upstream_branch
1268 return self.upstream_branch
1269
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001270 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001271 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001272 remote, branch = None, self.GetBranch()
1273 seen_branches = set()
1274 while branch not in seen_branches:
1275 seen_branches.add(branch)
1276 remote, branch = self.FetchUpstreamTuple(branch)
1277 branch = ShortBranchName(branch)
1278 if remote != '.' or branch.startswith('refs/remotes'):
1279 break
1280 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001281 remotes = RunGit(['remote'], error_ok=True).split()
1282 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001283 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001284 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001285 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001286 logging.warn('Could not determine which remote this change is '
1287 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001288 else:
1289 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001290 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001291 branch = 'HEAD'
1292 if branch.startswith('refs/remotes'):
1293 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001294 elif branch.startswith('refs/branch-heads/'):
1295 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001296 else:
1297 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001298 return self._remote
1299
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 def GitSanityChecks(self, upstream_git_obj):
1301 """Checks git repo status and ensures diff is from local commits."""
1302
sbc@chromium.org79706062015-01-14 21:18:12 +00001303 if upstream_git_obj is None:
1304 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001305 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001306 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001307 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001308 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001309 return False
1310
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001311 # Verify the commit we're diffing against is in our current branch.
1312 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1313 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1314 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001315 print('ERROR: %s is not in the current branch. You may need to rebase '
1316 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001317 return False
1318
1319 # List the commits inside the diff, and verify they are all local.
1320 commits_in_diff = RunGit(
1321 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1322 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1323 remote_branch = remote_branch.strip()
1324 if code != 0:
1325 _, remote_branch = self.GetRemoteBranch()
1326
1327 commits_in_remote = RunGit(
1328 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1329
1330 common_commits = set(commits_in_diff) & set(commits_in_remote)
1331 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001332 print('ERROR: Your diff contains %d commits already in %s.\n'
1333 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1334 'the diff. If you are using a custom git flow, you can override'
1335 ' the reference used for this check with "git config '
1336 'gitcl.remotebranch <git-ref>".' % (
1337 len(common_commits), remote_branch, upstream_git_obj),
1338 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001339 return False
1340 return True
1341
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001342 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001343 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001344
1345 Returns None if it is not set.
1346 """
tandrii5d48c322016-08-18 16:19:37 -07001347 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001348
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 def GetRemoteUrl(self):
1350 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1351
1352 Returns None if there is no remote.
1353 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001354 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001355 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1356
1357 # If URL is pointing to a local directory, it is probably a git cache.
1358 if os.path.isdir(url):
1359 url = RunGit(['config', 'remote.%s.url' % remote],
1360 error_ok=True,
1361 cwd=url).strip()
1362 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001364 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001365 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001366 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001367 self.issue = self._GitGetBranchConfigValue(
1368 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001369 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370 return self.issue
1371
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 def GetIssueURL(self):
1373 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001374 issue = self.GetIssue()
1375 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001376 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001377 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001379 def GetDescription(self, pretty=False, force=False):
1380 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001382 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383 self.has_description = True
1384 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001385 # Set width to 72 columns + 2 space indent.
1386 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001387 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001388 lines = self.description.splitlines()
1389 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 return self.description
1391
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001392 def GetDescriptionFooters(self):
1393 """Returns (non_footer_lines, footers) for the commit message.
1394
1395 Returns:
1396 non_footer_lines (list(str)) - Simple list of description lines without
1397 any footer. The lines do not contain newlines, nor does the list contain
1398 the empty line between the message and the footers.
1399 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1400 [("Change-Id", "Ideadbeef...."), ...]
1401 """
1402 raw_description = self.GetDescription()
1403 msg_lines, _, footers = git_footers.split_footers(raw_description)
1404 if footers:
1405 msg_lines = msg_lines[:len(msg_lines)-1]
1406 return msg_lines, footers
1407
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001409 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001410 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001411 self.patchset = self._GitGetBranchConfigValue(
1412 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001413 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 return self.patchset
1415
1416 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001417 """Set this branch's patchset. If patchset=0, clears the patchset."""
1418 assert self.GetBranch()
1419 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001420 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001421 else:
1422 self.patchset = int(patchset)
1423 self._GitSetBranchConfigValue(
1424 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001426 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001427 """Set this branch's issue. If issue isn't given, clears the issue."""
1428 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001429 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001430 issue = int(issue)
1431 self._GitSetBranchConfigValue(
1432 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001433 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001434 codereview_server = self._codereview_impl.GetCodereviewServer()
1435 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001436 self._GitSetBranchConfigValue(
1437 self._codereview_impl.CodereviewServerConfigKey(),
1438 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 else:
tandrii5d48c322016-08-18 16:19:37 -07001440 # Reset all of these just to be clean.
1441 reset_suffixes = [
1442 'last-upload-hash',
1443 self._codereview_impl.IssueConfigKey(),
1444 self._codereview_impl.PatchsetConfigKey(),
1445 self._codereview_impl.CodereviewServerConfigKey(),
1446 ] + self._PostUnsetIssueProperties()
1447 for prop in reset_suffixes:
1448 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001449 msg = RunGit(['log', '-1', '--format=%B']).strip()
1450 if msg and git_footers.get_footer_change_id(msg):
1451 print('WARNING: The change patched into this branch has a Change-Id. '
1452 'Removing it.')
1453 RunGit(['commit', '--amend', '-m',
1454 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001455 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001456 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457
dnjba1b0f32016-09-02 12:37:42 -07001458 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001459 if not self.GitSanityChecks(upstream_branch):
1460 DieWithError('\nGit sanity check failure')
1461
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001462 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001463 if not root:
1464 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001465 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001466
1467 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001468 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001469 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001470 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001471 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001472 except subprocess2.CalledProcessError:
1473 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001474 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001475 'This branch probably doesn\'t exist anymore. To reset the\n'
1476 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001477 ' git branch --set-upstream-to origin/master %s\n'
1478 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001479 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001480
maruel@chromium.org52424302012-08-29 15:14:30 +00001481 issue = self.GetIssue()
1482 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001483 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001484 description = self.GetDescription()
1485 else:
1486 # If the change was never uploaded, use the log messages of all commits
1487 # up to the branch point, as git cl upload will prefill the description
1488 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001489 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1490 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001491
1492 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001493 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001494 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001495 name,
1496 description,
1497 absroot,
1498 files,
1499 issue,
1500 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001501 author,
1502 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001503
dsansomee2d6fd92016-09-08 00:10:47 -07001504 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001505 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001506 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001507 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001508
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001509 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1510 """Sets the description for this CL remotely.
1511
1512 You can get description_lines and footers with GetDescriptionFooters.
1513
1514 Args:
1515 description_lines (list(str)) - List of CL description lines without
1516 newline characters.
1517 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1518 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1519 `List-Of-Tokens`). It will be case-normalized so that each token is
1520 title-cased.
1521 """
1522 new_description = '\n'.join(description_lines)
1523 if footers:
1524 new_description += '\n'
1525 for k, v in footers:
1526 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1527 if not git_footers.FOOTER_PATTERN.match(foot):
1528 raise ValueError('Invalid footer %r' % foot)
1529 new_description += foot + '\n'
1530 self.UpdateDescription(new_description, force)
1531
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532 def RunHook(self, committing, may_prompt, verbose, change):
1533 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1534 try:
1535 return presubmit_support.DoPresubmitChecks(change, committing,
1536 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1537 default_presubmit=None, may_prompt=may_prompt,
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001538 rietveld_obj=self._codereview_impl.GetRietveldObjForPresubmit(),
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001539 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001540 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001541 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001542
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001543 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1544 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001545 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1546 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001547 else:
1548 # Assume url.
1549 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1550 urlparse.urlparse(issue_arg))
1551 if not parsed_issue_arg or not parsed_issue_arg.valid:
1552 DieWithError('Failed to parse issue argument "%s". '
1553 'Must be an issue number or a valid URL.' % issue_arg)
1554 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001555 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001556
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001557 def CMDUpload(self, options, git_diff_args, orig_args):
1558 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001559 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001560 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001561 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001562 else:
1563 if self.GetBranch() is None:
1564 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1565
1566 # Default to diffing against common ancestor of upstream branch
1567 base_branch = self.GetCommonAncestorWithUpstream()
1568 git_diff_args = [base_branch, 'HEAD']
1569
Aaron Gablec4c40d12017-05-22 11:49:53 -07001570 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1571 if not self.IsGerrit() and not self.GetIssue():
1572 print('=====================================')
1573 print('NOTICE: Rietveld is being deprecated. '
1574 'You can upload changes to Gerrit with')
1575 print(' git cl upload --gerrit')
1576 print('or set Gerrit to be your default code review tool with')
1577 print(' git config gerrit.host true')
1578 print('=====================================')
1579
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001580 # Fast best-effort checks to abort before running potentially
1581 # expensive hooks if uploading is likely to fail anyway. Passing these
1582 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001583 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001584 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001585
1586 # Apply watchlists on upload.
1587 change = self.GetChange(base_branch, None)
1588 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1589 files = [f.LocalPath() for f in change.AffectedFiles()]
1590 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001591 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001592
1593 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001594 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001595 # Set the reviewer list now so that presubmit checks can access it.
1596 change_description = ChangeDescription(change.FullDescriptionText())
1597 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001598 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001599 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001600 change)
1601 change.SetDescriptionText(change_description.description)
1602 hook_results = self.RunHook(committing=False,
1603 may_prompt=not options.force,
1604 verbose=options.verbose,
1605 change=change)
1606 if not hook_results.should_continue():
1607 return 1
1608 if not options.reviewers and hook_results.reviewers:
1609 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001610 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001611
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001612 # TODO(tandrii): Checking local patchset against remote patchset is only
1613 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1614 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001615 latest_patchset = self.GetMostRecentPatchset()
1616 local_patchset = self.GetPatchset()
1617 if (latest_patchset and local_patchset and
1618 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001619 print('The last upload made from this repository was patchset #%d but '
1620 'the most recent patchset on the server is #%d.'
1621 % (local_patchset, latest_patchset))
1622 print('Uploading will still work, but if you\'ve uploaded to this '
1623 'issue from another machine or branch the patch you\'re '
1624 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001625 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001626
Aaron Gable13101a62018-02-09 13:20:41 -08001627 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001628 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001629 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001630 if options.use_commit_queue:
1631 self.SetCQState(_CQState.COMMIT)
1632 elif options.cq_dry_run:
1633 self.SetCQState(_CQState.DRY_RUN)
1634
tandrii5d48c322016-08-18 16:19:37 -07001635 _git_set_branch_config_value('last-upload-hash',
1636 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001637 # Run post upload hooks, if specified.
1638 if settings.GetRunPostUploadHook():
1639 presubmit_support.DoPostUploadExecuter(
1640 change,
1641 self,
1642 settings.GetRoot(),
1643 options.verbose,
1644 sys.stdout)
1645
1646 # Upload all dependencies if specified.
1647 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001648 print()
1649 print('--dependencies has been specified.')
1650 print('All dependent local branches will be re-uploaded.')
1651 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001652 # Remove the dependencies flag from args so that we do not end up in a
1653 # loop.
1654 orig_args.remove('--dependencies')
1655 ret = upload_branch_deps(self, orig_args)
1656 return ret
1657
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001658 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001659 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001660
1661 Issue must have been already uploaded and known.
1662 """
1663 assert new_state in _CQState.ALL_STATES
1664 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001665 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001666 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001667 return 0
1668 except KeyboardInterrupt:
1669 raise
1670 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001671 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001672 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001673 ' * Your project has no CQ,\n'
1674 ' * You don\'t have permission to change the CQ state,\n'
1675 ' * There\'s a bug in this code (see stack trace below).\n'
1676 'Consider specifying which bots to trigger manually or asking your '
1677 'project owners for permissions or contacting Chrome Infra at:\n'
1678 'https://www.chromium.org/infra\n\n' %
1679 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001680 # Still raise exception so that stack trace is printed.
1681 raise
1682
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001683 # Forward methods to codereview specific implementation.
1684
Aaron Gable636b13f2017-07-14 10:42:48 -07001685 def AddComment(self, message, publish=None):
1686 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001687
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001688 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001689 """Returns list of _CommentSummary for each comment.
1690
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001691 args:
1692 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001693 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001694 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001695
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001696 def CloseIssue(self):
1697 return self._codereview_impl.CloseIssue()
1698
1699 def GetStatus(self):
1700 return self._codereview_impl.GetStatus()
1701
1702 def GetCodereviewServer(self):
1703 return self._codereview_impl.GetCodereviewServer()
1704
tandriide281ae2016-10-12 06:02:30 -07001705 def GetIssueOwner(self):
1706 """Get owner from codereview, which may differ from this checkout."""
1707 return self._codereview_impl.GetIssueOwner()
1708
Edward Lemur707d70b2018-02-07 00:50:14 +01001709 def GetReviewers(self):
1710 return self._codereview_impl.GetReviewers()
1711
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001712 def GetMostRecentPatchset(self):
1713 return self._codereview_impl.GetMostRecentPatchset()
1714
tandriide281ae2016-10-12 06:02:30 -07001715 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001716 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001717 return self._codereview_impl.CannotTriggerTryJobReason()
1718
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001719 def GetTryJobProperties(self, patchset=None):
1720 """Returns dictionary of properties to launch try job."""
1721 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001722
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001723 def __getattr__(self, attr):
1724 # This is because lots of untested code accesses Rietveld-specific stuff
1725 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001726 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001727 # Note that child method defines __getattr__ as well, and forwards it here,
1728 # because _RietveldChangelistImpl is not cleaned up yet, and given
1729 # deprecation of Rietveld, it should probably be just removed.
1730 # Until that time, avoid infinite recursion by bypassing __getattr__
1731 # of implementation class.
1732 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001733
1734
1735class _ChangelistCodereviewBase(object):
1736 """Abstract base class encapsulating codereview specifics of a changelist."""
1737 def __init__(self, changelist):
1738 self._changelist = changelist # instance of Changelist
1739
1740 def __getattr__(self, attr):
1741 # Forward methods to changelist.
1742 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1743 # _RietveldChangelistImpl to avoid this hack?
1744 return getattr(self._changelist, attr)
1745
1746 def GetStatus(self):
1747 """Apply a rough heuristic to give a simple summary of an issue's review
1748 or CQ status, assuming adherence to a common workflow.
1749
1750 Returns None if no issue for this branch, or specific string keywords.
1751 """
1752 raise NotImplementedError()
1753
1754 def GetCodereviewServer(self):
1755 """Returns server URL without end slash, like "https://codereview.com"."""
1756 raise NotImplementedError()
1757
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001758 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001759 """Fetches and returns description from the codereview server."""
1760 raise NotImplementedError()
1761
tandrii5d48c322016-08-18 16:19:37 -07001762 @classmethod
1763 def IssueConfigKey(cls):
1764 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001765 raise NotImplementedError()
1766
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001767 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001768 def PatchsetConfigKey(cls):
1769 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001770 raise NotImplementedError()
1771
tandrii5d48c322016-08-18 16:19:37 -07001772 @classmethod
1773 def CodereviewServerConfigKey(cls):
1774 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001775 raise NotImplementedError()
1776
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001777 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001778 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001779 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001780
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001781 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001782 # This is an unfortunate Rietveld-embeddedness in presubmit.
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001783 # For non-Rietveld code reviews, this probably should return a dummy object.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001784 raise NotImplementedError()
1785
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001786 def GetGerritObjForPresubmit(self):
1787 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1788 return None
1789
dsansomee2d6fd92016-09-08 00:10:47 -07001790 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001791 """Update the description on codereview site."""
1792 raise NotImplementedError()
1793
Aaron Gable636b13f2017-07-14 10:42:48 -07001794 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001795 """Posts a comment to the codereview site."""
1796 raise NotImplementedError()
1797
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001798 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001799 raise NotImplementedError()
1800
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001801 def CloseIssue(self):
1802 """Closes the issue."""
1803 raise NotImplementedError()
1804
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001805 def GetMostRecentPatchset(self):
1806 """Returns the most recent patchset number from the codereview site."""
1807 raise NotImplementedError()
1808
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001809 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001810 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001811 """Fetches and applies the issue.
1812
1813 Arguments:
1814 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1815 reject: if True, reject the failed patch instead of switching to 3-way
1816 merge. Rietveld only.
1817 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1818 only.
1819 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001820 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001821 """
1822 raise NotImplementedError()
1823
1824 @staticmethod
1825 def ParseIssueURL(parsed_url):
1826 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1827 failed."""
1828 raise NotImplementedError()
1829
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001830 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001831 """Best effort check that user is authenticated with codereview server.
1832
1833 Arguments:
1834 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001835 refresh: whether to attempt to refresh credentials. Ignored if not
1836 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001837 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001838 raise NotImplementedError()
1839
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001840 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001841 """Best effort check that uploading isn't supposed to fail for predictable
1842 reasons.
1843
1844 This method should raise informative exception if uploading shouldn't
1845 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001846
1847 Arguments:
1848 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001849 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001850 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001851
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001852 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001853 """Uploads a change to codereview."""
1854 raise NotImplementedError()
1855
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001856 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001857 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001858
1859 Issue must have been already uploaded and known.
1860 """
1861 raise NotImplementedError()
1862
tandriie113dfd2016-10-11 10:20:12 -07001863 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001864 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001865 raise NotImplementedError()
1866
tandriide281ae2016-10-12 06:02:30 -07001867 def GetIssueOwner(self):
1868 raise NotImplementedError()
1869
Edward Lemur707d70b2018-02-07 00:50:14 +01001870 def GetReviewers(self):
1871 raise NotImplementedError()
1872
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001873 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001874 raise NotImplementedError()
1875
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001876
1877class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001878
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001879 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001880 super(_RietveldChangelistImpl, self).__init__(changelist)
1881 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001882 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001883 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001884
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001885 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001886 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001887 self._props = None
1888 self._rpc_server = None
1889
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001890 def GetCodereviewServer(self):
1891 if not self._rietveld_server:
1892 # If we're on a branch then get the server potentially associated
1893 # with that branch.
1894 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001895 self._rietveld_server = gclient_utils.UpgradeToHttps(
1896 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001897 if not self._rietveld_server:
1898 self._rietveld_server = settings.GetDefaultServerUrl()
1899 return self._rietveld_server
1900
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001901 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001902 """Best effort check that user is authenticated with Rietveld server."""
1903 if self._auth_config.use_oauth2:
1904 authenticator = auth.get_authenticator_for_host(
1905 self.GetCodereviewServer(), self._auth_config)
1906 if not authenticator.has_cached_credentials():
1907 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001908 if refresh:
1909 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001910
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001911 def EnsureCanUploadPatchset(self, force):
1912 # No checks for Rietveld because we are deprecating Rietveld.
1913 pass
1914
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001915 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001916 issue = self.GetIssue()
1917 assert issue
1918 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001919 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 except urllib2.HTTPError as e:
1921 if e.code == 404:
1922 DieWithError(
1923 ('\nWhile fetching the description for issue %d, received a '
1924 '404 (not found)\n'
1925 'error. It is likely that you deleted this '
1926 'issue on the server. If this is the\n'
1927 'case, please run\n\n'
1928 ' git cl issue 0\n\n'
1929 'to clear the association with the deleted issue. Then run '
1930 'this command again.') % issue)
1931 else:
1932 DieWithError(
1933 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1934 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001935 print('Warning: Failed to retrieve CL description due to network '
1936 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001937 return ''
1938
1939 def GetMostRecentPatchset(self):
1940 return self.GetIssueProperties()['patchsets'][-1]
1941
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001942 def GetIssueProperties(self):
1943 if self._props is None:
1944 issue = self.GetIssue()
1945 if not issue:
1946 self._props = {}
1947 else:
1948 self._props = self.RpcServer().get_issue_properties(issue, True)
1949 return self._props
1950
tandriie113dfd2016-10-11 10:20:12 -07001951 def CannotTriggerTryJobReason(self):
1952 props = self.GetIssueProperties()
1953 if not props:
1954 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1955 if props.get('closed'):
1956 return 'CL %s is closed' % self.GetIssue()
1957 if props.get('private'):
1958 return 'CL %s is private' % self.GetIssue()
1959 return None
1960
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001961 def GetTryJobProperties(self, patchset=None):
1962 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07001963 project = (self.GetIssueProperties() or {}).get('project')
1964 return {
1965 'issue': self.GetIssue(),
1966 'patch_project': project,
1967 'patch_storage': 'rietveld',
1968 'patchset': patchset or self.GetPatchset(),
1969 'rietveld': self.GetCodereviewServer(),
1970 }
1971
tandriide281ae2016-10-12 06:02:30 -07001972 def GetIssueOwner(self):
1973 return (self.GetIssueProperties() or {}).get('owner_email')
1974
Edward Lemur707d70b2018-02-07 00:50:14 +01001975 def GetReviewers(self):
1976 return (self.GetIssueProperties() or {}).get('reviewers')
1977
Aaron Gable636b13f2017-07-14 10:42:48 -07001978 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001979 return self.RpcServer().add_comment(self.GetIssue(), message)
1980
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001981 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001982 summary = []
1983 for message in self.GetIssueProperties().get('messages', []):
1984 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
1985 summary.append(_CommentSummary(
1986 date=date,
1987 disapproval=bool(message['disapproval']),
1988 approval=bool(message['approval']),
1989 sender=message['sender'],
1990 message=message['text'],
1991 ))
1992 return summary
1993
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001994 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001995 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001996 or CQ status, assuming adherence to a common workflow.
1997
1998 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07001999 * 'error' - error from review tool (including deleted issues)
2000 * 'unsent' - not sent for review
2001 * 'waiting' - waiting for review
2002 * 'reply' - waiting for owner to reply to review
2003 * 'not lgtm' - Code-Review label has been set negatively
2004 * 'lgtm' - LGTM from at least one approved reviewer
2005 * 'commit' - in the commit queue
2006 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002007 """
2008 if not self.GetIssue():
2009 return None
2010
2011 try:
2012 props = self.GetIssueProperties()
2013 except urllib2.HTTPError:
2014 return 'error'
2015
2016 if props.get('closed'):
2017 # Issue is closed.
2018 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002019 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002020 # Issue is in the commit queue.
2021 return 'commit'
2022
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002023 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002024 if not messages:
2025 # No message was sent.
2026 return 'unsent'
2027
2028 if get_approving_reviewers(props):
2029 return 'lgtm'
2030 elif get_approving_reviewers(props, disapproval=True):
2031 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002032
tandrii9d2c7a32016-06-22 03:42:45 -07002033 # Skip CQ messages that don't require owner's action.
2034 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2035 if 'Dry run:' in messages[-1]['text']:
2036 messages.pop()
2037 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2038 # This message always follows prior messages from CQ,
2039 # so skip this too.
2040 messages.pop()
2041 else:
2042 # This is probably a CQ messages warranting user attention.
2043 break
2044
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002045 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002046 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002047 return 'reply'
2048 return 'waiting'
2049
dsansomee2d6fd92016-09-08 00:10:47 -07002050 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002051 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002052
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002054 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002056 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002057 return self.SetFlags({flag: value})
2058
2059 def SetFlags(self, flags):
2060 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002061 """
phajdan.jr68598232016-08-10 03:28:28 -07002062 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002063 try:
tandrii4b233bd2016-07-06 03:50:29 -07002064 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002065 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002066 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002067 if e.code == 404:
2068 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2069 if e.code == 403:
2070 DieWithError(
2071 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002072 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002073 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002074
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002075 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002076 """Returns an upload.RpcServer() to access this review's rietveld instance.
2077 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002078 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002079 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002080 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002081 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002082 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002083
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002084 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002085 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002086 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002087
tandrii5d48c322016-08-18 16:19:37 -07002088 @classmethod
2089 def PatchsetConfigKey(cls):
2090 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091
tandrii5d48c322016-08-18 16:19:37 -07002092 @classmethod
2093 def CodereviewServerConfigKey(cls):
2094 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002095
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002096 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002097 return self.RpcServer()
2098
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002099 def SetCQState(self, new_state):
2100 props = self.GetIssueProperties()
2101 if props.get('private'):
2102 DieWithError('Cannot set-commit on private issue')
2103
2104 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002105 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002106 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002107 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002108 else:
tandrii4b233bd2016-07-06 03:50:29 -07002109 assert new_state == _CQState.DRY_RUN
2110 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002111
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002112 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002113 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002114 # PatchIssue should never be called with a dirty tree. It is up to the
2115 # caller to check this, but just in case we assert here since the
2116 # consequences of the caller not checking this could be dire.
2117 assert(not git_common.is_dirty_git_tree('apply'))
2118 assert(parsed_issue_arg.valid)
2119 self._changelist.issue = parsed_issue_arg.issue
2120 if parsed_issue_arg.hostname:
2121 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2122
skobes6468b902016-10-24 08:45:10 -07002123 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2124 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2125 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002126 try:
skobes6468b902016-10-24 08:45:10 -07002127 scm_obj.apply_patch(patchset_object)
2128 except Exception as e:
2129 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002130 return 1
2131
2132 # If we had an issue, commit the current state and register the issue.
2133 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002134 self.SetIssue(self.GetIssue())
2135 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002136 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2137 'patch from issue %(i)s at patchset '
2138 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2139 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002140 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002141 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002142 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002143 return 0
2144
2145 @staticmethod
2146 def ParseIssueURL(parsed_url):
2147 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2148 return None
wychen3c1c1722016-08-04 11:46:36 -07002149 # Rietveld patch: https://domain/<number>/#ps<patchset>
2150 match = re.match(r'/(\d+)/$', parsed_url.path)
2151 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2152 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002153 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002154 issue=int(match.group(1)),
2155 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002156 hostname=parsed_url.netloc,
2157 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002158 # Typical url: https://domain/<issue_number>[/[other]]
2159 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2160 if match:
skobes6468b902016-10-24 08:45:10 -07002161 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002162 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002163 hostname=parsed_url.netloc,
2164 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002165 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2166 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2167 if match:
skobes6468b902016-10-24 08:45:10 -07002168 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002169 issue=int(match.group(1)),
2170 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002171 hostname=parsed_url.netloc,
2172 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002173 return None
2174
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002175 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002176 """Upload the patch to Rietveld."""
2177 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2178 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002179 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2180 if options.emulate_svn_auto_props:
2181 upload_args.append('--emulate_svn_auto_props')
2182
2183 change_desc = None
2184
2185 if options.email is not None:
2186 upload_args.extend(['--email', options.email])
2187
2188 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002189 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002190 upload_args.extend(['--title', options.title])
2191 if options.message:
2192 upload_args.extend(['--message', options.message])
2193 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002194 print('This branch is associated with issue %s. '
2195 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002196 else:
nodirca166002016-06-27 10:59:51 -07002197 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002198 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002199 if options.message:
2200 message = options.message
2201 else:
2202 message = CreateDescriptionFromLog(args)
2203 if options.title:
2204 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002205 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002206 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002207 change_desc.update_reviewers(options.reviewers, options.tbrs,
2208 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002209 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002210 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211
2212 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002213 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002214 return 1
2215
2216 upload_args.extend(['--message', change_desc.description])
2217 if change_desc.get_reviewers():
2218 upload_args.append('--reviewers=%s' % ','.join(
2219 change_desc.get_reviewers()))
2220 if options.send_mail:
2221 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002222 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002223 upload_args.append('--send_mail')
2224
2225 # We check this before applying rietveld.private assuming that in
2226 # rietveld.cc only addresses which we can send private CLs to are listed
2227 # if rietveld.private is set, and so we should ignore rietveld.cc only
2228 # when --private is specified explicitly on the command line.
2229 if options.private:
2230 logging.warn('rietveld.cc is ignored since private flag is specified. '
2231 'You need to review and add them manually if necessary.')
2232 cc = self.GetCCListWithoutDefault()
2233 else:
2234 cc = self.GetCCList()
2235 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002236 if change_desc.get_cced():
2237 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002238 if cc:
2239 upload_args.extend(['--cc', cc])
2240
2241 if options.private or settings.GetDefaultPrivateFlag() == "True":
2242 upload_args.append('--private')
2243
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002244 # Include the upstream repo's URL in the change -- this is useful for
2245 # projects that have their source spread across multiple repos.
2246 remote_url = self.GetGitBaseUrlFromConfig()
2247 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002248 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2249 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2250 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002251 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002252 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002253 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002254 if target_ref:
2255 upload_args.extend(['--target_ref', target_ref])
2256
2257 # Look for dependent patchsets. See crbug.com/480453 for more details.
2258 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2259 upstream_branch = ShortBranchName(upstream_branch)
2260 if remote is '.':
2261 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002262 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002263 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002264 print()
2265 print('Skipping dependency patchset upload because git config '
2266 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2267 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002268 else:
2269 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002270 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002271 auth_config=auth_config)
2272 branch_cl_issue_url = branch_cl.GetIssueURL()
2273 branch_cl_issue = branch_cl.GetIssue()
2274 branch_cl_patchset = branch_cl.GetPatchset()
2275 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2276 upload_args.extend(
2277 ['--depends_on_patchset', '%s:%s' % (
2278 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002279 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002280 '\n'
2281 'The current branch (%s) is tracking a local branch (%s) with '
2282 'an associated CL.\n'
2283 'Adding %s/#ps%s as a dependency patchset.\n'
2284 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2285 branch_cl_patchset))
2286
2287 project = settings.GetProject()
2288 if project:
2289 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002290 else:
2291 print()
2292 print('WARNING: Uploading without a project specified. Please ensure '
2293 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2294 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002295
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002296 try:
2297 upload_args = ['upload'] + upload_args + args
2298 logging.info('upload.RealMain(%s)', upload_args)
2299 issue, patchset = upload.RealMain(upload_args)
2300 issue = int(issue)
2301 patchset = int(patchset)
2302 except KeyboardInterrupt:
2303 sys.exit(1)
2304 except:
2305 # If we got an exception after the user typed a description for their
2306 # change, back up the description before re-raising.
2307 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002308 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002309 raise
2310
2311 if not self.GetIssue():
2312 self.SetIssue(issue)
2313 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002314 return 0
2315
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002316
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002317class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002318 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002319 # auth_config is Rietveld thing, kept here to preserve interface only.
2320 super(_GerritChangelistImpl, self).__init__(changelist)
2321 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002322 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002323 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002324 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002325 # Map from change number (issue) to its detail cache.
2326 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002327
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002328 if codereview_host is not None:
2329 assert not codereview_host.startswith('https://'), codereview_host
2330 self._gerrit_host = codereview_host
2331 self._gerrit_server = 'https://%s' % codereview_host
2332
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002333 def _GetGerritHost(self):
2334 # Lazy load of configs.
2335 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002336 if self._gerrit_host and '.' not in self._gerrit_host:
2337 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2338 # This happens for internal stuff http://crbug.com/614312.
2339 parsed = urlparse.urlparse(self.GetRemoteUrl())
2340 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002341 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002342 ' Your current remote is: %s' % self.GetRemoteUrl())
2343 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2344 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002345 return self._gerrit_host
2346
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002347 def _GetGitHost(self):
2348 """Returns git host to be used when uploading change to Gerrit."""
2349 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2350
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002351 def GetCodereviewServer(self):
2352 if not self._gerrit_server:
2353 # If we're on a branch then get the server potentially associated
2354 # with that branch.
2355 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002356 self._gerrit_server = self._GitGetBranchConfigValue(
2357 self.CodereviewServerConfigKey())
2358 if self._gerrit_server:
2359 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002360 if not self._gerrit_server:
2361 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2362 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002363 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002364 parts[0] = parts[0] + '-review'
2365 self._gerrit_host = '.'.join(parts)
2366 self._gerrit_server = 'https://%s' % self._gerrit_host
2367 return self._gerrit_server
2368
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002369 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002370 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002371 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002372
tandrii5d48c322016-08-18 16:19:37 -07002373 @classmethod
2374 def PatchsetConfigKey(cls):
2375 return 'gerritpatchset'
2376
2377 @classmethod
2378 def CodereviewServerConfigKey(cls):
2379 return 'gerritserver'
2380
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002381 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002382 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002383 if settings.GetGerritSkipEnsureAuthenticated():
2384 # For projects with unusual authentication schemes.
2385 # See http://crbug.com/603378.
2386 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002387 # Lazy-loader to identify Gerrit and Git hosts.
2388 if gerrit_util.GceAuthenticator.is_gce():
2389 return
2390 self.GetCodereviewServer()
2391 git_host = self._GetGitHost()
2392 assert self._gerrit_server and self._gerrit_host
2393 cookie_auth = gerrit_util.CookiesAuthenticator()
2394
2395 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2396 git_auth = cookie_auth.get_auth_header(git_host)
2397 if gerrit_auth and git_auth:
2398 if gerrit_auth == git_auth:
2399 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002400 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002401 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002402 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002403 ' %s\n'
2404 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002405 ' Consider running the following command:\n'
2406 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002407 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002408 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002409 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002410 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002411 cookie_auth.get_new_password_message(git_host)))
2412 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002413 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002414 return
2415 else:
2416 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002417 ([] if gerrit_auth else [self._gerrit_host]) +
2418 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002419 DieWithError('Credentials for the following hosts are required:\n'
2420 ' %s\n'
2421 'These are read from %s (or legacy %s)\n'
2422 '%s' % (
2423 '\n '.join(missing),
2424 cookie_auth.get_gitcookies_path(),
2425 cookie_auth.get_netrc_path(),
2426 cookie_auth.get_new_password_message(git_host)))
2427
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002428 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002429 if not self.GetIssue():
2430 return
2431
2432 # Warm change details cache now to avoid RPCs later, reducing latency for
2433 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002434 self._GetChangeDetail(
2435 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002436
2437 status = self._GetChangeDetail()['status']
2438 if status in ('MERGED', 'ABANDONED'):
2439 DieWithError('Change %s has been %s, new uploads are not allowed' %
2440 (self.GetIssueURL(),
2441 'submitted' if status == 'MERGED' else 'abandoned'))
2442
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002443 if gerrit_util.GceAuthenticator.is_gce():
2444 return
2445 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2446 self._GetGerritHost())
2447 if self.GetIssueOwner() == cookies_user:
2448 return
2449 logging.debug('change %s owner is %s, cookies user is %s',
2450 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002451 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002452 # so ask what Gerrit thinks of this user.
2453 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2454 if details['email'] == self.GetIssueOwner():
2455 return
2456 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002457 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002458 'as %s.\n'
2459 'Uploading may fail due to lack of permissions.' %
2460 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2461 confirm_or_exit(action='upload')
2462
2463
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002464 def _PostUnsetIssueProperties(self):
2465 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002466 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002467
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002468 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002469 class ThisIsNotRietveldIssue(object):
2470 def __nonzero__(self):
2471 # This is a hack to make presubmit_support think that rietveld is not
2472 # defined, yet still ensure that calls directly result in a decent
2473 # exception message below.
2474 return False
2475
2476 def __getattr__(self, attr):
2477 print(
2478 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2479 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002480 'Please, either change your PRESUBMIT to not use rietveld_obj.%s,\n'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002481 'or use Rietveld for codereview.\n'
2482 'See also http://crbug.com/579160.' % attr)
2483 raise NotImplementedError()
2484 return ThisIsNotRietveldIssue()
2485
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002486 def GetGerritObjForPresubmit(self):
2487 return presubmit_support.GerritAccessor(self._GetGerritHost())
2488
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002489 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002490 """Apply a rough heuristic to give a simple summary of an issue's review
2491 or CQ status, assuming adherence to a common workflow.
2492
2493 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002494 * 'error' - error from review tool (including deleted issues)
2495 * 'unsent' - no reviewers added
2496 * 'waiting' - waiting for review
2497 * 'reply' - waiting for uploader to reply to review
2498 * 'lgtm' - Code-Review label has been set
2499 * 'commit' - in the commit queue
2500 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002501 """
2502 if not self.GetIssue():
2503 return None
2504
2505 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002506 data = self._GetChangeDetail([
2507 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002508 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002509 return 'error'
2510
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002511 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002512 return 'closed'
2513
Aaron Gable9ab38c62017-04-06 14:36:33 -07002514 if data['labels'].get('Commit-Queue', {}).get('approved'):
2515 # The section will have an "approved" subsection if anyone has voted
2516 # the maximum value on the label.
2517 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002518
Aaron Gable9ab38c62017-04-06 14:36:33 -07002519 if data['labels'].get('Code-Review', {}).get('approved'):
2520 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002521
2522 if not data.get('reviewers', {}).get('REVIEWER', []):
2523 return 'unsent'
2524
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002525 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002526 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2527 last_message_author = messages.pop().get('author', {})
2528 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002529 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2530 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002531 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002532 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002533 if last_message_author.get('_account_id') == owner:
2534 # Most recent message was by owner.
2535 return 'waiting'
2536 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002537 # Some reply from non-owner.
2538 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002539
2540 # Somehow there are no messages even though there are reviewers.
2541 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002542
2543 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002544 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002545 patchset = data['revisions'][data['current_revision']]['_number']
2546 self.SetPatchset(patchset)
2547 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002548
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002549 def FetchDescription(self, force=False):
2550 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2551 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002552 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002553 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002554
dsansomee2d6fd92016-09-08 00:10:47 -07002555 def UpdateDescriptionRemote(self, description, force=False):
2556 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2557 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002558 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002559 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002560 'unpublished edit. Either publish the edit in the Gerrit web UI '
2561 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002562
2563 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2564 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002565 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002566 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002567
Aaron Gable636b13f2017-07-14 10:42:48 -07002568 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002569 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002570 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002571
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002572 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002573 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002574 messages = self._GetChangeDetail(
2575 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2576 file_comments = gerrit_util.GetChangeComments(
2577 self._GetGerritHost(), self.GetIssue())
2578
2579 # Build dictionary of file comments for easy access and sorting later.
2580 # {author+date: {path: {patchset: {line: url+message}}}}
2581 comments = collections.defaultdict(
2582 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2583 for path, line_comments in file_comments.iteritems():
2584 for comment in line_comments:
2585 if comment.get('tag', '').startswith('autogenerated'):
2586 continue
2587 key = (comment['author']['email'], comment['updated'])
2588 if comment.get('side', 'REVISION') == 'PARENT':
2589 patchset = 'Base'
2590 else:
2591 patchset = 'PS%d' % comment['patch_set']
2592 line = comment.get('line', 0)
2593 url = ('https://%s/c/%s/%s/%s#%s%s' %
2594 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2595 'b' if comment.get('side') == 'PARENT' else '',
2596 str(line) if line else ''))
2597 comments[key][path][patchset][line] = (url, comment['message'])
2598
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002599 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002600 for msg in messages:
2601 # Don't bother showing autogenerated messages.
2602 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2603 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002604 # Gerrit spits out nanoseconds.
2605 assert len(msg['date'].split('.')[-1]) == 9
2606 date = datetime.datetime.strptime(msg['date'][:-3],
2607 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002608 message = msg['message']
2609 key = (msg['author']['email'], msg['date'])
2610 if key in comments:
2611 message += '\n'
2612 for path, patchsets in sorted(comments.get(key, {}).items()):
2613 if readable:
2614 message += '\n%s' % path
2615 for patchset, lines in sorted(patchsets.items()):
2616 for line, (url, content) in sorted(lines.items()):
2617 if line:
2618 line_str = 'Line %d' % line
2619 path_str = '%s:%d:' % (path, line)
2620 else:
2621 line_str = 'File comment'
2622 path_str = '%s:0:' % path
2623 if readable:
2624 message += '\n %s, %s: %s' % (patchset, line_str, url)
2625 message += '\n %s\n' % content
2626 else:
2627 message += '\n%s ' % path_str
2628 message += '\n%s\n' % content
2629
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002630 summary.append(_CommentSummary(
2631 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002632 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002633 sender=msg['author']['email'],
2634 # These could be inferred from the text messages and correlated with
2635 # Code-Review label maximum, however this is not reliable.
2636 # Leaving as is until the need arises.
2637 approval=False,
2638 disapproval=False,
2639 ))
2640 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002641
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002642 def CloseIssue(self):
2643 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2644
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002645 def SubmitIssue(self, wait_for_merge=True):
2646 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2647 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002648
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002649 def _GetChangeDetail(self, options=None, issue=None,
2650 no_cache=False):
2651 """Returns details of the issue by querying Gerrit and caching results.
2652
2653 If fresh data is needed, set no_cache=True which will clear cache and
2654 thus new data will be fetched from Gerrit.
2655 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002656 options = options or []
2657 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002658 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002659
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002660 # Optimization to avoid multiple RPCs:
2661 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2662 'CURRENT_COMMIT' not in options):
2663 options.append('CURRENT_COMMIT')
2664
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002665 # Normalize issue and options for consistent keys in cache.
2666 issue = str(issue)
2667 options = [o.upper() for o in options]
2668
2669 # Check in cache first unless no_cache is True.
2670 if no_cache:
2671 self._detail_cache.pop(issue, None)
2672 else:
2673 options_set = frozenset(options)
2674 for cached_options_set, data in self._detail_cache.get(issue, []):
2675 # Assumption: data fetched before with extra options is suitable
2676 # for return for a smaller set of options.
2677 # For example, if we cached data for
2678 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2679 # and request is for options=[CURRENT_REVISION],
2680 # THEN we can return prior cached data.
2681 if options_set.issubset(cached_options_set):
2682 return data
2683
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002684 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002685 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002686 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002687 except gerrit_util.GerritError as e:
2688 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002689 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002690 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002691
2692 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002693 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002694
agable32978d92016-11-01 12:55:02 -07002695 def _GetChangeCommit(self, issue=None):
2696 issue = issue or self.GetIssue()
2697 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002698 try:
2699 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2700 except gerrit_util.GerritError as e:
2701 if e.http_status == 404:
2702 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2703 raise
agable32978d92016-11-01 12:55:02 -07002704 return data
2705
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002706 def CMDLand(self, force, bypass_hooks, verbose):
2707 if git_common.is_dirty_git_tree('land'):
2708 return 1
tandriid60367b2016-06-22 05:25:12 -07002709 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2710 if u'Commit-Queue' in detail.get('labels', {}):
2711 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002712 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2713 'which can test and land changes for you. '
2714 'Are you sure you wish to bypass it?\n',
2715 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002716
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002717 differs = True
tandriic4344b52016-08-29 06:04:54 -07002718 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002719 # Note: git diff outputs nothing if there is no diff.
2720 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002721 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002722 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002723 if detail['current_revision'] == last_upload:
2724 differs = False
2725 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002726 print('WARNING: Local branch contents differ from latest uploaded '
2727 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002728 if differs:
2729 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002730 confirm_or_exit(
2731 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2732 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002733 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002734 elif not bypass_hooks:
2735 hook_results = self.RunHook(
2736 committing=True,
2737 may_prompt=not force,
2738 verbose=verbose,
2739 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2740 if not hook_results.should_continue():
2741 return 1
2742
2743 self.SubmitIssue(wait_for_merge=True)
2744 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002745 links = self._GetChangeCommit().get('web_links', [])
2746 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002747 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002748 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002749 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002750 return 0
2751
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002752 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002753 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002754 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002755 assert not directory
2756 assert parsed_issue_arg.valid
2757
2758 self._changelist.issue = parsed_issue_arg.issue
2759
2760 if parsed_issue_arg.hostname:
2761 self._gerrit_host = parsed_issue_arg.hostname
2762 self._gerrit_server = 'https://%s' % self._gerrit_host
2763
tandriic2405f52016-10-10 08:13:15 -07002764 try:
2765 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002766 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002767 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002768
2769 if not parsed_issue_arg.patchset:
2770 # Use current revision by default.
2771 revision_info = detail['revisions'][detail['current_revision']]
2772 patchset = int(revision_info['_number'])
2773 else:
2774 patchset = parsed_issue_arg.patchset
2775 for revision_info in detail['revisions'].itervalues():
2776 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2777 break
2778 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002779 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002780 (parsed_issue_arg.patchset, self.GetIssue()))
2781
Aaron Gable697a91b2018-01-19 15:20:15 -08002782 remote_url = self._changelist.GetRemoteUrl()
2783 if remote_url.endswith('.git'):
2784 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002785 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002786
2787 if remote_url != fetch_info['url']:
2788 DieWithError('Trying to patch a change from %s but this repo appears '
2789 'to be %s.' % (fetch_info['url'], remote_url))
2790
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002791 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002792
Aaron Gable62619a32017-06-16 08:22:09 -07002793 if force:
2794 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2795 print('Checked out commit for change %i patchset %i locally' %
2796 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002797 elif nocommit:
2798 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2799 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002800 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002801 RunGit(['cherry-pick', 'FETCH_HEAD'])
2802 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002803 (parsed_issue_arg.issue, patchset))
2804 print('Note: this created a local commit which does not have '
2805 'the same hash as the one uploaded for review. This will make '
2806 'uploading changes based on top of this branch difficult.\n'
2807 'If you want to do that, use "git cl patch --force" instead.')
2808
Stefan Zagerd08043c2017-10-12 12:07:02 -07002809 if self.GetBranch():
2810 self.SetIssue(parsed_issue_arg.issue)
2811 self.SetPatchset(patchset)
2812 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2813 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2814 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2815 else:
2816 print('WARNING: You are in detached HEAD state.\n'
2817 'The patch has been applied to your checkout, but you will not be '
2818 'able to upload a new patch set to the gerrit issue.\n'
2819 'Try using the \'-b\' option if you would like to work on a '
2820 'branch and/or upload a new patch set.')
2821
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002822 return 0
2823
2824 @staticmethod
2825 def ParseIssueURL(parsed_url):
2826 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2827 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002828 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2829 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002830 # Short urls like https://domain/<issue_number> can be used, but don't allow
2831 # specifying the patchset (you'd 404), but we allow that here.
2832 if parsed_url.path == '/':
2833 part = parsed_url.fragment
2834 else:
2835 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002836 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002837 if match:
2838 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002839 issue=int(match.group(3)),
2840 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002841 hostname=parsed_url.netloc,
2842 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002843 return None
2844
tandrii16e0b4e2016-06-07 10:34:28 -07002845 def _GerritCommitMsgHookCheck(self, offer_removal):
2846 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2847 if not os.path.exists(hook):
2848 return
2849 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2850 # custom developer made one.
2851 data = gclient_utils.FileRead(hook)
2852 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2853 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002854 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002855 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002856 'and may interfere with it in subtle ways.\n'
2857 'We recommend you remove the commit-msg hook.')
2858 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002859 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002860 gclient_utils.rm_file_or_tree(hook)
2861 print('Gerrit commit-msg hook removed.')
2862 else:
2863 print('OK, will keep Gerrit commit-msg hook in place.')
2864
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002865 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002866 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002867 if options.squash and options.no_squash:
2868 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002869
2870 if not options.squash and not options.no_squash:
2871 # Load default for user, repo, squash=true, in this order.
2872 options.squash = settings.GetSquashGerritUploads()
2873 elif options.no_squash:
2874 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002875
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002876 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002877 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002878
Aaron Gableb56ad332017-01-06 15:24:31 -08002879 # This may be None; default fallback value is determined in logic below.
2880 title = options.title
2881
Dominic Battre7d1c4842017-10-27 09:17:28 +02002882 # Extract bug number from branch name.
2883 bug = options.bug
2884 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2885 if not bug and match:
2886 bug = match.group(1)
2887
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002888 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002889 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002890 if self.GetIssue():
2891 # Try to get the message from a previous upload.
2892 message = self.GetDescription()
2893 if not message:
2894 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002895 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002896 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002897 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002898 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002899 # When uploading a subsequent patchset, -m|--message is taken
2900 # as the patchset title if --title was not provided.
2901 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002902 else:
2903 default_title = RunGit(
2904 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002905 if options.force:
2906 title = default_title
2907 else:
2908 title = ask_for_data(
2909 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002910 change_id = self._GetChangeDetail()['change_id']
2911 while True:
2912 footer_change_ids = git_footers.get_footer_change_id(message)
2913 if footer_change_ids == [change_id]:
2914 break
2915 if not footer_change_ids:
2916 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002917 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002918 continue
2919 # There is already a valid footer but with different or several ids.
2920 # Doing this automatically is non-trivial as we don't want to lose
2921 # existing other footers, yet we want to append just 1 desired
2922 # Change-Id. Thus, just create a new footer, but let user verify the
2923 # new description.
2924 message = '%s\n\nChange-Id: %s' % (message, change_id)
2925 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002926 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002927 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002928 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002929 'Please, check the proposed correction to the description, '
2930 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2931 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2932 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002933 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002934 if not options.force:
2935 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002936 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002937 message = change_desc.description
2938 if not message:
2939 DieWithError("Description is empty. Aborting...")
2940 # Continue the while loop.
2941 # Sanity check of this code - we should end up with proper message
2942 # footer.
2943 assert [change_id] == git_footers.get_footer_change_id(message)
2944 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002945 else: # if not self.GetIssue()
2946 if options.message:
2947 message = options.message
2948 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002949 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002950 if options.title:
2951 message = options.title + '\n\n' + message
2952 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002953
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002954 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002955 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002956 # On first upload, patchset title is always this string, while
2957 # --title flag gets converted to first line of message.
2958 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002959 if not change_desc.description:
2960 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002961 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002962 if len(change_ids) > 1:
2963 DieWithError('too many Change-Id footers, at most 1 allowed.')
2964 if not change_ids:
2965 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002966 change_desc.set_description(git_footers.add_footer_change_id(
2967 change_desc.description,
2968 GenerateGerritChangeId(change_desc.description)))
2969 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002970 assert len(change_ids) == 1
2971 change_id = change_ids[0]
2972
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002973 if options.reviewers or options.tbrs or options.add_owners_to:
2974 change_desc.update_reviewers(options.reviewers, options.tbrs,
2975 options.add_owners_to, change)
2976
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002977 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002978 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2979 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002980 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002981 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2982 desc_tempfile.write(change_desc.description)
2983 desc_tempfile.close()
2984 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2985 '-F', desc_tempfile.name]).strip()
2986 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002987 else:
2988 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002989 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002990 if not change_desc.description:
2991 DieWithError("Description is empty. Aborting...")
2992
2993 if not git_footers.get_footer_change_id(change_desc.description):
2994 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002995 change_desc.set_description(
2996 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002997 if options.reviewers or options.tbrs or options.add_owners_to:
2998 change_desc.update_reviewers(options.reviewers, options.tbrs,
2999 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003000 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003001 # For no-squash mode, we assume the remote called "origin" is the one we
3002 # want. It is not worthwhile to support different workflows for
3003 # no-squash mode.
3004 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003005 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3006
3007 assert change_desc
3008 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3009 ref_to_push)]).splitlines()
3010 if len(commits) > 1:
3011 print('WARNING: This will upload %d commits. Run the following command '
3012 'to see which commits will be uploaded: ' % len(commits))
3013 print('git log %s..%s' % (parent, ref_to_push))
3014 print('You can also use `git squash-branch` to squash these into a '
3015 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003016 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003017
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003018 if options.reviewers or options.tbrs or options.add_owners_to:
3019 change_desc.update_reviewers(options.reviewers, options.tbrs,
3020 options.add_owners_to, change)
3021
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003022 # Extra options that can be specified at push time. Doc:
3023 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003024 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003025
Aaron Gable844cf292017-06-28 11:32:59 -07003026 # By default, new changes are started in WIP mode, and subsequent patchsets
3027 # don't send email. At any time, passing --send-mail will mark the change
3028 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003029 if options.send_mail:
3030 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003031 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003032 elif not self.GetIssue():
3033 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003034 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003035 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003036
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003037 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003038 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003039
Aaron Gable9b713dd2016-12-14 16:04:21 -08003040 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003041 # Punctuation and whitespace in |title| must be percent-encoded.
3042 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003043
agablec6787972016-09-09 16:13:34 -07003044 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003045 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003046
rmistry9eadede2016-09-19 11:22:43 -07003047 if options.topic:
3048 # Documentation on Gerrit topics is here:
3049 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003050 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003051
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003052 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003053 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003054 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003055 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003056 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3057
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003058 refspec_suffix = ''
3059 if refspec_opts:
3060 refspec_suffix = '%' + ','.join(refspec_opts)
3061 assert ' ' not in refspec_suffix, (
3062 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3063 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3064
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003065 try:
3066 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003067 ['git', 'push', self.GetRemoteUrl(), refspec],
3068 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003069 # Flush after every line: useful for seeing progress when running as
3070 # recipe.
3071 filter_fn=lambda _: sys.stdout.flush())
3072 except subprocess2.CalledProcessError:
3073 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003074 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003075 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003076 'credential problems:\n'
3077 ' git cl creds-check\n',
3078 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003079
3080 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003081 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003082 change_numbers = [m.group(1)
3083 for m in map(regex.match, push_stdout.splitlines())
3084 if m]
3085 if len(change_numbers) != 1:
3086 DieWithError(
3087 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003088 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003089 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003090 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003091
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003092 reviewers = sorted(change_desc.get_reviewers())
3093
tandrii88189772016-09-29 04:29:57 -07003094 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003095 if not options.private:
3096 cc = self.GetCCList().split(',')
3097 else:
3098 cc = []
tandrii88189772016-09-29 04:29:57 -07003099 if options.cc:
3100 cc.extend(options.cc)
3101 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003102 if change_desc.get_cced():
3103 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003104
3105 gerrit_util.AddReviewers(
3106 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3107 notify=bool(options.send_mail))
3108
Aaron Gablefd238082017-06-07 13:42:34 -07003109 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003110 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3111 score = 1
3112 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3113 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3114 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003115 gerrit_util.SetReview(
3116 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003117 msg='Self-approving for TBR',
3118 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003119
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003120 return 0
3121
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003122 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3123 change_desc):
3124 """Computes parent of the generated commit to be uploaded to Gerrit.
3125
3126 Returns revision or a ref name.
3127 """
3128 if custom_cl_base:
3129 # Try to avoid creating additional unintended CLs when uploading, unless
3130 # user wants to take this risk.
3131 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3132 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3133 local_ref_of_target_remote])
3134 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003135 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003136 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3137 'If you proceed with upload, more than 1 CL may be created by '
3138 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3139 'If you are certain that specified base `%s` has already been '
3140 'uploaded to Gerrit as another CL, you may proceed.\n' %
3141 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3142 if not force:
3143 confirm_or_exit(
3144 'Do you take responsibility for cleaning up potential mess '
3145 'resulting from proceeding with upload?',
3146 action='upload')
3147 return custom_cl_base
3148
Aaron Gablef97e33d2017-03-30 15:44:27 -07003149 if remote != '.':
3150 return self.GetCommonAncestorWithUpstream()
3151
3152 # If our upstream branch is local, we base our squashed commit on its
3153 # squashed version.
3154 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3155
Aaron Gablef97e33d2017-03-30 15:44:27 -07003156 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003157 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003158
3159 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003160 # TODO(tandrii): consider checking parent change in Gerrit and using its
3161 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3162 # the tree hash of the parent branch. The upside is less likely bogus
3163 # requests to reupload parent change just because it's uploadhash is
3164 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003165 parent = RunGit(['config',
3166 'branch.%s.gerritsquashhash' % upstream_branch_name],
3167 error_ok=True).strip()
3168 # Verify that the upstream branch has been uploaded too, otherwise
3169 # Gerrit will create additional CLs when uploading.
3170 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3171 RunGitSilent(['rev-parse', parent + ':'])):
3172 DieWithError(
3173 '\nUpload upstream branch %s first.\n'
3174 'It is likely that this branch has been rebased since its last '
3175 'upload, so you just need to upload it again.\n'
3176 '(If you uploaded it with --no-squash, then branch dependencies '
3177 'are not supported, and you should reupload with --squash.)'
3178 % upstream_branch_name,
3179 change_desc)
3180 return parent
3181
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003182 def _AddChangeIdToCommitMessage(self, options, args):
3183 """Re-commits using the current message, assumes the commit hook is in
3184 place.
3185 """
3186 log_desc = options.message or CreateDescriptionFromLog(args)
3187 git_command = ['commit', '--amend', '-m', log_desc]
3188 RunGit(git_command)
3189 new_log_desc = CreateDescriptionFromLog(args)
3190 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003191 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003192 return new_log_desc
3193 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003194 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003195
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003196 def SetCQState(self, new_state):
3197 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003198 vote_map = {
3199 _CQState.NONE: 0,
3200 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003201 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003202 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003203 labels = {'Commit-Queue': vote_map[new_state]}
3204 notify = False if new_state == _CQState.DRY_RUN else None
3205 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3206 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003207
tandriie113dfd2016-10-11 10:20:12 -07003208 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003209 try:
3210 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003211 except GerritChangeNotExists:
3212 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003213
3214 if data['status'] in ('ABANDONED', 'MERGED'):
3215 return 'CL %s is closed' % self.GetIssue()
3216
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003217 def GetTryJobProperties(self, patchset=None):
3218 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003219 data = self._GetChangeDetail(['ALL_REVISIONS'])
3220 patchset = int(patchset or self.GetPatchset())
3221 assert patchset
3222 revision_data = None # Pylint wants it to be defined.
3223 for revision_data in data['revisions'].itervalues():
3224 if int(revision_data['_number']) == patchset:
3225 break
3226 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003227 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003228 (patchset, self.GetIssue()))
3229 return {
3230 'patch_issue': self.GetIssue(),
3231 'patch_set': patchset or self.GetPatchset(),
3232 'patch_project': data['project'],
3233 'patch_storage': 'gerrit',
3234 'patch_ref': revision_data['fetch']['http']['ref'],
3235 'patch_repository_url': revision_data['fetch']['http']['url'],
3236 'patch_gerrit_url': self.GetCodereviewServer(),
3237 }
tandriie113dfd2016-10-11 10:20:12 -07003238
tandriide281ae2016-10-12 06:02:30 -07003239 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003240 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003241
Edward Lemur707d70b2018-02-07 00:50:14 +01003242 def GetReviewers(self):
3243 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3244 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3245
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003246
3247_CODEREVIEW_IMPLEMENTATIONS = {
3248 'rietveld': _RietveldChangelistImpl,
3249 'gerrit': _GerritChangelistImpl,
3250}
3251
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003252
iannuccie53c9352016-08-17 14:40:40 -07003253def _add_codereview_issue_select_options(parser, extra=""):
3254 _add_codereview_select_options(parser)
3255
3256 text = ('Operate on this issue number instead of the current branch\'s '
3257 'implicit issue.')
3258 if extra:
3259 text += ' '+extra
3260 parser.add_option('-i', '--issue', type=int, help=text)
3261
3262
3263def _process_codereview_issue_select_options(parser, options):
3264 _process_codereview_select_options(parser, options)
3265 if options.issue is not None and not options.forced_codereview:
3266 parser.error('--issue must be specified with either --rietveld or --gerrit')
3267
3268
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003269def _add_codereview_select_options(parser):
3270 """Appends --gerrit and --rietveld options to force specific codereview."""
3271 parser.codereview_group = optparse.OptionGroup(
3272 parser, 'EXPERIMENTAL! Codereview override options')
3273 parser.add_option_group(parser.codereview_group)
3274 parser.codereview_group.add_option(
3275 '--gerrit', action='store_true',
3276 help='Force the use of Gerrit for codereview')
3277 parser.codereview_group.add_option(
3278 '--rietveld', action='store_true',
3279 help='Force the use of Rietveld for codereview')
3280
3281
3282def _process_codereview_select_options(parser, options):
3283 if options.gerrit and options.rietveld:
3284 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3285 options.forced_codereview = None
3286 if options.gerrit:
3287 options.forced_codereview = 'gerrit'
3288 elif options.rietveld:
3289 options.forced_codereview = 'rietveld'
3290
3291
tandriif9aefb72016-07-01 09:06:51 -07003292def _get_bug_line_values(default_project, bugs):
3293 """Given default_project and comma separated list of bugs, yields bug line
3294 values.
3295
3296 Each bug can be either:
3297 * a number, which is combined with default_project
3298 * string, which is left as is.
3299
3300 This function may produce more than one line, because bugdroid expects one
3301 project per line.
3302
3303 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3304 ['v8:123', 'chromium:789']
3305 """
3306 default_bugs = []
3307 others = []
3308 for bug in bugs.split(','):
3309 bug = bug.strip()
3310 if bug:
3311 try:
3312 default_bugs.append(int(bug))
3313 except ValueError:
3314 others.append(bug)
3315
3316 if default_bugs:
3317 default_bugs = ','.join(map(str, default_bugs))
3318 if default_project:
3319 yield '%s:%s' % (default_project, default_bugs)
3320 else:
3321 yield default_bugs
3322 for other in sorted(others):
3323 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3324 yield other
3325
3326
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003327class ChangeDescription(object):
3328 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003329 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003330 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003331 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003332 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003333 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3334 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3335 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3336 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003337
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003338 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003339 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003340
agable@chromium.org42c20792013-09-12 17:34:49 +00003341 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003342 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003343 return '\n'.join(self._description_lines)
3344
3345 def set_description(self, desc):
3346 if isinstance(desc, basestring):
3347 lines = desc.splitlines()
3348 else:
3349 lines = [line.rstrip() for line in desc]
3350 while lines and not lines[0]:
3351 lines.pop(0)
3352 while lines and not lines[-1]:
3353 lines.pop(-1)
3354 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003355
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003356 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3357 """Rewrites the R=/TBR= line(s) as a single line each.
3358
3359 Args:
3360 reviewers (list(str)) - list of additional emails to use for reviewers.
3361 tbrs (list(str)) - list of additional emails to use for TBRs.
3362 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3363 the change that are missing OWNER coverage. If this is not None, you
3364 must also pass a value for `change`.
3365 change (Change) - The Change that should be used for OWNERS lookups.
3366 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003367 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003368 assert isinstance(tbrs, list), tbrs
3369
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003370 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003371 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003372
3373 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003374 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003375
3376 reviewers = set(reviewers)
3377 tbrs = set(tbrs)
3378 LOOKUP = {
3379 'TBR': tbrs,
3380 'R': reviewers,
3381 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003382
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003383 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003384 regexp = re.compile(self.R_LINE)
3385 matches = [regexp.match(line) for line in self._description_lines]
3386 new_desc = [l for i, l in enumerate(self._description_lines)
3387 if not matches[i]]
3388 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003389
agable@chromium.org42c20792013-09-12 17:34:49 +00003390 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003391
3392 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003393 for match in matches:
3394 if not match:
3395 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003396 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3397
3398 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003399 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003400 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003401 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003402 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003403 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003404 LOOKUP[add_owners_to].update(
3405 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003406
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003407 # If any folks ended up in both groups, remove them from tbrs.
3408 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003409
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003410 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3411 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003412
3413 # Put the new lines in the description where the old first R= line was.
3414 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3415 if 0 <= line_loc < len(self._description_lines):
3416 if new_tbr_line:
3417 self._description_lines.insert(line_loc, new_tbr_line)
3418 if new_r_line:
3419 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003420 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003421 if new_r_line:
3422 self.append_footer(new_r_line)
3423 if new_tbr_line:
3424 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003425
Aaron Gable3a16ed12017-03-23 10:51:55 -07003426 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003427 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003428 self.set_description([
3429 '# Enter a description of the change.',
3430 '# This will be displayed on the codereview site.',
3431 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003432 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003433 '--------------------',
3434 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003435
agable@chromium.org42c20792013-09-12 17:34:49 +00003436 regexp = re.compile(self.BUG_LINE)
3437 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003438 prefix = settings.GetBugPrefix()
3439 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003440 if git_footer:
3441 self.append_footer('Bug: %s' % ', '.join(values))
3442 else:
3443 for value in values:
3444 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003445
agable@chromium.org42c20792013-09-12 17:34:49 +00003446 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003447 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003448 if not content:
3449 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003450 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003451
Bruce Dawson2377b012018-01-11 16:46:49 -08003452 # Strip off comments and default inserted "Bug:" line.
3453 clean_lines = [line.rstrip() for line in lines if not
3454 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003455 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003456 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003457 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003458
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003459 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003460 """Adds a footer line to the description.
3461
3462 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3463 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3464 that Gerrit footers are always at the end.
3465 """
3466 parsed_footer_line = git_footers.parse_footer(line)
3467 if parsed_footer_line:
3468 # Line is a gerrit footer in the form: Footer-Key: any value.
3469 # Thus, must be appended observing Gerrit footer rules.
3470 self.set_description(
3471 git_footers.add_footer(self.description,
3472 key=parsed_footer_line[0],
3473 value=parsed_footer_line[1]))
3474 return
3475
3476 if not self._description_lines:
3477 self._description_lines.append(line)
3478 return
3479
3480 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3481 if gerrit_footers:
3482 # git_footers.split_footers ensures that there is an empty line before
3483 # actual (gerrit) footers, if any. We have to keep it that way.
3484 assert top_lines and top_lines[-1] == ''
3485 top_lines, separator = top_lines[:-1], top_lines[-1:]
3486 else:
3487 separator = [] # No need for separator if there are no gerrit_footers.
3488
3489 prev_line = top_lines[-1] if top_lines else ''
3490 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3491 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3492 top_lines.append('')
3493 top_lines.append(line)
3494 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003495
tandrii99a72f22016-08-17 14:33:24 -07003496 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003497 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003498 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003499 reviewers = [match.group(2).strip()
3500 for match in matches
3501 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003502 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003503
bradnelsond975b302016-10-23 12:20:23 -07003504 def get_cced(self):
3505 """Retrieves the list of reviewers."""
3506 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3507 cced = [match.group(2).strip() for match in matches if match]
3508 return cleanup_list(cced)
3509
Nodir Turakulov23b82142017-11-16 11:04:25 -08003510 def get_hash_tags(self):
3511 """Extracts and sanitizes a list of Gerrit hashtags."""
3512 subject = (self._description_lines or ('',))[0]
3513 subject = re.sub(
3514 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3515
3516 tags = []
3517 start = 0
3518 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3519 while True:
3520 m = bracket_exp.match(subject, start)
3521 if not m:
3522 break
3523 tags.append(self.sanitize_hash_tag(m.group(1)))
3524 start = m.end()
3525
3526 if not tags:
3527 # Try "Tag: " prefix.
3528 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3529 if m:
3530 tags.append(self.sanitize_hash_tag(m.group(1)))
3531 return tags
3532
3533 @classmethod
3534 def sanitize_hash_tag(cls, tag):
3535 """Returns a sanitized Gerrit hash tag.
3536
3537 A sanitized hashtag can be used as a git push refspec parameter value.
3538 """
3539 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3540
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003541 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3542 """Updates this commit description given the parent.
3543
3544 This is essentially what Gnumbd used to do.
3545 Consult https://goo.gl/WMmpDe for more details.
3546 """
3547 assert parent_msg # No, orphan branch creation isn't supported.
3548 assert parent_hash
3549 assert dest_ref
3550 parent_footer_map = git_footers.parse_footers(parent_msg)
3551 # This will also happily parse svn-position, which GnumbD is no longer
3552 # supporting. While we'd generate correct footers, the verifier plugin
3553 # installed in Gerrit will block such commit (ie git push below will fail).
3554 parent_position = git_footers.get_position(parent_footer_map)
3555
3556 # Cherry-picks may have last line obscuring their prior footers,
3557 # from git_footers perspective. This is also what Gnumbd did.
3558 cp_line = None
3559 if (self._description_lines and
3560 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3561 cp_line = self._description_lines.pop()
3562
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003563 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003564
3565 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3566 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003567 for i, line in enumerate(footer_lines):
3568 k, v = git_footers.parse_footer(line) or (None, None)
3569 if k and k.startswith('Cr-'):
3570 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003571
3572 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003573 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003574 if parent_position[0] == dest_ref:
3575 # Same branch as parent.
3576 number = int(parent_position[1]) + 1
3577 else:
3578 number = 1 # New branch, and extra lineage.
3579 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3580 int(parent_position[1])))
3581
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003582 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3583 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003584
3585 self._description_lines = top_lines
3586 if cp_line:
3587 self._description_lines.append(cp_line)
3588 if self._description_lines[-1] != '':
3589 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003590 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003591
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003592
Aaron Gablea1bab272017-04-11 16:38:18 -07003593def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003594 """Retrieves the reviewers that approved a CL from the issue properties with
3595 messages.
3596
3597 Note that the list may contain reviewers that are not committer, thus are not
3598 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003599
3600 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003601 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003602 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003603 return sorted(
3604 set(
3605 message['sender']
3606 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003607 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003608 )
3609 )
3610
3611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003612def FindCodereviewSettingsFile(filename='codereview.settings'):
3613 """Finds the given file starting in the cwd and going up.
3614
3615 Only looks up to the top of the repository unless an
3616 'inherit-review-settings-ok' file exists in the root of the repository.
3617 """
3618 inherit_ok_file = 'inherit-review-settings-ok'
3619 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003620 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003621 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3622 root = '/'
3623 while True:
3624 if filename in os.listdir(cwd):
3625 if os.path.isfile(os.path.join(cwd, filename)):
3626 return open(os.path.join(cwd, filename))
3627 if cwd == root:
3628 break
3629 cwd = os.path.dirname(cwd)
3630
3631
3632def LoadCodereviewSettingsFromFile(fileobj):
3633 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003634 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003635
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003636 def SetProperty(name, setting, unset_error_ok=False):
3637 fullname = 'rietveld.' + name
3638 if setting in keyvals:
3639 RunGit(['config', fullname, keyvals[setting]])
3640 else:
3641 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3642
tandrii48df5812016-10-17 03:55:37 -07003643 if not keyvals.get('GERRIT_HOST', False):
3644 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003645 # Only server setting is required. Other settings can be absent.
3646 # In that case, we ignore errors raised during option deletion attempt.
3647 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003648 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003649 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3650 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003651 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003652 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3653 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003654 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003655 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3656 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003657
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003658 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003659 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003660
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003661 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003662 RunGit(['config', 'gerrit.squash-uploads',
3663 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003664
tandrii@chromium.org28253532016-04-14 13:46:56 +00003665 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003666 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003667 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3668
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003669 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003670 # should be of the form
3671 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3672 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003673 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3674 keyvals['ORIGIN_URL_CONFIG']])
3675
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003676
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003677def urlretrieve(source, destination):
3678 """urllib is broken for SSL connections via a proxy therefore we
3679 can't use urllib.urlretrieve()."""
3680 with open(destination, 'w') as f:
3681 f.write(urllib2.urlopen(source).read())
3682
3683
ukai@chromium.org712d6102013-11-27 00:52:58 +00003684def hasSheBang(fname):
3685 """Checks fname is a #! script."""
3686 with open(fname) as f:
3687 return f.read(2).startswith('#!')
3688
3689
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003690# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3691def DownloadHooks(*args, **kwargs):
3692 pass
3693
3694
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003695def DownloadGerritHook(force):
3696 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003697
3698 Args:
3699 force: True to update hooks. False to install hooks if not present.
3700 """
3701 if not settings.GetIsGerrit():
3702 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003703 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003704 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3705 if not os.access(dst, os.X_OK):
3706 if os.path.exists(dst):
3707 if not force:
3708 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003709 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003710 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003711 if not hasSheBang(dst):
3712 DieWithError('Not a script: %s\n'
3713 'You need to download from\n%s\n'
3714 'into .git/hooks/commit-msg and '
3715 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003716 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3717 except Exception:
3718 if os.path.exists(dst):
3719 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003720 DieWithError('\nFailed to download hooks.\n'
3721 'You need to download from\n%s\n'
3722 'into .git/hooks/commit-msg and '
3723 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003724
3725
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003726def GetRietveldCodereviewSettingsInteractively():
3727 """Prompt the user for settings."""
3728 server = settings.GetDefaultServerUrl(error_ok=True)
3729 prompt = 'Rietveld server (host[:port])'
3730 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3731 newserver = ask_for_data(prompt + ':')
3732 if not server and not newserver:
3733 newserver = DEFAULT_SERVER
3734 if newserver:
3735 newserver = gclient_utils.UpgradeToHttps(newserver)
3736 if newserver != server:
3737 RunGit(['config', 'rietveld.server', newserver])
3738
3739 def SetProperty(initial, caption, name, is_url):
3740 prompt = caption
3741 if initial:
3742 prompt += ' ("x" to clear) [%s]' % initial
3743 new_val = ask_for_data(prompt + ':')
3744 if new_val == 'x':
3745 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3746 elif new_val:
3747 if is_url:
3748 new_val = gclient_utils.UpgradeToHttps(new_val)
3749 if new_val != initial:
3750 RunGit(['config', 'rietveld.' + name, new_val])
3751
3752 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3753 SetProperty(settings.GetDefaultPrivateFlag(),
3754 'Private flag (rietveld only)', 'private', False)
3755 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3756 'tree-status-url', False)
3757 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3758 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3759 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3760 'run-post-upload-hook', False)
3761
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003762
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003763class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003764 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003765
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003766 _GOOGLESOURCE = 'googlesource.com'
3767
3768 def __init__(self):
3769 # Cached list of [host, identity, source], where source is either
3770 # .gitcookies or .netrc.
3771 self._all_hosts = None
3772
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003773 def ensure_configured_gitcookies(self):
3774 """Runs checks and suggests fixes to make git use .gitcookies from default
3775 path."""
3776 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3777 configured_path = RunGitSilent(
3778 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003779 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003780 if configured_path:
3781 self._ensure_default_gitcookies_path(configured_path, default)
3782 else:
3783 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003784
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003785 @staticmethod
3786 def _ensure_default_gitcookies_path(configured_path, default_path):
3787 assert configured_path
3788 if configured_path == default_path:
3789 print('git is already configured to use your .gitcookies from %s' %
3790 configured_path)
3791 return
3792
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003793 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003794 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3795 (configured_path, default_path))
3796
3797 if not os.path.exists(configured_path):
3798 print('However, your configured .gitcookies file is missing.')
3799 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3800 action='reconfigure')
3801 RunGit(['config', '--global', 'http.cookiefile', default_path])
3802 return
3803
3804 if os.path.exists(default_path):
3805 print('WARNING: default .gitcookies file already exists %s' %
3806 default_path)
3807 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3808 default_path)
3809
3810 confirm_or_exit('Move existing .gitcookies to default location?',
3811 action='move')
3812 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003813 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003814 print('Moved and reconfigured git to use .gitcookies from %s' %
3815 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003816
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003817 @staticmethod
3818 def _configure_gitcookies_path(default_path):
3819 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3820 if os.path.exists(netrc_path):
3821 print('You seem to be using outdated .netrc for git credentials: %s' %
3822 netrc_path)
3823 print('This tool will guide you through setting up recommended '
3824 '.gitcookies store for git credentials.\n'
3825 '\n'
3826 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3827 ' git config --global --unset http.cookiefile\n'
3828 ' mv %s %s.backup\n\n' % (default_path, default_path))
3829 confirm_or_exit(action='setup .gitcookies')
3830 RunGit(['config', '--global', 'http.cookiefile', default_path])
3831 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003832
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003833 def get_hosts_with_creds(self, include_netrc=False):
3834 if self._all_hosts is None:
3835 a = gerrit_util.CookiesAuthenticator()
3836 self._all_hosts = [
3837 (h, u, s)
3838 for h, u, s in itertools.chain(
3839 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3840 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3841 )
3842 if h.endswith(self._GOOGLESOURCE)
3843 ]
3844
3845 if include_netrc:
3846 return self._all_hosts
3847 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3848
3849 def print_current_creds(self, include_netrc=False):
3850 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3851 if not hosts:
3852 print('No Git/Gerrit credentials found')
3853 return
3854 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3855 header = [('Host', 'User', 'Which file'),
3856 ['=' * l for l in lengths]]
3857 for row in (header + hosts):
3858 print('\t'.join((('%%+%ds' % l) % s)
3859 for l, s in zip(lengths, row)))
3860
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003861 @staticmethod
3862 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003863 """Parses identity "git-<username>.domain" into <username> and domain."""
3864 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003865 # distinguishable from sub-domains. But we do know typical domains:
3866 if identity.endswith('.chromium.org'):
3867 domain = 'chromium.org'
3868 username = identity[:-len('.chromium.org')]
3869 else:
3870 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003871 if username.startswith('git-'):
3872 username = username[len('git-'):]
3873 return username, domain
3874
3875 def _get_usernames_of_domain(self, domain):
3876 """Returns list of usernames referenced by .gitcookies in a given domain."""
3877 identities_by_domain = {}
3878 for _, identity, _ in self.get_hosts_with_creds():
3879 username, domain = self._parse_identity(identity)
3880 identities_by_domain.setdefault(domain, []).append(username)
3881 return identities_by_domain.get(domain)
3882
3883 def _canonical_git_googlesource_host(self, host):
3884 """Normalizes Gerrit hosts (with '-review') to Git host."""
3885 assert host.endswith(self._GOOGLESOURCE)
3886 # Prefix doesn't include '.' at the end.
3887 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3888 if prefix.endswith('-review'):
3889 prefix = prefix[:-len('-review')]
3890 return prefix + '.' + self._GOOGLESOURCE
3891
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003892 def _canonical_gerrit_googlesource_host(self, host):
3893 git_host = self._canonical_git_googlesource_host(host)
3894 prefix = git_host.split('.', 1)[0]
3895 return prefix + '-review.' + self._GOOGLESOURCE
3896
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003897 def _get_counterpart_host(self, host):
3898 assert host.endswith(self._GOOGLESOURCE)
3899 git = self._canonical_git_googlesource_host(host)
3900 gerrit = self._canonical_gerrit_googlesource_host(git)
3901 return git if gerrit == host else gerrit
3902
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003903 def has_generic_host(self):
3904 """Returns whether generic .googlesource.com has been configured.
3905
3906 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3907 """
3908 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3909 if host == '.' + self._GOOGLESOURCE:
3910 return True
3911 return False
3912
3913 def _get_git_gerrit_identity_pairs(self):
3914 """Returns map from canonic host to pair of identities (Git, Gerrit).
3915
3916 One of identities might be None, meaning not configured.
3917 """
3918 host_to_identity_pairs = {}
3919 for host, identity, _ in self.get_hosts_with_creds():
3920 canonical = self._canonical_git_googlesource_host(host)
3921 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3922 idx = 0 if canonical == host else 1
3923 pair[idx] = identity
3924 return host_to_identity_pairs
3925
3926 def get_partially_configured_hosts(self):
3927 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003928 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3929 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3930 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003931
3932 def get_conflicting_hosts(self):
3933 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003934 host
3935 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003936 if None not in (i1, i2) and i1 != i2)
3937
3938 def get_duplicated_hosts(self):
3939 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3940 return set(host for host, count in counters.iteritems() if count > 1)
3941
3942 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3943 'chromium.googlesource.com': 'chromium.org',
3944 'chrome-internal.googlesource.com': 'google.com',
3945 }
3946
3947 def get_hosts_with_wrong_identities(self):
3948 """Finds hosts which **likely** reference wrong identities.
3949
3950 Note: skips hosts which have conflicting identities for Git and Gerrit.
3951 """
3952 hosts = set()
3953 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3954 pair = self._get_git_gerrit_identity_pairs().get(host)
3955 if pair and pair[0] == pair[1]:
3956 _, domain = self._parse_identity(pair[0])
3957 if domain != expected:
3958 hosts.add(host)
3959 return hosts
3960
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003961 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003962 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003963 hosts = sorted(hosts)
3964 assert hosts
3965 if extra_column_func is None:
3966 extras = [''] * len(hosts)
3967 else:
3968 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003969 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3970 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003971 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003972 lines.append(tmpl % he)
3973 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003974
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003975 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003976 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003977 yield ('.googlesource.com wildcard record detected',
3978 ['Chrome Infrastructure team recommends to list full host names '
3979 'explicitly.'],
3980 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003981
3982 dups = self.get_duplicated_hosts()
3983 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003984 yield ('The following hosts were defined twice',
3985 self._format_hosts(dups),
3986 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003987
3988 partial = self.get_partially_configured_hosts()
3989 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003990 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3991 'These hosts are missing',
3992 self._format_hosts(partial, lambda host: 'but %s defined' %
3993 self._get_counterpart_host(host)),
3994 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003995
3996 conflicting = self.get_conflicting_hosts()
3997 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003998 yield ('The following Git hosts have differing credentials from their '
3999 'Gerrit counterparts',
4000 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4001 tuple(self._get_git_gerrit_identity_pairs()[host])),
4002 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004003
4004 wrong = self.get_hosts_with_wrong_identities()
4005 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004006 yield ('These hosts likely use wrong identity',
4007 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4008 (self._get_git_gerrit_identity_pairs()[host][0],
4009 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4010 wrong)
4011
4012 def find_and_report_problems(self):
4013 """Returns True if there was at least one problem, else False."""
4014 found = False
4015 bad_hosts = set()
4016 for title, sublines, hosts in self._find_problems():
4017 if not found:
4018 found = True
4019 print('\n\n.gitcookies problem report:\n')
4020 bad_hosts.update(hosts or [])
4021 print(' %s%s' % (title , (':' if sublines else '')))
4022 if sublines:
4023 print()
4024 print(' %s' % '\n '.join(sublines))
4025 print()
4026
4027 if bad_hosts:
4028 assert found
4029 print(' You can manually remove corresponding lines in your %s file and '
4030 'visit the following URLs with correct account to generate '
4031 'correct credential lines:\n' %
4032 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4033 print(' %s' % '\n '.join(sorted(set(
4034 gerrit_util.CookiesAuthenticator().get_new_password_url(
4035 self._canonical_git_googlesource_host(host))
4036 for host in bad_hosts
4037 ))))
4038 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004039
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004040
4041def CMDcreds_check(parser, args):
4042 """Checks credentials and suggests changes."""
4043 _, _ = parser.parse_args(args)
4044
4045 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004046 DieWithError(
4047 'This command is not designed for GCE, are you on a bot?\n'
4048 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004049
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004050 checker = _GitCookiesChecker()
4051 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004052
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004053 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004054 checker.print_current_creds(include_netrc=True)
4055
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004056 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004057 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004058 return 0
4059 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004060
4061
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004062@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004063def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004064 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004065
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004066 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004067 # TODO(tandrii): remove this once we switch to Gerrit.
4068 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004069 parser.add_option('--activate-update', action='store_true',
4070 help='activate auto-updating [rietveld] section in '
4071 '.git/config')
4072 parser.add_option('--deactivate-update', action='store_true',
4073 help='deactivate auto-updating [rietveld] section in '
4074 '.git/config')
4075 options, args = parser.parse_args(args)
4076
4077 if options.deactivate_update:
4078 RunGit(['config', 'rietveld.autoupdate', 'false'])
4079 return
4080
4081 if options.activate_update:
4082 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4083 return
4084
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004085 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004086 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004087 return 0
4088
4089 url = args[0]
4090 if not url.endswith('codereview.settings'):
4091 url = os.path.join(url, 'codereview.settings')
4092
4093 # Load code review settings and download hooks (if available).
4094 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4095 return 0
4096
4097
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004098def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004099 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004100 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4101 branch = ShortBranchName(branchref)
4102 _, args = parser.parse_args(args)
4103 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004104 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004105 return RunGit(['config', 'branch.%s.base-url' % branch],
4106 error_ok=False).strip()
4107 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004108 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004109 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4110 error_ok=False).strip()
4111
4112
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004113def color_for_status(status):
4114 """Maps a Changelist status to color, for CMDstatus and other tools."""
4115 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004116 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004117 'waiting': Fore.BLUE,
4118 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004119 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004120 'lgtm': Fore.GREEN,
4121 'commit': Fore.MAGENTA,
4122 'closed': Fore.CYAN,
4123 'error': Fore.WHITE,
4124 }.get(status, Fore.WHITE)
4125
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004126
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004127def get_cl_statuses(changes, fine_grained, max_processes=None):
4128 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004129
4130 If fine_grained is true, this will fetch CL statuses from the server.
4131 Otherwise, simply indicate if there's a matching url for the given branches.
4132
4133 If max_processes is specified, it is used as the maximum number of processes
4134 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4135 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004136
4137 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004138 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004139 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004140 upload.verbosity = 0
4141
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004142 if not changes:
4143 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004144
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004145 if not fine_grained:
4146 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004147 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004148 for cl in changes:
4149 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004150 return
4151
4152 # First, sort out authentication issues.
4153 logging.debug('ensuring credentials exist')
4154 for cl in changes:
4155 cl.EnsureAuthenticated(force=False, refresh=True)
4156
4157 def fetch(cl):
4158 try:
4159 return (cl, cl.GetStatus())
4160 except:
4161 # See http://crbug.com/629863.
4162 logging.exception('failed to fetch status for %s:', cl)
4163 raise
4164
4165 threads_count = len(changes)
4166 if max_processes:
4167 threads_count = max(1, min(threads_count, max_processes))
4168 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4169
4170 pool = ThreadPool(threads_count)
4171 fetched_cls = set()
4172 try:
4173 it = pool.imap_unordered(fetch, changes).__iter__()
4174 while True:
4175 try:
4176 cl, status = it.next(timeout=5)
4177 except multiprocessing.TimeoutError:
4178 break
4179 fetched_cls.add(cl)
4180 yield cl, status
4181 finally:
4182 pool.close()
4183
4184 # Add any branches that failed to fetch.
4185 for cl in set(changes) - fetched_cls:
4186 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004187
rmistry@google.com2dd99862015-06-22 12:22:18 +00004188
4189def upload_branch_deps(cl, args):
4190 """Uploads CLs of local branches that are dependents of the current branch.
4191
4192 If the local branch dependency tree looks like:
4193 test1 -> test2.1 -> test3.1
4194 -> test3.2
4195 -> test2.2 -> test3.3
4196
4197 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4198 run on the dependent branches in this order:
4199 test2.1, test3.1, test3.2, test2.2, test3.3
4200
4201 Note: This function does not rebase your local dependent branches. Use it when
4202 you make a change to the parent branch that will not conflict with its
4203 dependent branches, and you would like their dependencies updated in
4204 Rietveld.
4205 """
4206 if git_common.is_dirty_git_tree('upload-branch-deps'):
4207 return 1
4208
4209 root_branch = cl.GetBranch()
4210 if root_branch is None:
4211 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4212 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004213 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004214 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4215 'patchset dependencies without an uploaded CL.')
4216
4217 branches = RunGit(['for-each-ref',
4218 '--format=%(refname:short) %(upstream:short)',
4219 'refs/heads'])
4220 if not branches:
4221 print('No local branches found.')
4222 return 0
4223
4224 # Create a dictionary of all local branches to the branches that are dependent
4225 # on it.
4226 tracked_to_dependents = collections.defaultdict(list)
4227 for b in branches.splitlines():
4228 tokens = b.split()
4229 if len(tokens) == 2:
4230 branch_name, tracked = tokens
4231 tracked_to_dependents[tracked].append(branch_name)
4232
vapiera7fbd5a2016-06-16 09:17:49 -07004233 print()
4234 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004235 dependents = []
4236 def traverse_dependents_preorder(branch, padding=''):
4237 dependents_to_process = tracked_to_dependents.get(branch, [])
4238 padding += ' '
4239 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004240 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004241 dependents.append(dependent)
4242 traverse_dependents_preorder(dependent, padding)
4243 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004244 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004245
4246 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004247 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004248 return 0
4249
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004250 confirm_or_exit('This command will checkout all dependent branches and run '
4251 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004252
andybons@chromium.org962f9462016-02-03 20:00:42 +00004253 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004254 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004255 args.extend(['-t', 'Updated patchset dependency'])
4256
rmistry@google.com2dd99862015-06-22 12:22:18 +00004257 # Record all dependents that failed to upload.
4258 failures = {}
4259 # Go through all dependents, checkout the branch and upload.
4260 try:
4261 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004262 print()
4263 print('--------------------------------------')
4264 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004265 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004266 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004267 try:
4268 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004269 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004270 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004271 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004272 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004273 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004274 finally:
4275 # Swap back to the original root branch.
4276 RunGit(['checkout', '-q', root_branch])
4277
vapiera7fbd5a2016-06-16 09:17:49 -07004278 print()
4279 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004280 for dependent_branch in dependents:
4281 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004282 print(' %s : %s' % (dependent_branch, upload_status))
4283 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004284
4285 return 0
4286
4287
kmarshall3bff56b2016-06-06 18:31:47 -07004288def CMDarchive(parser, args):
4289 """Archives and deletes branches associated with closed changelists."""
4290 parser.add_option(
4291 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004292 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004293 parser.add_option(
4294 '-f', '--force', action='store_true',
4295 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004296 parser.add_option(
4297 '-d', '--dry-run', action='store_true',
4298 help='Skip the branch tagging and removal steps.')
4299 parser.add_option(
4300 '-t', '--notags', action='store_true',
4301 help='Do not tag archived branches. '
4302 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004303
4304 auth.add_auth_options(parser)
4305 options, args = parser.parse_args(args)
4306 if args:
4307 parser.error('Unsupported args: %s' % ' '.join(args))
4308 auth_config = auth.extract_auth_config_from_options(options)
4309
4310 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4311 if not branches:
4312 return 0
4313
vapiera7fbd5a2016-06-16 09:17:49 -07004314 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004315 changes = [Changelist(branchref=b, auth_config=auth_config)
4316 for b in branches.splitlines()]
4317 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4318 statuses = get_cl_statuses(changes,
4319 fine_grained=True,
4320 max_processes=options.maxjobs)
4321 proposal = [(cl.GetBranch(),
4322 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4323 for cl, status in statuses
4324 if status == 'closed']
4325 proposal.sort()
4326
4327 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004328 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004329 return 0
4330
4331 current_branch = GetCurrentBranch()
4332
vapiera7fbd5a2016-06-16 09:17:49 -07004333 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004334 if options.notags:
4335 for next_item in proposal:
4336 print(' ' + next_item[0])
4337 else:
4338 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4339 for next_item in proposal:
4340 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004341
kmarshall9249e012016-08-23 12:02:16 -07004342 # Quit now on precondition failure or if instructed by the user, either
4343 # via an interactive prompt or by command line flags.
4344 if options.dry_run:
4345 print('\nNo changes were made (dry run).\n')
4346 return 0
4347 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004348 print('You are currently on a branch \'%s\' which is associated with a '
4349 'closed codereview issue, so archive cannot proceed. Please '
4350 'checkout another branch and run this command again.' %
4351 current_branch)
4352 return 1
kmarshall9249e012016-08-23 12:02:16 -07004353 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004354 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4355 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004356 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004357 return 1
4358
4359 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004360 if not options.notags:
4361 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004362 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004363
vapiera7fbd5a2016-06-16 09:17:49 -07004364 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004365
4366 return 0
4367
4368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004369def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004370 """Show status of changelists.
4371
4372 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004373 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004374 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004375 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004376 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004377 - Magenta in the commit queue
4378 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004379 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004380
4381 Also see 'git cl comments'.
4382 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004383 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004384 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004385 parser.add_option('-f', '--fast', action='store_true',
4386 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004387 parser.add_option(
4388 '-j', '--maxjobs', action='store', type=int,
4389 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004390
4391 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004392 _add_codereview_issue_select_options(
4393 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004394 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004395 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004396 if args:
4397 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004398 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004399
iannuccie53c9352016-08-17 14:40:40 -07004400 if options.issue is not None and not options.field:
4401 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004402
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004403 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004404 cl = Changelist(auth_config=auth_config, issue=options.issue,
4405 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004406 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004407 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004408 elif options.field == 'id':
4409 issueid = cl.GetIssue()
4410 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004411 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004412 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004413 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004414 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004415 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004416 elif options.field == 'status':
4417 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004418 elif options.field == 'url':
4419 url = cl.GetIssueURL()
4420 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004421 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004422 return 0
4423
4424 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4425 if not branches:
4426 print('No local branch found.')
4427 return 0
4428
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004429 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004430 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004431 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004432 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004433 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004434 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004435 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004436
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004437 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004438 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4439 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4440 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004441 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004442 c, status = output.next()
4443 branch_statuses[c.GetBranch()] = status
4444 status = branch_statuses.pop(branch)
4445 url = cl.GetIssueURL()
4446 if url and (not status or status == 'error'):
4447 # The issue probably doesn't exist anymore.
4448 url += ' (broken)'
4449
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004450 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004451 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004452 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004453 color = ''
4454 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004455 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004456 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004457 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004458 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004459
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004460
4461 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004462 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004463 print('Current branch: %s' % branch)
4464 for cl in changes:
4465 if cl.GetBranch() == branch:
4466 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004467 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004468 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004469 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004470 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004471 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004472 print('Issue description:')
4473 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004474 return 0
4475
4476
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004477def colorize_CMDstatus_doc():
4478 """To be called once in main() to add colors to git cl status help."""
4479 colors = [i for i in dir(Fore) if i[0].isupper()]
4480
4481 def colorize_line(line):
4482 for color in colors:
4483 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004484 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004485 indent = len(line) - len(line.lstrip(' ')) + 1
4486 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4487 return line
4488
4489 lines = CMDstatus.__doc__.splitlines()
4490 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4491
4492
phajdan.jre328cf92016-08-22 04:12:17 -07004493def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004494 if path == '-':
4495 json.dump(contents, sys.stdout)
4496 else:
4497 with open(path, 'w') as f:
4498 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004499
4500
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004501@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004502def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004503 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004504
4505 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004506 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004507 parser.add_option('-r', '--reverse', action='store_true',
4508 help='Lookup the branch(es) for the specified issues. If '
4509 'no issues are specified, all branches with mapped '
4510 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004511 parser.add_option('--json',
4512 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004513 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004514 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004515 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004516
dnj@chromium.org406c4402015-03-03 17:22:28 +00004517 if options.reverse:
4518 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004519 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004520 # Reverse issue lookup.
4521 issue_branch_map = {}
4522 for branch in branches:
4523 cl = Changelist(branchref=branch)
4524 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4525 if not args:
4526 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004527 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004528 for issue in args:
4529 if not issue:
4530 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004531 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004532 print('Branch for issue number %s: %s' % (
4533 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004534 if options.json:
4535 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004536 return 0
4537
4538 if len(args) > 0:
4539 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4540 if not issue.valid:
4541 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4542 'or no argument to list it.\n'
4543 'Maybe you want to run git cl status?')
4544 cl = Changelist(codereview=issue.codereview)
4545 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004546 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004547 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004548 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4549 if options.json:
4550 write_json(options.json, {
4551 'issue': cl.GetIssue(),
4552 'issue_url': cl.GetIssueURL(),
4553 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004554 return 0
4555
4556
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004557def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004558 """Shows or posts review comments for any changelist."""
4559 parser.add_option('-a', '--add-comment', dest='comment',
4560 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004561 parser.add_option('-i', '--issue', dest='issue',
4562 help='review issue id (defaults to current issue). '
4563 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004564 parser.add_option('-m', '--machine-readable', dest='readable',
4565 action='store_false', default=True,
4566 help='output comments in a format compatible with '
4567 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004568 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004569 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004570 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004571 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004572 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004573 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004574 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004575
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004576 issue = None
4577 if options.issue:
4578 try:
4579 issue = int(options.issue)
4580 except ValueError:
4581 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004582 if not options.forced_codereview:
4583 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004584
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004585 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004586 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004587 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004588
4589 if options.comment:
4590 cl.AddComment(options.comment)
4591 return 0
4592
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004593 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4594 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004595 for comment in summary:
4596 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004597 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004598 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004599 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004600 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004601 color = Fore.MAGENTA
4602 else:
4603 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004604 print('\n%s%s %s%s\n%s' % (
4605 color,
4606 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4607 comment.sender,
4608 Fore.RESET,
4609 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4610
smut@google.comc85ac942015-09-15 16:34:43 +00004611 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004612 def pre_serialize(c):
4613 dct = c.__dict__.copy()
4614 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4615 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004616 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004617 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004618 return 0
4619
4620
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004621@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004622def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004623 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004624 parser.add_option('-d', '--display', action='store_true',
4625 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004626 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004627 help='New description to set for this issue (- for stdin, '
4628 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004629 parser.add_option('-f', '--force', action='store_true',
4630 help='Delete any unpublished Gerrit edits for this issue '
4631 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004632
4633 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004634 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004635 options, args = parser.parse_args(args)
4636 _process_codereview_select_options(parser, options)
4637
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004638 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004639 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004640 target_issue_arg = ParseIssueNumberArgument(args[0],
4641 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004642 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004643 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004644
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004645 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004646
martiniss6eda05f2016-06-30 10:18:35 -07004647 kwargs = {
4648 'auth_config': auth_config,
4649 'codereview': options.forced_codereview,
4650 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004651 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004652 if target_issue_arg:
4653 kwargs['issue'] = target_issue_arg.issue
4654 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004655 if target_issue_arg.codereview and not options.forced_codereview:
4656 detected_codereview_from_url = True
4657 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004658
4659 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004660 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004661 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004662 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004663
4664 if detected_codereview_from_url:
4665 logging.info('canonical issue/change URL: %s (type: %s)\n',
4666 cl.GetIssueURL(), target_issue_arg.codereview)
4667
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004668 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004669
smut@google.com34fb6b12015-07-13 20:03:26 +00004670 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004671 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004672 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004673
4674 if options.new_description:
4675 text = options.new_description
4676 if text == '-':
4677 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004678 elif text == '+':
4679 base_branch = cl.GetCommonAncestorWithUpstream()
4680 change = cl.GetChange(base_branch, None, local_description=True)
4681 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004682
4683 description.set_description(text)
4684 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004685 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004686
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004687 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004688 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004689 return 0
4690
4691
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004692def CreateDescriptionFromLog(args):
4693 """Pulls out the commit log to use as a base for the CL description."""
4694 log_args = []
4695 if len(args) == 1 and not args[0].endswith('.'):
4696 log_args = [args[0] + '..']
4697 elif len(args) == 1 and args[0].endswith('...'):
4698 log_args = [args[0][:-1]]
4699 elif len(args) == 2:
4700 log_args = [args[0] + '..' + args[1]]
4701 else:
4702 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004703 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004704
4705
thestig@chromium.org44202a22014-03-11 19:22:18 +00004706def CMDlint(parser, args):
4707 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004708 parser.add_option('--filter', action='append', metavar='-x,+y',
4709 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004710 auth.add_auth_options(parser)
4711 options, args = parser.parse_args(args)
4712 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004713
4714 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004715 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004716 try:
4717 import cpplint
4718 import cpplint_chromium
4719 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004720 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004721 return 1
4722
4723 # Change the current working directory before calling lint so that it
4724 # shows the correct base.
4725 previous_cwd = os.getcwd()
4726 os.chdir(settings.GetRoot())
4727 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004728 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004729 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4730 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004731 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004732 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004733 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004734
4735 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004736 command = args + files
4737 if options.filter:
4738 command = ['--filter=' + ','.join(options.filter)] + command
4739 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004740
4741 white_regex = re.compile(settings.GetLintRegex())
4742 black_regex = re.compile(settings.GetLintIgnoreRegex())
4743 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4744 for filename in filenames:
4745 if white_regex.match(filename):
4746 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004747 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004748 else:
4749 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4750 extra_check_functions)
4751 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004752 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004753 finally:
4754 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004755 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004756 if cpplint._cpplint_state.error_count != 0:
4757 return 1
4758 return 0
4759
4760
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004761def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004762 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004763 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004764 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004765 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004766 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004767 parser.add_option('--all', action='store_true',
4768 help='Run checks against all files, not just modified ones')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004769 auth.add_auth_options(parser)
4770 options, args = parser.parse_args(args)
4771 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004772
sbc@chromium.org71437c02015-04-09 19:29:40 +00004773 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004774 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004775 return 1
4776
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004777 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004778 if args:
4779 base_branch = args[0]
4780 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004781 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004782 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004783
Aaron Gable8076c282017-11-29 14:39:41 -08004784 if options.all:
4785 base_change = cl.GetChange(base_branch, None)
4786 files = [('M', f) for f in base_change.AllFiles()]
4787 change = presubmit_support.GitChange(
4788 base_change.Name(),
4789 base_change.FullDescriptionText(),
4790 base_change.RepositoryRoot(),
4791 files,
4792 base_change.issue,
4793 base_change.patchset,
4794 base_change.author_email,
4795 base_change._upstream)
4796 else:
4797 change = cl.GetChange(base_branch, None)
4798
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004799 cl.RunHook(
4800 committing=not options.upload,
4801 may_prompt=False,
4802 verbose=options.verbose,
Aaron Gable8076c282017-11-29 14:39:41 -08004803 change=change)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004804 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004805
4806
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004807def GenerateGerritChangeId(message):
4808 """Returns Ixxxxxx...xxx change id.
4809
4810 Works the same way as
4811 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4812 but can be called on demand on all platforms.
4813
4814 The basic idea is to generate git hash of a state of the tree, original commit
4815 message, author/committer info and timestamps.
4816 """
4817 lines = []
4818 tree_hash = RunGitSilent(['write-tree'])
4819 lines.append('tree %s' % tree_hash.strip())
4820 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4821 if code == 0:
4822 lines.append('parent %s' % parent.strip())
4823 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4824 lines.append('author %s' % author.strip())
4825 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4826 lines.append('committer %s' % committer.strip())
4827 lines.append('')
4828 # Note: Gerrit's commit-hook actually cleans message of some lines and
4829 # whitespace. This code is not doing this, but it clearly won't decrease
4830 # entropy.
4831 lines.append(message)
4832 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4833 stdin='\n'.join(lines))
4834 return 'I%s' % change_hash.strip()
4835
4836
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004837def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004838 """Computes the remote branch ref to use for the CL.
4839
4840 Args:
4841 remote (str): The git remote for the CL.
4842 remote_branch (str): The git remote branch for the CL.
4843 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004844 """
4845 if not (remote and remote_branch):
4846 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004847
wittman@chromium.org455dc922015-01-26 20:15:50 +00004848 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004849 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004850 # refs, which are then translated into the remote full symbolic refs
4851 # below.
4852 if '/' not in target_branch:
4853 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4854 else:
4855 prefix_replacements = (
4856 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4857 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4858 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4859 )
4860 match = None
4861 for regex, replacement in prefix_replacements:
4862 match = re.search(regex, target_branch)
4863 if match:
4864 remote_branch = target_branch.replace(match.group(0), replacement)
4865 break
4866 if not match:
4867 # This is a branch path but not one we recognize; use as-is.
4868 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004869 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4870 # Handle the refs that need to land in different refs.
4871 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004872
wittman@chromium.org455dc922015-01-26 20:15:50 +00004873 # Create the true path to the remote branch.
4874 # Does the following translation:
4875 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4876 # * refs/remotes/origin/master -> refs/heads/master
4877 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4878 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4879 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4880 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4881 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4882 'refs/heads/')
4883 elif remote_branch.startswith('refs/remotes/branch-heads'):
4884 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004885
wittman@chromium.org455dc922015-01-26 20:15:50 +00004886 return remote_branch
4887
4888
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004889def cleanup_list(l):
4890 """Fixes a list so that comma separated items are put as individual items.
4891
4892 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4893 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4894 """
4895 items = sum((i.split(',') for i in l), [])
4896 stripped_items = (i.strip() for i in items)
4897 return sorted(filter(None, stripped_items))
4898
4899
Aaron Gable4db38df2017-11-03 14:59:07 -07004900@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004901def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004902 """Uploads the current changelist to codereview.
4903
4904 Can skip dependency patchset uploads for a branch by running:
4905 git config branch.branch_name.skip-deps-uploads True
4906 To unset run:
4907 git config --unset branch.branch_name.skip-deps-uploads
4908 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004909
4910 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4911 a bug number, this bug number is automatically populated in the CL
4912 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004913
4914 If subject contains text in square brackets or has "<text>: " prefix, such
4915 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4916 [git-cl] add support for hashtags
4917 Foo bar: implement foo
4918 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004919 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004920 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4921 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004922 parser.add_option('--bypass-watchlists', action='store_true',
4923 dest='bypass_watchlists',
4924 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004925 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004926 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004927 parser.add_option('--message', '-m', dest='message',
4928 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004929 parser.add_option('-b', '--bug',
4930 help='pre-populate the bug number(s) for this issue. '
4931 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004932 parser.add_option('--message-file', dest='message_file',
4933 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004934 parser.add_option('--title', '-t', dest='title',
4935 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004936 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004937 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004938 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004939 parser.add_option('--tbrs',
4940 action='append', default=[],
4941 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004942 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004943 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004944 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004945 parser.add_option('--hashtag', dest='hashtags',
4946 action='append', default=[],
4947 help=('Gerrit hashtag for new CL; '
4948 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004949 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004950 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004951 parser.add_option('--emulate_svn_auto_props',
4952 '--emulate-svn-auto-props',
4953 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004954 dest="emulate_svn_auto_props",
4955 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004956 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004957 help='tell the commit queue to commit this patchset; '
4958 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004959 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004960 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004961 metavar='TARGET',
4962 help='Apply CL to remote ref TARGET. ' +
4963 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004964 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004965 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004966 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004967 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004968 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004969 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004970 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4971 const='TBR', help='add a set of OWNERS to TBR')
4972 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4973 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004974 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4975 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004976 help='Send the patchset to do a CQ dry run right after '
4977 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004978 parser.add_option('--dependencies', action='store_true',
4979 help='Uploads CLs of all the local branches that depend on '
4980 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004981
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004982 # TODO: remove Rietveld flags
4983 parser.add_option('--private', action='store_true',
4984 help='set the review private (rietveld only)')
4985 parser.add_option('--email', default=None,
4986 help='email address to use to connect to Rietveld')
4987
rmistry@google.com2dd99862015-06-22 12:22:18 +00004988 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004989 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004990 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004991 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004992 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004993 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004994
sbc@chromium.org71437c02015-04-09 19:29:40 +00004995 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004996 return 1
4997
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004998 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004999 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005000 options.cc = cleanup_list(options.cc)
5001
tandriib80458a2016-06-23 12:20:07 -07005002 if options.message_file:
5003 if options.message:
5004 parser.error('only one of --message and --message-file allowed.')
5005 options.message = gclient_utils.FileRead(options.message_file)
5006 options.message_file = None
5007
tandrii4d0545a2016-07-06 03:56:49 -07005008 if options.cq_dry_run and options.use_commit_queue:
5009 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5010
Aaron Gableedbc4132017-09-11 13:22:28 -07005011 if options.use_commit_queue:
5012 options.send_mail = True
5013
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005014 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5015 settings.GetIsGerrit()
5016
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005017 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005018 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005019
5020
Francois Dorayd42c6812017-05-30 15:10:20 -04005021@subcommand.usage('--description=<description file>')
5022def CMDsplit(parser, args):
5023 """Splits a branch into smaller branches and uploads CLs.
5024
5025 Creates a branch and uploads a CL for each group of files modified in the
5026 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005027 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005028 the shared OWNERS file.
5029 """
5030 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005031 help="A text file containing a CL description in which "
5032 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005033 parser.add_option("-c", "--comment", dest="comment_file",
5034 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005035 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5036 default=False,
5037 help="List the files and reviewers for each CL that would "
5038 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005039 options, _ = parser.parse_args(args)
5040
5041 if not options.description_file:
5042 parser.error('No --description flag specified.')
5043
5044 def WrappedCMDupload(args):
5045 return CMDupload(OptionParser(), args)
5046
5047 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005048 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005049
5050
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005051@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005052def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005053 """DEPRECATED: Used to commit the current changelist via git-svn."""
5054 message = ('git-cl no longer supports committing to SVN repositories via '
5055 'git-svn. You probably want to use `git cl land` instead.')
5056 print(message)
5057 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005058
5059
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005060# Two special branches used by git cl land.
5061MERGE_BRANCH = 'git-cl-commit'
5062CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5063
5064
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005065@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005066def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005067 """Commits the current changelist via git.
5068
5069 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5070 upstream and closes the issue automatically and atomically.
5071
5072 Otherwise (in case of Rietveld):
5073 Squashes branch into a single commit.
5074 Updates commit message with metadata (e.g. pointer to review).
5075 Pushes the code upstream.
5076 Updates review and closes.
5077 """
5078 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5079 help='bypass upload presubmit hook')
5080 parser.add_option('-m', dest='message',
5081 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005082 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005083 help="force yes to questions (don't prompt)")
5084 parser.add_option('-c', dest='contributor',
5085 help="external contributor for patch (appended to " +
5086 "description and used as author for git). Should be " +
5087 "formatted as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005088 auth.add_auth_options(parser)
5089 (options, args) = parser.parse_args(args)
5090 auth_config = auth.extract_auth_config_from_options(options)
5091
5092 cl = Changelist(auth_config=auth_config)
5093
5094 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
5095 if cl.IsGerrit():
5096 if options.message:
5097 # This could be implemented, but it requires sending a new patch to
5098 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5099 # Besides, Gerrit has the ability to change the commit message on submit
5100 # automatically, thus there is no need to support this option (so far?).
5101 parser.error('-m MESSAGE option is not supported for Gerrit.')
5102 if options.contributor:
5103 parser.error(
5104 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5105 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5106 'the contributor\'s "name <email>". If you can\'t upload such a '
5107 'commit for review, contact your repository admin and request'
5108 '"Forge-Author" permission.')
5109 if not cl.GetIssue():
5110 DieWithError('You must upload the change first to Gerrit.\n'
5111 ' If you would rather have `git cl land` upload '
5112 'automatically for you, see http://crbug.com/642759')
5113 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
5114 options.verbose)
5115
5116 current = cl.GetBranch()
5117 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
5118 if remote == '.':
5119 print()
5120 print('Attempting to push branch %r into another local branch!' % current)
5121 print()
5122 print('Either reparent this branch on top of origin/master:')
5123 print(' git reparent-branch --root')
5124 print()
5125 print('OR run `git rebase-update` if you think the parent branch is ')
5126 print('already committed.')
5127 print()
5128 print(' Current parent: %r' % upstream_branch)
5129 return 1
5130
5131 if not args:
5132 # Default to merging against our best guess of the upstream branch.
5133 args = [cl.GetUpstreamBranch()]
5134
5135 if options.contributor:
5136 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005137 print("Please provide contributor as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005138 return 1
5139
5140 base_branch = args[0]
5141
5142 if git_common.is_dirty_git_tree('land'):
5143 return 1
5144
5145 # This rev-list syntax means "show all commits not in my branch that
5146 # are in base_branch".
5147 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
5148 base_branch]).splitlines()
5149 if upstream_commits:
5150 print('Base branch "%s" has %d commits '
5151 'not in this branch.' % (base_branch, len(upstream_commits)))
5152 print('Run "git merge %s" before attempting to land.' % base_branch)
5153 return 1
5154
5155 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
5156 if not options.bypass_hooks:
5157 author = None
5158 if options.contributor:
5159 author = re.search(r'\<(.*)\>', options.contributor).group(1)
5160 hook_results = cl.RunHook(
5161 committing=True,
5162 may_prompt=not options.force,
5163 verbose=options.verbose,
5164 change=cl.GetChange(merge_base, author))
5165 if not hook_results.should_continue():
5166 return 1
5167
5168 # Check the tree status if the tree status URL is set.
5169 status = GetTreeStatus()
5170 if 'closed' == status:
5171 print('The tree is closed. Please wait for it to reopen. Use '
5172 '"git cl land --bypass-hooks" to commit on a closed tree.')
5173 return 1
5174 elif 'unknown' == status:
5175 print('Unable to determine tree status. Please verify manually and '
5176 'use "git cl land --bypass-hooks" to commit on a closed tree.')
5177 return 1
5178
5179 change_desc = ChangeDescription(options.message)
5180 if not change_desc.description and cl.GetIssue():
5181 change_desc = ChangeDescription(cl.GetDescription())
5182
5183 if not change_desc.description:
5184 if not cl.GetIssue() and options.bypass_hooks:
5185 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
5186 else:
5187 print('No description set.')
5188 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
5189 return 1
5190
5191 # Keep a separate copy for the commit message, because the commit message
5192 # contains the link to the Rietveld issue, while the Rietveld message contains
5193 # the commit viewvc url.
5194 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07005195 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005196 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005197
5198 commit_desc = ChangeDescription(change_desc.description)
5199 if cl.GetIssue():
5200 # Xcode won't linkify this URL unless there is a non-whitespace character
5201 # after it. Add a period on a new line to circumvent this. Also add a space
5202 # before the period to make sure that Gitiles continues to correctly resolve
5203 # the URL.
5204 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
5205 if options.contributor:
5206 commit_desc.append_footer('Patch from %s.' % options.contributor)
5207
5208 print('Description:')
5209 print(commit_desc.description)
5210
5211 branches = [merge_base, cl.GetBranchRef()]
5212 if not options.force:
Aaron Gable13101a62018-02-09 13:20:41 -08005213 print_stats(branches)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005214
5215 # We want to squash all this branch's commits into one commit with the proper
5216 # description. We do this by doing a "reset --soft" to the base branch (which
5217 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005218 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005219 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
5220 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
5221 result = RunGitWithCode(showref_cmd)
5222 if result[0] == 0:
5223 RunGit(['branch', '-D', branch])
5224
5225 # We might be in a directory that's present in this branch but not in the
5226 # trunk. Move up to the top of the tree so that git commands that expect a
5227 # valid CWD won't fail after we check out the merge branch.
5228 rel_base_path = settings.GetRelativeRoot()
5229 if rel_base_path:
5230 os.chdir(rel_base_path)
5231
5232 # Stuff our change into the merge branch.
5233 # We wrap in a try...finally block so if anything goes wrong,
5234 # we clean up the branches.
5235 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005236 revision = None
5237 try:
5238 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5239 RunGit(['reset', '--soft', merge_base])
5240 if options.contributor:
5241 RunGit(
5242 [
5243 'commit', '--author', options.contributor,
5244 '-m', commit_desc.description,
5245 ])
5246 else:
5247 RunGit(['commit', '-m', commit_desc.description])
5248
5249 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5250 mirror = settings.GetGitMirror(remote)
5251 if mirror:
5252 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005253 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005254 else:
5255 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005256 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005257 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5258
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005259 retcode = PushToGitWithAutoRebase(
5260 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005261 if retcode == 0:
5262 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005263 if git_numberer_enabled:
5264 change_desc = ChangeDescription(
5265 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005266 except: # pylint: disable=bare-except
5267 if _IS_BEING_TESTED:
5268 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5269 + '-' * 30 + '8<' + '-' * 30)
5270 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5271 raise
5272 finally:
5273 # And then swap back to the original branch and clean up.
5274 RunGit(['checkout', '-q', cl.GetBranch()])
5275 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005276 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005277
5278 if not revision:
5279 print('Failed to push. If this persists, please file a bug.')
5280 return 1
5281
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005282 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005283 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005284 if viewvc_url and revision:
5285 change_desc.append_footer(
5286 'Committed: %s%s' % (viewvc_url, revision))
5287 elif revision:
5288 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005289 print('Closing issue '
5290 '(you may be prompted for your codereview password)...')
5291 cl.UpdateDescription(change_desc.description)
5292 cl.CloseIssue()
5293 props = cl.GetIssueProperties()
5294 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005295 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5296 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005297 if options.bypass_hooks:
5298 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5299 else:
5300 comment += ' (presubmit successful).'
5301 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5302
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005303 if os.path.isfile(POSTUPSTREAM_HOOK):
5304 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5305
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005306 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005307
5308
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005309def PushToGitWithAutoRebase(remote, branch, original_description,
5310 git_numberer_enabled, max_attempts=3):
5311 """Pushes current HEAD commit on top of remote's branch.
5312
5313 Attempts to fetch and autorebase on push failures.
5314 Adds git number footers on the fly.
5315
5316 Returns integer code from last command.
5317 """
5318 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5319 code = 0
5320 attempts_left = max_attempts
5321 while attempts_left:
5322 attempts_left -= 1
5323 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5324
5325 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5326 # If fetch fails, retry.
5327 print('Fetching %s/%s...' % (remote, branch))
5328 code, out = RunGitWithCode(
5329 ['retry', 'fetch', remote,
5330 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5331 if code:
5332 print('Fetch failed with exit code %d.' % code)
5333 print(out.strip())
5334 continue
5335
5336 print('Cherry-picking commit on top of latest %s' % branch)
5337 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5338 suppress_stderr=True)
5339 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5340 code, out = RunGitWithCode(['cherry-pick', cherry])
5341 if code:
5342 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5343 'the following files have merge conflicts:' %
5344 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005345 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5346 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005347 print('Please rebase your patch and try again.')
5348 RunGitWithCode(['cherry-pick', '--abort'])
5349 break
5350
5351 commit_desc = ChangeDescription(original_description)
5352 if git_numberer_enabled:
5353 logging.debug('Adding git number footers')
5354 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5355 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5356 branch)
5357 # Ensure timestamps are monotonically increasing.
5358 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5359 _get_committer_timestamp('HEAD'))
5360 _git_amend_head(commit_desc.description, timestamp)
5361
5362 code, out = RunGitWithCode(
5363 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5364 print(out)
5365 if code == 0:
5366 break
5367 if IsFatalPushFailure(out):
5368 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005369 'user.email are correct and you have push access to the repo.\n'
5370 'Hint: run command below to diangose common Git/Gerrit credential '
5371 'problems:\n'
5372 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005373 break
5374 return code
5375
5376
5377def IsFatalPushFailure(push_stdout):
5378 """True if retrying push won't help."""
5379 return '(prohibited by Gerrit)' in push_stdout
5380
5381
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005382@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005383def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005384 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005385 parser.add_option('-b', dest='newbranch',
5386 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005387 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005388 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005389 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005390 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005391 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005392 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005393 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005394 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005395 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005396 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005397
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005398
5399 group = optparse.OptionGroup(
5400 parser,
5401 'Options for continuing work on the current issue uploaded from a '
5402 'different clone (e.g. different machine). Must be used independently '
5403 'from the other options. No issue number should be specified, and the '
5404 'branch must have an issue number associated with it')
5405 group.add_option('--reapply', action='store_true', dest='reapply',
5406 help='Reset the branch and reapply the issue.\n'
5407 'CAUTION: This will undo any local changes in this '
5408 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005409
5410 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005411 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005412 parser.add_option_group(group)
5413
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005414 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005415 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005416 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005417 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005418 auth_config = auth.extract_auth_config_from_options(options)
5419
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005420 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005421 if options.newbranch:
5422 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005423 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005424 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005425
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005426 cl = Changelist(auth_config=auth_config,
5427 codereview=options.forced_codereview)
5428 if not cl.GetIssue():
5429 parser.error('current branch must have an associated issue')
5430
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005431 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005432 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005433 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005434
5435 RunGit(['reset', '--hard', upstream])
5436 if options.pull:
5437 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005438
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005439 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5440 options.directory)
5441
5442 if len(args) != 1 or not args[0]:
5443 parser.error('Must specify issue number or url')
5444
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005445 target_issue_arg = ParseIssueNumberArgument(args[0],
5446 options.forced_codereview)
5447 if not target_issue_arg.valid:
5448 parser.error('invalid codereview url or CL id')
5449
5450 cl_kwargs = {
5451 'auth_config': auth_config,
5452 'codereview_host': target_issue_arg.hostname,
5453 'codereview': options.forced_codereview,
5454 }
5455 detected_codereview_from_url = False
5456 if target_issue_arg.codereview and not options.forced_codereview:
5457 detected_codereview_from_url = True
5458 cl_kwargs['codereview'] = target_issue_arg.codereview
5459 cl_kwargs['issue'] = target_issue_arg.issue
5460
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005461 # We don't want uncommitted changes mixed up with the patch.
5462 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005463 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005464
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005465 if options.newbranch:
5466 if options.force:
5467 RunGit(['branch', '-D', options.newbranch],
5468 stderr=subprocess2.PIPE, error_ok=True)
5469 RunGit(['new-branch', options.newbranch])
5470
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005471 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005472
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005473 if cl.IsGerrit():
5474 if options.reject:
5475 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005476 if options.directory:
5477 parser.error('--directory is not supported with Gerrit codereview.')
5478
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005479 if detected_codereview_from_url:
5480 print('canonical issue/change URL: %s (type: %s)\n' %
5481 (cl.GetIssueURL(), target_issue_arg.codereview))
5482
5483 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005484 options.nocommit, options.directory,
5485 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005486
5487
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005488def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005489 """Fetches the tree status and returns either 'open', 'closed',
5490 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005491 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005492 if url:
5493 status = urllib2.urlopen(url).read().lower()
5494 if status.find('closed') != -1 or status == '0':
5495 return 'closed'
5496 elif status.find('open') != -1 or status == '1':
5497 return 'open'
5498 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005499 return 'unset'
5500
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005501
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005502def GetTreeStatusReason():
5503 """Fetches the tree status from a json url and returns the message
5504 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005505 url = settings.GetTreeStatusUrl()
5506 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005507 connection = urllib2.urlopen(json_url)
5508 status = json.loads(connection.read())
5509 connection.close()
5510 return status['message']
5511
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005512
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005513def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005514 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005515 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005516 status = GetTreeStatus()
5517 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005518 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005519 return 2
5520
vapiera7fbd5a2016-06-16 09:17:49 -07005521 print('The tree is %s' % status)
5522 print()
5523 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005524 if status != 'open':
5525 return 1
5526 return 0
5527
5528
maruel@chromium.org15192402012-09-06 12:38:29 +00005529def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005530 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005531 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005532 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005533 '-b', '--bot', action='append',
5534 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5535 'times to specify multiple builders. ex: '
5536 '"-b win_rel -b win_layout". See '
5537 'the try server waterfall for the builders name and the tests '
5538 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005539 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005540 '-B', '--bucket', default='',
5541 help=('Buildbucket bucket to send the try requests.'))
5542 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005543 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005544 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005545 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005546 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005547 help='Revision to use for the try job; default: the revision will '
5548 'be determined by the try recipe that builder runs, which usually '
5549 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005550 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005551 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005552 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005553 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005554 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005555 '--project',
5556 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005557 'in recipe to determine to which repository or directory to '
5558 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005559 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005560 '-p', '--property', dest='properties', action='append', default=[],
5561 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005562 'key2=value2 etc. The value will be treated as '
5563 'json if decodable, or as string otherwise. '
5564 'NOTE: using this may make your try job not usable for CQ, '
5565 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005566 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005567 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5568 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005569 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005570 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005571 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005572 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005573 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005574 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005575
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005576 if options.master and options.master.startswith('luci.'):
5577 parser.error(
5578 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005579 # Make sure that all properties are prop=value pairs.
5580 bad_params = [x for x in options.properties if '=' not in x]
5581 if bad_params:
5582 parser.error('Got properties with missing "=": %s' % bad_params)
5583
maruel@chromium.org15192402012-09-06 12:38:29 +00005584 if args:
5585 parser.error('Unknown arguments: %s' % args)
5586
Koji Ishii31c14782018-01-08 17:17:33 +09005587 cl = Changelist(auth_config=auth_config, issue=options.issue,
5588 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005589 if not cl.GetIssue():
5590 parser.error('Need to upload first')
5591
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005592 if cl.IsGerrit():
5593 # HACK: warm up Gerrit change detail cache to save on RPCs.
5594 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5595
tandriie113dfd2016-10-11 10:20:12 -07005596 error_message = cl.CannotTriggerTryJobReason()
5597 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005598 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005599
borenet6c0efe62016-10-19 08:13:29 -07005600 if options.bucket and options.master:
5601 parser.error('Only one of --bucket and --master may be used.')
5602
qyearsley1fdfcb62016-10-24 13:22:03 -07005603 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005604
qyearsleydd49f942016-10-28 11:57:22 -07005605 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5606 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005607 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005608 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005609 print('git cl try with no bots now defaults to CQ dry run.')
5610 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5611 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005612
borenet6c0efe62016-10-19 08:13:29 -07005613 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005614 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005615 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005616 'of bot requires an initial job from a parent (usually a builder). '
5617 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005618 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005619 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005620
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005621 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005622 # TODO(tandrii): Checking local patchset against remote patchset is only
5623 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5624 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005625 print('Warning: Codereview server has newer patchsets (%s) than most '
5626 'recent upload from local checkout (%s). Did a previous upload '
5627 'fail?\n'
5628 'By default, git cl try uses the latest patchset from '
5629 'codereview, continuing to use patchset %s.\n' %
5630 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005631
tandrii568043b2016-10-11 07:49:18 -07005632 try:
borenet6c0efe62016-10-19 08:13:29 -07005633 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5634 patchset)
tandrii568043b2016-10-11 07:49:18 -07005635 except BuildbucketResponseException as ex:
5636 print('ERROR: %s' % ex)
5637 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005638 return 0
5639
5640
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005641def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005642 """Prints info about try jobs associated with current CL."""
5643 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005644 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005645 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005646 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005647 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005648 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005649 '--color', action='store_true', default=setup_color.IS_TTY,
5650 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005651 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005652 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5653 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005654 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005655 '--json', help=('Path of JSON output file to write try job results to,'
5656 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005657 parser.add_option_group(group)
5658 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005659 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005660 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005661 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005662 if args:
5663 parser.error('Unrecognized args: %s' % ' '.join(args))
5664
5665 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005666 cl = Changelist(
5667 issue=options.issue, codereview=options.forced_codereview,
5668 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005669 if not cl.GetIssue():
5670 parser.error('Need to upload first')
5671
tandrii221ab252016-10-06 08:12:04 -07005672 patchset = options.patchset
5673 if not patchset:
5674 patchset = cl.GetMostRecentPatchset()
5675 if not patchset:
5676 parser.error('Codereview doesn\'t know about issue %s. '
5677 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005678 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005679 cl.GetIssue())
5680
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005681 # TODO(tandrii): Checking local patchset against remote patchset is only
5682 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5683 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005684 print('Warning: Codereview server has newer patchsets (%s) than most '
5685 'recent upload from local checkout (%s). Did a previous upload '
5686 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005687 'By default, git cl try-results uses the latest patchset from '
5688 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005689 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005690 try:
tandrii221ab252016-10-06 08:12:04 -07005691 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005692 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005693 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005694 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005695 if options.json:
5696 write_try_results_json(options.json, jobs)
5697 else:
5698 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005699 return 0
5700
5701
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005702@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005703def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005704 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005705 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005706 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005707 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005708
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005709 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005710 if args:
5711 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005712 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005713 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005714 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005715 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005716
5717 # Clear configured merge-base, if there is one.
5718 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005719 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005720 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005721 return 0
5722
5723
thestig@chromium.org00858c82013-12-02 23:08:03 +00005724def CMDweb(parser, args):
5725 """Opens the current CL in the web browser."""
5726 _, args = parser.parse_args(args)
5727 if args:
5728 parser.error('Unrecognized args: %s' % ' '.join(args))
5729
5730 issue_url = Changelist().GetIssueURL()
5731 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005732 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005733 return 1
5734
5735 webbrowser.open(issue_url)
5736 return 0
5737
5738
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005739def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005740 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005741 parser.add_option('-d', '--dry-run', action='store_true',
5742 help='trigger in dry run mode')
5743 parser.add_option('-c', '--clear', action='store_true',
5744 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005745 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005746 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005747 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005748 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005749 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005750 if args:
5751 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005752 if options.dry_run and options.clear:
5753 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5754
iannuccie53c9352016-08-17 14:40:40 -07005755 cl = Changelist(auth_config=auth_config, issue=options.issue,
5756 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005757 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005758 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005759 elif options.dry_run:
5760 state = _CQState.DRY_RUN
5761 else:
5762 state = _CQState.COMMIT
5763 if not cl.GetIssue():
5764 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005765 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005766 return 0
5767
5768
groby@chromium.org411034a2013-02-26 15:12:01 +00005769def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005770 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005771 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005772 auth.add_auth_options(parser)
5773 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005774 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005775 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005776 if args:
5777 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005778 cl = Changelist(auth_config=auth_config, issue=options.issue,
5779 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005780 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005781 if not cl.GetIssue():
5782 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005783 cl.CloseIssue()
5784 return 0
5785
5786
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005787def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005788 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005789 parser.add_option(
5790 '--stat',
5791 action='store_true',
5792 dest='stat',
5793 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005794 auth.add_auth_options(parser)
5795 options, args = parser.parse_args(args)
5796 auth_config = auth.extract_auth_config_from_options(options)
5797 if args:
5798 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005799
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005800 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005801 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005802 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005803 if not issue:
5804 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005805
Aaron Gablea718c3e2017-08-28 17:47:28 -07005806 base = cl._GitGetBranchConfigValue('last-upload-hash')
5807 if not base:
5808 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5809 if not base:
5810 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5811 revision_info = detail['revisions'][detail['current_revision']]
5812 fetch_info = revision_info['fetch']['http']
5813 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5814 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005815
Aaron Gablea718c3e2017-08-28 17:47:28 -07005816 cmd = ['git', 'diff']
5817 if options.stat:
5818 cmd.append('--stat')
5819 cmd.append(base)
5820 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005821
5822 return 0
5823
5824
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005825def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005826 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005827 parser.add_option(
5828 '--no-color',
5829 action='store_true',
5830 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005831 parser.add_option(
5832 '--batch',
5833 action='store_true',
5834 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005835 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005836 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005837 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005838
5839 author = RunGit(['config', 'user.email']).strip() or None
5840
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005841 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005842
5843 if args:
5844 if len(args) > 1:
5845 parser.error('Unknown args')
5846 base_branch = args[0]
5847 else:
5848 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005849 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005850
5851 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005852 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5853
5854 if options.batch:
5855 db = owners.Database(change.RepositoryRoot(), file, os.path)
5856 print('\n'.join(db.reviewers_for(affected_files, author)))
5857 return 0
5858
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005859 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005860 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005861 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005862 author,
5863 cl.GetReviewers(),
5864 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005865 disable_color=options.no_color,
5866 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005867
5868
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005869def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005870 """Generates a diff command."""
5871 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005872 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5873 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005874 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005875
5876 if args:
5877 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005878 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005879 diff_cmd.append(arg)
5880 else:
5881 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005882
5883 return diff_cmd
5884
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005885
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005886def MatchingFileType(file_name, extensions):
5887 """Returns true if the file name ends with one of the given extensions."""
5888 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005889
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005890
enne@chromium.org555cfe42014-01-29 18:21:39 +00005891@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005892def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005893 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005894 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005895 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005896 parser.add_option('--full', action='store_true',
5897 help='Reformat the full content of all touched files')
5898 parser.add_option('--dry-run', action='store_true',
5899 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005900 parser.add_option('--python', action='store_true',
5901 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005902 parser.add_option('--js', action='store_true',
5903 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005904 parser.add_option('--diff', action='store_true',
5905 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005906 parser.add_option('--presubmit', action='store_true',
5907 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005908 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005909
Daniel Chengc55eecf2016-12-30 03:11:02 -08005910 # Normalize any remaining args against the current path, so paths relative to
5911 # the current directory are still resolved as expected.
5912 args = [os.path.join(os.getcwd(), arg) for arg in args]
5913
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005914 # git diff generates paths against the root of the repository. Change
5915 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005916 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005917 if rel_base_path:
5918 os.chdir(rel_base_path)
5919
digit@chromium.org29e47272013-05-17 17:01:46 +00005920 # Grab the merge-base commit, i.e. the upstream commit of the current
5921 # branch when it was created or the last time it was rebased. This is
5922 # to cover the case where the user may have called "git fetch origin",
5923 # moving the origin branch to a newer commit, but hasn't rebased yet.
5924 upstream_commit = None
5925 cl = Changelist()
5926 upstream_branch = cl.GetUpstreamBranch()
5927 if upstream_branch:
5928 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5929 upstream_commit = upstream_commit.strip()
5930
5931 if not upstream_commit:
5932 DieWithError('Could not find base commit for this branch. '
5933 'Are you in detached state?')
5934
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005935 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5936 diff_output = RunGit(changed_files_cmd)
5937 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005938 # Filter out files deleted by this CL
5939 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005940
Christopher Lamc5ba6922017-01-24 11:19:14 +11005941 if opts.js:
5942 CLANG_EXTS.append('.js')
5943
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005944 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5945 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5946 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005947 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005948
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005949 top_dir = os.path.normpath(
5950 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5951
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005952 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5953 # formatted. This is used to block during the presubmit.
5954 return_value = 0
5955
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005956 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005957 # Locate the clang-format binary in the checkout
5958 try:
5959 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005960 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005961 DieWithError(e)
5962
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005963 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005964 cmd = [clang_format_tool]
5965 if not opts.dry_run and not opts.diff:
5966 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005967 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005968 if opts.diff:
5969 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005970 else:
5971 env = os.environ.copy()
5972 env['PATH'] = str(os.path.dirname(clang_format_tool))
5973 try:
5974 script = clang_format.FindClangFormatScriptInChromiumTree(
5975 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005976 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005977 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005978
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005979 cmd = [sys.executable, script, '-p0']
5980 if not opts.dry_run and not opts.diff:
5981 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005982
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005983 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5984 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005985
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005986 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5987 if opts.diff:
5988 sys.stdout.write(stdout)
5989 if opts.dry_run and len(stdout) > 0:
5990 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005991
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005992 # Similar code to above, but using yapf on .py files rather than clang-format
5993 # on C/C++ files
5994 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005995 yapf_tool = gclient_utils.FindExecutable('yapf')
5996 if yapf_tool is None:
5997 DieWithError('yapf not found in PATH')
5998
5999 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006000 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006001 cmd = [yapf_tool]
6002 if not opts.dry_run and not opts.diff:
6003 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006004 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006005 if opts.diff:
6006 sys.stdout.write(stdout)
6007 else:
6008 # TODO(sbc): yapf --lines mode still has some issues.
6009 # https://github.com/google/yapf/issues/154
6010 DieWithError('--python currently only works with --full')
6011
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006012 # Dart's formatter does not have the nice property of only operating on
6013 # modified chunks, so hard code full.
6014 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006015 try:
6016 command = [dart_format.FindDartFmtToolInChromiumTree()]
6017 if not opts.dry_run and not opts.diff:
6018 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006019 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006020
ppi@chromium.org6593d932016-03-03 15:41:15 +00006021 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006022 if opts.dry_run and stdout:
6023 return_value = 2
6024 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006025 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6026 'found in this checkout. Files in other languages are still '
6027 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006028
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006029 # Format GN build files. Always run on full build files for canonical form.
6030 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006031 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006032 if opts.dry_run or opts.diff:
6033 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006034 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006035 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6036 shell=sys.platform == 'win32',
6037 cwd=top_dir)
6038 if opts.dry_run and gn_ret == 2:
6039 return_value = 2 # Not formatted.
6040 elif opts.diff and gn_ret == 2:
6041 # TODO this should compute and print the actual diff.
6042 print("This change has GN build file diff for " + gn_diff_file)
6043 elif gn_ret != 0:
6044 # For non-dry run cases (and non-2 return values for dry-run), a
6045 # nonzero error code indicates a failure, probably because the file
6046 # doesn't parse.
6047 DieWithError("gn format failed on " + gn_diff_file +
6048 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006049
Ilya Shermane081cbe2017-08-15 17:51:04 -07006050 # Skip the metrics formatting from the global presubmit hook. These files have
6051 # a separate presubmit hook that issues an error if the files need formatting,
6052 # whereas the top-level presubmit script merely issues a warning. Formatting
6053 # these files is somewhat slow, so it's important not to duplicate the work.
6054 if not opts.presubmit:
6055 for xml_dir in GetDirtyMetricsDirs(diff_files):
6056 tool_dir = os.path.join(top_dir, xml_dir)
6057 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6058 if opts.dry_run or opts.diff:
6059 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006060 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006061 if opts.diff:
6062 sys.stdout.write(stdout)
6063 if opts.dry_run and stdout:
6064 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006065
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006066 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006067
Steven Holte2e664bf2017-04-21 13:10:47 -07006068def GetDirtyMetricsDirs(diff_files):
6069 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6070 metrics_xml_dirs = [
6071 os.path.join('tools', 'metrics', 'actions'),
6072 os.path.join('tools', 'metrics', 'histograms'),
6073 os.path.join('tools', 'metrics', 'rappor'),
6074 os.path.join('tools', 'metrics', 'ukm')]
6075 for xml_dir in metrics_xml_dirs:
6076 if any(file.startswith(xml_dir) for file in xml_diff_files):
6077 yield xml_dir
6078
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006079
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006080@subcommand.usage('<codereview url or issue id>')
6081def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006082 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006083 _, args = parser.parse_args(args)
6084
6085 if len(args) != 1:
6086 parser.print_help()
6087 return 1
6088
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006089 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006090 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006091 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006092
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006093 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006094
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006095 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006096 output = RunGit(['config', '--local', '--get-regexp',
6097 r'branch\..*\.%s' % issueprefix],
6098 error_ok=True)
6099 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006100 if issue == target_issue:
6101 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006102
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006103 branches = []
6104 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006105 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006106 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006107 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006108 return 1
6109 if len(branches) == 1:
6110 RunGit(['checkout', branches[0]])
6111 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006112 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006113 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006114 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006115 which = raw_input('Choose by index: ')
6116 try:
6117 RunGit(['checkout', branches[int(which)]])
6118 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006119 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006120 return 1
6121
6122 return 0
6123
6124
maruel@chromium.org29404b52014-09-08 22:58:00 +00006125def CMDlol(parser, args):
6126 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006127 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006128 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6129 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6130 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006131 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006132 return 0
6133
6134
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006135class OptionParser(optparse.OptionParser):
6136 """Creates the option parse and add --verbose support."""
6137 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006138 optparse.OptionParser.__init__(
6139 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006140 self.add_option(
6141 '-v', '--verbose', action='count', default=0,
6142 help='Use 2 times for more debugging info')
6143
6144 def parse_args(self, args=None, values=None):
6145 options, args = optparse.OptionParser.parse_args(self, args, values)
6146 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006147 logging.basicConfig(
6148 level=levels[min(options.verbose, len(levels) - 1)],
6149 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6150 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006151 return options, args
6152
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006154def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006155 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006156 print('\nYour python version %s is unsupported, please upgrade.\n' %
6157 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006158 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006159
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006160 # Reload settings.
6161 global settings
6162 settings = Settings()
6163
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006164 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006165 dispatcher = subcommand.CommandDispatcher(__name__)
6166 try:
6167 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006168 except auth.AuthenticationError as e:
6169 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006170 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006171 if e.code != 500:
6172 raise
6173 DieWithError(
6174 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6175 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006176 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006177
6178
6179if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006180 # These affect sys.stdout so do it outside of main() to simplify mocks in
6181 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006182 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006183 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006184 try:
6185 sys.exit(main(sys.argv[1:]))
6186 except KeyboardInterrupt:
6187 sys.stderr.write('interrupted\n')
6188 sys.exit(1)