blob: 52c9ddffbc25dc40627068df90e0323766663a32 [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 Turakulovb422e682018-02-20 22:51:30 -0800423 bucket = builders_map.get(builder, {}).get('bucket')
424 if bucket:
425 bucket_map.setdefault(bucket, {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700426 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700427
428
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800429def _trigger_try_jobs(auth_config, changelist, buckets, options, patchset):
qyearsley1fdfcb62016-10-24 13:22:03 -0700430 """Sends a request to Buildbucket to trigger try jobs for a changelist.
431
432 Args:
433 auth_config: AuthConfig for Rietveld.
434 changelist: Changelist that the try jobs are associated with.
435 buckets: A nested dict mapping bucket names to builders to tests.
436 options: Command-line options.
437 """
tandriide281ae2016-10-12 06:02:30 -0700438 assert changelist.GetIssue(), 'CL must be uploaded first'
439 codereview_url = changelist.GetCodereviewServer()
440 assert codereview_url, 'CL must be uploaded first'
441 patchset = patchset or changelist.GetMostRecentPatchset()
442 assert patchset, 'CL must be uploaded first'
443
444 codereview_host = urlparse.urlparse(codereview_url).hostname
445 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000446 http = authenticator.authorize(httplib2.Http())
447 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700448
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000449 buildbucket_put_url = (
450 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000451 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700452 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
453 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
454 hostname=codereview_host,
455 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000456 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700457
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700458 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -0800459 shared_parameters_properties['category'] = options.category
tandrii8c5a3532016-11-04 07:52:02 -0700460 if options.clobber:
461 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700462 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700463 if extra_properties:
464 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000465
466 batch_req_body = {'builds': []}
467 print_text = []
468 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700469 for bucket, builders_and_tests in sorted(buckets.iteritems()):
470 print_text.append('Bucket: %s' % bucket)
471 master = None
472 if bucket.startswith(MASTER_PREFIX):
473 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000474 for builder, tests in sorted(builders_and_tests.iteritems()):
475 print_text.append(' %s: %s' % (builder, tests))
476 parameters = {
477 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000478 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100479 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000480 'revision': options.revision,
481 }],
tandrii8c5a3532016-11-04 07:52:02 -0700482 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000484 if 'presubmit' in builder.lower():
485 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000486 if tests:
487 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700488
489 tags = [
490 'builder:%s' % builder,
491 'buildset:%s' % buildset,
492 'user_agent:git_cl_try',
493 ]
494 if master:
495 parameters['properties']['master'] = master
496 tags.append('master:%s' % master)
497
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000498 batch_req_body['builds'].append(
499 {
500 'bucket': bucket,
501 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000502 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700503 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000504 }
505 )
506
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000507 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700508 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000509 http,
510 buildbucket_put_url,
511 'PUT',
512 body=json.dumps(batch_req_body),
513 headers={'Content-Type': 'application/json'}
514 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000515 print_text.append('To see results here, run: git cl try-results')
516 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700517 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000518
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000519
tandrii221ab252016-10-06 08:12:04 -0700520def fetch_try_jobs(auth_config, changelist, buildbucket_host,
521 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700522 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000523
qyearsley53f48a12016-09-01 10:45:13 -0700524 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000525 """
tandrii221ab252016-10-06 08:12:04 -0700526 assert buildbucket_host
527 assert changelist.GetIssue(), 'CL must be uploaded first'
528 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
529 patchset = patchset or changelist.GetMostRecentPatchset()
530 assert patchset, 'CL must be uploaded first'
531
532 codereview_url = changelist.GetCodereviewServer()
533 codereview_host = urlparse.urlparse(codereview_url).hostname
534 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535 if authenticator.has_cached_credentials():
536 http = authenticator.authorize(httplib2.Http())
537 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700538 print('Warning: Some results might be missing because %s' %
539 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700540 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000541 http = httplib2.Http()
542
543 http.force_exception_to_status_code = True
544
tandrii221ab252016-10-06 08:12:04 -0700545 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
546 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
547 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000548 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700549 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000550 params = {'tag': 'buildset:%s' % buildset}
551
552 builds = {}
553 while True:
554 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700555 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700557 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000558 for build in content.get('builds', []):
559 builds[build['id']] = build
560 if 'next_cursor' in content:
561 params['start_cursor'] = content['next_cursor']
562 else:
563 break
564 return builds
565
566
qyearsleyeab3c042016-08-24 09:18:28 -0700567def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000568 """Prints nicely result of fetch_try_jobs."""
569 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700570 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000571 return
572
573 # Make a copy, because we'll be modifying builds dictionary.
574 builds = builds.copy()
575 builder_names_cache = {}
576
577 def get_builder(b):
578 try:
579 return builder_names_cache[b['id']]
580 except KeyError:
581 try:
582 parameters = json.loads(b['parameters_json'])
583 name = parameters['builder_name']
584 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700585 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700586 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000587 name = None
588 builder_names_cache[b['id']] = name
589 return name
590
591 def get_bucket(b):
592 bucket = b['bucket']
593 if bucket.startswith('master.'):
594 return bucket[len('master.'):]
595 return bucket
596
597 if options.print_master:
598 name_fmt = '%%-%ds %%-%ds' % (
599 max(len(str(get_bucket(b))) for b in builds.itervalues()),
600 max(len(str(get_builder(b))) for b in builds.itervalues()))
601 def get_name(b):
602 return name_fmt % (get_bucket(b), get_builder(b))
603 else:
604 name_fmt = '%%-%ds' % (
605 max(len(str(get_builder(b))) for b in builds.itervalues()))
606 def get_name(b):
607 return name_fmt % get_builder(b)
608
609 def sort_key(b):
610 return b['status'], b.get('result'), get_name(b), b.get('url')
611
612 def pop(title, f, color=None, **kwargs):
613 """Pop matching builds from `builds` dict and print them."""
614
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000615 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000616 colorize = str
617 else:
618 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
619
620 result = []
621 for b in builds.values():
622 if all(b.get(k) == v for k, v in kwargs.iteritems()):
623 builds.pop(b['id'])
624 result.append(b)
625 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700626 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000627 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700628 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000629
630 total = len(builds)
631 pop(status='COMPLETED', result='SUCCESS',
632 title='Successes:', color=Fore.GREEN,
633 f=lambda b: (get_name(b), b.get('url')))
634 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
635 title='Infra Failures:', color=Fore.MAGENTA,
636 f=lambda b: (get_name(b), b.get('url')))
637 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
638 title='Failures:', color=Fore.RED,
639 f=lambda b: (get_name(b), b.get('url')))
640 pop(status='COMPLETED', result='CANCELED',
641 title='Canceled:', color=Fore.MAGENTA,
642 f=lambda b: (get_name(b),))
643 pop(status='COMPLETED', result='FAILURE',
644 failure_reason='INVALID_BUILD_DEFINITION',
645 title='Wrong master/builder name:', color=Fore.MAGENTA,
646 f=lambda b: (get_name(b),))
647 pop(status='COMPLETED', result='FAILURE',
648 title='Other failures:',
649 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
650 pop(status='COMPLETED',
651 title='Other finished:',
652 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
653 pop(status='STARTED',
654 title='Started:', color=Fore.YELLOW,
655 f=lambda b: (get_name(b), b.get('url')))
656 pop(status='SCHEDULED',
657 title='Scheduled:',
658 f=lambda b: (get_name(b), 'id=%s' % b['id']))
659 # The last section is just in case buildbucket API changes OR there is a bug.
660 pop(title='Other:',
661 f=lambda b: (get_name(b), 'id=%s' % b['id']))
662 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700663 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000664
665
qyearsley53f48a12016-09-01 10:45:13 -0700666def write_try_results_json(output_file, builds):
667 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
668
669 The input |builds| dict is assumed to be generated by Buildbucket.
670 Buildbucket documentation: http://goo.gl/G0s101
671 """
672
673 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800674 """Extracts some of the information from one build dict."""
675 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700676 return {
677 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700678 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800679 'builder_name': parameters.get('builder_name'),
680 'created_ts': build.get('created_ts'),
681 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700682 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800683 'result': build.get('result'),
684 'status': build.get('status'),
685 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700686 'url': build.get('url'),
687 }
688
689 converted = []
690 for _, build in sorted(builds.items()):
691 converted.append(convert_build_dict(build))
692 write_json(output_file, converted)
693
694
Aaron Gable13101a62018-02-09 13:20:41 -0800695def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000696 """Prints statistics about the change to the user."""
697 # --no-ext-diff is broken in some versions of Git, so try to work around
698 # this by overriding the environment (but there is still a problem if the
699 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000700 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000701 if 'GIT_EXTERNAL_DIFF' in env:
702 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000703
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000704 try:
705 stdout = sys.stdout.fileno()
706 except AttributeError:
707 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000708 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800709 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000710 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000711
712
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000713class BuildbucketResponseException(Exception):
714 pass
715
716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717class Settings(object):
718 def __init__(self):
719 self.default_server = None
720 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000721 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722 self.tree_status_url = None
723 self.viewvc_url = None
724 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000725 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000726 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000727 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000728 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000729 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000730 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731
732 def LazyUpdateIfNeeded(self):
733 """Updates the settings from a codereview.settings file, if available."""
734 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000735 # The only value that actually changes the behavior is
736 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000737 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000738 error_ok=True
739 ).strip().lower()
740
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000741 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000742 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 LoadCodereviewSettingsFromFile(cr_settings_file)
744 self.updated = True
745
746 def GetDefaultServerUrl(self, error_ok=False):
747 if not self.default_server:
748 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000749 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000750 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751 if error_ok:
752 return self.default_server
753 if not self.default_server:
754 error_message = ('Could not find settings file. You must configure '
755 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000756 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000757 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758 return self.default_server
759
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000760 @staticmethod
761 def GetRelativeRoot():
762 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000763
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000765 if self.root is None:
766 self.root = os.path.abspath(self.GetRelativeRoot())
767 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000769 def GetGitMirror(self, remote='origin'):
770 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000771 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000772 if not os.path.isdir(local_url):
773 return None
774 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
775 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100776 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100777 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000778 if mirror.exists():
779 return mirror
780 return None
781
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 def GetTreeStatusUrl(self, error_ok=False):
783 if not self.tree_status_url:
784 error_message = ('You must configure your tree status URL by running '
785 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000786 self.tree_status_url = self._GetRietveldConfig(
787 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 return self.tree_status_url
789
790 def GetViewVCUrl(self):
791 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000792 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793 return self.viewvc_url
794
rmistry@google.com90752582014-01-14 21:04:50 +0000795 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000796 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000797
rmistry@google.com78948ed2015-07-08 23:09:57 +0000798 def GetIsSkipDependencyUpload(self, branch_name):
799 """Returns true if specified branch should skip dep uploads."""
800 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
801 error_ok=True)
802
rmistry@google.com5626a922015-02-26 14:03:30 +0000803 def GetRunPostUploadHook(self):
804 run_post_upload_hook = self._GetRietveldConfig(
805 'run-post-upload-hook', error_ok=True)
806 return run_post_upload_hook == "True"
807
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000808 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000809 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000810
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000811 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000812 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000813
ukai@chromium.orge8077812012-02-03 03:41:46 +0000814 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700815 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000816 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700817 self.is_gerrit = (
818 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000819 return self.is_gerrit
820
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000821 def GetSquashGerritUploads(self):
822 """Return true if uploads to Gerrit should be squashed by default."""
823 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700824 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
825 if self.squash_gerrit_uploads is None:
826 # Default is squash now (http://crbug.com/611892#c23).
827 self.squash_gerrit_uploads = not (
828 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
829 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000830 return self.squash_gerrit_uploads
831
tandriia60502f2016-06-20 02:01:53 -0700832 def GetSquashGerritUploadsOverride(self):
833 """Return True or False if codereview.settings should be overridden.
834
835 Returns None if no override has been defined.
836 """
837 # See also http://crbug.com/611892#c23
838 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
839 error_ok=True).strip()
840 if result == 'true':
841 return True
842 if result == 'false':
843 return False
844 return None
845
tandrii@chromium.org28253532016-04-14 13:46:56 +0000846 def GetGerritSkipEnsureAuthenticated(self):
847 """Return True if EnsureAuthenticated should not be done for Gerrit
848 uploads."""
849 if self.gerrit_skip_ensure_authenticated is None:
850 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000851 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000852 error_ok=True).strip() == 'true')
853 return self.gerrit_skip_ensure_authenticated
854
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000855 def GetGitEditor(self):
856 """Return the editor specified in the git config, or None if none is."""
857 if self.git_editor is None:
858 self.git_editor = self._GetConfig('core.editor', error_ok=True)
859 return self.git_editor or None
860
thestig@chromium.org44202a22014-03-11 19:22:18 +0000861 def GetLintRegex(self):
862 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
863 DEFAULT_LINT_REGEX)
864
865 def GetLintIgnoreRegex(self):
866 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
867 DEFAULT_LINT_IGNORE_REGEX)
868
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000869 def GetProject(self):
870 if not self.project:
871 self.project = self._GetRietveldConfig('project', error_ok=True)
872 return self.project
873
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000874 def _GetRietveldConfig(self, param, **kwargs):
875 return self._GetConfig('rietveld.' + param, **kwargs)
876
rmistry@google.com78948ed2015-07-08 23:09:57 +0000877 def _GetBranchConfig(self, branch_name, param, **kwargs):
878 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
879
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 def _GetConfig(self, param, **kwargs):
881 self.LazyUpdateIfNeeded()
882 return RunGit(['config', param], **kwargs).strip()
883
884
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100885@contextlib.contextmanager
886def _get_gerrit_project_config_file(remote_url):
887 """Context manager to fetch and store Gerrit's project.config from
888 refs/meta/config branch and store it in temp file.
889
890 Provides a temporary filename or None if there was error.
891 """
892 error, _ = RunGitWithCode([
893 'fetch', remote_url,
894 '+refs/meta/config:refs/git_cl/meta/config'])
895 if error:
896 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700897 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100898 (remote_url, error))
899 yield None
900 return
901
902 error, project_config_data = RunGitWithCode(
903 ['show', 'refs/git_cl/meta/config:project.config'])
904 if error:
905 print('WARNING: project.config file not found')
906 yield None
907 return
908
909 with gclient_utils.temporary_directory() as tempdir:
910 project_config_file = os.path.join(tempdir, 'project.config')
911 gclient_utils.FileWrite(project_config_file, project_config_data)
912 yield project_config_file
913
914
915def _is_git_numberer_enabled(remote_url, remote_ref):
916 """Returns True if Git Numberer is enabled on this ref."""
917 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100918 KNOWN_PROJECTS_WHITELIST = [
919 'chromium/src',
920 'external/webrtc',
921 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100922 'infra/experimental',
Edward Lemur32357d32017-09-11 20:22:45 +0200923 # For webrtc.googlesource.com/src.
924 'src',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100925 ]
926
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100927 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
928 url_parts = urlparse.urlparse(remote_url)
929 project_name = url_parts.path.lstrip('/').rstrip('git./')
930 for known in KNOWN_PROJECTS_WHITELIST:
931 if project_name.endswith(known):
932 break
933 else:
934 # Early exit to avoid extra fetches for repos that aren't using Git
935 # Numberer.
936 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100937
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100938 with _get_gerrit_project_config_file(remote_url) as project_config_file:
939 if project_config_file is None:
940 # Failed to fetch project.config, which shouldn't happen on open source
941 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100942 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100943 def get_opts(x):
944 code, out = RunGitWithCode(
945 ['config', '-f', project_config_file, '--get-all',
946 'plugin.git-numberer.validate-%s-refglob' % x])
947 if code == 0:
948 return out.strip().splitlines()
949 return []
950 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100951
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100952 logging.info('validator config enabled %s disabled %s refglobs for '
953 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000954
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100955 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100956 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100957 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100958 return True
959 return False
960
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100961 if match_refglobs(disabled):
962 return False
963 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100964
965
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000966def ShortBranchName(branch):
967 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000968 return branch.replace('refs/heads/', '', 1)
969
970
971def GetCurrentBranchRef():
972 """Returns branch ref (e.g., refs/heads/master) or None."""
973 return RunGit(['symbolic-ref', 'HEAD'],
974 stderr=subprocess2.VOID, error_ok=True).strip() or None
975
976
977def GetCurrentBranch():
978 """Returns current branch or None.
979
980 For refs/heads/* branches, returns just last part. For others, full ref.
981 """
982 branchref = GetCurrentBranchRef()
983 if branchref:
984 return ShortBranchName(branchref)
985 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000986
987
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000988class _CQState(object):
989 """Enum for states of CL with respect to Commit Queue."""
990 NONE = 'none'
991 DRY_RUN = 'dry_run'
992 COMMIT = 'commit'
993
994 ALL_STATES = [NONE, DRY_RUN, COMMIT]
995
996
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000997class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200998 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000999 self.issue = issue
1000 self.patchset = patchset
1001 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001002 assert codereview in (None, 'rietveld', 'gerrit')
1003 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001004
1005 @property
1006 def valid(self):
1007 return self.issue is not None
1008
1009
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001010def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001011 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1012 fail_result = _ParsedIssueNumberArgument()
1013
1014 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001015 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001016 if not arg.startswith('http'):
1017 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001018
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001019 url = gclient_utils.UpgradeToHttps(arg)
1020 try:
1021 parsed_url = urlparse.urlparse(url)
1022 except ValueError:
1023 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001024
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001025 if codereview is not None:
1026 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1027 return parsed or fail_result
1028
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001029 results = {}
1030 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1031 parsed = cls.ParseIssueURL(parsed_url)
1032 if parsed is not None:
1033 results[name] = parsed
1034
1035 if not results:
1036 return fail_result
1037 if len(results) == 1:
1038 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001039
1040 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1041 # This is likely Gerrit.
1042 return results['gerrit']
1043 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001044 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001045
1046
Aaron Gablea45ee112016-11-22 15:14:38 -08001047class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001048 def __init__(self, issue, url):
1049 self.issue = issue
1050 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001051 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001052
1053 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001054 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001055 self.issue, self.url)
1056
1057
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001058_CommentSummary = collections.namedtuple(
1059 '_CommentSummary', ['date', 'message', 'sender',
1060 # TODO(tandrii): these two aren't known in Gerrit.
1061 'approval', 'disapproval'])
1062
1063
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001065 """Changelist works with one changelist in local branch.
1066
1067 Supports two codereview backends: Rietveld or Gerrit, selected at object
1068 creation.
1069
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001070 Notes:
1071 * Not safe for concurrent multi-{thread,process} use.
1072 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001073 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001074 """
1075
1076 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1077 """Create a new ChangeList instance.
1078
1079 If issue is given, the codereview must be given too.
1080
1081 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1082 Otherwise, it's decided based on current configuration of the local branch,
1083 with default being 'rietveld' for backwards compatibility.
1084 See _load_codereview_impl for more details.
1085
1086 **kwargs will be passed directly to codereview implementation.
1087 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001088 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001089 global settings
1090 if not settings:
1091 # Happens when git_cl.py is used as a utility library.
1092 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001093
1094 if issue:
1095 assert codereview, 'codereview must be known, if issue is known'
1096
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097 self.branchref = branchref
1098 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001099 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 self.branch = ShortBranchName(self.branchref)
1101 else:
1102 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001104 self.lookedup_issue = False
1105 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 self.has_description = False
1107 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001108 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001110 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001111 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001112 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001113
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001114 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001115 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001116 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001117 assert self._codereview_impl
1118 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001119
1120 def _load_codereview_impl(self, codereview=None, **kwargs):
1121 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001122 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1123 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1124 self._codereview = codereview
1125 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001126 return
1127
1128 # Automatic selection based on issue number set for a current branch.
1129 # Rietveld takes precedence over Gerrit.
1130 assert not self.issue
1131 # Whether we find issue or not, we are doing the lookup.
1132 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001133 if self.GetBranch():
1134 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1135 issue = _git_get_branch_config_value(
1136 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1137 if issue:
1138 self._codereview = codereview
1139 self._codereview_impl = cls(self, **kwargs)
1140 self.issue = int(issue)
1141 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001142
1143 # No issue is set for this branch, so decide based on repo-wide settings.
1144 return self._load_codereview_impl(
1145 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1146 **kwargs)
1147
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001148 def IsGerrit(self):
1149 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001150
1151 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001152 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001153
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001154 The return value is a string suitable for passing to git cl with the --cc
1155 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001156 """
1157 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001158 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001159 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001160 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1161 return self.cc
1162
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001163 def GetCCListWithoutDefault(self):
1164 """Return the users cc'd on this CL excluding default ones."""
1165 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001166 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001167 return self.cc
1168
Daniel Cheng7227d212017-11-17 08:12:37 -08001169 def ExtendCC(self, more_cc):
1170 """Extends the list of users to cc on this CL based on the changed files."""
1171 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172
1173 def GetBranch(self):
1174 """Returns the short branch name, e.g. 'master'."""
1175 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001176 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001177 if not branchref:
1178 return None
1179 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 self.branch = ShortBranchName(self.branchref)
1181 return self.branch
1182
1183 def GetBranchRef(self):
1184 """Returns the full branch name, e.g. 'refs/heads/master'."""
1185 self.GetBranch() # Poke the lazy loader.
1186 return self.branchref
1187
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001188 def ClearBranch(self):
1189 """Clears cached branch data of this object."""
1190 self.branch = self.branchref = None
1191
tandrii5d48c322016-08-18 16:19:37 -07001192 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1193 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1194 kwargs['branch'] = self.GetBranch()
1195 return _git_get_branch_config_value(key, default, **kwargs)
1196
1197 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1198 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1199 assert self.GetBranch(), (
1200 'this CL must have an associated branch to %sset %s%s' %
1201 ('un' if value is None else '',
1202 key,
1203 '' if value is None else ' to %r' % value))
1204 kwargs['branch'] = self.GetBranch()
1205 return _git_set_branch_config_value(key, value, **kwargs)
1206
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001207 @staticmethod
1208 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001209 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 e.g. 'origin', 'refs/heads/master'
1211 """
1212 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001213 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1214
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001216 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001218 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1219 error_ok=True).strip()
1220 if upstream_branch:
1221 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001222 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001223 # Else, try to guess the origin remote.
1224 remote_branches = RunGit(['branch', '-r']).split()
1225 if 'origin/master' in remote_branches:
1226 # Fall back on origin/master if it exits.
1227 remote = 'origin'
1228 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001230 DieWithError(
1231 'Unable to determine default branch to diff against.\n'
1232 'Either pass complete "git diff"-style arguments, like\n'
1233 ' git cl upload origin/master\n'
1234 'or verify this branch is set up to track another \n'
1235 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236
1237 return remote, upstream_branch
1238
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001239 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001240 upstream_branch = self.GetUpstreamBranch()
1241 if not BranchExists(upstream_branch):
1242 DieWithError('The upstream for the current branch (%s) does not exist '
1243 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001244 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001245 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 def GetUpstreamBranch(self):
1248 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001249 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001251 upstream_branch = upstream_branch.replace('refs/heads/',
1252 'refs/remotes/%s/' % remote)
1253 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1254 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 self.upstream_branch = upstream_branch
1256 return self.upstream_branch
1257
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001258 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001259 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001260 remote, branch = None, self.GetBranch()
1261 seen_branches = set()
1262 while branch not in seen_branches:
1263 seen_branches.add(branch)
1264 remote, branch = self.FetchUpstreamTuple(branch)
1265 branch = ShortBranchName(branch)
1266 if remote != '.' or branch.startswith('refs/remotes'):
1267 break
1268 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001269 remotes = RunGit(['remote'], error_ok=True).split()
1270 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001271 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001272 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001273 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001274 logging.warn('Could not determine which remote this change is '
1275 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001276 else:
1277 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001278 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001279 branch = 'HEAD'
1280 if branch.startswith('refs/remotes'):
1281 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001282 elif branch.startswith('refs/branch-heads/'):
1283 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001284 else:
1285 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001286 return self._remote
1287
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001288 def GitSanityChecks(self, upstream_git_obj):
1289 """Checks git repo status and ensures diff is from local commits."""
1290
sbc@chromium.org79706062015-01-14 21:18:12 +00001291 if upstream_git_obj is None:
1292 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001293 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001294 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001295 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001296 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001297 return False
1298
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 # Verify the commit we're diffing against is in our current branch.
1300 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1301 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1302 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001303 print('ERROR: %s is not in the current branch. You may need to rebase '
1304 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001305 return False
1306
1307 # List the commits inside the diff, and verify they are all local.
1308 commits_in_diff = RunGit(
1309 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1310 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1311 remote_branch = remote_branch.strip()
1312 if code != 0:
1313 _, remote_branch = self.GetRemoteBranch()
1314
1315 commits_in_remote = RunGit(
1316 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1317
1318 common_commits = set(commits_in_diff) & set(commits_in_remote)
1319 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001320 print('ERROR: Your diff contains %d commits already in %s.\n'
1321 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1322 'the diff. If you are using a custom git flow, you can override'
1323 ' the reference used for this check with "git config '
1324 'gitcl.remotebranch <git-ref>".' % (
1325 len(common_commits), remote_branch, upstream_git_obj),
1326 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 return False
1328 return True
1329
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001330 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001331 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001332
1333 Returns None if it is not set.
1334 """
tandrii5d48c322016-08-18 16:19:37 -07001335 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001336
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 def GetRemoteUrl(self):
1338 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1339
1340 Returns None if there is no remote.
1341 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001342 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001343 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1344
1345 # If URL is pointing to a local directory, it is probably a git cache.
1346 if os.path.isdir(url):
1347 url = RunGit(['config', 'remote.%s.url' % remote],
1348 error_ok=True,
1349 cwd=url).strip()
1350 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001352 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001353 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001354 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001355 self.issue = self._GitGetBranchConfigValue(
1356 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001357 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001358 return self.issue
1359
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 def GetIssueURL(self):
1361 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001362 issue = self.GetIssue()
1363 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001364 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001365 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001367 def GetDescription(self, pretty=False, force=False):
1368 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001370 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 self.has_description = True
1372 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001373 # Set width to 72 columns + 2 space indent.
1374 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001376 lines = self.description.splitlines()
1377 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378 return self.description
1379
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001380 def GetDescriptionFooters(self):
1381 """Returns (non_footer_lines, footers) for the commit message.
1382
1383 Returns:
1384 non_footer_lines (list(str)) - Simple list of description lines without
1385 any footer. The lines do not contain newlines, nor does the list contain
1386 the empty line between the message and the footers.
1387 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1388 [("Change-Id", "Ideadbeef...."), ...]
1389 """
1390 raw_description = self.GetDescription()
1391 msg_lines, _, footers = git_footers.split_footers(raw_description)
1392 if footers:
1393 msg_lines = msg_lines[:len(msg_lines)-1]
1394 return msg_lines, footers
1395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001396 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001397 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001398 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001399 self.patchset = self._GitGetBranchConfigValue(
1400 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001401 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 return self.patchset
1403
1404 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001405 """Set this branch's patchset. If patchset=0, clears the patchset."""
1406 assert self.GetBranch()
1407 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001408 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001409 else:
1410 self.patchset = int(patchset)
1411 self._GitSetBranchConfigValue(
1412 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001414 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001415 """Set this branch's issue. If issue isn't given, clears the issue."""
1416 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001418 issue = int(issue)
1419 self._GitSetBranchConfigValue(
1420 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001421 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001422 codereview_server = self._codereview_impl.GetCodereviewServer()
1423 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001424 self._GitSetBranchConfigValue(
1425 self._codereview_impl.CodereviewServerConfigKey(),
1426 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 else:
tandrii5d48c322016-08-18 16:19:37 -07001428 # Reset all of these just to be clean.
1429 reset_suffixes = [
1430 'last-upload-hash',
1431 self._codereview_impl.IssueConfigKey(),
1432 self._codereview_impl.PatchsetConfigKey(),
1433 self._codereview_impl.CodereviewServerConfigKey(),
1434 ] + self._PostUnsetIssueProperties()
1435 for prop in reset_suffixes:
1436 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001437 msg = RunGit(['log', '-1', '--format=%B']).strip()
1438 if msg and git_footers.get_footer_change_id(msg):
1439 print('WARNING: The change patched into this branch has a Change-Id. '
1440 'Removing it.')
1441 RunGit(['commit', '--amend', '-m',
1442 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001443 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001444 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001445
dnjba1b0f32016-09-02 12:37:42 -07001446 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001447 if not self.GitSanityChecks(upstream_branch):
1448 DieWithError('\nGit sanity check failure')
1449
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001450 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001451 if not root:
1452 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001453 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001454
1455 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001456 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001457 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001458 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001459 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001460 except subprocess2.CalledProcessError:
1461 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001462 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001463 'This branch probably doesn\'t exist anymore. To reset the\n'
1464 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001465 ' git branch --set-upstream-to origin/master %s\n'
1466 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001467 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001468
maruel@chromium.org52424302012-08-29 15:14:30 +00001469 issue = self.GetIssue()
1470 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001471 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001472 description = self.GetDescription()
1473 else:
1474 # If the change was never uploaded, use the log messages of all commits
1475 # up to the branch point, as git cl upload will prefill the description
1476 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001477 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1478 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001479
1480 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001481 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001482 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001483 name,
1484 description,
1485 absroot,
1486 files,
1487 issue,
1488 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001489 author,
1490 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001491
dsansomee2d6fd92016-09-08 00:10:47 -07001492 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001493 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001494 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001495 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001496
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001497 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1498 """Sets the description for this CL remotely.
1499
1500 You can get description_lines and footers with GetDescriptionFooters.
1501
1502 Args:
1503 description_lines (list(str)) - List of CL description lines without
1504 newline characters.
1505 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1506 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1507 `List-Of-Tokens`). It will be case-normalized so that each token is
1508 title-cased.
1509 """
1510 new_description = '\n'.join(description_lines)
1511 if footers:
1512 new_description += '\n'
1513 for k, v in footers:
1514 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1515 if not git_footers.FOOTER_PATTERN.match(foot):
1516 raise ValueError('Invalid footer %r' % foot)
1517 new_description += foot + '\n'
1518 self.UpdateDescription(new_description, force)
1519
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001520 def RunHook(self, committing, may_prompt, verbose, change):
1521 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1522 try:
1523 return presubmit_support.DoPresubmitChecks(change, committing,
1524 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1525 default_presubmit=None, may_prompt=may_prompt,
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001526 rietveld_obj=self._codereview_impl.GetRietveldObjForPresubmit(),
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001527 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001528 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001529 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001530
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001531 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1532 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001533 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1534 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001535 else:
1536 # Assume url.
1537 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1538 urlparse.urlparse(issue_arg))
1539 if not parsed_issue_arg or not parsed_issue_arg.valid:
1540 DieWithError('Failed to parse issue argument "%s". '
1541 'Must be an issue number or a valid URL.' % issue_arg)
1542 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001543 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001544
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001545 def CMDUpload(self, options, git_diff_args, orig_args):
1546 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001547 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001548 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001549 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001550 else:
1551 if self.GetBranch() is None:
1552 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1553
1554 # Default to diffing against common ancestor of upstream branch
1555 base_branch = self.GetCommonAncestorWithUpstream()
1556 git_diff_args = [base_branch, 'HEAD']
1557
Aaron Gablec4c40d12017-05-22 11:49:53 -07001558 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1559 if not self.IsGerrit() and not self.GetIssue():
1560 print('=====================================')
1561 print('NOTICE: Rietveld is being deprecated. '
1562 'You can upload changes to Gerrit with')
1563 print(' git cl upload --gerrit')
1564 print('or set Gerrit to be your default code review tool with')
1565 print(' git config gerrit.host true')
1566 print('=====================================')
1567
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001568 # Fast best-effort checks to abort before running potentially
1569 # expensive hooks if uploading is likely to fail anyway. Passing these
1570 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001571 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001572 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001573
1574 # Apply watchlists on upload.
1575 change = self.GetChange(base_branch, None)
1576 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1577 files = [f.LocalPath() for f in change.AffectedFiles()]
1578 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001579 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001580
1581 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001582 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001583 # Set the reviewer list now so that presubmit checks can access it.
1584 change_description = ChangeDescription(change.FullDescriptionText())
1585 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001586 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001587 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001588 change)
1589 change.SetDescriptionText(change_description.description)
1590 hook_results = self.RunHook(committing=False,
1591 may_prompt=not options.force,
1592 verbose=options.verbose,
1593 change=change)
1594 if not hook_results.should_continue():
1595 return 1
1596 if not options.reviewers and hook_results.reviewers:
1597 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001598 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001599
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001600 # TODO(tandrii): Checking local patchset against remote patchset is only
1601 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1602 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001603 latest_patchset = self.GetMostRecentPatchset()
1604 local_patchset = self.GetPatchset()
1605 if (latest_patchset and local_patchset and
1606 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001607 print('The last upload made from this repository was patchset #%d but '
1608 'the most recent patchset on the server is #%d.'
1609 % (local_patchset, latest_patchset))
1610 print('Uploading will still work, but if you\'ve uploaded to this '
1611 'issue from another machine or branch the patch you\'re '
1612 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001613 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001614
Aaron Gable13101a62018-02-09 13:20:41 -08001615 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001616 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001617 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001618 if options.use_commit_queue:
1619 self.SetCQState(_CQState.COMMIT)
1620 elif options.cq_dry_run:
1621 self.SetCQState(_CQState.DRY_RUN)
1622
tandrii5d48c322016-08-18 16:19:37 -07001623 _git_set_branch_config_value('last-upload-hash',
1624 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001625 # Run post upload hooks, if specified.
1626 if settings.GetRunPostUploadHook():
1627 presubmit_support.DoPostUploadExecuter(
1628 change,
1629 self,
1630 settings.GetRoot(),
1631 options.verbose,
1632 sys.stdout)
1633
1634 # Upload all dependencies if specified.
1635 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001636 print()
1637 print('--dependencies has been specified.')
1638 print('All dependent local branches will be re-uploaded.')
1639 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001640 # Remove the dependencies flag from args so that we do not end up in a
1641 # loop.
1642 orig_args.remove('--dependencies')
1643 ret = upload_branch_deps(self, orig_args)
1644 return ret
1645
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001646 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001647 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001648
1649 Issue must have been already uploaded and known.
1650 """
1651 assert new_state in _CQState.ALL_STATES
1652 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001653 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001654 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001655 return 0
1656 except KeyboardInterrupt:
1657 raise
1658 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001659 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001660 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001661 ' * Your project has no CQ,\n'
1662 ' * You don\'t have permission to change the CQ state,\n'
1663 ' * There\'s a bug in this code (see stack trace below).\n'
1664 'Consider specifying which bots to trigger manually or asking your '
1665 'project owners for permissions or contacting Chrome Infra at:\n'
1666 'https://www.chromium.org/infra\n\n' %
1667 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001668 # Still raise exception so that stack trace is printed.
1669 raise
1670
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001671 # Forward methods to codereview specific implementation.
1672
Aaron Gable636b13f2017-07-14 10:42:48 -07001673 def AddComment(self, message, publish=None):
1674 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001675
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001676 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001677 """Returns list of _CommentSummary for each comment.
1678
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001679 args:
1680 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001681 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001682 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001683
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001684 def CloseIssue(self):
1685 return self._codereview_impl.CloseIssue()
1686
1687 def GetStatus(self):
1688 return self._codereview_impl.GetStatus()
1689
1690 def GetCodereviewServer(self):
1691 return self._codereview_impl.GetCodereviewServer()
1692
tandriide281ae2016-10-12 06:02:30 -07001693 def GetIssueOwner(self):
1694 """Get owner from codereview, which may differ from this checkout."""
1695 return self._codereview_impl.GetIssueOwner()
1696
Edward Lemur707d70b2018-02-07 00:50:14 +01001697 def GetReviewers(self):
1698 return self._codereview_impl.GetReviewers()
1699
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001700 def GetMostRecentPatchset(self):
1701 return self._codereview_impl.GetMostRecentPatchset()
1702
tandriide281ae2016-10-12 06:02:30 -07001703 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001704 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001705 return self._codereview_impl.CannotTriggerTryJobReason()
1706
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001707 def GetTryJobProperties(self, patchset=None):
1708 """Returns dictionary of properties to launch try job."""
1709 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001710
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001711 def __getattr__(self, attr):
1712 # This is because lots of untested code accesses Rietveld-specific stuff
1713 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001714 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001715 # Note that child method defines __getattr__ as well, and forwards it here,
1716 # because _RietveldChangelistImpl is not cleaned up yet, and given
1717 # deprecation of Rietveld, it should probably be just removed.
1718 # Until that time, avoid infinite recursion by bypassing __getattr__
1719 # of implementation class.
1720 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001721
1722
1723class _ChangelistCodereviewBase(object):
1724 """Abstract base class encapsulating codereview specifics of a changelist."""
1725 def __init__(self, changelist):
1726 self._changelist = changelist # instance of Changelist
1727
1728 def __getattr__(self, attr):
1729 # Forward methods to changelist.
1730 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1731 # _RietveldChangelistImpl to avoid this hack?
1732 return getattr(self._changelist, attr)
1733
1734 def GetStatus(self):
1735 """Apply a rough heuristic to give a simple summary of an issue's review
1736 or CQ status, assuming adherence to a common workflow.
1737
1738 Returns None if no issue for this branch, or specific string keywords.
1739 """
1740 raise NotImplementedError()
1741
1742 def GetCodereviewServer(self):
1743 """Returns server URL without end slash, like "https://codereview.com"."""
1744 raise NotImplementedError()
1745
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001746 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001747 """Fetches and returns description from the codereview server."""
1748 raise NotImplementedError()
1749
tandrii5d48c322016-08-18 16:19:37 -07001750 @classmethod
1751 def IssueConfigKey(cls):
1752 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 raise NotImplementedError()
1754
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001755 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001756 def PatchsetConfigKey(cls):
1757 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 raise NotImplementedError()
1759
tandrii5d48c322016-08-18 16:19:37 -07001760 @classmethod
1761 def CodereviewServerConfigKey(cls):
1762 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001763 raise NotImplementedError()
1764
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001765 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001766 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001767 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001768
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001769 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001770 # This is an unfortunate Rietveld-embeddedness in presubmit.
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001771 # For non-Rietveld code reviews, this probably should return a dummy object.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001772 raise NotImplementedError()
1773
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001774 def GetGerritObjForPresubmit(self):
1775 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1776 return None
1777
dsansomee2d6fd92016-09-08 00:10:47 -07001778 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001779 """Update the description on codereview site."""
1780 raise NotImplementedError()
1781
Aaron Gable636b13f2017-07-14 10:42:48 -07001782 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001783 """Posts a comment to the codereview site."""
1784 raise NotImplementedError()
1785
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001786 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001787 raise NotImplementedError()
1788
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001789 def CloseIssue(self):
1790 """Closes the issue."""
1791 raise NotImplementedError()
1792
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001793 def GetMostRecentPatchset(self):
1794 """Returns the most recent patchset number from the codereview site."""
1795 raise NotImplementedError()
1796
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001797 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001798 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001799 """Fetches and applies the issue.
1800
1801 Arguments:
1802 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1803 reject: if True, reject the failed patch instead of switching to 3-way
1804 merge. Rietveld only.
1805 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1806 only.
1807 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001808 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001809 """
1810 raise NotImplementedError()
1811
1812 @staticmethod
1813 def ParseIssueURL(parsed_url):
1814 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1815 failed."""
1816 raise NotImplementedError()
1817
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001818 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001819 """Best effort check that user is authenticated with codereview server.
1820
1821 Arguments:
1822 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001823 refresh: whether to attempt to refresh credentials. Ignored if not
1824 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001825 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001826 raise NotImplementedError()
1827
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001828 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001829 """Best effort check that uploading isn't supposed to fail for predictable
1830 reasons.
1831
1832 This method should raise informative exception if uploading shouldn't
1833 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001834
1835 Arguments:
1836 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001837 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001838 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001839
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001840 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001841 """Uploads a change to codereview."""
1842 raise NotImplementedError()
1843
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001844 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001845 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001846
1847 Issue must have been already uploaded and known.
1848 """
1849 raise NotImplementedError()
1850
tandriie113dfd2016-10-11 10:20:12 -07001851 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001852 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001853 raise NotImplementedError()
1854
tandriide281ae2016-10-12 06:02:30 -07001855 def GetIssueOwner(self):
1856 raise NotImplementedError()
1857
Edward Lemur707d70b2018-02-07 00:50:14 +01001858 def GetReviewers(self):
1859 raise NotImplementedError()
1860
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001861 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001862 raise NotImplementedError()
1863
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001864
1865class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001866
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001867 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001868 super(_RietveldChangelistImpl, self).__init__(changelist)
1869 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001870 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001871 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001872
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001873 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001874 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001875 self._props = None
1876 self._rpc_server = None
1877
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001878 def GetCodereviewServer(self):
1879 if not self._rietveld_server:
1880 # If we're on a branch then get the server potentially associated
1881 # with that branch.
1882 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001883 self._rietveld_server = gclient_utils.UpgradeToHttps(
1884 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001885 if not self._rietveld_server:
1886 self._rietveld_server = settings.GetDefaultServerUrl()
1887 return self._rietveld_server
1888
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001889 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001890 """Best effort check that user is authenticated with Rietveld server."""
1891 if self._auth_config.use_oauth2:
1892 authenticator = auth.get_authenticator_for_host(
1893 self.GetCodereviewServer(), self._auth_config)
1894 if not authenticator.has_cached_credentials():
1895 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001896 if refresh:
1897 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001898
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001899 def EnsureCanUploadPatchset(self, force):
1900 # No checks for Rietveld because we are deprecating Rietveld.
1901 pass
1902
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001903 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001904 issue = self.GetIssue()
1905 assert issue
1906 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001907 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001908 except urllib2.HTTPError as e:
1909 if e.code == 404:
1910 DieWithError(
1911 ('\nWhile fetching the description for issue %d, received a '
1912 '404 (not found)\n'
1913 'error. It is likely that you deleted this '
1914 'issue on the server. If this is the\n'
1915 'case, please run\n\n'
1916 ' git cl issue 0\n\n'
1917 'to clear the association with the deleted issue. Then run '
1918 'this command again.') % issue)
1919 else:
1920 DieWithError(
1921 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1922 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001923 print('Warning: Failed to retrieve CL description due to network '
1924 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001925 return ''
1926
1927 def GetMostRecentPatchset(self):
1928 return self.GetIssueProperties()['patchsets'][-1]
1929
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001930 def GetIssueProperties(self):
1931 if self._props is None:
1932 issue = self.GetIssue()
1933 if not issue:
1934 self._props = {}
1935 else:
1936 self._props = self.RpcServer().get_issue_properties(issue, True)
1937 return self._props
1938
tandriie113dfd2016-10-11 10:20:12 -07001939 def CannotTriggerTryJobReason(self):
1940 props = self.GetIssueProperties()
1941 if not props:
1942 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1943 if props.get('closed'):
1944 return 'CL %s is closed' % self.GetIssue()
1945 if props.get('private'):
1946 return 'CL %s is private' % self.GetIssue()
1947 return None
1948
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001949 def GetTryJobProperties(self, patchset=None):
1950 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07001951 project = (self.GetIssueProperties() or {}).get('project')
1952 return {
1953 'issue': self.GetIssue(),
1954 'patch_project': project,
1955 'patch_storage': 'rietveld',
1956 'patchset': patchset or self.GetPatchset(),
1957 'rietveld': self.GetCodereviewServer(),
1958 }
1959
tandriide281ae2016-10-12 06:02:30 -07001960 def GetIssueOwner(self):
1961 return (self.GetIssueProperties() or {}).get('owner_email')
1962
Edward Lemur707d70b2018-02-07 00:50:14 +01001963 def GetReviewers(self):
1964 return (self.GetIssueProperties() or {}).get('reviewers')
1965
Aaron Gable636b13f2017-07-14 10:42:48 -07001966 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001967 return self.RpcServer().add_comment(self.GetIssue(), message)
1968
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001969 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001970 summary = []
1971 for message in self.GetIssueProperties().get('messages', []):
1972 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
1973 summary.append(_CommentSummary(
1974 date=date,
1975 disapproval=bool(message['disapproval']),
1976 approval=bool(message['approval']),
1977 sender=message['sender'],
1978 message=message['text'],
1979 ))
1980 return summary
1981
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001982 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001983 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001984 or CQ status, assuming adherence to a common workflow.
1985
1986 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07001987 * 'error' - error from review tool (including deleted issues)
1988 * 'unsent' - not sent for review
1989 * 'waiting' - waiting for review
1990 * 'reply' - waiting for owner to reply to review
1991 * 'not lgtm' - Code-Review label has been set negatively
1992 * 'lgtm' - LGTM from at least one approved reviewer
1993 * 'commit' - in the commit queue
1994 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001995 """
1996 if not self.GetIssue():
1997 return None
1998
1999 try:
2000 props = self.GetIssueProperties()
2001 except urllib2.HTTPError:
2002 return 'error'
2003
2004 if props.get('closed'):
2005 # Issue is closed.
2006 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002007 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002008 # Issue is in the commit queue.
2009 return 'commit'
2010
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002011 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002012 if not messages:
2013 # No message was sent.
2014 return 'unsent'
2015
2016 if get_approving_reviewers(props):
2017 return 'lgtm'
2018 elif get_approving_reviewers(props, disapproval=True):
2019 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002020
tandrii9d2c7a32016-06-22 03:42:45 -07002021 # Skip CQ messages that don't require owner's action.
2022 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2023 if 'Dry run:' in messages[-1]['text']:
2024 messages.pop()
2025 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2026 # This message always follows prior messages from CQ,
2027 # so skip this too.
2028 messages.pop()
2029 else:
2030 # This is probably a CQ messages warranting user attention.
2031 break
2032
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002033 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002034 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002035 return 'reply'
2036 return 'waiting'
2037
dsansomee2d6fd92016-09-08 00:10:47 -07002038 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002039 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002042 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002044 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002045 return self.SetFlags({flag: value})
2046
2047 def SetFlags(self, flags):
2048 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002049 """
phajdan.jr68598232016-08-10 03:28:28 -07002050 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002051 try:
tandrii4b233bd2016-07-06 03:50:29 -07002052 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002053 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002054 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002055 if e.code == 404:
2056 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2057 if e.code == 403:
2058 DieWithError(
2059 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002060 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002061 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002062
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002063 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002064 """Returns an upload.RpcServer() to access this review's rietveld instance.
2065 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002066 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002067 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002068 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002069 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002070 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002072 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002073 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002074 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002075
tandrii5d48c322016-08-18 16:19:37 -07002076 @classmethod
2077 def PatchsetConfigKey(cls):
2078 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079
tandrii5d48c322016-08-18 16:19:37 -07002080 @classmethod
2081 def CodereviewServerConfigKey(cls):
2082 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002083
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002084 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002085 return self.RpcServer()
2086
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002087 def SetCQState(self, new_state):
2088 props = self.GetIssueProperties()
2089 if props.get('private'):
2090 DieWithError('Cannot set-commit on private issue')
2091
2092 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002093 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002094 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002095 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002096 else:
tandrii4b233bd2016-07-06 03:50:29 -07002097 assert new_state == _CQState.DRY_RUN
2098 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002099
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002100 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002101 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002102 # PatchIssue should never be called with a dirty tree. It is up to the
2103 # caller to check this, but just in case we assert here since the
2104 # consequences of the caller not checking this could be dire.
2105 assert(not git_common.is_dirty_git_tree('apply'))
2106 assert(parsed_issue_arg.valid)
2107 self._changelist.issue = parsed_issue_arg.issue
2108 if parsed_issue_arg.hostname:
2109 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2110
skobes6468b902016-10-24 08:45:10 -07002111 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2112 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2113 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002114 try:
skobes6468b902016-10-24 08:45:10 -07002115 scm_obj.apply_patch(patchset_object)
2116 except Exception as e:
2117 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002118 return 1
2119
2120 # If we had an issue, commit the current state and register the issue.
2121 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002122 self.SetIssue(self.GetIssue())
2123 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002124 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2125 'patch from issue %(i)s at patchset '
2126 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2127 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002128 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002129 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002130 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002131 return 0
2132
2133 @staticmethod
2134 def ParseIssueURL(parsed_url):
2135 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2136 return None
wychen3c1c1722016-08-04 11:46:36 -07002137 # Rietveld patch: https://domain/<number>/#ps<patchset>
2138 match = re.match(r'/(\d+)/$', parsed_url.path)
2139 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2140 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002141 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002142 issue=int(match.group(1)),
2143 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002144 hostname=parsed_url.netloc,
2145 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002146 # Typical url: https://domain/<issue_number>[/[other]]
2147 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2148 if match:
skobes6468b902016-10-24 08:45:10 -07002149 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002150 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002151 hostname=parsed_url.netloc,
2152 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002153 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2154 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2155 if match:
skobes6468b902016-10-24 08:45:10 -07002156 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002157 issue=int(match.group(1)),
2158 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002159 hostname=parsed_url.netloc,
2160 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002161 return None
2162
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002163 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002164 """Upload the patch to Rietveld."""
2165 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2166 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002167 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2168 if options.emulate_svn_auto_props:
2169 upload_args.append('--emulate_svn_auto_props')
2170
2171 change_desc = None
2172
2173 if options.email is not None:
2174 upload_args.extend(['--email', options.email])
2175
2176 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002177 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002178 upload_args.extend(['--title', options.title])
2179 if options.message:
2180 upload_args.extend(['--message', options.message])
2181 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002182 print('This branch is associated with issue %s. '
2183 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002184 else:
nodirca166002016-06-27 10:59:51 -07002185 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002186 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002187 if options.message:
2188 message = options.message
2189 else:
2190 message = CreateDescriptionFromLog(args)
2191 if options.title:
2192 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002193 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002194 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002195 change_desc.update_reviewers(options.reviewers, options.tbrs,
2196 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002197 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002198 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002199
2200 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002201 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002202 return 1
2203
2204 upload_args.extend(['--message', change_desc.description])
2205 if change_desc.get_reviewers():
2206 upload_args.append('--reviewers=%s' % ','.join(
2207 change_desc.get_reviewers()))
2208 if options.send_mail:
2209 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002210 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002211 upload_args.append('--send_mail')
2212
2213 # We check this before applying rietveld.private assuming that in
2214 # rietveld.cc only addresses which we can send private CLs to are listed
2215 # if rietveld.private is set, and so we should ignore rietveld.cc only
2216 # when --private is specified explicitly on the command line.
2217 if options.private:
2218 logging.warn('rietveld.cc is ignored since private flag is specified. '
2219 'You need to review and add them manually if necessary.')
2220 cc = self.GetCCListWithoutDefault()
2221 else:
2222 cc = self.GetCCList()
2223 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002224 if change_desc.get_cced():
2225 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002226 if cc:
2227 upload_args.extend(['--cc', cc])
2228
2229 if options.private or settings.GetDefaultPrivateFlag() == "True":
2230 upload_args.append('--private')
2231
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002232 # Include the upstream repo's URL in the change -- this is useful for
2233 # projects that have their source spread across multiple repos.
2234 remote_url = self.GetGitBaseUrlFromConfig()
2235 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002236 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2237 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2238 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002239 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002241 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002242 if target_ref:
2243 upload_args.extend(['--target_ref', target_ref])
2244
2245 # Look for dependent patchsets. See crbug.com/480453 for more details.
2246 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2247 upstream_branch = ShortBranchName(upstream_branch)
2248 if remote is '.':
2249 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002250 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002251 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002252 print()
2253 print('Skipping dependency patchset upload because git config '
2254 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2255 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002256 else:
2257 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002258 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002259 auth_config=auth_config)
2260 branch_cl_issue_url = branch_cl.GetIssueURL()
2261 branch_cl_issue = branch_cl.GetIssue()
2262 branch_cl_patchset = branch_cl.GetPatchset()
2263 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2264 upload_args.extend(
2265 ['--depends_on_patchset', '%s:%s' % (
2266 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002267 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002268 '\n'
2269 'The current branch (%s) is tracking a local branch (%s) with '
2270 'an associated CL.\n'
2271 'Adding %s/#ps%s as a dependency patchset.\n'
2272 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2273 branch_cl_patchset))
2274
2275 project = settings.GetProject()
2276 if project:
2277 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002278 else:
2279 print()
2280 print('WARNING: Uploading without a project specified. Please ensure '
2281 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2282 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002283
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002284 try:
2285 upload_args = ['upload'] + upload_args + args
2286 logging.info('upload.RealMain(%s)', upload_args)
2287 issue, patchset = upload.RealMain(upload_args)
2288 issue = int(issue)
2289 patchset = int(patchset)
2290 except KeyboardInterrupt:
2291 sys.exit(1)
2292 except:
2293 # If we got an exception after the user typed a description for their
2294 # change, back up the description before re-raising.
2295 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002296 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002297 raise
2298
2299 if not self.GetIssue():
2300 self.SetIssue(issue)
2301 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002302 return 0
2303
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002304
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002305class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002306 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002307 # auth_config is Rietveld thing, kept here to preserve interface only.
2308 super(_GerritChangelistImpl, self).__init__(changelist)
2309 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002310 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002311 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002312 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002313 # Map from change number (issue) to its detail cache.
2314 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002315
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002316 if codereview_host is not None:
2317 assert not codereview_host.startswith('https://'), codereview_host
2318 self._gerrit_host = codereview_host
2319 self._gerrit_server = 'https://%s' % codereview_host
2320
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002321 def _GetGerritHost(self):
2322 # Lazy load of configs.
2323 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002324 if self._gerrit_host and '.' not in self._gerrit_host:
2325 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2326 # This happens for internal stuff http://crbug.com/614312.
2327 parsed = urlparse.urlparse(self.GetRemoteUrl())
2328 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002329 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002330 ' Your current remote is: %s' % self.GetRemoteUrl())
2331 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2332 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002333 return self._gerrit_host
2334
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002335 def _GetGitHost(self):
2336 """Returns git host to be used when uploading change to Gerrit."""
2337 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2338
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002339 def GetCodereviewServer(self):
2340 if not self._gerrit_server:
2341 # If we're on a branch then get the server potentially associated
2342 # with that branch.
2343 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002344 self._gerrit_server = self._GitGetBranchConfigValue(
2345 self.CodereviewServerConfigKey())
2346 if self._gerrit_server:
2347 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002348 if not self._gerrit_server:
2349 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2350 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002351 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002352 parts[0] = parts[0] + '-review'
2353 self._gerrit_host = '.'.join(parts)
2354 self._gerrit_server = 'https://%s' % self._gerrit_host
2355 return self._gerrit_server
2356
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002357 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002358 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002359 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002360
tandrii5d48c322016-08-18 16:19:37 -07002361 @classmethod
2362 def PatchsetConfigKey(cls):
2363 return 'gerritpatchset'
2364
2365 @classmethod
2366 def CodereviewServerConfigKey(cls):
2367 return 'gerritserver'
2368
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002369 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002370 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002371 if settings.GetGerritSkipEnsureAuthenticated():
2372 # For projects with unusual authentication schemes.
2373 # See http://crbug.com/603378.
2374 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002375 # Lazy-loader to identify Gerrit and Git hosts.
2376 if gerrit_util.GceAuthenticator.is_gce():
2377 return
2378 self.GetCodereviewServer()
2379 git_host = self._GetGitHost()
2380 assert self._gerrit_server and self._gerrit_host
2381 cookie_auth = gerrit_util.CookiesAuthenticator()
2382
2383 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2384 git_auth = cookie_auth.get_auth_header(git_host)
2385 if gerrit_auth and git_auth:
2386 if gerrit_auth == git_auth:
2387 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002388 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002389 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002390 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002391 ' %s\n'
2392 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002393 ' Consider running the following command:\n'
2394 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002395 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002396 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002397 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002398 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002399 cookie_auth.get_new_password_message(git_host)))
2400 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002401 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002402 return
2403 else:
2404 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002405 ([] if gerrit_auth else [self._gerrit_host]) +
2406 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002407 DieWithError('Credentials for the following hosts are required:\n'
2408 ' %s\n'
2409 'These are read from %s (or legacy %s)\n'
2410 '%s' % (
2411 '\n '.join(missing),
2412 cookie_auth.get_gitcookies_path(),
2413 cookie_auth.get_netrc_path(),
2414 cookie_auth.get_new_password_message(git_host)))
2415
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002416 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002417 if not self.GetIssue():
2418 return
2419
2420 # Warm change details cache now to avoid RPCs later, reducing latency for
2421 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002422 self._GetChangeDetail(
2423 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002424
2425 status = self._GetChangeDetail()['status']
2426 if status in ('MERGED', 'ABANDONED'):
2427 DieWithError('Change %s has been %s, new uploads are not allowed' %
2428 (self.GetIssueURL(),
2429 'submitted' if status == 'MERGED' else 'abandoned'))
2430
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002431 if gerrit_util.GceAuthenticator.is_gce():
2432 return
2433 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2434 self._GetGerritHost())
2435 if self.GetIssueOwner() == cookies_user:
2436 return
2437 logging.debug('change %s owner is %s, cookies user is %s',
2438 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002439 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002440 # so ask what Gerrit thinks of this user.
2441 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2442 if details['email'] == self.GetIssueOwner():
2443 return
2444 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002445 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002446 'as %s.\n'
2447 'Uploading may fail due to lack of permissions.' %
2448 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2449 confirm_or_exit(action='upload')
2450
2451
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002452 def _PostUnsetIssueProperties(self):
2453 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002454 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002455
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002456 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002457 class ThisIsNotRietveldIssue(object):
2458 def __nonzero__(self):
2459 # This is a hack to make presubmit_support think that rietveld is not
2460 # defined, yet still ensure that calls directly result in a decent
2461 # exception message below.
2462 return False
2463
2464 def __getattr__(self, attr):
2465 print(
2466 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2467 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002468 'Please, either change your PRESUBMIT to not use rietveld_obj.%s,\n'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002469 'or use Rietveld for codereview.\n'
2470 'See also http://crbug.com/579160.' % attr)
2471 raise NotImplementedError()
2472 return ThisIsNotRietveldIssue()
2473
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002474 def GetGerritObjForPresubmit(self):
2475 return presubmit_support.GerritAccessor(self._GetGerritHost())
2476
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002477 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002478 """Apply a rough heuristic to give a simple summary of an issue's review
2479 or CQ status, assuming adherence to a common workflow.
2480
2481 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002482 * 'error' - error from review tool (including deleted issues)
2483 * 'unsent' - no reviewers added
2484 * 'waiting' - waiting for review
2485 * 'reply' - waiting for uploader to reply to review
2486 * 'lgtm' - Code-Review label has been set
2487 * 'commit' - in the commit queue
2488 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002489 """
2490 if not self.GetIssue():
2491 return None
2492
2493 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002494 data = self._GetChangeDetail([
2495 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002496 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002497 return 'error'
2498
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002499 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002500 return 'closed'
2501
Aaron Gable9ab38c62017-04-06 14:36:33 -07002502 if data['labels'].get('Commit-Queue', {}).get('approved'):
2503 # The section will have an "approved" subsection if anyone has voted
2504 # the maximum value on the label.
2505 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002506
Aaron Gable9ab38c62017-04-06 14:36:33 -07002507 if data['labels'].get('Code-Review', {}).get('approved'):
2508 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002509
2510 if not data.get('reviewers', {}).get('REVIEWER', []):
2511 return 'unsent'
2512
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002513 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002514 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2515 last_message_author = messages.pop().get('author', {})
2516 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002517 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2518 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002519 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002520 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002521 if last_message_author.get('_account_id') == owner:
2522 # Most recent message was by owner.
2523 return 'waiting'
2524 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002525 # Some reply from non-owner.
2526 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002527
2528 # Somehow there are no messages even though there are reviewers.
2529 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002530
2531 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002532 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002533 patchset = data['revisions'][data['current_revision']]['_number']
2534 self.SetPatchset(patchset)
2535 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002536
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002537 def FetchDescription(self, force=False):
2538 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2539 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002540 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002541 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002542
dsansomee2d6fd92016-09-08 00:10:47 -07002543 def UpdateDescriptionRemote(self, description, force=False):
2544 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2545 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002546 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002547 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002548 'unpublished edit. Either publish the edit in the Gerrit web UI '
2549 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002550
2551 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2552 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002553 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002554 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002555
Aaron Gable636b13f2017-07-14 10:42:48 -07002556 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002557 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002558 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002559
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002560 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002561 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002562 messages = self._GetChangeDetail(
2563 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2564 file_comments = gerrit_util.GetChangeComments(
2565 self._GetGerritHost(), self.GetIssue())
2566
2567 # Build dictionary of file comments for easy access and sorting later.
2568 # {author+date: {path: {patchset: {line: url+message}}}}
2569 comments = collections.defaultdict(
2570 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2571 for path, line_comments in file_comments.iteritems():
2572 for comment in line_comments:
2573 if comment.get('tag', '').startswith('autogenerated'):
2574 continue
2575 key = (comment['author']['email'], comment['updated'])
2576 if comment.get('side', 'REVISION') == 'PARENT':
2577 patchset = 'Base'
2578 else:
2579 patchset = 'PS%d' % comment['patch_set']
2580 line = comment.get('line', 0)
2581 url = ('https://%s/c/%s/%s/%s#%s%s' %
2582 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2583 'b' if comment.get('side') == 'PARENT' else '',
2584 str(line) if line else ''))
2585 comments[key][path][patchset][line] = (url, comment['message'])
2586
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002587 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002588 for msg in messages:
2589 # Don't bother showing autogenerated messages.
2590 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2591 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002592 # Gerrit spits out nanoseconds.
2593 assert len(msg['date'].split('.')[-1]) == 9
2594 date = datetime.datetime.strptime(msg['date'][:-3],
2595 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002596 message = msg['message']
2597 key = (msg['author']['email'], msg['date'])
2598 if key in comments:
2599 message += '\n'
2600 for path, patchsets in sorted(comments.get(key, {}).items()):
2601 if readable:
2602 message += '\n%s' % path
2603 for patchset, lines in sorted(patchsets.items()):
2604 for line, (url, content) in sorted(lines.items()):
2605 if line:
2606 line_str = 'Line %d' % line
2607 path_str = '%s:%d:' % (path, line)
2608 else:
2609 line_str = 'File comment'
2610 path_str = '%s:0:' % path
2611 if readable:
2612 message += '\n %s, %s: %s' % (patchset, line_str, url)
2613 message += '\n %s\n' % content
2614 else:
2615 message += '\n%s ' % path_str
2616 message += '\n%s\n' % content
2617
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002618 summary.append(_CommentSummary(
2619 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002620 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002621 sender=msg['author']['email'],
2622 # These could be inferred from the text messages and correlated with
2623 # Code-Review label maximum, however this is not reliable.
2624 # Leaving as is until the need arises.
2625 approval=False,
2626 disapproval=False,
2627 ))
2628 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002629
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002630 def CloseIssue(self):
2631 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2632
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002633 def SubmitIssue(self, wait_for_merge=True):
2634 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2635 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002636
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002637 def _GetChangeDetail(self, options=None, issue=None,
2638 no_cache=False):
2639 """Returns details of the issue by querying Gerrit and caching results.
2640
2641 If fresh data is needed, set no_cache=True which will clear cache and
2642 thus new data will be fetched from Gerrit.
2643 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002644 options = options or []
2645 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002646 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002647
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002648 # Optimization to avoid multiple RPCs:
2649 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2650 'CURRENT_COMMIT' not in options):
2651 options.append('CURRENT_COMMIT')
2652
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002653 # Normalize issue and options for consistent keys in cache.
2654 issue = str(issue)
2655 options = [o.upper() for o in options]
2656
2657 # Check in cache first unless no_cache is True.
2658 if no_cache:
2659 self._detail_cache.pop(issue, None)
2660 else:
2661 options_set = frozenset(options)
2662 for cached_options_set, data in self._detail_cache.get(issue, []):
2663 # Assumption: data fetched before with extra options is suitable
2664 # for return for a smaller set of options.
2665 # For example, if we cached data for
2666 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2667 # and request is for options=[CURRENT_REVISION],
2668 # THEN we can return prior cached data.
2669 if options_set.issubset(cached_options_set):
2670 return data
2671
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002672 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002673 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002674 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002675 except gerrit_util.GerritError as e:
2676 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002677 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002678 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002679
2680 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002681 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002682
agable32978d92016-11-01 12:55:02 -07002683 def _GetChangeCommit(self, issue=None):
2684 issue = issue or self.GetIssue()
2685 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002686 try:
2687 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2688 except gerrit_util.GerritError as e:
2689 if e.http_status == 404:
2690 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2691 raise
agable32978d92016-11-01 12:55:02 -07002692 return data
2693
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002694 def CMDLand(self, force, bypass_hooks, verbose):
2695 if git_common.is_dirty_git_tree('land'):
2696 return 1
tandriid60367b2016-06-22 05:25:12 -07002697 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2698 if u'Commit-Queue' in detail.get('labels', {}):
2699 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002700 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2701 'which can test and land changes for you. '
2702 'Are you sure you wish to bypass it?\n',
2703 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002704
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002705 differs = True
tandriic4344b52016-08-29 06:04:54 -07002706 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002707 # Note: git diff outputs nothing if there is no diff.
2708 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002709 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002710 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002711 if detail['current_revision'] == last_upload:
2712 differs = False
2713 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002714 print('WARNING: Local branch contents differ from latest uploaded '
2715 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002716 if differs:
2717 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002718 confirm_or_exit(
2719 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2720 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002721 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002722 elif not bypass_hooks:
2723 hook_results = self.RunHook(
2724 committing=True,
2725 may_prompt=not force,
2726 verbose=verbose,
2727 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2728 if not hook_results.should_continue():
2729 return 1
2730
2731 self.SubmitIssue(wait_for_merge=True)
2732 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002733 links = self._GetChangeCommit().get('web_links', [])
2734 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002735 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002736 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002737 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002738 return 0
2739
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002740 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002741 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002742 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002743 assert not directory
2744 assert parsed_issue_arg.valid
2745
2746 self._changelist.issue = parsed_issue_arg.issue
2747
2748 if parsed_issue_arg.hostname:
2749 self._gerrit_host = parsed_issue_arg.hostname
2750 self._gerrit_server = 'https://%s' % self._gerrit_host
2751
tandriic2405f52016-10-10 08:13:15 -07002752 try:
2753 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002754 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002755 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002756
2757 if not parsed_issue_arg.patchset:
2758 # Use current revision by default.
2759 revision_info = detail['revisions'][detail['current_revision']]
2760 patchset = int(revision_info['_number'])
2761 else:
2762 patchset = parsed_issue_arg.patchset
2763 for revision_info in detail['revisions'].itervalues():
2764 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2765 break
2766 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002767 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002768 (parsed_issue_arg.patchset, self.GetIssue()))
2769
Aaron Gable697a91b2018-01-19 15:20:15 -08002770 remote_url = self._changelist.GetRemoteUrl()
2771 if remote_url.endswith('.git'):
2772 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002773 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002774
2775 if remote_url != fetch_info['url']:
2776 DieWithError('Trying to patch a change from %s but this repo appears '
2777 'to be %s.' % (fetch_info['url'], remote_url))
2778
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002779 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002780
Aaron Gable62619a32017-06-16 08:22:09 -07002781 if force:
2782 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2783 print('Checked out commit for change %i patchset %i locally' %
2784 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002785 elif nocommit:
2786 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2787 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002788 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002789 RunGit(['cherry-pick', 'FETCH_HEAD'])
2790 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002791 (parsed_issue_arg.issue, patchset))
2792 print('Note: this created a local commit which does not have '
2793 'the same hash as the one uploaded for review. This will make '
2794 'uploading changes based on top of this branch difficult.\n'
2795 'If you want to do that, use "git cl patch --force" instead.')
2796
Stefan Zagerd08043c2017-10-12 12:07:02 -07002797 if self.GetBranch():
2798 self.SetIssue(parsed_issue_arg.issue)
2799 self.SetPatchset(patchset)
2800 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2801 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2802 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2803 else:
2804 print('WARNING: You are in detached HEAD state.\n'
2805 'The patch has been applied to your checkout, but you will not be '
2806 'able to upload a new patch set to the gerrit issue.\n'
2807 'Try using the \'-b\' option if you would like to work on a '
2808 'branch and/or upload a new patch set.')
2809
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002810 return 0
2811
2812 @staticmethod
2813 def ParseIssueURL(parsed_url):
2814 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2815 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002816 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2817 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002818 # Short urls like https://domain/<issue_number> can be used, but don't allow
2819 # specifying the patchset (you'd 404), but we allow that here.
2820 if parsed_url.path == '/':
2821 part = parsed_url.fragment
2822 else:
2823 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002824 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002825 if match:
2826 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002827 issue=int(match.group(3)),
2828 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002829 hostname=parsed_url.netloc,
2830 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002831 return None
2832
tandrii16e0b4e2016-06-07 10:34:28 -07002833 def _GerritCommitMsgHookCheck(self, offer_removal):
2834 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2835 if not os.path.exists(hook):
2836 return
2837 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2838 # custom developer made one.
2839 data = gclient_utils.FileRead(hook)
2840 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2841 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002842 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002843 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002844 'and may interfere with it in subtle ways.\n'
2845 'We recommend you remove the commit-msg hook.')
2846 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002847 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002848 gclient_utils.rm_file_or_tree(hook)
2849 print('Gerrit commit-msg hook removed.')
2850 else:
2851 print('OK, will keep Gerrit commit-msg hook in place.')
2852
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002853 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002854 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002855 if options.squash and options.no_squash:
2856 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002857
2858 if not options.squash and not options.no_squash:
2859 # Load default for user, repo, squash=true, in this order.
2860 options.squash = settings.GetSquashGerritUploads()
2861 elif options.no_squash:
2862 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002863
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002864 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002865 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002866
Aaron Gableb56ad332017-01-06 15:24:31 -08002867 # This may be None; default fallback value is determined in logic below.
2868 title = options.title
2869
Dominic Battre7d1c4842017-10-27 09:17:28 +02002870 # Extract bug number from branch name.
2871 bug = options.bug
2872 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2873 if not bug and match:
2874 bug = match.group(1)
2875
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002876 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002877 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002878 if self.GetIssue():
2879 # Try to get the message from a previous upload.
2880 message = self.GetDescription()
2881 if not message:
2882 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002883 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002884 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002885 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002886 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002887 # When uploading a subsequent patchset, -m|--message is taken
2888 # as the patchset title if --title was not provided.
2889 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002890 else:
2891 default_title = RunGit(
2892 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002893 if options.force:
2894 title = default_title
2895 else:
2896 title = ask_for_data(
2897 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002898 change_id = self._GetChangeDetail()['change_id']
2899 while True:
2900 footer_change_ids = git_footers.get_footer_change_id(message)
2901 if footer_change_ids == [change_id]:
2902 break
2903 if not footer_change_ids:
2904 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002905 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002906 continue
2907 # There is already a valid footer but with different or several ids.
2908 # Doing this automatically is non-trivial as we don't want to lose
2909 # existing other footers, yet we want to append just 1 desired
2910 # Change-Id. Thus, just create a new footer, but let user verify the
2911 # new description.
2912 message = '%s\n\nChange-Id: %s' % (message, change_id)
2913 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002914 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002915 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002916 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002917 'Please, check the proposed correction to the description, '
2918 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2919 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2920 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002921 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002922 if not options.force:
2923 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002924 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002925 message = change_desc.description
2926 if not message:
2927 DieWithError("Description is empty. Aborting...")
2928 # Continue the while loop.
2929 # Sanity check of this code - we should end up with proper message
2930 # footer.
2931 assert [change_id] == git_footers.get_footer_change_id(message)
2932 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002933 else: # if not self.GetIssue()
2934 if options.message:
2935 message = options.message
2936 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002937 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002938 if options.title:
2939 message = options.title + '\n\n' + message
2940 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002941
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002942 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002943 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002944 # On first upload, patchset title is always this string, while
2945 # --title flag gets converted to first line of message.
2946 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002947 if not change_desc.description:
2948 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002949 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002950 if len(change_ids) > 1:
2951 DieWithError('too many Change-Id footers, at most 1 allowed.')
2952 if not change_ids:
2953 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002954 change_desc.set_description(git_footers.add_footer_change_id(
2955 change_desc.description,
2956 GenerateGerritChangeId(change_desc.description)))
2957 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002958 assert len(change_ids) == 1
2959 change_id = change_ids[0]
2960
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002961 if options.reviewers or options.tbrs or options.add_owners_to:
2962 change_desc.update_reviewers(options.reviewers, options.tbrs,
2963 options.add_owners_to, change)
2964
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002965 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002966 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2967 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002968 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002969 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2970 desc_tempfile.write(change_desc.description)
2971 desc_tempfile.close()
2972 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2973 '-F', desc_tempfile.name]).strip()
2974 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002975 else:
2976 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002977 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002978 if not change_desc.description:
2979 DieWithError("Description is empty. Aborting...")
2980
2981 if not git_footers.get_footer_change_id(change_desc.description):
2982 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002983 change_desc.set_description(
2984 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002985 if options.reviewers or options.tbrs or options.add_owners_to:
2986 change_desc.update_reviewers(options.reviewers, options.tbrs,
2987 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002988 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002989 # For no-squash mode, we assume the remote called "origin" is the one we
2990 # want. It is not worthwhile to support different workflows for
2991 # no-squash mode.
2992 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002993 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2994
2995 assert change_desc
2996 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2997 ref_to_push)]).splitlines()
2998 if len(commits) > 1:
2999 print('WARNING: This will upload %d commits. Run the following command '
3000 'to see which commits will be uploaded: ' % len(commits))
3001 print('git log %s..%s' % (parent, ref_to_push))
3002 print('You can also use `git squash-branch` to squash these into a '
3003 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003004 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003005
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003006 if options.reviewers or options.tbrs or options.add_owners_to:
3007 change_desc.update_reviewers(options.reviewers, options.tbrs,
3008 options.add_owners_to, change)
3009
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003010 # Extra options that can be specified at push time. Doc:
3011 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003012 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003013
Aaron Gable844cf292017-06-28 11:32:59 -07003014 # By default, new changes are started in WIP mode, and subsequent patchsets
3015 # don't send email. At any time, passing --send-mail will mark the change
3016 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003017 if options.send_mail:
3018 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003019 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003020 elif not self.GetIssue():
3021 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003022 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003023 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003024
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003025 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003026 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003027
Aaron Gable9b713dd2016-12-14 16:04:21 -08003028 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003029 # Punctuation and whitespace in |title| must be percent-encoded.
3030 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003031
agablec6787972016-09-09 16:13:34 -07003032 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003033 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003034
rmistry9eadede2016-09-19 11:22:43 -07003035 if options.topic:
3036 # Documentation on Gerrit topics is here:
3037 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003038 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003039
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003040 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003041 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003042 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003043 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003044 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3045
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003046 refspec_suffix = ''
3047 if refspec_opts:
3048 refspec_suffix = '%' + ','.join(refspec_opts)
3049 assert ' ' not in refspec_suffix, (
3050 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3051 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3052
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003053 try:
3054 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003055 ['git', 'push', self.GetRemoteUrl(), refspec],
3056 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003057 # Flush after every line: useful for seeing progress when running as
3058 # recipe.
3059 filter_fn=lambda _: sys.stdout.flush())
3060 except subprocess2.CalledProcessError:
3061 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003062 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003063 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003064 'credential problems:\n'
3065 ' git cl creds-check\n',
3066 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003067
3068 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003069 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003070 change_numbers = [m.group(1)
3071 for m in map(regex.match, push_stdout.splitlines())
3072 if m]
3073 if len(change_numbers) != 1:
3074 DieWithError(
3075 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003076 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003077 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003078 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003079
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003080 reviewers = sorted(change_desc.get_reviewers())
3081
tandrii88189772016-09-29 04:29:57 -07003082 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003083 if not options.private:
3084 cc = self.GetCCList().split(',')
3085 else:
3086 cc = []
tandrii88189772016-09-29 04:29:57 -07003087 if options.cc:
3088 cc.extend(options.cc)
3089 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003090 if change_desc.get_cced():
3091 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003092
3093 gerrit_util.AddReviewers(
3094 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3095 notify=bool(options.send_mail))
3096
Aaron Gablefd238082017-06-07 13:42:34 -07003097 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003098 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3099 score = 1
3100 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3101 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3102 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003103 gerrit_util.SetReview(
3104 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003105 msg='Self-approving for TBR',
3106 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003107
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003108 return 0
3109
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003110 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3111 change_desc):
3112 """Computes parent of the generated commit to be uploaded to Gerrit.
3113
3114 Returns revision or a ref name.
3115 """
3116 if custom_cl_base:
3117 # Try to avoid creating additional unintended CLs when uploading, unless
3118 # user wants to take this risk.
3119 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3120 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3121 local_ref_of_target_remote])
3122 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003123 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003124 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3125 'If you proceed with upload, more than 1 CL may be created by '
3126 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3127 'If you are certain that specified base `%s` has already been '
3128 'uploaded to Gerrit as another CL, you may proceed.\n' %
3129 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3130 if not force:
3131 confirm_or_exit(
3132 'Do you take responsibility for cleaning up potential mess '
3133 'resulting from proceeding with upload?',
3134 action='upload')
3135 return custom_cl_base
3136
Aaron Gablef97e33d2017-03-30 15:44:27 -07003137 if remote != '.':
3138 return self.GetCommonAncestorWithUpstream()
3139
3140 # If our upstream branch is local, we base our squashed commit on its
3141 # squashed version.
3142 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3143
Aaron Gablef97e33d2017-03-30 15:44:27 -07003144 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003145 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003146
3147 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003148 # TODO(tandrii): consider checking parent change in Gerrit and using its
3149 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3150 # the tree hash of the parent branch. The upside is less likely bogus
3151 # requests to reupload parent change just because it's uploadhash is
3152 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003153 parent = RunGit(['config',
3154 'branch.%s.gerritsquashhash' % upstream_branch_name],
3155 error_ok=True).strip()
3156 # Verify that the upstream branch has been uploaded too, otherwise
3157 # Gerrit will create additional CLs when uploading.
3158 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3159 RunGitSilent(['rev-parse', parent + ':'])):
3160 DieWithError(
3161 '\nUpload upstream branch %s first.\n'
3162 'It is likely that this branch has been rebased since its last '
3163 'upload, so you just need to upload it again.\n'
3164 '(If you uploaded it with --no-squash, then branch dependencies '
3165 'are not supported, and you should reupload with --squash.)'
3166 % upstream_branch_name,
3167 change_desc)
3168 return parent
3169
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003170 def _AddChangeIdToCommitMessage(self, options, args):
3171 """Re-commits using the current message, assumes the commit hook is in
3172 place.
3173 """
3174 log_desc = options.message or CreateDescriptionFromLog(args)
3175 git_command = ['commit', '--amend', '-m', log_desc]
3176 RunGit(git_command)
3177 new_log_desc = CreateDescriptionFromLog(args)
3178 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003179 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003180 return new_log_desc
3181 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003182 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003183
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003184 def SetCQState(self, new_state):
3185 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003186 vote_map = {
3187 _CQState.NONE: 0,
3188 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003189 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003190 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003191 labels = {'Commit-Queue': vote_map[new_state]}
3192 notify = False if new_state == _CQState.DRY_RUN else None
3193 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3194 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003195
tandriie113dfd2016-10-11 10:20:12 -07003196 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003197 try:
3198 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003199 except GerritChangeNotExists:
3200 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003201
3202 if data['status'] in ('ABANDONED', 'MERGED'):
3203 return 'CL %s is closed' % self.GetIssue()
3204
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003205 def GetTryJobProperties(self, patchset=None):
3206 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003207 data = self._GetChangeDetail(['ALL_REVISIONS'])
3208 patchset = int(patchset or self.GetPatchset())
3209 assert patchset
3210 revision_data = None # Pylint wants it to be defined.
3211 for revision_data in data['revisions'].itervalues():
3212 if int(revision_data['_number']) == patchset:
3213 break
3214 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003215 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003216 (patchset, self.GetIssue()))
3217 return {
3218 'patch_issue': self.GetIssue(),
3219 'patch_set': patchset or self.GetPatchset(),
3220 'patch_project': data['project'],
3221 'patch_storage': 'gerrit',
3222 'patch_ref': revision_data['fetch']['http']['ref'],
3223 'patch_repository_url': revision_data['fetch']['http']['url'],
3224 'patch_gerrit_url': self.GetCodereviewServer(),
3225 }
tandriie113dfd2016-10-11 10:20:12 -07003226
tandriide281ae2016-10-12 06:02:30 -07003227 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003228 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003229
Edward Lemur707d70b2018-02-07 00:50:14 +01003230 def GetReviewers(self):
3231 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3232 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3233
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003234
3235_CODEREVIEW_IMPLEMENTATIONS = {
3236 'rietveld': _RietveldChangelistImpl,
3237 'gerrit': _GerritChangelistImpl,
3238}
3239
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003240
iannuccie53c9352016-08-17 14:40:40 -07003241def _add_codereview_issue_select_options(parser, extra=""):
3242 _add_codereview_select_options(parser)
3243
3244 text = ('Operate on this issue number instead of the current branch\'s '
3245 'implicit issue.')
3246 if extra:
3247 text += ' '+extra
3248 parser.add_option('-i', '--issue', type=int, help=text)
3249
3250
3251def _process_codereview_issue_select_options(parser, options):
3252 _process_codereview_select_options(parser, options)
3253 if options.issue is not None and not options.forced_codereview:
3254 parser.error('--issue must be specified with either --rietveld or --gerrit')
3255
3256
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003257def _add_codereview_select_options(parser):
3258 """Appends --gerrit and --rietveld options to force specific codereview."""
3259 parser.codereview_group = optparse.OptionGroup(
3260 parser, 'EXPERIMENTAL! Codereview override options')
3261 parser.add_option_group(parser.codereview_group)
3262 parser.codereview_group.add_option(
3263 '--gerrit', action='store_true',
3264 help='Force the use of Gerrit for codereview')
3265 parser.codereview_group.add_option(
3266 '--rietveld', action='store_true',
3267 help='Force the use of Rietveld for codereview')
3268
3269
3270def _process_codereview_select_options(parser, options):
3271 if options.gerrit and options.rietveld:
3272 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3273 options.forced_codereview = None
3274 if options.gerrit:
3275 options.forced_codereview = 'gerrit'
3276 elif options.rietveld:
3277 options.forced_codereview = 'rietveld'
3278
3279
tandriif9aefb72016-07-01 09:06:51 -07003280def _get_bug_line_values(default_project, bugs):
3281 """Given default_project and comma separated list of bugs, yields bug line
3282 values.
3283
3284 Each bug can be either:
3285 * a number, which is combined with default_project
3286 * string, which is left as is.
3287
3288 This function may produce more than one line, because bugdroid expects one
3289 project per line.
3290
3291 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3292 ['v8:123', 'chromium:789']
3293 """
3294 default_bugs = []
3295 others = []
3296 for bug in bugs.split(','):
3297 bug = bug.strip()
3298 if bug:
3299 try:
3300 default_bugs.append(int(bug))
3301 except ValueError:
3302 others.append(bug)
3303
3304 if default_bugs:
3305 default_bugs = ','.join(map(str, default_bugs))
3306 if default_project:
3307 yield '%s:%s' % (default_project, default_bugs)
3308 else:
3309 yield default_bugs
3310 for other in sorted(others):
3311 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3312 yield other
3313
3314
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003315class ChangeDescription(object):
3316 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003317 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003318 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003319 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003320 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003321 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3322 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3323 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3324 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003325
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003326 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003327 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003328
agable@chromium.org42c20792013-09-12 17:34:49 +00003329 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003330 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003331 return '\n'.join(self._description_lines)
3332
3333 def set_description(self, desc):
3334 if isinstance(desc, basestring):
3335 lines = desc.splitlines()
3336 else:
3337 lines = [line.rstrip() for line in desc]
3338 while lines and not lines[0]:
3339 lines.pop(0)
3340 while lines and not lines[-1]:
3341 lines.pop(-1)
3342 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003343
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003344 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3345 """Rewrites the R=/TBR= line(s) as a single line each.
3346
3347 Args:
3348 reviewers (list(str)) - list of additional emails to use for reviewers.
3349 tbrs (list(str)) - list of additional emails to use for TBRs.
3350 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3351 the change that are missing OWNER coverage. If this is not None, you
3352 must also pass a value for `change`.
3353 change (Change) - The Change that should be used for OWNERS lookups.
3354 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003355 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003356 assert isinstance(tbrs, list), tbrs
3357
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003358 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003359 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003360
3361 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003362 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003363
3364 reviewers = set(reviewers)
3365 tbrs = set(tbrs)
3366 LOOKUP = {
3367 'TBR': tbrs,
3368 'R': reviewers,
3369 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003370
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003371 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003372 regexp = re.compile(self.R_LINE)
3373 matches = [regexp.match(line) for line in self._description_lines]
3374 new_desc = [l for i, l in enumerate(self._description_lines)
3375 if not matches[i]]
3376 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003377
agable@chromium.org42c20792013-09-12 17:34:49 +00003378 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003379
3380 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003381 for match in matches:
3382 if not match:
3383 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003384 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3385
3386 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003387 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003388 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003389 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003390 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003391 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003392 LOOKUP[add_owners_to].update(
3393 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003394
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003395 # If any folks ended up in both groups, remove them from tbrs.
3396 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003397
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003398 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3399 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003400
3401 # Put the new lines in the description where the old first R= line was.
3402 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3403 if 0 <= line_loc < len(self._description_lines):
3404 if new_tbr_line:
3405 self._description_lines.insert(line_loc, new_tbr_line)
3406 if new_r_line:
3407 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003408 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003409 if new_r_line:
3410 self.append_footer(new_r_line)
3411 if new_tbr_line:
3412 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003413
Aaron Gable3a16ed12017-03-23 10:51:55 -07003414 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003415 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003416 self.set_description([
3417 '# Enter a description of the change.',
3418 '# This will be displayed on the codereview site.',
3419 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003420 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003421 '--------------------',
3422 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003423
agable@chromium.org42c20792013-09-12 17:34:49 +00003424 regexp = re.compile(self.BUG_LINE)
3425 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003426 prefix = settings.GetBugPrefix()
3427 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003428 if git_footer:
3429 self.append_footer('Bug: %s' % ', '.join(values))
3430 else:
3431 for value in values:
3432 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003433
agable@chromium.org42c20792013-09-12 17:34:49 +00003434 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003435 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003436 if not content:
3437 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003438 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003439
Bruce Dawson2377b012018-01-11 16:46:49 -08003440 # Strip off comments and default inserted "Bug:" line.
3441 clean_lines = [line.rstrip() for line in lines if not
3442 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003443 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003444 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003445 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003446
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003447 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003448 """Adds a footer line to the description.
3449
3450 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3451 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3452 that Gerrit footers are always at the end.
3453 """
3454 parsed_footer_line = git_footers.parse_footer(line)
3455 if parsed_footer_line:
3456 # Line is a gerrit footer in the form: Footer-Key: any value.
3457 # Thus, must be appended observing Gerrit footer rules.
3458 self.set_description(
3459 git_footers.add_footer(self.description,
3460 key=parsed_footer_line[0],
3461 value=parsed_footer_line[1]))
3462 return
3463
3464 if not self._description_lines:
3465 self._description_lines.append(line)
3466 return
3467
3468 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3469 if gerrit_footers:
3470 # git_footers.split_footers ensures that there is an empty line before
3471 # actual (gerrit) footers, if any. We have to keep it that way.
3472 assert top_lines and top_lines[-1] == ''
3473 top_lines, separator = top_lines[:-1], top_lines[-1:]
3474 else:
3475 separator = [] # No need for separator if there are no gerrit_footers.
3476
3477 prev_line = top_lines[-1] if top_lines else ''
3478 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3479 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3480 top_lines.append('')
3481 top_lines.append(line)
3482 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003483
tandrii99a72f22016-08-17 14:33:24 -07003484 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003485 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003486 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003487 reviewers = [match.group(2).strip()
3488 for match in matches
3489 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003490 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003491
bradnelsond975b302016-10-23 12:20:23 -07003492 def get_cced(self):
3493 """Retrieves the list of reviewers."""
3494 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3495 cced = [match.group(2).strip() for match in matches if match]
3496 return cleanup_list(cced)
3497
Nodir Turakulov23b82142017-11-16 11:04:25 -08003498 def get_hash_tags(self):
3499 """Extracts and sanitizes a list of Gerrit hashtags."""
3500 subject = (self._description_lines or ('',))[0]
3501 subject = re.sub(
3502 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3503
3504 tags = []
3505 start = 0
3506 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3507 while True:
3508 m = bracket_exp.match(subject, start)
3509 if not m:
3510 break
3511 tags.append(self.sanitize_hash_tag(m.group(1)))
3512 start = m.end()
3513
3514 if not tags:
3515 # Try "Tag: " prefix.
3516 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3517 if m:
3518 tags.append(self.sanitize_hash_tag(m.group(1)))
3519 return tags
3520
3521 @classmethod
3522 def sanitize_hash_tag(cls, tag):
3523 """Returns a sanitized Gerrit hash tag.
3524
3525 A sanitized hashtag can be used as a git push refspec parameter value.
3526 """
3527 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3528
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003529 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3530 """Updates this commit description given the parent.
3531
3532 This is essentially what Gnumbd used to do.
3533 Consult https://goo.gl/WMmpDe for more details.
3534 """
3535 assert parent_msg # No, orphan branch creation isn't supported.
3536 assert parent_hash
3537 assert dest_ref
3538 parent_footer_map = git_footers.parse_footers(parent_msg)
3539 # This will also happily parse svn-position, which GnumbD is no longer
3540 # supporting. While we'd generate correct footers, the verifier plugin
3541 # installed in Gerrit will block such commit (ie git push below will fail).
3542 parent_position = git_footers.get_position(parent_footer_map)
3543
3544 # Cherry-picks may have last line obscuring their prior footers,
3545 # from git_footers perspective. This is also what Gnumbd did.
3546 cp_line = None
3547 if (self._description_lines and
3548 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3549 cp_line = self._description_lines.pop()
3550
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003551 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003552
3553 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3554 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003555 for i, line in enumerate(footer_lines):
3556 k, v = git_footers.parse_footer(line) or (None, None)
3557 if k and k.startswith('Cr-'):
3558 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003559
3560 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003561 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003562 if parent_position[0] == dest_ref:
3563 # Same branch as parent.
3564 number = int(parent_position[1]) + 1
3565 else:
3566 number = 1 # New branch, and extra lineage.
3567 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3568 int(parent_position[1])))
3569
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003570 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3571 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003572
3573 self._description_lines = top_lines
3574 if cp_line:
3575 self._description_lines.append(cp_line)
3576 if self._description_lines[-1] != '':
3577 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003578 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003579
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003580
Aaron Gablea1bab272017-04-11 16:38:18 -07003581def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003582 """Retrieves the reviewers that approved a CL from the issue properties with
3583 messages.
3584
3585 Note that the list may contain reviewers that are not committer, thus are not
3586 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003587
3588 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003589 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003590 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003591 return sorted(
3592 set(
3593 message['sender']
3594 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003595 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003596 )
3597 )
3598
3599
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003600def FindCodereviewSettingsFile(filename='codereview.settings'):
3601 """Finds the given file starting in the cwd and going up.
3602
3603 Only looks up to the top of the repository unless an
3604 'inherit-review-settings-ok' file exists in the root of the repository.
3605 """
3606 inherit_ok_file = 'inherit-review-settings-ok'
3607 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003608 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003609 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3610 root = '/'
3611 while True:
3612 if filename in os.listdir(cwd):
3613 if os.path.isfile(os.path.join(cwd, filename)):
3614 return open(os.path.join(cwd, filename))
3615 if cwd == root:
3616 break
3617 cwd = os.path.dirname(cwd)
3618
3619
3620def LoadCodereviewSettingsFromFile(fileobj):
3621 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003622 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003623
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003624 def SetProperty(name, setting, unset_error_ok=False):
3625 fullname = 'rietveld.' + name
3626 if setting in keyvals:
3627 RunGit(['config', fullname, keyvals[setting]])
3628 else:
3629 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3630
tandrii48df5812016-10-17 03:55:37 -07003631 if not keyvals.get('GERRIT_HOST', False):
3632 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003633 # Only server setting is required. Other settings can be absent.
3634 # In that case, we ignore errors raised during option deletion attempt.
3635 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003636 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003637 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3638 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003639 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003640 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3641 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003642 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003643 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3644 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003645
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003646 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003647 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003648
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003649 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003650 RunGit(['config', 'gerrit.squash-uploads',
3651 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003652
tandrii@chromium.org28253532016-04-14 13:46:56 +00003653 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003654 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003655 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3656
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003657 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003658 # should be of the form
3659 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3660 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003661 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3662 keyvals['ORIGIN_URL_CONFIG']])
3663
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003665def urlretrieve(source, destination):
3666 """urllib is broken for SSL connections via a proxy therefore we
3667 can't use urllib.urlretrieve()."""
3668 with open(destination, 'w') as f:
3669 f.write(urllib2.urlopen(source).read())
3670
3671
ukai@chromium.org712d6102013-11-27 00:52:58 +00003672def hasSheBang(fname):
3673 """Checks fname is a #! script."""
3674 with open(fname) as f:
3675 return f.read(2).startswith('#!')
3676
3677
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003678# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3679def DownloadHooks(*args, **kwargs):
3680 pass
3681
3682
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003683def DownloadGerritHook(force):
3684 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003685
3686 Args:
3687 force: True to update hooks. False to install hooks if not present.
3688 """
3689 if not settings.GetIsGerrit():
3690 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003691 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003692 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3693 if not os.access(dst, os.X_OK):
3694 if os.path.exists(dst):
3695 if not force:
3696 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003697 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003698 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003699 if not hasSheBang(dst):
3700 DieWithError('Not a script: %s\n'
3701 'You need to download from\n%s\n'
3702 'into .git/hooks/commit-msg and '
3703 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003704 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3705 except Exception:
3706 if os.path.exists(dst):
3707 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003708 DieWithError('\nFailed to download hooks.\n'
3709 'You need to download from\n%s\n'
3710 'into .git/hooks/commit-msg and '
3711 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003712
3713
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003714def GetRietveldCodereviewSettingsInteractively():
3715 """Prompt the user for settings."""
3716 server = settings.GetDefaultServerUrl(error_ok=True)
3717 prompt = 'Rietveld server (host[:port])'
3718 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3719 newserver = ask_for_data(prompt + ':')
3720 if not server and not newserver:
3721 newserver = DEFAULT_SERVER
3722 if newserver:
3723 newserver = gclient_utils.UpgradeToHttps(newserver)
3724 if newserver != server:
3725 RunGit(['config', 'rietveld.server', newserver])
3726
3727 def SetProperty(initial, caption, name, is_url):
3728 prompt = caption
3729 if initial:
3730 prompt += ' ("x" to clear) [%s]' % initial
3731 new_val = ask_for_data(prompt + ':')
3732 if new_val == 'x':
3733 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3734 elif new_val:
3735 if is_url:
3736 new_val = gclient_utils.UpgradeToHttps(new_val)
3737 if new_val != initial:
3738 RunGit(['config', 'rietveld.' + name, new_val])
3739
3740 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3741 SetProperty(settings.GetDefaultPrivateFlag(),
3742 'Private flag (rietveld only)', 'private', False)
3743 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3744 'tree-status-url', False)
3745 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3746 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3747 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3748 'run-post-upload-hook', False)
3749
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003750
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003751class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003752 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003753
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003754 _GOOGLESOURCE = 'googlesource.com'
3755
3756 def __init__(self):
3757 # Cached list of [host, identity, source], where source is either
3758 # .gitcookies or .netrc.
3759 self._all_hosts = None
3760
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003761 def ensure_configured_gitcookies(self):
3762 """Runs checks and suggests fixes to make git use .gitcookies from default
3763 path."""
3764 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3765 configured_path = RunGitSilent(
3766 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003767 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003768 if configured_path:
3769 self._ensure_default_gitcookies_path(configured_path, default)
3770 else:
3771 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003772
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003773 @staticmethod
3774 def _ensure_default_gitcookies_path(configured_path, default_path):
3775 assert configured_path
3776 if configured_path == default_path:
3777 print('git is already configured to use your .gitcookies from %s' %
3778 configured_path)
3779 return
3780
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003781 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003782 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3783 (configured_path, default_path))
3784
3785 if not os.path.exists(configured_path):
3786 print('However, your configured .gitcookies file is missing.')
3787 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3788 action='reconfigure')
3789 RunGit(['config', '--global', 'http.cookiefile', default_path])
3790 return
3791
3792 if os.path.exists(default_path):
3793 print('WARNING: default .gitcookies file already exists %s' %
3794 default_path)
3795 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3796 default_path)
3797
3798 confirm_or_exit('Move existing .gitcookies to default location?',
3799 action='move')
3800 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003801 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003802 print('Moved and reconfigured git to use .gitcookies from %s' %
3803 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003804
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003805 @staticmethod
3806 def _configure_gitcookies_path(default_path):
3807 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3808 if os.path.exists(netrc_path):
3809 print('You seem to be using outdated .netrc for git credentials: %s' %
3810 netrc_path)
3811 print('This tool will guide you through setting up recommended '
3812 '.gitcookies store for git credentials.\n'
3813 '\n'
3814 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3815 ' git config --global --unset http.cookiefile\n'
3816 ' mv %s %s.backup\n\n' % (default_path, default_path))
3817 confirm_or_exit(action='setup .gitcookies')
3818 RunGit(['config', '--global', 'http.cookiefile', default_path])
3819 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003820
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003821 def get_hosts_with_creds(self, include_netrc=False):
3822 if self._all_hosts is None:
3823 a = gerrit_util.CookiesAuthenticator()
3824 self._all_hosts = [
3825 (h, u, s)
3826 for h, u, s in itertools.chain(
3827 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3828 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3829 )
3830 if h.endswith(self._GOOGLESOURCE)
3831 ]
3832
3833 if include_netrc:
3834 return self._all_hosts
3835 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3836
3837 def print_current_creds(self, include_netrc=False):
3838 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3839 if not hosts:
3840 print('No Git/Gerrit credentials found')
3841 return
3842 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3843 header = [('Host', 'User', 'Which file'),
3844 ['=' * l for l in lengths]]
3845 for row in (header + hosts):
3846 print('\t'.join((('%%+%ds' % l) % s)
3847 for l, s in zip(lengths, row)))
3848
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003849 @staticmethod
3850 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003851 """Parses identity "git-<username>.domain" into <username> and domain."""
3852 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003853 # distinguishable from sub-domains. But we do know typical domains:
3854 if identity.endswith('.chromium.org'):
3855 domain = 'chromium.org'
3856 username = identity[:-len('.chromium.org')]
3857 else:
3858 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003859 if username.startswith('git-'):
3860 username = username[len('git-'):]
3861 return username, domain
3862
3863 def _get_usernames_of_domain(self, domain):
3864 """Returns list of usernames referenced by .gitcookies in a given domain."""
3865 identities_by_domain = {}
3866 for _, identity, _ in self.get_hosts_with_creds():
3867 username, domain = self._parse_identity(identity)
3868 identities_by_domain.setdefault(domain, []).append(username)
3869 return identities_by_domain.get(domain)
3870
3871 def _canonical_git_googlesource_host(self, host):
3872 """Normalizes Gerrit hosts (with '-review') to Git host."""
3873 assert host.endswith(self._GOOGLESOURCE)
3874 # Prefix doesn't include '.' at the end.
3875 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3876 if prefix.endswith('-review'):
3877 prefix = prefix[:-len('-review')]
3878 return prefix + '.' + self._GOOGLESOURCE
3879
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003880 def _canonical_gerrit_googlesource_host(self, host):
3881 git_host = self._canonical_git_googlesource_host(host)
3882 prefix = git_host.split('.', 1)[0]
3883 return prefix + '-review.' + self._GOOGLESOURCE
3884
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003885 def _get_counterpart_host(self, host):
3886 assert host.endswith(self._GOOGLESOURCE)
3887 git = self._canonical_git_googlesource_host(host)
3888 gerrit = self._canonical_gerrit_googlesource_host(git)
3889 return git if gerrit == host else gerrit
3890
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003891 def has_generic_host(self):
3892 """Returns whether generic .googlesource.com has been configured.
3893
3894 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3895 """
3896 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3897 if host == '.' + self._GOOGLESOURCE:
3898 return True
3899 return False
3900
3901 def _get_git_gerrit_identity_pairs(self):
3902 """Returns map from canonic host to pair of identities (Git, Gerrit).
3903
3904 One of identities might be None, meaning not configured.
3905 """
3906 host_to_identity_pairs = {}
3907 for host, identity, _ in self.get_hosts_with_creds():
3908 canonical = self._canonical_git_googlesource_host(host)
3909 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3910 idx = 0 if canonical == host else 1
3911 pair[idx] = identity
3912 return host_to_identity_pairs
3913
3914 def get_partially_configured_hosts(self):
3915 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003916 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3917 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3918 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003919
3920 def get_conflicting_hosts(self):
3921 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003922 host
3923 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003924 if None not in (i1, i2) and i1 != i2)
3925
3926 def get_duplicated_hosts(self):
3927 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3928 return set(host for host, count in counters.iteritems() if count > 1)
3929
3930 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3931 'chromium.googlesource.com': 'chromium.org',
3932 'chrome-internal.googlesource.com': 'google.com',
3933 }
3934
3935 def get_hosts_with_wrong_identities(self):
3936 """Finds hosts which **likely** reference wrong identities.
3937
3938 Note: skips hosts which have conflicting identities for Git and Gerrit.
3939 """
3940 hosts = set()
3941 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3942 pair = self._get_git_gerrit_identity_pairs().get(host)
3943 if pair and pair[0] == pair[1]:
3944 _, domain = self._parse_identity(pair[0])
3945 if domain != expected:
3946 hosts.add(host)
3947 return hosts
3948
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003949 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003950 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003951 hosts = sorted(hosts)
3952 assert hosts
3953 if extra_column_func is None:
3954 extras = [''] * len(hosts)
3955 else:
3956 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003957 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3958 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003959 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003960 lines.append(tmpl % he)
3961 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003962
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003963 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003964 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003965 yield ('.googlesource.com wildcard record detected',
3966 ['Chrome Infrastructure team recommends to list full host names '
3967 'explicitly.'],
3968 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003969
3970 dups = self.get_duplicated_hosts()
3971 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003972 yield ('The following hosts were defined twice',
3973 self._format_hosts(dups),
3974 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003975
3976 partial = self.get_partially_configured_hosts()
3977 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003978 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3979 'These hosts are missing',
3980 self._format_hosts(partial, lambda host: 'but %s defined' %
3981 self._get_counterpart_host(host)),
3982 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003983
3984 conflicting = self.get_conflicting_hosts()
3985 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003986 yield ('The following Git hosts have differing credentials from their '
3987 'Gerrit counterparts',
3988 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3989 tuple(self._get_git_gerrit_identity_pairs()[host])),
3990 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003991
3992 wrong = self.get_hosts_with_wrong_identities()
3993 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003994 yield ('These hosts likely use wrong identity',
3995 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3996 (self._get_git_gerrit_identity_pairs()[host][0],
3997 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3998 wrong)
3999
4000 def find_and_report_problems(self):
4001 """Returns True if there was at least one problem, else False."""
4002 found = False
4003 bad_hosts = set()
4004 for title, sublines, hosts in self._find_problems():
4005 if not found:
4006 found = True
4007 print('\n\n.gitcookies problem report:\n')
4008 bad_hosts.update(hosts or [])
4009 print(' %s%s' % (title , (':' if sublines else '')))
4010 if sublines:
4011 print()
4012 print(' %s' % '\n '.join(sublines))
4013 print()
4014
4015 if bad_hosts:
4016 assert found
4017 print(' You can manually remove corresponding lines in your %s file and '
4018 'visit the following URLs with correct account to generate '
4019 'correct credential lines:\n' %
4020 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4021 print(' %s' % '\n '.join(sorted(set(
4022 gerrit_util.CookiesAuthenticator().get_new_password_url(
4023 self._canonical_git_googlesource_host(host))
4024 for host in bad_hosts
4025 ))))
4026 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004027
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004028
4029def CMDcreds_check(parser, args):
4030 """Checks credentials and suggests changes."""
4031 _, _ = parser.parse_args(args)
4032
4033 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004034 DieWithError(
4035 'This command is not designed for GCE, are you on a bot?\n'
4036 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004037
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004038 checker = _GitCookiesChecker()
4039 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004040
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004041 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004042 checker.print_current_creds(include_netrc=True)
4043
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004044 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004045 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004046 return 0
4047 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004048
4049
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004050@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004051def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004052 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004053
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004054 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004055 # TODO(tandrii): remove this once we switch to Gerrit.
4056 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004057 parser.add_option('--activate-update', action='store_true',
4058 help='activate auto-updating [rietveld] section in '
4059 '.git/config')
4060 parser.add_option('--deactivate-update', action='store_true',
4061 help='deactivate auto-updating [rietveld] section in '
4062 '.git/config')
4063 options, args = parser.parse_args(args)
4064
4065 if options.deactivate_update:
4066 RunGit(['config', 'rietveld.autoupdate', 'false'])
4067 return
4068
4069 if options.activate_update:
4070 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4071 return
4072
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004073 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004074 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004075 return 0
4076
4077 url = args[0]
4078 if not url.endswith('codereview.settings'):
4079 url = os.path.join(url, 'codereview.settings')
4080
4081 # Load code review settings and download hooks (if available).
4082 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4083 return 0
4084
4085
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004086def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004087 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004088 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4089 branch = ShortBranchName(branchref)
4090 _, args = parser.parse_args(args)
4091 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004092 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004093 return RunGit(['config', 'branch.%s.base-url' % branch],
4094 error_ok=False).strip()
4095 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004096 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004097 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4098 error_ok=False).strip()
4099
4100
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004101def color_for_status(status):
4102 """Maps a Changelist status to color, for CMDstatus and other tools."""
4103 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004104 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004105 'waiting': Fore.BLUE,
4106 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004107 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004108 'lgtm': Fore.GREEN,
4109 'commit': Fore.MAGENTA,
4110 'closed': Fore.CYAN,
4111 'error': Fore.WHITE,
4112 }.get(status, Fore.WHITE)
4113
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004114
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004115def get_cl_statuses(changes, fine_grained, max_processes=None):
4116 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004117
4118 If fine_grained is true, this will fetch CL statuses from the server.
4119 Otherwise, simply indicate if there's a matching url for the given branches.
4120
4121 If max_processes is specified, it is used as the maximum number of processes
4122 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4123 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004124
4125 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004126 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004127 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004128 upload.verbosity = 0
4129
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004130 if not changes:
4131 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004132
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004133 if not fine_grained:
4134 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004135 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004136 for cl in changes:
4137 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004138 return
4139
4140 # First, sort out authentication issues.
4141 logging.debug('ensuring credentials exist')
4142 for cl in changes:
4143 cl.EnsureAuthenticated(force=False, refresh=True)
4144
4145 def fetch(cl):
4146 try:
4147 return (cl, cl.GetStatus())
4148 except:
4149 # See http://crbug.com/629863.
4150 logging.exception('failed to fetch status for %s:', cl)
4151 raise
4152
4153 threads_count = len(changes)
4154 if max_processes:
4155 threads_count = max(1, min(threads_count, max_processes))
4156 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4157
4158 pool = ThreadPool(threads_count)
4159 fetched_cls = set()
4160 try:
4161 it = pool.imap_unordered(fetch, changes).__iter__()
4162 while True:
4163 try:
4164 cl, status = it.next(timeout=5)
4165 except multiprocessing.TimeoutError:
4166 break
4167 fetched_cls.add(cl)
4168 yield cl, status
4169 finally:
4170 pool.close()
4171
4172 # Add any branches that failed to fetch.
4173 for cl in set(changes) - fetched_cls:
4174 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004175
rmistry@google.com2dd99862015-06-22 12:22:18 +00004176
4177def upload_branch_deps(cl, args):
4178 """Uploads CLs of local branches that are dependents of the current branch.
4179
4180 If the local branch dependency tree looks like:
4181 test1 -> test2.1 -> test3.1
4182 -> test3.2
4183 -> test2.2 -> test3.3
4184
4185 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4186 run on the dependent branches in this order:
4187 test2.1, test3.1, test3.2, test2.2, test3.3
4188
4189 Note: This function does not rebase your local dependent branches. Use it when
4190 you make a change to the parent branch that will not conflict with its
4191 dependent branches, and you would like their dependencies updated in
4192 Rietveld.
4193 """
4194 if git_common.is_dirty_git_tree('upload-branch-deps'):
4195 return 1
4196
4197 root_branch = cl.GetBranch()
4198 if root_branch is None:
4199 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4200 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004201 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004202 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4203 'patchset dependencies without an uploaded CL.')
4204
4205 branches = RunGit(['for-each-ref',
4206 '--format=%(refname:short) %(upstream:short)',
4207 'refs/heads'])
4208 if not branches:
4209 print('No local branches found.')
4210 return 0
4211
4212 # Create a dictionary of all local branches to the branches that are dependent
4213 # on it.
4214 tracked_to_dependents = collections.defaultdict(list)
4215 for b in branches.splitlines():
4216 tokens = b.split()
4217 if len(tokens) == 2:
4218 branch_name, tracked = tokens
4219 tracked_to_dependents[tracked].append(branch_name)
4220
vapiera7fbd5a2016-06-16 09:17:49 -07004221 print()
4222 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004223 dependents = []
4224 def traverse_dependents_preorder(branch, padding=''):
4225 dependents_to_process = tracked_to_dependents.get(branch, [])
4226 padding += ' '
4227 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004228 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004229 dependents.append(dependent)
4230 traverse_dependents_preorder(dependent, padding)
4231 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004232 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004233
4234 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004235 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004236 return 0
4237
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004238 confirm_or_exit('This command will checkout all dependent branches and run '
4239 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004240
andybons@chromium.org962f9462016-02-03 20:00:42 +00004241 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004242 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004243 args.extend(['-t', 'Updated patchset dependency'])
4244
rmistry@google.com2dd99862015-06-22 12:22:18 +00004245 # Record all dependents that failed to upload.
4246 failures = {}
4247 # Go through all dependents, checkout the branch and upload.
4248 try:
4249 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004250 print()
4251 print('--------------------------------------')
4252 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004253 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004254 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004255 try:
4256 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004257 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004258 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004259 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004260 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004261 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004262 finally:
4263 # Swap back to the original root branch.
4264 RunGit(['checkout', '-q', root_branch])
4265
vapiera7fbd5a2016-06-16 09:17:49 -07004266 print()
4267 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004268 for dependent_branch in dependents:
4269 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004270 print(' %s : %s' % (dependent_branch, upload_status))
4271 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004272
4273 return 0
4274
4275
kmarshall3bff56b2016-06-06 18:31:47 -07004276def CMDarchive(parser, args):
4277 """Archives and deletes branches associated with closed changelists."""
4278 parser.add_option(
4279 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004280 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004281 parser.add_option(
4282 '-f', '--force', action='store_true',
4283 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004284 parser.add_option(
4285 '-d', '--dry-run', action='store_true',
4286 help='Skip the branch tagging and removal steps.')
4287 parser.add_option(
4288 '-t', '--notags', action='store_true',
4289 help='Do not tag archived branches. '
4290 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004291
4292 auth.add_auth_options(parser)
4293 options, args = parser.parse_args(args)
4294 if args:
4295 parser.error('Unsupported args: %s' % ' '.join(args))
4296 auth_config = auth.extract_auth_config_from_options(options)
4297
4298 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4299 if not branches:
4300 return 0
4301
vapiera7fbd5a2016-06-16 09:17:49 -07004302 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004303 changes = [Changelist(branchref=b, auth_config=auth_config)
4304 for b in branches.splitlines()]
4305 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4306 statuses = get_cl_statuses(changes,
4307 fine_grained=True,
4308 max_processes=options.maxjobs)
4309 proposal = [(cl.GetBranch(),
4310 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4311 for cl, status in statuses
4312 if status == 'closed']
4313 proposal.sort()
4314
4315 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004316 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004317 return 0
4318
4319 current_branch = GetCurrentBranch()
4320
vapiera7fbd5a2016-06-16 09:17:49 -07004321 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004322 if options.notags:
4323 for next_item in proposal:
4324 print(' ' + next_item[0])
4325 else:
4326 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4327 for next_item in proposal:
4328 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004329
kmarshall9249e012016-08-23 12:02:16 -07004330 # Quit now on precondition failure or if instructed by the user, either
4331 # via an interactive prompt or by command line flags.
4332 if options.dry_run:
4333 print('\nNo changes were made (dry run).\n')
4334 return 0
4335 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004336 print('You are currently on a branch \'%s\' which is associated with a '
4337 'closed codereview issue, so archive cannot proceed. Please '
4338 'checkout another branch and run this command again.' %
4339 current_branch)
4340 return 1
kmarshall9249e012016-08-23 12:02:16 -07004341 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004342 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4343 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004344 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004345 return 1
4346
4347 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004348 if not options.notags:
4349 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004350 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004351
vapiera7fbd5a2016-06-16 09:17:49 -07004352 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004353
4354 return 0
4355
4356
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004357def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004358 """Show status of changelists.
4359
4360 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004361 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004362 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004363 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004364 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004365 - Magenta in the commit queue
4366 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004367 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004368
4369 Also see 'git cl comments'.
4370 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004371 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004372 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004373 parser.add_option('-f', '--fast', action='store_true',
4374 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004375 parser.add_option(
4376 '-j', '--maxjobs', action='store', type=int,
4377 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004378
4379 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004380 _add_codereview_issue_select_options(
4381 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004382 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004383 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004384 if args:
4385 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004386 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004387
iannuccie53c9352016-08-17 14:40:40 -07004388 if options.issue is not None and not options.field:
4389 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004391 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004392 cl = Changelist(auth_config=auth_config, issue=options.issue,
4393 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004394 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004395 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004396 elif options.field == 'id':
4397 issueid = cl.GetIssue()
4398 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004399 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004400 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004401 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004402 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004403 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004404 elif options.field == 'status':
4405 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004406 elif options.field == 'url':
4407 url = cl.GetIssueURL()
4408 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004409 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004410 return 0
4411
4412 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4413 if not branches:
4414 print('No local branch found.')
4415 return 0
4416
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004417 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004418 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004419 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004420 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004421 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004422 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004423 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004424
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004425 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004426 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4427 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4428 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004429 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004430 c, status = output.next()
4431 branch_statuses[c.GetBranch()] = status
4432 status = branch_statuses.pop(branch)
4433 url = cl.GetIssueURL()
4434 if url and (not status or status == 'error'):
4435 # The issue probably doesn't exist anymore.
4436 url += ' (broken)'
4437
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004438 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004439 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004440 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004441 color = ''
4442 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004443 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004444 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004445 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004446 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004447
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004448
4449 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004450 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004451 print('Current branch: %s' % branch)
4452 for cl in changes:
4453 if cl.GetBranch() == branch:
4454 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004455 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004456 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004457 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004458 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004459 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004460 print('Issue description:')
4461 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004462 return 0
4463
4464
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004465def colorize_CMDstatus_doc():
4466 """To be called once in main() to add colors to git cl status help."""
4467 colors = [i for i in dir(Fore) if i[0].isupper()]
4468
4469 def colorize_line(line):
4470 for color in colors:
4471 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004472 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004473 indent = len(line) - len(line.lstrip(' ')) + 1
4474 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4475 return line
4476
4477 lines = CMDstatus.__doc__.splitlines()
4478 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4479
4480
phajdan.jre328cf92016-08-22 04:12:17 -07004481def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004482 if path == '-':
4483 json.dump(contents, sys.stdout)
4484 else:
4485 with open(path, 'w') as f:
4486 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004487
4488
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004489@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004490def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004491 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004492
4493 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004494 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004495 parser.add_option('-r', '--reverse', action='store_true',
4496 help='Lookup the branch(es) for the specified issues. If '
4497 'no issues are specified, all branches with mapped '
4498 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004499 parser.add_option('--json',
4500 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004501 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004502 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004503 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004504
dnj@chromium.org406c4402015-03-03 17:22:28 +00004505 if options.reverse:
4506 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004507 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004508 # Reverse issue lookup.
4509 issue_branch_map = {}
4510 for branch in branches:
4511 cl = Changelist(branchref=branch)
4512 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4513 if not args:
4514 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004515 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004516 for issue in args:
4517 if not issue:
4518 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004519 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004520 print('Branch for issue number %s: %s' % (
4521 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004522 if options.json:
4523 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004524 return 0
4525
4526 if len(args) > 0:
4527 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4528 if not issue.valid:
4529 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4530 'or no argument to list it.\n'
4531 'Maybe you want to run git cl status?')
4532 cl = Changelist(codereview=issue.codereview)
4533 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004534 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004535 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004536 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4537 if options.json:
4538 write_json(options.json, {
4539 'issue': cl.GetIssue(),
4540 'issue_url': cl.GetIssueURL(),
4541 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004542 return 0
4543
4544
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004545def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004546 """Shows or posts review comments for any changelist."""
4547 parser.add_option('-a', '--add-comment', dest='comment',
4548 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004549 parser.add_option('-i', '--issue', dest='issue',
4550 help='review issue id (defaults to current issue). '
4551 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004552 parser.add_option('-m', '--machine-readable', dest='readable',
4553 action='store_false', default=True,
4554 help='output comments in a format compatible with '
4555 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004556 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004557 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004558 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004559 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004560 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004561 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004562 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004563
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004564 issue = None
4565 if options.issue:
4566 try:
4567 issue = int(options.issue)
4568 except ValueError:
4569 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004570 if not options.forced_codereview:
4571 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004572
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004573 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004574 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004575 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004576
4577 if options.comment:
4578 cl.AddComment(options.comment)
4579 return 0
4580
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004581 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4582 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004583 for comment in summary:
4584 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004585 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004586 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004587 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004588 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004589 color = Fore.MAGENTA
4590 else:
4591 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004592 print('\n%s%s %s%s\n%s' % (
4593 color,
4594 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4595 comment.sender,
4596 Fore.RESET,
4597 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4598
smut@google.comc85ac942015-09-15 16:34:43 +00004599 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004600 def pre_serialize(c):
4601 dct = c.__dict__.copy()
4602 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4603 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004604 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004605 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004606 return 0
4607
4608
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004609@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004610def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004611 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004612 parser.add_option('-d', '--display', action='store_true',
4613 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004614 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004615 help='New description to set for this issue (- for stdin, '
4616 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004617 parser.add_option('-f', '--force', action='store_true',
4618 help='Delete any unpublished Gerrit edits for this issue '
4619 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004620
4621 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004622 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004623 options, args = parser.parse_args(args)
4624 _process_codereview_select_options(parser, options)
4625
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004626 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004627 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004628 target_issue_arg = ParseIssueNumberArgument(args[0],
4629 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004630 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004631 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004632
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004633 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004634
martiniss6eda05f2016-06-30 10:18:35 -07004635 kwargs = {
4636 'auth_config': auth_config,
4637 'codereview': options.forced_codereview,
4638 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004639 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004640 if target_issue_arg:
4641 kwargs['issue'] = target_issue_arg.issue
4642 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004643 if target_issue_arg.codereview and not options.forced_codereview:
4644 detected_codereview_from_url = True
4645 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004646
4647 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004648 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004649 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004650 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004651
4652 if detected_codereview_from_url:
4653 logging.info('canonical issue/change URL: %s (type: %s)\n',
4654 cl.GetIssueURL(), target_issue_arg.codereview)
4655
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004656 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004657
smut@google.com34fb6b12015-07-13 20:03:26 +00004658 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004659 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004660 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004661
4662 if options.new_description:
4663 text = options.new_description
4664 if text == '-':
4665 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004666 elif text == '+':
4667 base_branch = cl.GetCommonAncestorWithUpstream()
4668 change = cl.GetChange(base_branch, None, local_description=True)
4669 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004670
4671 description.set_description(text)
4672 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004673 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004674
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004675 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004676 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004677 return 0
4678
4679
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004680def CreateDescriptionFromLog(args):
4681 """Pulls out the commit log to use as a base for the CL description."""
4682 log_args = []
4683 if len(args) == 1 and not args[0].endswith('.'):
4684 log_args = [args[0] + '..']
4685 elif len(args) == 1 and args[0].endswith('...'):
4686 log_args = [args[0][:-1]]
4687 elif len(args) == 2:
4688 log_args = [args[0] + '..' + args[1]]
4689 else:
4690 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004691 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004692
4693
thestig@chromium.org44202a22014-03-11 19:22:18 +00004694def CMDlint(parser, args):
4695 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004696 parser.add_option('--filter', action='append', metavar='-x,+y',
4697 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004698 auth.add_auth_options(parser)
4699 options, args = parser.parse_args(args)
4700 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004701
4702 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004703 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004704 try:
4705 import cpplint
4706 import cpplint_chromium
4707 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004708 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004709 return 1
4710
4711 # Change the current working directory before calling lint so that it
4712 # shows the correct base.
4713 previous_cwd = os.getcwd()
4714 os.chdir(settings.GetRoot())
4715 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004716 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004717 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4718 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004719 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004720 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004721 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004722
4723 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004724 command = args + files
4725 if options.filter:
4726 command = ['--filter=' + ','.join(options.filter)] + command
4727 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004728
4729 white_regex = re.compile(settings.GetLintRegex())
4730 black_regex = re.compile(settings.GetLintIgnoreRegex())
4731 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4732 for filename in filenames:
4733 if white_regex.match(filename):
4734 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004735 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004736 else:
4737 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4738 extra_check_functions)
4739 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004740 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004741 finally:
4742 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004743 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004744 if cpplint._cpplint_state.error_count != 0:
4745 return 1
4746 return 0
4747
4748
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004749def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004750 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004751 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004752 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004753 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004754 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004755 parser.add_option('--all', action='store_true',
4756 help='Run checks against all files, not just modified ones')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004757 auth.add_auth_options(parser)
4758 options, args = parser.parse_args(args)
4759 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004760
sbc@chromium.org71437c02015-04-09 19:29:40 +00004761 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004762 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004763 return 1
4764
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004765 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004766 if args:
4767 base_branch = args[0]
4768 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004769 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004770 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004771
Aaron Gable8076c282017-11-29 14:39:41 -08004772 if options.all:
4773 base_change = cl.GetChange(base_branch, None)
4774 files = [('M', f) for f in base_change.AllFiles()]
4775 change = presubmit_support.GitChange(
4776 base_change.Name(),
4777 base_change.FullDescriptionText(),
4778 base_change.RepositoryRoot(),
4779 files,
4780 base_change.issue,
4781 base_change.patchset,
4782 base_change.author_email,
4783 base_change._upstream)
4784 else:
4785 change = cl.GetChange(base_branch, None)
4786
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004787 cl.RunHook(
4788 committing=not options.upload,
4789 may_prompt=False,
4790 verbose=options.verbose,
Aaron Gable8076c282017-11-29 14:39:41 -08004791 change=change)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004792 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004793
4794
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004795def GenerateGerritChangeId(message):
4796 """Returns Ixxxxxx...xxx change id.
4797
4798 Works the same way as
4799 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4800 but can be called on demand on all platforms.
4801
4802 The basic idea is to generate git hash of a state of the tree, original commit
4803 message, author/committer info and timestamps.
4804 """
4805 lines = []
4806 tree_hash = RunGitSilent(['write-tree'])
4807 lines.append('tree %s' % tree_hash.strip())
4808 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4809 if code == 0:
4810 lines.append('parent %s' % parent.strip())
4811 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4812 lines.append('author %s' % author.strip())
4813 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4814 lines.append('committer %s' % committer.strip())
4815 lines.append('')
4816 # Note: Gerrit's commit-hook actually cleans message of some lines and
4817 # whitespace. This code is not doing this, but it clearly won't decrease
4818 # entropy.
4819 lines.append(message)
4820 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4821 stdin='\n'.join(lines))
4822 return 'I%s' % change_hash.strip()
4823
4824
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004825def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004826 """Computes the remote branch ref to use for the CL.
4827
4828 Args:
4829 remote (str): The git remote for the CL.
4830 remote_branch (str): The git remote branch for the CL.
4831 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004832 """
4833 if not (remote and remote_branch):
4834 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004835
wittman@chromium.org455dc922015-01-26 20:15:50 +00004836 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004837 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004838 # refs, which are then translated into the remote full symbolic refs
4839 # below.
4840 if '/' not in target_branch:
4841 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4842 else:
4843 prefix_replacements = (
4844 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4845 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4846 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4847 )
4848 match = None
4849 for regex, replacement in prefix_replacements:
4850 match = re.search(regex, target_branch)
4851 if match:
4852 remote_branch = target_branch.replace(match.group(0), replacement)
4853 break
4854 if not match:
4855 # This is a branch path but not one we recognize; use as-is.
4856 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004857 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4858 # Handle the refs that need to land in different refs.
4859 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004860
wittman@chromium.org455dc922015-01-26 20:15:50 +00004861 # Create the true path to the remote branch.
4862 # Does the following translation:
4863 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4864 # * refs/remotes/origin/master -> refs/heads/master
4865 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4866 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4867 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4868 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4869 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4870 'refs/heads/')
4871 elif remote_branch.startswith('refs/remotes/branch-heads'):
4872 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004873
wittman@chromium.org455dc922015-01-26 20:15:50 +00004874 return remote_branch
4875
4876
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004877def cleanup_list(l):
4878 """Fixes a list so that comma separated items are put as individual items.
4879
4880 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4881 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4882 """
4883 items = sum((i.split(',') for i in l), [])
4884 stripped_items = (i.strip() for i in items)
4885 return sorted(filter(None, stripped_items))
4886
4887
Aaron Gable4db38df2017-11-03 14:59:07 -07004888@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004889def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004890 """Uploads the current changelist to codereview.
4891
4892 Can skip dependency patchset uploads for a branch by running:
4893 git config branch.branch_name.skip-deps-uploads True
4894 To unset run:
4895 git config --unset branch.branch_name.skip-deps-uploads
4896 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004897
4898 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4899 a bug number, this bug number is automatically populated in the CL
4900 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004901
4902 If subject contains text in square brackets or has "<text>: " prefix, such
4903 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4904 [git-cl] add support for hashtags
4905 Foo bar: implement foo
4906 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004907 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004908 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4909 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004910 parser.add_option('--bypass-watchlists', action='store_true',
4911 dest='bypass_watchlists',
4912 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004913 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004914 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004915 parser.add_option('--message', '-m', dest='message',
4916 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004917 parser.add_option('-b', '--bug',
4918 help='pre-populate the bug number(s) for this issue. '
4919 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004920 parser.add_option('--message-file', dest='message_file',
4921 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004922 parser.add_option('--title', '-t', dest='title',
4923 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004924 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004925 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004926 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004927 parser.add_option('--tbrs',
4928 action='append', default=[],
4929 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004930 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004931 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004932 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004933 parser.add_option('--hashtag', dest='hashtags',
4934 action='append', default=[],
4935 help=('Gerrit hashtag for new CL; '
4936 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004937 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004938 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004939 parser.add_option('--emulate_svn_auto_props',
4940 '--emulate-svn-auto-props',
4941 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004942 dest="emulate_svn_auto_props",
4943 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004944 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004945 help='tell the commit queue to commit this patchset; '
4946 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004947 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004948 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004949 metavar='TARGET',
4950 help='Apply CL to remote ref TARGET. ' +
4951 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004952 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004953 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004954 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004955 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004956 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004957 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004958 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4959 const='TBR', help='add a set of OWNERS to TBR')
4960 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4961 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004962 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4963 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004964 help='Send the patchset to do a CQ dry run right after '
4965 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004966 parser.add_option('--dependencies', action='store_true',
4967 help='Uploads CLs of all the local branches that depend on '
4968 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004969
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004970 # TODO: remove Rietveld flags
4971 parser.add_option('--private', action='store_true',
4972 help='set the review private (rietveld only)')
4973 parser.add_option('--email', default=None,
4974 help='email address to use to connect to Rietveld')
4975
rmistry@google.com2dd99862015-06-22 12:22:18 +00004976 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004977 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004978 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004979 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004980 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004981 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004982
sbc@chromium.org71437c02015-04-09 19:29:40 +00004983 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004984 return 1
4985
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004986 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004987 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004988 options.cc = cleanup_list(options.cc)
4989
tandriib80458a2016-06-23 12:20:07 -07004990 if options.message_file:
4991 if options.message:
4992 parser.error('only one of --message and --message-file allowed.')
4993 options.message = gclient_utils.FileRead(options.message_file)
4994 options.message_file = None
4995
tandrii4d0545a2016-07-06 03:56:49 -07004996 if options.cq_dry_run and options.use_commit_queue:
4997 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4998
Aaron Gableedbc4132017-09-11 13:22:28 -07004999 if options.use_commit_queue:
5000 options.send_mail = True
5001
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005002 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5003 settings.GetIsGerrit()
5004
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005005 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005006 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005007
5008
Francois Dorayd42c6812017-05-30 15:10:20 -04005009@subcommand.usage('--description=<description file>')
5010def CMDsplit(parser, args):
5011 """Splits a branch into smaller branches and uploads CLs.
5012
5013 Creates a branch and uploads a CL for each group of files modified in the
5014 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005015 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005016 the shared OWNERS file.
5017 """
5018 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005019 help="A text file containing a CL description in which "
5020 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005021 parser.add_option("-c", "--comment", dest="comment_file",
5022 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005023 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5024 default=False,
5025 help="List the files and reviewers for each CL that would "
5026 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005027 options, _ = parser.parse_args(args)
5028
5029 if not options.description_file:
5030 parser.error('No --description flag specified.')
5031
5032 def WrappedCMDupload(args):
5033 return CMDupload(OptionParser(), args)
5034
5035 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005036 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005037
5038
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005039@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005040def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005041 """DEPRECATED: Used to commit the current changelist via git-svn."""
5042 message = ('git-cl no longer supports committing to SVN repositories via '
5043 'git-svn. You probably want to use `git cl land` instead.')
5044 print(message)
5045 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005046
5047
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005048# Two special branches used by git cl land.
5049MERGE_BRANCH = 'git-cl-commit'
5050CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5051
5052
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005053@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005054def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005055 """Commits the current changelist via git.
5056
5057 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5058 upstream and closes the issue automatically and atomically.
5059
5060 Otherwise (in case of Rietveld):
5061 Squashes branch into a single commit.
5062 Updates commit message with metadata (e.g. pointer to review).
5063 Pushes the code upstream.
5064 Updates review and closes.
5065 """
5066 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5067 help='bypass upload presubmit hook')
5068 parser.add_option('-m', dest='message',
5069 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005070 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005071 help="force yes to questions (don't prompt)")
5072 parser.add_option('-c', dest='contributor',
5073 help="external contributor for patch (appended to " +
5074 "description and used as author for git). Should be " +
5075 "formatted as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005076 auth.add_auth_options(parser)
5077 (options, args) = parser.parse_args(args)
5078 auth_config = auth.extract_auth_config_from_options(options)
5079
5080 cl = Changelist(auth_config=auth_config)
5081
5082 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
5083 if cl.IsGerrit():
5084 if options.message:
5085 # This could be implemented, but it requires sending a new patch to
5086 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5087 # Besides, Gerrit has the ability to change the commit message on submit
5088 # automatically, thus there is no need to support this option (so far?).
5089 parser.error('-m MESSAGE option is not supported for Gerrit.')
5090 if options.contributor:
5091 parser.error(
5092 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5093 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5094 'the contributor\'s "name <email>". If you can\'t upload such a '
5095 'commit for review, contact your repository admin and request'
5096 '"Forge-Author" permission.')
5097 if not cl.GetIssue():
5098 DieWithError('You must upload the change first to Gerrit.\n'
5099 ' If you would rather have `git cl land` upload '
5100 'automatically for you, see http://crbug.com/642759')
5101 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
5102 options.verbose)
5103
5104 current = cl.GetBranch()
5105 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
5106 if remote == '.':
5107 print()
5108 print('Attempting to push branch %r into another local branch!' % current)
5109 print()
5110 print('Either reparent this branch on top of origin/master:')
5111 print(' git reparent-branch --root')
5112 print()
5113 print('OR run `git rebase-update` if you think the parent branch is ')
5114 print('already committed.')
5115 print()
5116 print(' Current parent: %r' % upstream_branch)
5117 return 1
5118
5119 if not args:
5120 # Default to merging against our best guess of the upstream branch.
5121 args = [cl.GetUpstreamBranch()]
5122
5123 if options.contributor:
5124 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005125 print("Please provide contributor as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005126 return 1
5127
5128 base_branch = args[0]
5129
5130 if git_common.is_dirty_git_tree('land'):
5131 return 1
5132
5133 # This rev-list syntax means "show all commits not in my branch that
5134 # are in base_branch".
5135 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
5136 base_branch]).splitlines()
5137 if upstream_commits:
5138 print('Base branch "%s" has %d commits '
5139 'not in this branch.' % (base_branch, len(upstream_commits)))
5140 print('Run "git merge %s" before attempting to land.' % base_branch)
5141 return 1
5142
5143 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
5144 if not options.bypass_hooks:
5145 author = None
5146 if options.contributor:
5147 author = re.search(r'\<(.*)\>', options.contributor).group(1)
5148 hook_results = cl.RunHook(
5149 committing=True,
5150 may_prompt=not options.force,
5151 verbose=options.verbose,
5152 change=cl.GetChange(merge_base, author))
5153 if not hook_results.should_continue():
5154 return 1
5155
5156 # Check the tree status if the tree status URL is set.
5157 status = GetTreeStatus()
5158 if 'closed' == status:
5159 print('The tree is closed. Please wait for it to reopen. Use '
5160 '"git cl land --bypass-hooks" to commit on a closed tree.')
5161 return 1
5162 elif 'unknown' == status:
5163 print('Unable to determine tree status. Please verify manually and '
5164 'use "git cl land --bypass-hooks" to commit on a closed tree.')
5165 return 1
5166
5167 change_desc = ChangeDescription(options.message)
5168 if not change_desc.description and cl.GetIssue():
5169 change_desc = ChangeDescription(cl.GetDescription())
5170
5171 if not change_desc.description:
5172 if not cl.GetIssue() and options.bypass_hooks:
5173 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
5174 else:
5175 print('No description set.')
5176 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
5177 return 1
5178
5179 # Keep a separate copy for the commit message, because the commit message
5180 # contains the link to the Rietveld issue, while the Rietveld message contains
5181 # the commit viewvc url.
5182 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07005183 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005184 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005185
5186 commit_desc = ChangeDescription(change_desc.description)
5187 if cl.GetIssue():
5188 # Xcode won't linkify this URL unless there is a non-whitespace character
5189 # after it. Add a period on a new line to circumvent this. Also add a space
5190 # before the period to make sure that Gitiles continues to correctly resolve
5191 # the URL.
5192 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
5193 if options.contributor:
5194 commit_desc.append_footer('Patch from %s.' % options.contributor)
5195
5196 print('Description:')
5197 print(commit_desc.description)
5198
5199 branches = [merge_base, cl.GetBranchRef()]
5200 if not options.force:
Aaron Gable13101a62018-02-09 13:20:41 -08005201 print_stats(branches)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005202
5203 # We want to squash all this branch's commits into one commit with the proper
5204 # description. We do this by doing a "reset --soft" to the base branch (which
5205 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005206 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005207 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
5208 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
5209 result = RunGitWithCode(showref_cmd)
5210 if result[0] == 0:
5211 RunGit(['branch', '-D', branch])
5212
5213 # We might be in a directory that's present in this branch but not in the
5214 # trunk. Move up to the top of the tree so that git commands that expect a
5215 # valid CWD won't fail after we check out the merge branch.
5216 rel_base_path = settings.GetRelativeRoot()
5217 if rel_base_path:
5218 os.chdir(rel_base_path)
5219
5220 # Stuff our change into the merge branch.
5221 # We wrap in a try...finally block so if anything goes wrong,
5222 # we clean up the branches.
5223 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005224 revision = None
5225 try:
5226 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5227 RunGit(['reset', '--soft', merge_base])
5228 if options.contributor:
5229 RunGit(
5230 [
5231 'commit', '--author', options.contributor,
5232 '-m', commit_desc.description,
5233 ])
5234 else:
5235 RunGit(['commit', '-m', commit_desc.description])
5236
5237 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5238 mirror = settings.GetGitMirror(remote)
5239 if mirror:
5240 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005241 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005242 else:
5243 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005244 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005245 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5246
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005247 retcode = PushToGitWithAutoRebase(
5248 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005249 if retcode == 0:
5250 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005251 if git_numberer_enabled:
5252 change_desc = ChangeDescription(
5253 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005254 except: # pylint: disable=bare-except
5255 if _IS_BEING_TESTED:
5256 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5257 + '-' * 30 + '8<' + '-' * 30)
5258 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5259 raise
5260 finally:
5261 # And then swap back to the original branch and clean up.
5262 RunGit(['checkout', '-q', cl.GetBranch()])
5263 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005264 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005265
5266 if not revision:
5267 print('Failed to push. If this persists, please file a bug.')
5268 return 1
5269
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005270 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005271 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005272 if viewvc_url and revision:
5273 change_desc.append_footer(
5274 'Committed: %s%s' % (viewvc_url, revision))
5275 elif revision:
5276 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005277 print('Closing issue '
5278 '(you may be prompted for your codereview password)...')
5279 cl.UpdateDescription(change_desc.description)
5280 cl.CloseIssue()
5281 props = cl.GetIssueProperties()
5282 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005283 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5284 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005285 if options.bypass_hooks:
5286 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5287 else:
5288 comment += ' (presubmit successful).'
5289 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5290
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005291 if os.path.isfile(POSTUPSTREAM_HOOK):
5292 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5293
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005294 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005295
5296
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005297def PushToGitWithAutoRebase(remote, branch, original_description,
5298 git_numberer_enabled, max_attempts=3):
5299 """Pushes current HEAD commit on top of remote's branch.
5300
5301 Attempts to fetch and autorebase on push failures.
5302 Adds git number footers on the fly.
5303
5304 Returns integer code from last command.
5305 """
5306 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5307 code = 0
5308 attempts_left = max_attempts
5309 while attempts_left:
5310 attempts_left -= 1
5311 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5312
5313 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5314 # If fetch fails, retry.
5315 print('Fetching %s/%s...' % (remote, branch))
5316 code, out = RunGitWithCode(
5317 ['retry', 'fetch', remote,
5318 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5319 if code:
5320 print('Fetch failed with exit code %d.' % code)
5321 print(out.strip())
5322 continue
5323
5324 print('Cherry-picking commit on top of latest %s' % branch)
5325 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5326 suppress_stderr=True)
5327 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5328 code, out = RunGitWithCode(['cherry-pick', cherry])
5329 if code:
5330 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5331 'the following files have merge conflicts:' %
5332 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005333 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5334 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005335 print('Please rebase your patch and try again.')
5336 RunGitWithCode(['cherry-pick', '--abort'])
5337 break
5338
5339 commit_desc = ChangeDescription(original_description)
5340 if git_numberer_enabled:
5341 logging.debug('Adding git number footers')
5342 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5343 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5344 branch)
5345 # Ensure timestamps are monotonically increasing.
5346 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5347 _get_committer_timestamp('HEAD'))
5348 _git_amend_head(commit_desc.description, timestamp)
5349
5350 code, out = RunGitWithCode(
5351 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5352 print(out)
5353 if code == 0:
5354 break
5355 if IsFatalPushFailure(out):
5356 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005357 'user.email are correct and you have push access to the repo.\n'
5358 'Hint: run command below to diangose common Git/Gerrit credential '
5359 'problems:\n'
5360 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005361 break
5362 return code
5363
5364
5365def IsFatalPushFailure(push_stdout):
5366 """True if retrying push won't help."""
5367 return '(prohibited by Gerrit)' in push_stdout
5368
5369
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005370@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005371def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005372 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005373 parser.add_option('-b', dest='newbranch',
5374 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005375 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005376 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005377 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005378 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005379 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005380 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005381 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005382 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005383 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005384 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005385
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005386
5387 group = optparse.OptionGroup(
5388 parser,
5389 'Options for continuing work on the current issue uploaded from a '
5390 'different clone (e.g. different machine). Must be used independently '
5391 'from the other options. No issue number should be specified, and the '
5392 'branch must have an issue number associated with it')
5393 group.add_option('--reapply', action='store_true', dest='reapply',
5394 help='Reset the branch and reapply the issue.\n'
5395 'CAUTION: This will undo any local changes in this '
5396 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005397
5398 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005399 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005400 parser.add_option_group(group)
5401
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005402 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005403 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005404 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005405 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005406 auth_config = auth.extract_auth_config_from_options(options)
5407
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005408 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005409 if options.newbranch:
5410 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005411 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005412 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005413
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005414 cl = Changelist(auth_config=auth_config,
5415 codereview=options.forced_codereview)
5416 if not cl.GetIssue():
5417 parser.error('current branch must have an associated issue')
5418
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005419 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005420 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005421 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005422
5423 RunGit(['reset', '--hard', upstream])
5424 if options.pull:
5425 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005426
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005427 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5428 options.directory)
5429
5430 if len(args) != 1 or not args[0]:
5431 parser.error('Must specify issue number or url')
5432
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005433 target_issue_arg = ParseIssueNumberArgument(args[0],
5434 options.forced_codereview)
5435 if not target_issue_arg.valid:
5436 parser.error('invalid codereview url or CL id')
5437
5438 cl_kwargs = {
5439 'auth_config': auth_config,
5440 'codereview_host': target_issue_arg.hostname,
5441 'codereview': options.forced_codereview,
5442 }
5443 detected_codereview_from_url = False
5444 if target_issue_arg.codereview and not options.forced_codereview:
5445 detected_codereview_from_url = True
5446 cl_kwargs['codereview'] = target_issue_arg.codereview
5447 cl_kwargs['issue'] = target_issue_arg.issue
5448
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005449 # We don't want uncommitted changes mixed up with the patch.
5450 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005451 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005452
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005453 if options.newbranch:
5454 if options.force:
5455 RunGit(['branch', '-D', options.newbranch],
5456 stderr=subprocess2.PIPE, error_ok=True)
5457 RunGit(['new-branch', options.newbranch])
5458
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005459 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005460
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005461 if cl.IsGerrit():
5462 if options.reject:
5463 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005464 if options.directory:
5465 parser.error('--directory is not supported with Gerrit codereview.')
5466
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005467 if detected_codereview_from_url:
5468 print('canonical issue/change URL: %s (type: %s)\n' %
5469 (cl.GetIssueURL(), target_issue_arg.codereview))
5470
5471 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005472 options.nocommit, options.directory,
5473 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005474
5475
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005476def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005477 """Fetches the tree status and returns either 'open', 'closed',
5478 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005479 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005480 if url:
5481 status = urllib2.urlopen(url).read().lower()
5482 if status.find('closed') != -1 or status == '0':
5483 return 'closed'
5484 elif status.find('open') != -1 or status == '1':
5485 return 'open'
5486 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005487 return 'unset'
5488
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005489
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005490def GetTreeStatusReason():
5491 """Fetches the tree status from a json url and returns the message
5492 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005493 url = settings.GetTreeStatusUrl()
5494 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005495 connection = urllib2.urlopen(json_url)
5496 status = json.loads(connection.read())
5497 connection.close()
5498 return status['message']
5499
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005500
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005501def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005502 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005503 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005504 status = GetTreeStatus()
5505 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005506 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005507 return 2
5508
vapiera7fbd5a2016-06-16 09:17:49 -07005509 print('The tree is %s' % status)
5510 print()
5511 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005512 if status != 'open':
5513 return 1
5514 return 0
5515
5516
maruel@chromium.org15192402012-09-06 12:38:29 +00005517def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005518 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005519 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005520 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005521 '-b', '--bot', action='append',
5522 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5523 'times to specify multiple builders. ex: '
5524 '"-b win_rel -b win_layout". See '
5525 'the try server waterfall for the builders name and the tests '
5526 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005527 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005528 '-B', '--bucket', default='',
5529 help=('Buildbucket bucket to send the try requests.'))
5530 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005531 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005532 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005533 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005534 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005535 help='Revision to use for the try job; default: the revision will '
5536 'be determined by the try recipe that builder runs, which usually '
5537 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005538 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005539 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005540 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005541 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005542 group.add_option(
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005543 '--category', default='git_cl_try', help='Specify custom build category.')
5544 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005545 '--project',
5546 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005547 'in recipe to determine to which repository or directory to '
5548 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005549 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005550 '-p', '--property', dest='properties', action='append', default=[],
5551 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005552 'key2=value2 etc. The value will be treated as '
5553 'json if decodable, or as string otherwise. '
5554 'NOTE: using this may make your try job not usable for CQ, '
5555 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005556 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005557 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5558 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005559 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005560 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005561 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005562 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005563 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005564 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005565
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005566 if options.master and options.master.startswith('luci.'):
5567 parser.error(
5568 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005569 # Make sure that all properties are prop=value pairs.
5570 bad_params = [x for x in options.properties if '=' not in x]
5571 if bad_params:
5572 parser.error('Got properties with missing "=": %s' % bad_params)
5573
maruel@chromium.org15192402012-09-06 12:38:29 +00005574 if args:
5575 parser.error('Unknown arguments: %s' % args)
5576
Koji Ishii31c14782018-01-08 17:17:33 +09005577 cl = Changelist(auth_config=auth_config, issue=options.issue,
5578 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005579 if not cl.GetIssue():
5580 parser.error('Need to upload first')
5581
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005582 if cl.IsGerrit():
5583 # HACK: warm up Gerrit change detail cache to save on RPCs.
5584 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5585
tandriie113dfd2016-10-11 10:20:12 -07005586 error_message = cl.CannotTriggerTryJobReason()
5587 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005588 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005589
borenet6c0efe62016-10-19 08:13:29 -07005590 if options.bucket and options.master:
5591 parser.error('Only one of --bucket and --master may be used.')
5592
qyearsley1fdfcb62016-10-24 13:22:03 -07005593 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005594
qyearsleydd49f942016-10-28 11:57:22 -07005595 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5596 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005597 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005598 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005599 print('git cl try with no bots now defaults to CQ dry run.')
5600 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5601 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005602
borenet6c0efe62016-10-19 08:13:29 -07005603 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005604 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005605 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005606 'of bot requires an initial job from a parent (usually a builder). '
5607 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005608 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005609 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005610
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005611 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005612 # TODO(tandrii): Checking local patchset against remote patchset is only
5613 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5614 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005615 print('Warning: Codereview server has newer patchsets (%s) than most '
5616 'recent upload from local checkout (%s). Did a previous upload '
5617 'fail?\n'
5618 'By default, git cl try uses the latest patchset from '
5619 'codereview, continuing to use patchset %s.\n' %
5620 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005621
tandrii568043b2016-10-11 07:49:18 -07005622 try:
Andrii Shyshkalovf9648b52018-02-21 22:32:42 -08005623 _trigger_try_jobs(auth_config, cl, buckets, options, patchset)
tandrii568043b2016-10-11 07:49:18 -07005624 except BuildbucketResponseException as ex:
5625 print('ERROR: %s' % ex)
5626 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005627 return 0
5628
5629
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005630def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005631 """Prints info about try jobs associated with current CL."""
5632 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005633 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005634 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005635 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005636 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005637 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005638 '--color', action='store_true', default=setup_color.IS_TTY,
5639 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005640 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005641 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5642 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005643 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005644 '--json', help=('Path of JSON output file to write try job results to,'
5645 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005646 parser.add_option_group(group)
5647 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005648 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005649 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005650 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005651 if args:
5652 parser.error('Unrecognized args: %s' % ' '.join(args))
5653
5654 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005655 cl = Changelist(
5656 issue=options.issue, codereview=options.forced_codereview,
5657 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005658 if not cl.GetIssue():
5659 parser.error('Need to upload first')
5660
tandrii221ab252016-10-06 08:12:04 -07005661 patchset = options.patchset
5662 if not patchset:
5663 patchset = cl.GetMostRecentPatchset()
5664 if not patchset:
5665 parser.error('Codereview doesn\'t know about issue %s. '
5666 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005667 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005668 cl.GetIssue())
5669
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005670 # TODO(tandrii): Checking local patchset against remote patchset is only
5671 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5672 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005673 print('Warning: Codereview server has newer patchsets (%s) than most '
5674 'recent upload from local checkout (%s). Did a previous upload '
5675 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005676 'By default, git cl try-results uses the latest patchset from '
5677 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005678 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005679 try:
tandrii221ab252016-10-06 08:12:04 -07005680 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005681 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005682 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005683 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005684 if options.json:
5685 write_try_results_json(options.json, jobs)
5686 else:
5687 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005688 return 0
5689
5690
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005691@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005692def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005693 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005694 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005695 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005696 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005698 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005699 if args:
5700 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005701 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005702 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005703 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005704 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005705
5706 # Clear configured merge-base, if there is one.
5707 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005708 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005709 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005710 return 0
5711
5712
thestig@chromium.org00858c82013-12-02 23:08:03 +00005713def CMDweb(parser, args):
5714 """Opens the current CL in the web browser."""
5715 _, args = parser.parse_args(args)
5716 if args:
5717 parser.error('Unrecognized args: %s' % ' '.join(args))
5718
5719 issue_url = Changelist().GetIssueURL()
5720 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005721 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005722 return 1
5723
5724 webbrowser.open(issue_url)
5725 return 0
5726
5727
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005728def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005729 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005730 parser.add_option('-d', '--dry-run', action='store_true',
5731 help='trigger in dry run mode')
5732 parser.add_option('-c', '--clear', action='store_true',
5733 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005734 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005735 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005736 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005737 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005738 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005739 if args:
5740 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005741 if options.dry_run and options.clear:
5742 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5743
iannuccie53c9352016-08-17 14:40:40 -07005744 cl = Changelist(auth_config=auth_config, issue=options.issue,
5745 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005746 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005747 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005748 elif options.dry_run:
5749 state = _CQState.DRY_RUN
5750 else:
5751 state = _CQState.COMMIT
5752 if not cl.GetIssue():
5753 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005754 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005755 return 0
5756
5757
groby@chromium.org411034a2013-02-26 15:12:01 +00005758def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005759 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005760 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005761 auth.add_auth_options(parser)
5762 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005763 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005764 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005765 if args:
5766 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005767 cl = Changelist(auth_config=auth_config, issue=options.issue,
5768 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005769 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005770 if not cl.GetIssue():
5771 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005772 cl.CloseIssue()
5773 return 0
5774
5775
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005776def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005777 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005778 parser.add_option(
5779 '--stat',
5780 action='store_true',
5781 dest='stat',
5782 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005783 auth.add_auth_options(parser)
5784 options, args = parser.parse_args(args)
5785 auth_config = auth.extract_auth_config_from_options(options)
5786 if args:
5787 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005788
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005789 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005790 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005791 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005792 if not issue:
5793 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005794
Aaron Gablea718c3e2017-08-28 17:47:28 -07005795 base = cl._GitGetBranchConfigValue('last-upload-hash')
5796 if not base:
5797 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5798 if not base:
5799 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5800 revision_info = detail['revisions'][detail['current_revision']]
5801 fetch_info = revision_info['fetch']['http']
5802 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5803 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005804
Aaron Gablea718c3e2017-08-28 17:47:28 -07005805 cmd = ['git', 'diff']
5806 if options.stat:
5807 cmd.append('--stat')
5808 cmd.append(base)
5809 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005810
5811 return 0
5812
5813
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005814def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005815 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005816 parser.add_option(
5817 '--no-color',
5818 action='store_true',
5819 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005820 parser.add_option(
5821 '--batch',
5822 action='store_true',
5823 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005824 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005825 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005826 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005827
5828 author = RunGit(['config', 'user.email']).strip() or None
5829
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005830 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005831
5832 if args:
5833 if len(args) > 1:
5834 parser.error('Unknown args')
5835 base_branch = args[0]
5836 else:
5837 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005838 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005839
5840 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005841 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5842
5843 if options.batch:
5844 db = owners.Database(change.RepositoryRoot(), file, os.path)
5845 print('\n'.join(db.reviewers_for(affected_files, author)))
5846 return 0
5847
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005848 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005849 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005850 change.RepositoryRoot(),
Edward Lemur707d70b2018-02-07 00:50:14 +01005851 author,
5852 cl.GetReviewers(),
5853 fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005854 disable_color=options.no_color,
5855 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005856
5857
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005858def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005859 """Generates a diff command."""
5860 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005861 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5862 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005863 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005864
5865 if args:
5866 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005867 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005868 diff_cmd.append(arg)
5869 else:
5870 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005871
5872 return diff_cmd
5873
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005874
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005875def MatchingFileType(file_name, extensions):
5876 """Returns true if the file name ends with one of the given extensions."""
5877 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005878
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005879
enne@chromium.org555cfe42014-01-29 18:21:39 +00005880@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005881def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005882 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005883 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005884 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005885 parser.add_option('--full', action='store_true',
5886 help='Reformat the full content of all touched files')
5887 parser.add_option('--dry-run', action='store_true',
5888 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005889 parser.add_option('--python', action='store_true',
5890 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005891 parser.add_option('--js', action='store_true',
5892 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005893 parser.add_option('--diff', action='store_true',
5894 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005895 parser.add_option('--presubmit', action='store_true',
5896 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005897 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005898
Daniel Chengc55eecf2016-12-30 03:11:02 -08005899 # Normalize any remaining args against the current path, so paths relative to
5900 # the current directory are still resolved as expected.
5901 args = [os.path.join(os.getcwd(), arg) for arg in args]
5902
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005903 # git diff generates paths against the root of the repository. Change
5904 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005905 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005906 if rel_base_path:
5907 os.chdir(rel_base_path)
5908
digit@chromium.org29e47272013-05-17 17:01:46 +00005909 # Grab the merge-base commit, i.e. the upstream commit of the current
5910 # branch when it was created or the last time it was rebased. This is
5911 # to cover the case where the user may have called "git fetch origin",
5912 # moving the origin branch to a newer commit, but hasn't rebased yet.
5913 upstream_commit = None
5914 cl = Changelist()
5915 upstream_branch = cl.GetUpstreamBranch()
5916 if upstream_branch:
5917 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5918 upstream_commit = upstream_commit.strip()
5919
5920 if not upstream_commit:
5921 DieWithError('Could not find base commit for this branch. '
5922 'Are you in detached state?')
5923
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005924 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5925 diff_output = RunGit(changed_files_cmd)
5926 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005927 # Filter out files deleted by this CL
5928 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005929
Christopher Lamc5ba6922017-01-24 11:19:14 +11005930 if opts.js:
5931 CLANG_EXTS.append('.js')
5932
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005933 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5934 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5935 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005936 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005937
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005938 top_dir = os.path.normpath(
5939 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5940
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005941 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5942 # formatted. This is used to block during the presubmit.
5943 return_value = 0
5944
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005945 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005946 # Locate the clang-format binary in the checkout
5947 try:
5948 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005949 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005950 DieWithError(e)
5951
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005952 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005953 cmd = [clang_format_tool]
5954 if not opts.dry_run and not opts.diff:
5955 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005956 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005957 if opts.diff:
5958 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005959 else:
5960 env = os.environ.copy()
5961 env['PATH'] = str(os.path.dirname(clang_format_tool))
5962 try:
5963 script = clang_format.FindClangFormatScriptInChromiumTree(
5964 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005965 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005966 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005967
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005968 cmd = [sys.executable, script, '-p0']
5969 if not opts.dry_run and not opts.diff:
5970 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005971
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005972 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5973 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005974
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005975 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5976 if opts.diff:
5977 sys.stdout.write(stdout)
5978 if opts.dry_run and len(stdout) > 0:
5979 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005980
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005981 # Similar code to above, but using yapf on .py files rather than clang-format
5982 # on C/C++ files
5983 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005984 yapf_tool = gclient_utils.FindExecutable('yapf')
5985 if yapf_tool is None:
5986 DieWithError('yapf not found in PATH')
5987
5988 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005989 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005990 cmd = [yapf_tool]
5991 if not opts.dry_run and not opts.diff:
5992 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005993 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005994 if opts.diff:
5995 sys.stdout.write(stdout)
5996 else:
5997 # TODO(sbc): yapf --lines mode still has some issues.
5998 # https://github.com/google/yapf/issues/154
5999 DieWithError('--python currently only works with --full')
6000
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006001 # Dart's formatter does not have the nice property of only operating on
6002 # modified chunks, so hard code full.
6003 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006004 try:
6005 command = [dart_format.FindDartFmtToolInChromiumTree()]
6006 if not opts.dry_run and not opts.diff:
6007 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006008 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006009
ppi@chromium.org6593d932016-03-03 15:41:15 +00006010 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006011 if opts.dry_run and stdout:
6012 return_value = 2
6013 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006014 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6015 'found in this checkout. Files in other languages are still '
6016 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006017
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006018 # Format GN build files. Always run on full build files for canonical form.
6019 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006020 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006021 if opts.dry_run or opts.diff:
6022 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006023 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006024 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6025 shell=sys.platform == 'win32',
6026 cwd=top_dir)
6027 if opts.dry_run and gn_ret == 2:
6028 return_value = 2 # Not formatted.
6029 elif opts.diff and gn_ret == 2:
6030 # TODO this should compute and print the actual diff.
6031 print("This change has GN build file diff for " + gn_diff_file)
6032 elif gn_ret != 0:
6033 # For non-dry run cases (and non-2 return values for dry-run), a
6034 # nonzero error code indicates a failure, probably because the file
6035 # doesn't parse.
6036 DieWithError("gn format failed on " + gn_diff_file +
6037 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006038
Ilya Shermane081cbe2017-08-15 17:51:04 -07006039 # Skip the metrics formatting from the global presubmit hook. These files have
6040 # a separate presubmit hook that issues an error if the files need formatting,
6041 # whereas the top-level presubmit script merely issues a warning. Formatting
6042 # these files is somewhat slow, so it's important not to duplicate the work.
6043 if not opts.presubmit:
6044 for xml_dir in GetDirtyMetricsDirs(diff_files):
6045 tool_dir = os.path.join(top_dir, xml_dir)
6046 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6047 if opts.dry_run or opts.diff:
6048 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006049 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006050 if opts.diff:
6051 sys.stdout.write(stdout)
6052 if opts.dry_run and stdout:
6053 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006054
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006055 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006056
Steven Holte2e664bf2017-04-21 13:10:47 -07006057def GetDirtyMetricsDirs(diff_files):
6058 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6059 metrics_xml_dirs = [
6060 os.path.join('tools', 'metrics', 'actions'),
6061 os.path.join('tools', 'metrics', 'histograms'),
6062 os.path.join('tools', 'metrics', 'rappor'),
6063 os.path.join('tools', 'metrics', 'ukm')]
6064 for xml_dir in metrics_xml_dirs:
6065 if any(file.startswith(xml_dir) for file in xml_diff_files):
6066 yield xml_dir
6067
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006068
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006069@subcommand.usage('<codereview url or issue id>')
6070def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006071 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006072 _, args = parser.parse_args(args)
6073
6074 if len(args) != 1:
6075 parser.print_help()
6076 return 1
6077
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006078 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006079 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006080 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006081
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006082 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006083
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006084 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006085 output = RunGit(['config', '--local', '--get-regexp',
6086 r'branch\..*\.%s' % issueprefix],
6087 error_ok=True)
6088 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006089 if issue == target_issue:
6090 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006091
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006092 branches = []
6093 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006094 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006095 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006096 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006097 return 1
6098 if len(branches) == 1:
6099 RunGit(['checkout', branches[0]])
6100 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006101 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006102 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006103 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006104 which = raw_input('Choose by index: ')
6105 try:
6106 RunGit(['checkout', branches[int(which)]])
6107 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006108 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006109 return 1
6110
6111 return 0
6112
6113
maruel@chromium.org29404b52014-09-08 22:58:00 +00006114def CMDlol(parser, args):
6115 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006116 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006117 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6118 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6119 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006120 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006121 return 0
6122
6123
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006124class OptionParser(optparse.OptionParser):
6125 """Creates the option parse and add --verbose support."""
6126 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006127 optparse.OptionParser.__init__(
6128 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006129 self.add_option(
6130 '-v', '--verbose', action='count', default=0,
6131 help='Use 2 times for more debugging info')
6132
6133 def parse_args(self, args=None, values=None):
6134 options, args = optparse.OptionParser.parse_args(self, args, values)
6135 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006136 logging.basicConfig(
6137 level=levels[min(options.verbose, len(levels) - 1)],
6138 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6139 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006140 return options, args
6141
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006142
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006143def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006144 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006145 print('\nYour python version %s is unsupported, please upgrade.\n' %
6146 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006147 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006148
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006149 # Reload settings.
6150 global settings
6151 settings = Settings()
6152
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006153 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006154 dispatcher = subcommand.CommandDispatcher(__name__)
6155 try:
6156 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006157 except auth.AuthenticationError as e:
6158 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006159 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006160 if e.code != 500:
6161 raise
6162 DieWithError(
6163 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6164 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006165 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006166
6167
6168if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006169 # These affect sys.stdout so do it outside of main() to simplify mocks in
6170 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006171 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006172 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006173 try:
6174 sys.exit(main(sys.argv[1:]))
6175 except KeyboardInterrupt:
6176 sys.stderr.write('interrupted\n')
6177 sys.exit(1)