blob: c9ec13cea77a78a55cabf70c08d3e554b5766ddb [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
borenet6c0efe62016-10-19 08:13:29 -0700429def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700430 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700431 """Sends a request to Buildbucket to trigger try jobs for a changelist.
432
433 Args:
434 auth_config: AuthConfig for Rietveld.
435 changelist: Changelist that the try jobs are associated with.
436 buckets: A nested dict mapping bucket names to builders to tests.
437 options: Command-line options.
438 """
tandriide281ae2016-10-12 06:02:30 -0700439 assert changelist.GetIssue(), 'CL must be uploaded first'
440 codereview_url = changelist.GetCodereviewServer()
441 assert codereview_url, 'CL must be uploaded first'
442 patchset = patchset or changelist.GetMostRecentPatchset()
443 assert patchset, 'CL must be uploaded first'
444
445 codereview_host = urlparse.urlparse(codereview_url).hostname
446 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000447 http = authenticator.authorize(httplib2.Http())
448 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700449
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000450 buildbucket_put_url = (
451 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000452 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700453 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
454 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
455 hostname=codereview_host,
456 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000457 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700458
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700459 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700460 shared_parameters_properties['category'] = category
461 if options.clobber:
462 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700463 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700464 if extra_properties:
465 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000466
467 batch_req_body = {'builds': []}
468 print_text = []
469 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700470 for bucket, builders_and_tests in sorted(buckets.iteritems()):
471 print_text.append('Bucket: %s' % bucket)
472 master = None
473 if bucket.startswith(MASTER_PREFIX):
474 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000475 for builder, tests in sorted(builders_and_tests.iteritems()):
476 print_text.append(' %s: %s' % (builder, tests))
477 parameters = {
478 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000479 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100480 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000481 'revision': options.revision,
482 }],
tandrii8c5a3532016-11-04 07:52:02 -0700483 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000484 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000485 if 'presubmit' in builder.lower():
486 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000487 if tests:
488 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700489
490 tags = [
491 'builder:%s' % builder,
492 'buildset:%s' % buildset,
493 'user_agent:git_cl_try',
494 ]
495 if master:
496 parameters['properties']['master'] = master
497 tags.append('master:%s' % master)
498
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000499 batch_req_body['builds'].append(
500 {
501 'bucket': bucket,
502 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000503 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700504 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000505 }
506 )
507
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000508 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700509 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510 http,
511 buildbucket_put_url,
512 'PUT',
513 body=json.dumps(batch_req_body),
514 headers={'Content-Type': 'application/json'}
515 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000516 print_text.append('To see results here, run: git cl try-results')
517 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700518 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000519
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000520
tandrii221ab252016-10-06 08:12:04 -0700521def fetch_try_jobs(auth_config, changelist, buildbucket_host,
522 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700523 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000524
qyearsley53f48a12016-09-01 10:45:13 -0700525 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000526 """
tandrii221ab252016-10-06 08:12:04 -0700527 assert buildbucket_host
528 assert changelist.GetIssue(), 'CL must be uploaded first'
529 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
530 patchset = patchset or changelist.GetMostRecentPatchset()
531 assert patchset, 'CL must be uploaded first'
532
533 codereview_url = changelist.GetCodereviewServer()
534 codereview_host = urlparse.urlparse(codereview_url).hostname
535 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000536 if authenticator.has_cached_credentials():
537 http = authenticator.authorize(httplib2.Http())
538 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700539 print('Warning: Some results might be missing because %s' %
540 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700541 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000542 http = httplib2.Http()
543
544 http.force_exception_to_status_code = True
545
tandrii221ab252016-10-06 08:12:04 -0700546 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
547 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
548 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000549 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700550 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000551 params = {'tag': 'buildset:%s' % buildset}
552
553 builds = {}
554 while True:
555 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700556 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700558 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 for build in content.get('builds', []):
560 builds[build['id']] = build
561 if 'next_cursor' in content:
562 params['start_cursor'] = content['next_cursor']
563 else:
564 break
565 return builds
566
567
qyearsleyeab3c042016-08-24 09:18:28 -0700568def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000569 """Prints nicely result of fetch_try_jobs."""
570 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700571 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 return
573
574 # Make a copy, because we'll be modifying builds dictionary.
575 builds = builds.copy()
576 builder_names_cache = {}
577
578 def get_builder(b):
579 try:
580 return builder_names_cache[b['id']]
581 except KeyError:
582 try:
583 parameters = json.loads(b['parameters_json'])
584 name = parameters['builder_name']
585 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700586 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700587 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000588 name = None
589 builder_names_cache[b['id']] = name
590 return name
591
592 def get_bucket(b):
593 bucket = b['bucket']
594 if bucket.startswith('master.'):
595 return bucket[len('master.'):]
596 return bucket
597
598 if options.print_master:
599 name_fmt = '%%-%ds %%-%ds' % (
600 max(len(str(get_bucket(b))) for b in builds.itervalues()),
601 max(len(str(get_builder(b))) for b in builds.itervalues()))
602 def get_name(b):
603 return name_fmt % (get_bucket(b), get_builder(b))
604 else:
605 name_fmt = '%%-%ds' % (
606 max(len(str(get_builder(b))) for b in builds.itervalues()))
607 def get_name(b):
608 return name_fmt % get_builder(b)
609
610 def sort_key(b):
611 return b['status'], b.get('result'), get_name(b), b.get('url')
612
613 def pop(title, f, color=None, **kwargs):
614 """Pop matching builds from `builds` dict and print them."""
615
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000616 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000617 colorize = str
618 else:
619 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
620
621 result = []
622 for b in builds.values():
623 if all(b.get(k) == v for k, v in kwargs.iteritems()):
624 builds.pop(b['id'])
625 result.append(b)
626 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700627 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000628 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700629 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000630
631 total = len(builds)
632 pop(status='COMPLETED', result='SUCCESS',
633 title='Successes:', color=Fore.GREEN,
634 f=lambda b: (get_name(b), b.get('url')))
635 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
636 title='Infra Failures:', color=Fore.MAGENTA,
637 f=lambda b: (get_name(b), b.get('url')))
638 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
639 title='Failures:', color=Fore.RED,
640 f=lambda b: (get_name(b), b.get('url')))
641 pop(status='COMPLETED', result='CANCELED',
642 title='Canceled:', color=Fore.MAGENTA,
643 f=lambda b: (get_name(b),))
644 pop(status='COMPLETED', result='FAILURE',
645 failure_reason='INVALID_BUILD_DEFINITION',
646 title='Wrong master/builder name:', color=Fore.MAGENTA,
647 f=lambda b: (get_name(b),))
648 pop(status='COMPLETED', result='FAILURE',
649 title='Other failures:',
650 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
651 pop(status='COMPLETED',
652 title='Other finished:',
653 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
654 pop(status='STARTED',
655 title='Started:', color=Fore.YELLOW,
656 f=lambda b: (get_name(b), b.get('url')))
657 pop(status='SCHEDULED',
658 title='Scheduled:',
659 f=lambda b: (get_name(b), 'id=%s' % b['id']))
660 # The last section is just in case buildbucket API changes OR there is a bug.
661 pop(title='Other:',
662 f=lambda b: (get_name(b), 'id=%s' % b['id']))
663 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700664 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000665
666
qyearsley53f48a12016-09-01 10:45:13 -0700667def write_try_results_json(output_file, builds):
668 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
669
670 The input |builds| dict is assumed to be generated by Buildbucket.
671 Buildbucket documentation: http://goo.gl/G0s101
672 """
673
674 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800675 """Extracts some of the information from one build dict."""
676 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700677 return {
678 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700679 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800680 'builder_name': parameters.get('builder_name'),
681 'created_ts': build.get('created_ts'),
682 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700683 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800684 'result': build.get('result'),
685 'status': build.get('status'),
686 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700687 'url': build.get('url'),
688 }
689
690 converted = []
691 for _, build in sorted(builds.items()):
692 converted.append(convert_build_dict(build))
693 write_json(output_file, converted)
694
695
Aaron Gable13101a62018-02-09 13:20:41 -0800696def print_stats(args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000697 """Prints statistics about the change to the user."""
698 # --no-ext-diff is broken in some versions of Git, so try to work around
699 # this by overriding the environment (but there is still a problem if the
700 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000701 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000702 if 'GIT_EXTERNAL_DIFF' in env:
703 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000704
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000705 try:
706 stdout = sys.stdout.fileno()
707 except AttributeError:
708 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000709 return subprocess2.call(
Aaron Gable13101a62018-02-09 13:20:41 -0800710 ['git', 'diff', '--no-ext-diff', '--stat', '-l100000', '-C50'] + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000711 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000712
713
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000714class BuildbucketResponseException(Exception):
715 pass
716
717
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718class Settings(object):
719 def __init__(self):
720 self.default_server = None
721 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000722 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723 self.tree_status_url = None
724 self.viewvc_url = None
725 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000726 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000727 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000728 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000729 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000730 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000731 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000732
733 def LazyUpdateIfNeeded(self):
734 """Updates the settings from a codereview.settings file, if available."""
735 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000736 # The only value that actually changes the behavior is
737 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000738 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000739 error_ok=True
740 ).strip().lower()
741
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000742 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000743 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000744 LoadCodereviewSettingsFromFile(cr_settings_file)
745 self.updated = True
746
747 def GetDefaultServerUrl(self, error_ok=False):
748 if not self.default_server:
749 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000750 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000751 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 if error_ok:
753 return self.default_server
754 if not self.default_server:
755 error_message = ('Could not find settings file. You must configure '
756 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000757 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000758 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000759 return self.default_server
760
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000761 @staticmethod
762 def GetRelativeRoot():
763 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000764
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000766 if self.root is None:
767 self.root = os.path.abspath(self.GetRelativeRoot())
768 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000769
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000770 def GetGitMirror(self, remote='origin'):
771 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000772 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000773 if not os.path.isdir(local_url):
774 return None
775 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
776 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100777 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100778 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000779 if mirror.exists():
780 return mirror
781 return None
782
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783 def GetTreeStatusUrl(self, error_ok=False):
784 if not self.tree_status_url:
785 error_message = ('You must configure your tree status URL by running '
786 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000787 self.tree_status_url = self._GetRietveldConfig(
788 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789 return self.tree_status_url
790
791 def GetViewVCUrl(self):
792 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000793 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 return self.viewvc_url
795
rmistry@google.com90752582014-01-14 21:04:50 +0000796 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000797 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000798
rmistry@google.com78948ed2015-07-08 23:09:57 +0000799 def GetIsSkipDependencyUpload(self, branch_name):
800 """Returns true if specified branch should skip dep uploads."""
801 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
802 error_ok=True)
803
rmistry@google.com5626a922015-02-26 14:03:30 +0000804 def GetRunPostUploadHook(self):
805 run_post_upload_hook = self._GetRietveldConfig(
806 'run-post-upload-hook', error_ok=True)
807 return run_post_upload_hook == "True"
808
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000809 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000810 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000811
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000812 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000813 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000814
ukai@chromium.orge8077812012-02-03 03:41:46 +0000815 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700816 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000817 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700818 self.is_gerrit = (
819 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000820 return self.is_gerrit
821
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000822 def GetSquashGerritUploads(self):
823 """Return true if uploads to Gerrit should be squashed by default."""
824 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700825 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
826 if self.squash_gerrit_uploads is None:
827 # Default is squash now (http://crbug.com/611892#c23).
828 self.squash_gerrit_uploads = not (
829 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
830 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000831 return self.squash_gerrit_uploads
832
tandriia60502f2016-06-20 02:01:53 -0700833 def GetSquashGerritUploadsOverride(self):
834 """Return True or False if codereview.settings should be overridden.
835
836 Returns None if no override has been defined.
837 """
838 # See also http://crbug.com/611892#c23
839 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
840 error_ok=True).strip()
841 if result == 'true':
842 return True
843 if result == 'false':
844 return False
845 return None
846
tandrii@chromium.org28253532016-04-14 13:46:56 +0000847 def GetGerritSkipEnsureAuthenticated(self):
848 """Return True if EnsureAuthenticated should not be done for Gerrit
849 uploads."""
850 if self.gerrit_skip_ensure_authenticated is None:
851 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000852 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000853 error_ok=True).strip() == 'true')
854 return self.gerrit_skip_ensure_authenticated
855
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000856 def GetGitEditor(self):
857 """Return the editor specified in the git config, or None if none is."""
858 if self.git_editor is None:
859 self.git_editor = self._GetConfig('core.editor', error_ok=True)
860 return self.git_editor or None
861
thestig@chromium.org44202a22014-03-11 19:22:18 +0000862 def GetLintRegex(self):
863 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
864 DEFAULT_LINT_REGEX)
865
866 def GetLintIgnoreRegex(self):
867 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
868 DEFAULT_LINT_IGNORE_REGEX)
869
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000870 def GetProject(self):
871 if not self.project:
872 self.project = self._GetRietveldConfig('project', error_ok=True)
873 return self.project
874
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000875 def _GetRietveldConfig(self, param, **kwargs):
876 return self._GetConfig('rietveld.' + param, **kwargs)
877
rmistry@google.com78948ed2015-07-08 23:09:57 +0000878 def _GetBranchConfig(self, branch_name, param, **kwargs):
879 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
880
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 def _GetConfig(self, param, **kwargs):
882 self.LazyUpdateIfNeeded()
883 return RunGit(['config', param], **kwargs).strip()
884
885
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100886@contextlib.contextmanager
887def _get_gerrit_project_config_file(remote_url):
888 """Context manager to fetch and store Gerrit's project.config from
889 refs/meta/config branch and store it in temp file.
890
891 Provides a temporary filename or None if there was error.
892 """
893 error, _ = RunGitWithCode([
894 'fetch', remote_url,
895 '+refs/meta/config:refs/git_cl/meta/config'])
896 if error:
897 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700898 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100899 (remote_url, error))
900 yield None
901 return
902
903 error, project_config_data = RunGitWithCode(
904 ['show', 'refs/git_cl/meta/config:project.config'])
905 if error:
906 print('WARNING: project.config file not found')
907 yield None
908 return
909
910 with gclient_utils.temporary_directory() as tempdir:
911 project_config_file = os.path.join(tempdir, 'project.config')
912 gclient_utils.FileWrite(project_config_file, project_config_data)
913 yield project_config_file
914
915
916def _is_git_numberer_enabled(remote_url, remote_ref):
917 """Returns True if Git Numberer is enabled on this ref."""
918 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100919 KNOWN_PROJECTS_WHITELIST = [
920 'chromium/src',
921 'external/webrtc',
922 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100923 'infra/experimental',
Edward Lemur32357d32017-09-11 20:22:45 +0200924 # For webrtc.googlesource.com/src.
925 'src',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100926 ]
927
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100928 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
929 url_parts = urlparse.urlparse(remote_url)
930 project_name = url_parts.path.lstrip('/').rstrip('git./')
931 for known in KNOWN_PROJECTS_WHITELIST:
932 if project_name.endswith(known):
933 break
934 else:
935 # Early exit to avoid extra fetches for repos that aren't using Git
936 # Numberer.
937 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100938
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100939 with _get_gerrit_project_config_file(remote_url) as project_config_file:
940 if project_config_file is None:
941 # Failed to fetch project.config, which shouldn't happen on open source
942 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100943 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100944 def get_opts(x):
945 code, out = RunGitWithCode(
946 ['config', '-f', project_config_file, '--get-all',
947 'plugin.git-numberer.validate-%s-refglob' % x])
948 if code == 0:
949 return out.strip().splitlines()
950 return []
951 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100952
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100953 logging.info('validator config enabled %s disabled %s refglobs for '
954 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +0000955
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100956 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100957 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100958 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100959 return True
960 return False
961
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100962 if match_refglobs(disabled):
963 return False
964 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100965
966
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967def ShortBranchName(branch):
968 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +0000969 return branch.replace('refs/heads/', '', 1)
970
971
972def GetCurrentBranchRef():
973 """Returns branch ref (e.g., refs/heads/master) or None."""
974 return RunGit(['symbolic-ref', 'HEAD'],
975 stderr=subprocess2.VOID, error_ok=True).strip() or None
976
977
978def GetCurrentBranch():
979 """Returns current branch or None.
980
981 For refs/heads/* branches, returns just last part. For others, full ref.
982 """
983 branchref = GetCurrentBranchRef()
984 if branchref:
985 return ShortBranchName(branchref)
986 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987
988
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +0000989class _CQState(object):
990 """Enum for states of CL with respect to Commit Queue."""
991 NONE = 'none'
992 DRY_RUN = 'dry_run'
993 COMMIT = 'commit'
994
995 ALL_STATES = [NONE, DRY_RUN, COMMIT]
996
997
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +0000998class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +0200999 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001000 self.issue = issue
1001 self.patchset = patchset
1002 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001003 assert codereview in (None, 'rietveld', 'gerrit')
1004 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001005
1006 @property
1007 def valid(self):
1008 return self.issue is not None
1009
1010
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001011def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001012 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1013 fail_result = _ParsedIssueNumberArgument()
1014
1015 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001016 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001017 if not arg.startswith('http'):
1018 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001019
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001020 url = gclient_utils.UpgradeToHttps(arg)
1021 try:
1022 parsed_url = urlparse.urlparse(url)
1023 except ValueError:
1024 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001025
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001026 if codereview is not None:
1027 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1028 return parsed or fail_result
1029
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001030 results = {}
1031 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1032 parsed = cls.ParseIssueURL(parsed_url)
1033 if parsed is not None:
1034 results[name] = parsed
1035
1036 if not results:
1037 return fail_result
1038 if len(results) == 1:
1039 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001040
1041 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1042 # This is likely Gerrit.
1043 return results['gerrit']
1044 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001045 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001046
1047
Aaron Gablea45ee112016-11-22 15:14:38 -08001048class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001049 def __init__(self, issue, url):
1050 self.issue = issue
1051 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001052 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001053
1054 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001055 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001056 self.issue, self.url)
1057
1058
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001059_CommentSummary = collections.namedtuple(
1060 '_CommentSummary', ['date', 'message', 'sender',
1061 # TODO(tandrii): these two aren't known in Gerrit.
1062 'approval', 'disapproval'])
1063
1064
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001066 """Changelist works with one changelist in local branch.
1067
1068 Supports two codereview backends: Rietveld or Gerrit, selected at object
1069 creation.
1070
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001071 Notes:
1072 * Not safe for concurrent multi-{thread,process} use.
1073 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001074 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001075 """
1076
1077 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1078 """Create a new ChangeList instance.
1079
1080 If issue is given, the codereview must be given too.
1081
1082 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1083 Otherwise, it's decided based on current configuration of the local branch,
1084 with default being 'rietveld' for backwards compatibility.
1085 See _load_codereview_impl for more details.
1086
1087 **kwargs will be passed directly to codereview implementation.
1088 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001090 global settings
1091 if not settings:
1092 # Happens when git_cl.py is used as a utility library.
1093 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001094
1095 if issue:
1096 assert codereview, 'codereview must be known, if issue is known'
1097
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098 self.branchref = branchref
1099 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001100 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001101 self.branch = ShortBranchName(self.branchref)
1102 else:
1103 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001105 self.lookedup_issue = False
1106 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001107 self.has_description = False
1108 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001109 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001111 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001112 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001113 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001114
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001115 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001116 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001117 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001118 assert self._codereview_impl
1119 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001120
1121 def _load_codereview_impl(self, codereview=None, **kwargs):
1122 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001123 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1124 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1125 self._codereview = codereview
1126 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001127 return
1128
1129 # Automatic selection based on issue number set for a current branch.
1130 # Rietveld takes precedence over Gerrit.
1131 assert not self.issue
1132 # Whether we find issue or not, we are doing the lookup.
1133 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001134 if self.GetBranch():
1135 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1136 issue = _git_get_branch_config_value(
1137 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1138 if issue:
1139 self._codereview = codereview
1140 self._codereview_impl = cls(self, **kwargs)
1141 self.issue = int(issue)
1142 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001143
1144 # No issue is set for this branch, so decide based on repo-wide settings.
1145 return self._load_codereview_impl(
1146 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1147 **kwargs)
1148
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001149 def IsGerrit(self):
1150 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001151
1152 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001153 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001154
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001155 The return value is a string suitable for passing to git cl with the --cc
1156 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001157 """
1158 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001159 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001160 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001161 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1162 return self.cc
1163
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001164 def GetCCListWithoutDefault(self):
1165 """Return the users cc'd on this CL excluding default ones."""
1166 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001167 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001168 return self.cc
1169
Daniel Cheng7227d212017-11-17 08:12:37 -08001170 def ExtendCC(self, more_cc):
1171 """Extends the list of users to cc on this CL based on the changed files."""
1172 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
1174 def GetBranch(self):
1175 """Returns the short branch name, e.g. 'master'."""
1176 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001177 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001178 if not branchref:
1179 return None
1180 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 self.branch = ShortBranchName(self.branchref)
1182 return self.branch
1183
1184 def GetBranchRef(self):
1185 """Returns the full branch name, e.g. 'refs/heads/master'."""
1186 self.GetBranch() # Poke the lazy loader.
1187 return self.branchref
1188
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001189 def ClearBranch(self):
1190 """Clears cached branch data of this object."""
1191 self.branch = self.branchref = None
1192
tandrii5d48c322016-08-18 16:19:37 -07001193 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1194 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1195 kwargs['branch'] = self.GetBranch()
1196 return _git_get_branch_config_value(key, default, **kwargs)
1197
1198 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1199 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1200 assert self.GetBranch(), (
1201 'this CL must have an associated branch to %sset %s%s' %
1202 ('un' if value is None else '',
1203 key,
1204 '' if value is None else ' to %r' % value))
1205 kwargs['branch'] = self.GetBranch()
1206 return _git_set_branch_config_value(key, value, **kwargs)
1207
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001208 @staticmethod
1209 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001210 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 e.g. 'origin', 'refs/heads/master'
1212 """
1213 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001214 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1215
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001216 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001217 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001219 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1220 error_ok=True).strip()
1221 if upstream_branch:
1222 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001224 # Else, try to guess the origin remote.
1225 remote_branches = RunGit(['branch', '-r']).split()
1226 if 'origin/master' in remote_branches:
1227 # Fall back on origin/master if it exits.
1228 remote = 'origin'
1229 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001231 DieWithError(
1232 'Unable to determine default branch to diff against.\n'
1233 'Either pass complete "git diff"-style arguments, like\n'
1234 ' git cl upload origin/master\n'
1235 'or verify this branch is set up to track another \n'
1236 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237
1238 return remote, upstream_branch
1239
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001240 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001241 upstream_branch = self.GetUpstreamBranch()
1242 if not BranchExists(upstream_branch):
1243 DieWithError('The upstream for the current branch (%s) does not exist '
1244 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001245 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001246 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001247
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001248 def GetUpstreamBranch(self):
1249 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001250 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001252 upstream_branch = upstream_branch.replace('refs/heads/',
1253 'refs/remotes/%s/' % remote)
1254 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1255 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001256 self.upstream_branch = upstream_branch
1257 return self.upstream_branch
1258
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001259 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001260 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001261 remote, branch = None, self.GetBranch()
1262 seen_branches = set()
1263 while branch not in seen_branches:
1264 seen_branches.add(branch)
1265 remote, branch = self.FetchUpstreamTuple(branch)
1266 branch = ShortBranchName(branch)
1267 if remote != '.' or branch.startswith('refs/remotes'):
1268 break
1269 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001270 remotes = RunGit(['remote'], error_ok=True).split()
1271 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001272 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001273 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001274 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001275 logging.warn('Could not determine which remote this change is '
1276 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001277 else:
1278 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001279 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001280 branch = 'HEAD'
1281 if branch.startswith('refs/remotes'):
1282 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001283 elif branch.startswith('refs/branch-heads/'):
1284 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001285 else:
1286 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001287 return self._remote
1288
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001289 def GitSanityChecks(self, upstream_git_obj):
1290 """Checks git repo status and ensures diff is from local commits."""
1291
sbc@chromium.org79706062015-01-14 21:18:12 +00001292 if upstream_git_obj is None:
1293 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001294 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001295 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001296 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001297 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001298 return False
1299
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001300 # Verify the commit we're diffing against is in our current branch.
1301 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1302 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1303 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001304 print('ERROR: %s is not in the current branch. You may need to rebase '
1305 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001306 return False
1307
1308 # List the commits inside the diff, and verify they are all local.
1309 commits_in_diff = RunGit(
1310 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1311 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1312 remote_branch = remote_branch.strip()
1313 if code != 0:
1314 _, remote_branch = self.GetRemoteBranch()
1315
1316 commits_in_remote = RunGit(
1317 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1318
1319 common_commits = set(commits_in_diff) & set(commits_in_remote)
1320 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001321 print('ERROR: Your diff contains %d commits already in %s.\n'
1322 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1323 'the diff. If you are using a custom git flow, you can override'
1324 ' the reference used for this check with "git config '
1325 'gitcl.remotebranch <git-ref>".' % (
1326 len(common_commits), remote_branch, upstream_git_obj),
1327 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001328 return False
1329 return True
1330
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001331 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001332 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001333
1334 Returns None if it is not set.
1335 """
tandrii5d48c322016-08-18 16:19:37 -07001336 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001337
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 def GetRemoteUrl(self):
1339 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1340
1341 Returns None if there is no remote.
1342 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001343 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001344 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1345
1346 # If URL is pointing to a local directory, it is probably a git cache.
1347 if os.path.isdir(url):
1348 url = RunGit(['config', 'remote.%s.url' % remote],
1349 error_ok=True,
1350 cwd=url).strip()
1351 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001353 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001354 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001355 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001356 self.issue = self._GitGetBranchConfigValue(
1357 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001358 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359 return self.issue
1360
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361 def GetIssueURL(self):
1362 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001363 issue = self.GetIssue()
1364 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001365 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001366 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001367
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001368 def GetDescription(self, pretty=False, force=False):
1369 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001371 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 self.has_description = True
1373 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001374 # Set width to 72 columns + 2 space indent.
1375 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001376 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001377 lines = self.description.splitlines()
1378 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001379 return self.description
1380
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001381 def GetDescriptionFooters(self):
1382 """Returns (non_footer_lines, footers) for the commit message.
1383
1384 Returns:
1385 non_footer_lines (list(str)) - Simple list of description lines without
1386 any footer. The lines do not contain newlines, nor does the list contain
1387 the empty line between the message and the footers.
1388 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1389 [("Change-Id", "Ideadbeef...."), ...]
1390 """
1391 raw_description = self.GetDescription()
1392 msg_lines, _, footers = git_footers.split_footers(raw_description)
1393 if footers:
1394 msg_lines = msg_lines[:len(msg_lines)-1]
1395 return msg_lines, footers
1396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001398 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001399 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001400 self.patchset = self._GitGetBranchConfigValue(
1401 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001402 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001403 return self.patchset
1404
1405 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001406 """Set this branch's patchset. If patchset=0, clears the patchset."""
1407 assert self.GetBranch()
1408 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001409 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001410 else:
1411 self.patchset = int(patchset)
1412 self._GitSetBranchConfigValue(
1413 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001415 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001416 """Set this branch's issue. If issue isn't given, clears the issue."""
1417 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001419 issue = int(issue)
1420 self._GitSetBranchConfigValue(
1421 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001422 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001423 codereview_server = self._codereview_impl.GetCodereviewServer()
1424 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001425 self._GitSetBranchConfigValue(
1426 self._codereview_impl.CodereviewServerConfigKey(),
1427 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001428 else:
tandrii5d48c322016-08-18 16:19:37 -07001429 # Reset all of these just to be clean.
1430 reset_suffixes = [
1431 'last-upload-hash',
1432 self._codereview_impl.IssueConfigKey(),
1433 self._codereview_impl.PatchsetConfigKey(),
1434 self._codereview_impl.CodereviewServerConfigKey(),
1435 ] + self._PostUnsetIssueProperties()
1436 for prop in reset_suffixes:
1437 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001438 msg = RunGit(['log', '-1', '--format=%B']).strip()
1439 if msg and git_footers.get_footer_change_id(msg):
1440 print('WARNING: The change patched into this branch has a Change-Id. '
1441 'Removing it.')
1442 RunGit(['commit', '--amend', '-m',
1443 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001444 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001445 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446
dnjba1b0f32016-09-02 12:37:42 -07001447 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001448 if not self.GitSanityChecks(upstream_branch):
1449 DieWithError('\nGit sanity check failure')
1450
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001451 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001452 if not root:
1453 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001454 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001455
1456 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001457 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001458 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001459 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001460 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001461 except subprocess2.CalledProcessError:
1462 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001463 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001464 'This branch probably doesn\'t exist anymore. To reset the\n'
1465 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001466 ' git branch --set-upstream-to origin/master %s\n'
1467 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001468 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001469
maruel@chromium.org52424302012-08-29 15:14:30 +00001470 issue = self.GetIssue()
1471 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001472 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001473 description = self.GetDescription()
1474 else:
1475 # If the change was never uploaded, use the log messages of all commits
1476 # up to the branch point, as git cl upload will prefill the description
1477 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001478 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1479 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001480
1481 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001482 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001483 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001484 name,
1485 description,
1486 absroot,
1487 files,
1488 issue,
1489 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001490 author,
1491 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001492
dsansomee2d6fd92016-09-08 00:10:47 -07001493 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001494 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001495 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001496 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001497
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001498 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1499 """Sets the description for this CL remotely.
1500
1501 You can get description_lines and footers with GetDescriptionFooters.
1502
1503 Args:
1504 description_lines (list(str)) - List of CL description lines without
1505 newline characters.
1506 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1507 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1508 `List-Of-Tokens`). It will be case-normalized so that each token is
1509 title-cased.
1510 """
1511 new_description = '\n'.join(description_lines)
1512 if footers:
1513 new_description += '\n'
1514 for k, v in footers:
1515 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1516 if not git_footers.FOOTER_PATTERN.match(foot):
1517 raise ValueError('Invalid footer %r' % foot)
1518 new_description += foot + '\n'
1519 self.UpdateDescription(new_description, force)
1520
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001521 def RunHook(self, committing, may_prompt, verbose, change):
1522 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1523 try:
1524 return presubmit_support.DoPresubmitChecks(change, committing,
1525 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1526 default_presubmit=None, may_prompt=may_prompt,
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001527 rietveld_obj=self._codereview_impl.GetRietveldObjForPresubmit(),
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001528 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001529 except presubmit_support.PresubmitFailure as e:
Aaron Gable5d5f22c2018-02-02 09:12:41 -08001530 DieWithError('%s\nMaybe your depot_tools is out of date?' % e)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001531
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001532 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1533 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001534 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1535 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001536 else:
1537 # Assume url.
1538 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1539 urlparse.urlparse(issue_arg))
1540 if not parsed_issue_arg or not parsed_issue_arg.valid:
1541 DieWithError('Failed to parse issue argument "%s". '
1542 'Must be an issue number or a valid URL.' % issue_arg)
1543 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001544 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001545
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001546 def CMDUpload(self, options, git_diff_args, orig_args):
1547 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001548 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001549 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001550 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001551 else:
1552 if self.GetBranch() is None:
1553 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1554
1555 # Default to diffing against common ancestor of upstream branch
1556 base_branch = self.GetCommonAncestorWithUpstream()
1557 git_diff_args = [base_branch, 'HEAD']
1558
Aaron Gablec4c40d12017-05-22 11:49:53 -07001559 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1560 if not self.IsGerrit() and not self.GetIssue():
1561 print('=====================================')
1562 print('NOTICE: Rietveld is being deprecated. '
1563 'You can upload changes to Gerrit with')
1564 print(' git cl upload --gerrit')
1565 print('or set Gerrit to be your default code review tool with')
1566 print(' git config gerrit.host true')
1567 print('=====================================')
1568
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001569 # Fast best-effort checks to abort before running potentially
1570 # expensive hooks if uploading is likely to fail anyway. Passing these
1571 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001572 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001573 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001574
1575 # Apply watchlists on upload.
1576 change = self.GetChange(base_branch, None)
1577 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1578 files = [f.LocalPath() for f in change.AffectedFiles()]
1579 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001580 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001581
1582 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001583 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001584 # Set the reviewer list now so that presubmit checks can access it.
1585 change_description = ChangeDescription(change.FullDescriptionText())
1586 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001587 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001588 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001589 change)
1590 change.SetDescriptionText(change_description.description)
1591 hook_results = self.RunHook(committing=False,
1592 may_prompt=not options.force,
1593 verbose=options.verbose,
1594 change=change)
1595 if not hook_results.should_continue():
1596 return 1
1597 if not options.reviewers and hook_results.reviewers:
1598 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001599 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001600
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001601 # TODO(tandrii): Checking local patchset against remote patchset is only
1602 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1603 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001604 latest_patchset = self.GetMostRecentPatchset()
1605 local_patchset = self.GetPatchset()
1606 if (latest_patchset and local_patchset and
1607 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001608 print('The last upload made from this repository was patchset #%d but '
1609 'the most recent patchset on the server is #%d.'
1610 % (local_patchset, latest_patchset))
1611 print('Uploading will still work, but if you\'ve uploaded to this '
1612 'issue from another machine or branch the patch you\'re '
1613 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001614 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001615
Aaron Gable13101a62018-02-09 13:20:41 -08001616 print_stats(git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001617 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001618 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001619 if options.use_commit_queue:
1620 self.SetCQState(_CQState.COMMIT)
1621 elif options.cq_dry_run:
1622 self.SetCQState(_CQState.DRY_RUN)
1623
tandrii5d48c322016-08-18 16:19:37 -07001624 _git_set_branch_config_value('last-upload-hash',
1625 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001626 # Run post upload hooks, if specified.
1627 if settings.GetRunPostUploadHook():
1628 presubmit_support.DoPostUploadExecuter(
1629 change,
1630 self,
1631 settings.GetRoot(),
1632 options.verbose,
1633 sys.stdout)
1634
1635 # Upload all dependencies if specified.
1636 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001637 print()
1638 print('--dependencies has been specified.')
1639 print('All dependent local branches will be re-uploaded.')
1640 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001641 # Remove the dependencies flag from args so that we do not end up in a
1642 # loop.
1643 orig_args.remove('--dependencies')
1644 ret = upload_branch_deps(self, orig_args)
1645 return ret
1646
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001647 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001648 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001649
1650 Issue must have been already uploaded and known.
1651 """
1652 assert new_state in _CQState.ALL_STATES
1653 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001654 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001655 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001656 return 0
1657 except KeyboardInterrupt:
1658 raise
1659 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001660 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001661 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001662 ' * Your project has no CQ,\n'
1663 ' * You don\'t have permission to change the CQ state,\n'
1664 ' * There\'s a bug in this code (see stack trace below).\n'
1665 'Consider specifying which bots to trigger manually or asking your '
1666 'project owners for permissions or contacting Chrome Infra at:\n'
1667 'https://www.chromium.org/infra\n\n' %
1668 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001669 # Still raise exception so that stack trace is printed.
1670 raise
1671
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001672 # Forward methods to codereview specific implementation.
1673
Aaron Gable636b13f2017-07-14 10:42:48 -07001674 def AddComment(self, message, publish=None):
1675 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001676
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001677 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001678 """Returns list of _CommentSummary for each comment.
1679
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001680 args:
1681 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001682 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001683 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001684
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001685 def CloseIssue(self):
1686 return self._codereview_impl.CloseIssue()
1687
1688 def GetStatus(self):
1689 return self._codereview_impl.GetStatus()
1690
1691 def GetCodereviewServer(self):
1692 return self._codereview_impl.GetCodereviewServer()
1693
tandriide281ae2016-10-12 06:02:30 -07001694 def GetIssueOwner(self):
1695 """Get owner from codereview, which may differ from this checkout."""
1696 return self._codereview_impl.GetIssueOwner()
1697
Edward Lemur707d70b2018-02-07 00:50:14 +01001698 def GetReviewers(self):
1699 return self._codereview_impl.GetReviewers()
1700
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001701 def GetMostRecentPatchset(self):
1702 return self._codereview_impl.GetMostRecentPatchset()
1703
tandriide281ae2016-10-12 06:02:30 -07001704 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001705 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001706 return self._codereview_impl.CannotTriggerTryJobReason()
1707
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001708 def GetTryJobProperties(self, patchset=None):
1709 """Returns dictionary of properties to launch try job."""
1710 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001711
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001712 def __getattr__(self, attr):
1713 # This is because lots of untested code accesses Rietveld-specific stuff
1714 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001715 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001716 # Note that child method defines __getattr__ as well, and forwards it here,
1717 # because _RietveldChangelistImpl is not cleaned up yet, and given
1718 # deprecation of Rietveld, it should probably be just removed.
1719 # Until that time, avoid infinite recursion by bypassing __getattr__
1720 # of implementation class.
1721 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001722
1723
1724class _ChangelistCodereviewBase(object):
1725 """Abstract base class encapsulating codereview specifics of a changelist."""
1726 def __init__(self, changelist):
1727 self._changelist = changelist # instance of Changelist
1728
1729 def __getattr__(self, attr):
1730 # Forward methods to changelist.
1731 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1732 # _RietveldChangelistImpl to avoid this hack?
1733 return getattr(self._changelist, attr)
1734
1735 def GetStatus(self):
1736 """Apply a rough heuristic to give a simple summary of an issue's review
1737 or CQ status, assuming adherence to a common workflow.
1738
1739 Returns None if no issue for this branch, or specific string keywords.
1740 """
1741 raise NotImplementedError()
1742
1743 def GetCodereviewServer(self):
1744 """Returns server URL without end slash, like "https://codereview.com"."""
1745 raise NotImplementedError()
1746
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001747 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001748 """Fetches and returns description from the codereview server."""
1749 raise NotImplementedError()
1750
tandrii5d48c322016-08-18 16:19:37 -07001751 @classmethod
1752 def IssueConfigKey(cls):
1753 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001754 raise NotImplementedError()
1755
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001756 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001757 def PatchsetConfigKey(cls):
1758 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001759 raise NotImplementedError()
1760
tandrii5d48c322016-08-18 16:19:37 -07001761 @classmethod
1762 def CodereviewServerConfigKey(cls):
1763 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001764 raise NotImplementedError()
1765
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001766 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001767 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001768 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001769
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001770 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001771 # This is an unfortunate Rietveld-embeddedness in presubmit.
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001772 # For non-Rietveld code reviews, this probably should return a dummy object.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001773 raise NotImplementedError()
1774
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001775 def GetGerritObjForPresubmit(self):
1776 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1777 return None
1778
dsansomee2d6fd92016-09-08 00:10:47 -07001779 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001780 """Update the description on codereview site."""
1781 raise NotImplementedError()
1782
Aaron Gable636b13f2017-07-14 10:42:48 -07001783 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001784 """Posts a comment to the codereview site."""
1785 raise NotImplementedError()
1786
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001787 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001788 raise NotImplementedError()
1789
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001790 def CloseIssue(self):
1791 """Closes the issue."""
1792 raise NotImplementedError()
1793
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001794 def GetMostRecentPatchset(self):
1795 """Returns the most recent patchset number from the codereview site."""
1796 raise NotImplementedError()
1797
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001798 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001799 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001800 """Fetches and applies the issue.
1801
1802 Arguments:
1803 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1804 reject: if True, reject the failed patch instead of switching to 3-way
1805 merge. Rietveld only.
1806 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1807 only.
1808 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001809 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001810 """
1811 raise NotImplementedError()
1812
1813 @staticmethod
1814 def ParseIssueURL(parsed_url):
1815 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1816 failed."""
1817 raise NotImplementedError()
1818
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001819 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001820 """Best effort check that user is authenticated with codereview server.
1821
1822 Arguments:
1823 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001824 refresh: whether to attempt to refresh credentials. Ignored if not
1825 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001826 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001827 raise NotImplementedError()
1828
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001829 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001830 """Best effort check that uploading isn't supposed to fail for predictable
1831 reasons.
1832
1833 This method should raise informative exception if uploading shouldn't
1834 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001835
1836 Arguments:
1837 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001838 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001839 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001840
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001841 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001842 """Uploads a change to codereview."""
1843 raise NotImplementedError()
1844
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001845 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001846 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001847
1848 Issue must have been already uploaded and known.
1849 """
1850 raise NotImplementedError()
1851
tandriie113dfd2016-10-11 10:20:12 -07001852 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001853 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001854 raise NotImplementedError()
1855
tandriide281ae2016-10-12 06:02:30 -07001856 def GetIssueOwner(self):
1857 raise NotImplementedError()
1858
Edward Lemur707d70b2018-02-07 00:50:14 +01001859 def GetReviewers(self):
1860 raise NotImplementedError()
1861
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001862 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001863 raise NotImplementedError()
1864
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001865
1866class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001867
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001868 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001869 super(_RietveldChangelistImpl, self).__init__(changelist)
1870 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001871 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001872 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001873
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001874 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001875 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001876 self._props = None
1877 self._rpc_server = None
1878
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001879 def GetCodereviewServer(self):
1880 if not self._rietveld_server:
1881 # If we're on a branch then get the server potentially associated
1882 # with that branch.
1883 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001884 self._rietveld_server = gclient_utils.UpgradeToHttps(
1885 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001886 if not self._rietveld_server:
1887 self._rietveld_server = settings.GetDefaultServerUrl()
1888 return self._rietveld_server
1889
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001890 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001891 """Best effort check that user is authenticated with Rietveld server."""
1892 if self._auth_config.use_oauth2:
1893 authenticator = auth.get_authenticator_for_host(
1894 self.GetCodereviewServer(), self._auth_config)
1895 if not authenticator.has_cached_credentials():
1896 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001897 if refresh:
1898 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001899
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001900 def EnsureCanUploadPatchset(self, force):
1901 # No checks for Rietveld because we are deprecating Rietveld.
1902 pass
1903
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001904 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001905 issue = self.GetIssue()
1906 assert issue
1907 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001908 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001909 except urllib2.HTTPError as e:
1910 if e.code == 404:
1911 DieWithError(
1912 ('\nWhile fetching the description for issue %d, received a '
1913 '404 (not found)\n'
1914 'error. It is likely that you deleted this '
1915 'issue on the server. If this is the\n'
1916 'case, please run\n\n'
1917 ' git cl issue 0\n\n'
1918 'to clear the association with the deleted issue. Then run '
1919 'this command again.') % issue)
1920 else:
1921 DieWithError(
1922 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1923 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001924 print('Warning: Failed to retrieve CL description due to network '
1925 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001926 return ''
1927
1928 def GetMostRecentPatchset(self):
1929 return self.GetIssueProperties()['patchsets'][-1]
1930
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001931 def GetIssueProperties(self):
1932 if self._props is None:
1933 issue = self.GetIssue()
1934 if not issue:
1935 self._props = {}
1936 else:
1937 self._props = self.RpcServer().get_issue_properties(issue, True)
1938 return self._props
1939
tandriie113dfd2016-10-11 10:20:12 -07001940 def CannotTriggerTryJobReason(self):
1941 props = self.GetIssueProperties()
1942 if not props:
1943 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1944 if props.get('closed'):
1945 return 'CL %s is closed' % self.GetIssue()
1946 if props.get('private'):
1947 return 'CL %s is private' % self.GetIssue()
1948 return None
1949
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001950 def GetTryJobProperties(self, patchset=None):
1951 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07001952 project = (self.GetIssueProperties() or {}).get('project')
1953 return {
1954 'issue': self.GetIssue(),
1955 'patch_project': project,
1956 'patch_storage': 'rietveld',
1957 'patchset': patchset or self.GetPatchset(),
1958 'rietveld': self.GetCodereviewServer(),
1959 }
1960
tandriide281ae2016-10-12 06:02:30 -07001961 def GetIssueOwner(self):
1962 return (self.GetIssueProperties() or {}).get('owner_email')
1963
Edward Lemur707d70b2018-02-07 00:50:14 +01001964 def GetReviewers(self):
1965 return (self.GetIssueProperties() or {}).get('reviewers')
1966
Aaron Gable636b13f2017-07-14 10:42:48 -07001967 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001968 return self.RpcServer().add_comment(self.GetIssue(), message)
1969
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001970 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001971 summary = []
1972 for message in self.GetIssueProperties().get('messages', []):
1973 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
1974 summary.append(_CommentSummary(
1975 date=date,
1976 disapproval=bool(message['disapproval']),
1977 approval=bool(message['approval']),
1978 sender=message['sender'],
1979 message=message['text'],
1980 ))
1981 return summary
1982
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001983 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001984 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001985 or CQ status, assuming adherence to a common workflow.
1986
1987 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07001988 * 'error' - error from review tool (including deleted issues)
1989 * 'unsent' - not sent for review
1990 * 'waiting' - waiting for review
1991 * 'reply' - waiting for owner to reply to review
1992 * 'not lgtm' - Code-Review label has been set negatively
1993 * 'lgtm' - LGTM from at least one approved reviewer
1994 * 'commit' - in the commit queue
1995 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001996 """
1997 if not self.GetIssue():
1998 return None
1999
2000 try:
2001 props = self.GetIssueProperties()
2002 except urllib2.HTTPError:
2003 return 'error'
2004
2005 if props.get('closed'):
2006 # Issue is closed.
2007 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002008 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002009 # Issue is in the commit queue.
2010 return 'commit'
2011
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002012 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002013 if not messages:
2014 # No message was sent.
2015 return 'unsent'
2016
2017 if get_approving_reviewers(props):
2018 return 'lgtm'
2019 elif get_approving_reviewers(props, disapproval=True):
2020 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002021
tandrii9d2c7a32016-06-22 03:42:45 -07002022 # Skip CQ messages that don't require owner's action.
2023 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2024 if 'Dry run:' in messages[-1]['text']:
2025 messages.pop()
2026 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2027 # This message always follows prior messages from CQ,
2028 # so skip this too.
2029 messages.pop()
2030 else:
2031 # This is probably a CQ messages warranting user attention.
2032 break
2033
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002034 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002035 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002036 return 'reply'
2037 return 'waiting'
2038
dsansomee2d6fd92016-09-08 00:10:47 -07002039 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002040 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002041
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002043 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002044
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002045 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002046 return self.SetFlags({flag: value})
2047
2048 def SetFlags(self, flags):
2049 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002050 """
phajdan.jr68598232016-08-10 03:28:28 -07002051 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002052 try:
tandrii4b233bd2016-07-06 03:50:29 -07002053 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002054 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002055 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002056 if e.code == 404:
2057 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2058 if e.code == 403:
2059 DieWithError(
2060 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002061 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002062 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002063
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002064 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065 """Returns an upload.RpcServer() to access this review's rietveld instance.
2066 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002067 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002068 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002069 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002070 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002071 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002072
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002073 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002074 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002075 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002076
tandrii5d48c322016-08-18 16:19:37 -07002077 @classmethod
2078 def PatchsetConfigKey(cls):
2079 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002080
tandrii5d48c322016-08-18 16:19:37 -07002081 @classmethod
2082 def CodereviewServerConfigKey(cls):
2083 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002084
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002085 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002086 return self.RpcServer()
2087
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002088 def SetCQState(self, new_state):
2089 props = self.GetIssueProperties()
2090 if props.get('private'):
2091 DieWithError('Cannot set-commit on private issue')
2092
2093 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002094 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002095 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002096 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002097 else:
tandrii4b233bd2016-07-06 03:50:29 -07002098 assert new_state == _CQState.DRY_RUN
2099 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002100
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002101 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002102 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002103 # PatchIssue should never be called with a dirty tree. It is up to the
2104 # caller to check this, but just in case we assert here since the
2105 # consequences of the caller not checking this could be dire.
2106 assert(not git_common.is_dirty_git_tree('apply'))
2107 assert(parsed_issue_arg.valid)
2108 self._changelist.issue = parsed_issue_arg.issue
2109 if parsed_issue_arg.hostname:
2110 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2111
skobes6468b902016-10-24 08:45:10 -07002112 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2113 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2114 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002115 try:
skobes6468b902016-10-24 08:45:10 -07002116 scm_obj.apply_patch(patchset_object)
2117 except Exception as e:
2118 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002119 return 1
2120
2121 # If we had an issue, commit the current state and register the issue.
2122 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002123 self.SetIssue(self.GetIssue())
2124 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002125 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2126 'patch from issue %(i)s at patchset '
2127 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2128 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002129 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002130 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002131 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002132 return 0
2133
2134 @staticmethod
2135 def ParseIssueURL(parsed_url):
2136 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2137 return None
wychen3c1c1722016-08-04 11:46:36 -07002138 # Rietveld patch: https://domain/<number>/#ps<patchset>
2139 match = re.match(r'/(\d+)/$', parsed_url.path)
2140 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2141 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002142 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002143 issue=int(match.group(1)),
2144 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002145 hostname=parsed_url.netloc,
2146 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002147 # Typical url: https://domain/<issue_number>[/[other]]
2148 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2149 if match:
skobes6468b902016-10-24 08:45:10 -07002150 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002151 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002152 hostname=parsed_url.netloc,
2153 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002154 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2155 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2156 if match:
skobes6468b902016-10-24 08:45:10 -07002157 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002158 issue=int(match.group(1)),
2159 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002160 hostname=parsed_url.netloc,
2161 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002162 return None
2163
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002164 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002165 """Upload the patch to Rietveld."""
2166 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2167 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002168 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2169 if options.emulate_svn_auto_props:
2170 upload_args.append('--emulate_svn_auto_props')
2171
2172 change_desc = None
2173
2174 if options.email is not None:
2175 upload_args.extend(['--email', options.email])
2176
2177 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002178 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002179 upload_args.extend(['--title', options.title])
2180 if options.message:
2181 upload_args.extend(['--message', options.message])
2182 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002183 print('This branch is associated with issue %s. '
2184 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002185 else:
nodirca166002016-06-27 10:59:51 -07002186 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002187 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002188 if options.message:
2189 message = options.message
2190 else:
2191 message = CreateDescriptionFromLog(args)
2192 if options.title:
2193 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002194 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002195 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002196 change_desc.update_reviewers(options.reviewers, options.tbrs,
2197 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002198 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002199 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002200
2201 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002202 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002203 return 1
2204
2205 upload_args.extend(['--message', change_desc.description])
2206 if change_desc.get_reviewers():
2207 upload_args.append('--reviewers=%s' % ','.join(
2208 change_desc.get_reviewers()))
2209 if options.send_mail:
2210 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002211 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002212 upload_args.append('--send_mail')
2213
2214 # We check this before applying rietveld.private assuming that in
2215 # rietveld.cc only addresses which we can send private CLs to are listed
2216 # if rietveld.private is set, and so we should ignore rietveld.cc only
2217 # when --private is specified explicitly on the command line.
2218 if options.private:
2219 logging.warn('rietveld.cc is ignored since private flag is specified. '
2220 'You need to review and add them manually if necessary.')
2221 cc = self.GetCCListWithoutDefault()
2222 else:
2223 cc = self.GetCCList()
2224 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002225 if change_desc.get_cced():
2226 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 if cc:
2228 upload_args.extend(['--cc', cc])
2229
2230 if options.private or settings.GetDefaultPrivateFlag() == "True":
2231 upload_args.append('--private')
2232
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002233 # Include the upstream repo's URL in the change -- this is useful for
2234 # projects that have their source spread across multiple repos.
2235 remote_url = self.GetGitBaseUrlFromConfig()
2236 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002237 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2238 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2239 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002240 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002241 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002242 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002243 if target_ref:
2244 upload_args.extend(['--target_ref', target_ref])
2245
2246 # Look for dependent patchsets. See crbug.com/480453 for more details.
2247 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2248 upstream_branch = ShortBranchName(upstream_branch)
2249 if remote is '.':
2250 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002251 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002252 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002253 print()
2254 print('Skipping dependency patchset upload because git config '
2255 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2256 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002257 else:
2258 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002259 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002260 auth_config=auth_config)
2261 branch_cl_issue_url = branch_cl.GetIssueURL()
2262 branch_cl_issue = branch_cl.GetIssue()
2263 branch_cl_patchset = branch_cl.GetPatchset()
2264 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2265 upload_args.extend(
2266 ['--depends_on_patchset', '%s:%s' % (
2267 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002268 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002269 '\n'
2270 'The current branch (%s) is tracking a local branch (%s) with '
2271 'an associated CL.\n'
2272 'Adding %s/#ps%s as a dependency patchset.\n'
2273 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2274 branch_cl_patchset))
2275
2276 project = settings.GetProject()
2277 if project:
2278 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002279 else:
2280 print()
2281 print('WARNING: Uploading without a project specified. Please ensure '
2282 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2283 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002284
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002285 try:
2286 upload_args = ['upload'] + upload_args + args
2287 logging.info('upload.RealMain(%s)', upload_args)
2288 issue, patchset = upload.RealMain(upload_args)
2289 issue = int(issue)
2290 patchset = int(patchset)
2291 except KeyboardInterrupt:
2292 sys.exit(1)
2293 except:
2294 # If we got an exception after the user typed a description for their
2295 # change, back up the description before re-raising.
2296 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002297 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002298 raise
2299
2300 if not self.GetIssue():
2301 self.SetIssue(issue)
2302 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002303 return 0
2304
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002305
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002306class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002307 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002308 # auth_config is Rietveld thing, kept here to preserve interface only.
2309 super(_GerritChangelistImpl, self).__init__(changelist)
2310 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002311 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002312 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002313 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002314 # Map from change number (issue) to its detail cache.
2315 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002316
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002317 if codereview_host is not None:
2318 assert not codereview_host.startswith('https://'), codereview_host
2319 self._gerrit_host = codereview_host
2320 self._gerrit_server = 'https://%s' % codereview_host
2321
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002322 def _GetGerritHost(self):
2323 # Lazy load of configs.
2324 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002325 if self._gerrit_host and '.' not in self._gerrit_host:
2326 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2327 # This happens for internal stuff http://crbug.com/614312.
2328 parsed = urlparse.urlparse(self.GetRemoteUrl())
2329 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002330 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002331 ' Your current remote is: %s' % self.GetRemoteUrl())
2332 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2333 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002334 return self._gerrit_host
2335
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002336 def _GetGitHost(self):
2337 """Returns git host to be used when uploading change to Gerrit."""
2338 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2339
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002340 def GetCodereviewServer(self):
2341 if not self._gerrit_server:
2342 # If we're on a branch then get the server potentially associated
2343 # with that branch.
2344 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002345 self._gerrit_server = self._GitGetBranchConfigValue(
2346 self.CodereviewServerConfigKey())
2347 if self._gerrit_server:
2348 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002349 if not self._gerrit_server:
2350 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2351 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002352 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002353 parts[0] = parts[0] + '-review'
2354 self._gerrit_host = '.'.join(parts)
2355 self._gerrit_server = 'https://%s' % self._gerrit_host
2356 return self._gerrit_server
2357
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002358 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002359 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002360 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002361
tandrii5d48c322016-08-18 16:19:37 -07002362 @classmethod
2363 def PatchsetConfigKey(cls):
2364 return 'gerritpatchset'
2365
2366 @classmethod
2367 def CodereviewServerConfigKey(cls):
2368 return 'gerritserver'
2369
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002370 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002371 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002372 if settings.GetGerritSkipEnsureAuthenticated():
2373 # For projects with unusual authentication schemes.
2374 # See http://crbug.com/603378.
2375 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002376 # Lazy-loader to identify Gerrit and Git hosts.
2377 if gerrit_util.GceAuthenticator.is_gce():
2378 return
2379 self.GetCodereviewServer()
2380 git_host = self._GetGitHost()
2381 assert self._gerrit_server and self._gerrit_host
2382 cookie_auth = gerrit_util.CookiesAuthenticator()
2383
2384 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2385 git_auth = cookie_auth.get_auth_header(git_host)
2386 if gerrit_auth and git_auth:
2387 if gerrit_auth == git_auth:
2388 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002389 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002390 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002391 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002392 ' %s\n'
2393 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002394 ' Consider running the following command:\n'
2395 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002396 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002397 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002398 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002399 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002400 cookie_auth.get_new_password_message(git_host)))
2401 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002402 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002403 return
2404 else:
2405 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002406 ([] if gerrit_auth else [self._gerrit_host]) +
2407 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002408 DieWithError('Credentials for the following hosts are required:\n'
2409 ' %s\n'
2410 'These are read from %s (or legacy %s)\n'
2411 '%s' % (
2412 '\n '.join(missing),
2413 cookie_auth.get_gitcookies_path(),
2414 cookie_auth.get_netrc_path(),
2415 cookie_auth.get_new_password_message(git_host)))
2416
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002417 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002418 if not self.GetIssue():
2419 return
2420
2421 # Warm change details cache now to avoid RPCs later, reducing latency for
2422 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002423 self._GetChangeDetail(
2424 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002425
2426 status = self._GetChangeDetail()['status']
2427 if status in ('MERGED', 'ABANDONED'):
2428 DieWithError('Change %s has been %s, new uploads are not allowed' %
2429 (self.GetIssueURL(),
2430 'submitted' if status == 'MERGED' else 'abandoned'))
2431
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002432 if gerrit_util.GceAuthenticator.is_gce():
2433 return
2434 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2435 self._GetGerritHost())
2436 if self.GetIssueOwner() == cookies_user:
2437 return
2438 logging.debug('change %s owner is %s, cookies user is %s',
2439 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002440 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002441 # so ask what Gerrit thinks of this user.
2442 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2443 if details['email'] == self.GetIssueOwner():
2444 return
2445 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002446 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002447 'as %s.\n'
2448 'Uploading may fail due to lack of permissions.' %
2449 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2450 confirm_or_exit(action='upload')
2451
2452
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002453 def _PostUnsetIssueProperties(self):
2454 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002455 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002456
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002457 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002458 class ThisIsNotRietveldIssue(object):
2459 def __nonzero__(self):
2460 # This is a hack to make presubmit_support think that rietveld is not
2461 # defined, yet still ensure that calls directly result in a decent
2462 # exception message below.
2463 return False
2464
2465 def __getattr__(self, attr):
2466 print(
2467 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2468 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002469 'Please, either change your PRESUBMIT to not use rietveld_obj.%s,\n'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002470 'or use Rietveld for codereview.\n'
2471 'See also http://crbug.com/579160.' % attr)
2472 raise NotImplementedError()
2473 return ThisIsNotRietveldIssue()
2474
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002475 def GetGerritObjForPresubmit(self):
2476 return presubmit_support.GerritAccessor(self._GetGerritHost())
2477
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002478 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002479 """Apply a rough heuristic to give a simple summary of an issue's review
2480 or CQ status, assuming adherence to a common workflow.
2481
2482 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002483 * 'error' - error from review tool (including deleted issues)
2484 * 'unsent' - no reviewers added
2485 * 'waiting' - waiting for review
2486 * 'reply' - waiting for uploader to reply to review
2487 * 'lgtm' - Code-Review label has been set
2488 * 'commit' - in the commit queue
2489 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002490 """
2491 if not self.GetIssue():
2492 return None
2493
2494 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002495 data = self._GetChangeDetail([
2496 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002497 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002498 return 'error'
2499
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002500 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002501 return 'closed'
2502
Aaron Gable9ab38c62017-04-06 14:36:33 -07002503 if data['labels'].get('Commit-Queue', {}).get('approved'):
2504 # The section will have an "approved" subsection if anyone has voted
2505 # the maximum value on the label.
2506 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002507
Aaron Gable9ab38c62017-04-06 14:36:33 -07002508 if data['labels'].get('Code-Review', {}).get('approved'):
2509 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002510
2511 if not data.get('reviewers', {}).get('REVIEWER', []):
2512 return 'unsent'
2513
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002514 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002515 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2516 last_message_author = messages.pop().get('author', {})
2517 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002518 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2519 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002520 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002521 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002522 if last_message_author.get('_account_id') == owner:
2523 # Most recent message was by owner.
2524 return 'waiting'
2525 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002526 # Some reply from non-owner.
2527 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002528
2529 # Somehow there are no messages even though there are reviewers.
2530 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002531
2532 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002533 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002534 patchset = data['revisions'][data['current_revision']]['_number']
2535 self.SetPatchset(patchset)
2536 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002537
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002538 def FetchDescription(self, force=False):
2539 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2540 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002541 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002542 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002543
dsansomee2d6fd92016-09-08 00:10:47 -07002544 def UpdateDescriptionRemote(self, description, force=False):
2545 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2546 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002547 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002548 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002549 'unpublished edit. Either publish the edit in the Gerrit web UI '
2550 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002551
2552 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2553 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002554 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002555 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002556
Aaron Gable636b13f2017-07-14 10:42:48 -07002557 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002558 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002559 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002560
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002561 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002562 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002563 messages = self._GetChangeDetail(
2564 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2565 file_comments = gerrit_util.GetChangeComments(
2566 self._GetGerritHost(), self.GetIssue())
2567
2568 # Build dictionary of file comments for easy access and sorting later.
2569 # {author+date: {path: {patchset: {line: url+message}}}}
2570 comments = collections.defaultdict(
2571 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2572 for path, line_comments in file_comments.iteritems():
2573 for comment in line_comments:
2574 if comment.get('tag', '').startswith('autogenerated'):
2575 continue
2576 key = (comment['author']['email'], comment['updated'])
2577 if comment.get('side', 'REVISION') == 'PARENT':
2578 patchset = 'Base'
2579 else:
2580 patchset = 'PS%d' % comment['patch_set']
2581 line = comment.get('line', 0)
2582 url = ('https://%s/c/%s/%s/%s#%s%s' %
2583 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2584 'b' if comment.get('side') == 'PARENT' else '',
2585 str(line) if line else ''))
2586 comments[key][path][patchset][line] = (url, comment['message'])
2587
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002588 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002589 for msg in messages:
2590 # Don't bother showing autogenerated messages.
2591 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2592 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002593 # Gerrit spits out nanoseconds.
2594 assert len(msg['date'].split('.')[-1]) == 9
2595 date = datetime.datetime.strptime(msg['date'][:-3],
2596 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002597 message = msg['message']
2598 key = (msg['author']['email'], msg['date'])
2599 if key in comments:
2600 message += '\n'
2601 for path, patchsets in sorted(comments.get(key, {}).items()):
2602 if readable:
2603 message += '\n%s' % path
2604 for patchset, lines in sorted(patchsets.items()):
2605 for line, (url, content) in sorted(lines.items()):
2606 if line:
2607 line_str = 'Line %d' % line
2608 path_str = '%s:%d:' % (path, line)
2609 else:
2610 line_str = 'File comment'
2611 path_str = '%s:0:' % path
2612 if readable:
2613 message += '\n %s, %s: %s' % (patchset, line_str, url)
2614 message += '\n %s\n' % content
2615 else:
2616 message += '\n%s ' % path_str
2617 message += '\n%s\n' % content
2618
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002619 summary.append(_CommentSummary(
2620 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002621 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002622 sender=msg['author']['email'],
2623 # These could be inferred from the text messages and correlated with
2624 # Code-Review label maximum, however this is not reliable.
2625 # Leaving as is until the need arises.
2626 approval=False,
2627 disapproval=False,
2628 ))
2629 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002630
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002631 def CloseIssue(self):
2632 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2633
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002634 def SubmitIssue(self, wait_for_merge=True):
2635 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2636 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002637
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002638 def _GetChangeDetail(self, options=None, issue=None,
2639 no_cache=False):
2640 """Returns details of the issue by querying Gerrit and caching results.
2641
2642 If fresh data is needed, set no_cache=True which will clear cache and
2643 thus new data will be fetched from Gerrit.
2644 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002645 options = options or []
2646 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002647 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002648
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002649 # Optimization to avoid multiple RPCs:
2650 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2651 'CURRENT_COMMIT' not in options):
2652 options.append('CURRENT_COMMIT')
2653
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002654 # Normalize issue and options for consistent keys in cache.
2655 issue = str(issue)
2656 options = [o.upper() for o in options]
2657
2658 # Check in cache first unless no_cache is True.
2659 if no_cache:
2660 self._detail_cache.pop(issue, None)
2661 else:
2662 options_set = frozenset(options)
2663 for cached_options_set, data in self._detail_cache.get(issue, []):
2664 # Assumption: data fetched before with extra options is suitable
2665 # for return for a smaller set of options.
2666 # For example, if we cached data for
2667 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2668 # and request is for options=[CURRENT_REVISION],
2669 # THEN we can return prior cached data.
2670 if options_set.issubset(cached_options_set):
2671 return data
2672
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002673 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002674 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002675 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002676 except gerrit_util.GerritError as e:
2677 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002678 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002679 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002680
2681 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002682 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002683
agable32978d92016-11-01 12:55:02 -07002684 def _GetChangeCommit(self, issue=None):
2685 issue = issue or self.GetIssue()
2686 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002687 try:
2688 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2689 except gerrit_util.GerritError as e:
2690 if e.http_status == 404:
2691 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2692 raise
agable32978d92016-11-01 12:55:02 -07002693 return data
2694
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002695 def CMDLand(self, force, bypass_hooks, verbose):
2696 if git_common.is_dirty_git_tree('land'):
2697 return 1
tandriid60367b2016-06-22 05:25:12 -07002698 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2699 if u'Commit-Queue' in detail.get('labels', {}):
2700 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002701 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2702 'which can test and land changes for you. '
2703 'Are you sure you wish to bypass it?\n',
2704 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002705
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002706 differs = True
tandriic4344b52016-08-29 06:04:54 -07002707 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002708 # Note: git diff outputs nothing if there is no diff.
2709 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002710 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002711 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002712 if detail['current_revision'] == last_upload:
2713 differs = False
2714 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002715 print('WARNING: Local branch contents differ from latest uploaded '
2716 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002717 if differs:
2718 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002719 confirm_or_exit(
2720 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2721 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002722 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002723 elif not bypass_hooks:
2724 hook_results = self.RunHook(
2725 committing=True,
2726 may_prompt=not force,
2727 verbose=verbose,
2728 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2729 if not hook_results.should_continue():
2730 return 1
2731
2732 self.SubmitIssue(wait_for_merge=True)
2733 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002734 links = self._GetChangeCommit().get('web_links', [])
2735 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002736 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002737 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002738 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002739 return 0
2740
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002741 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002742 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002743 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002744 assert not directory
2745 assert parsed_issue_arg.valid
2746
2747 self._changelist.issue = parsed_issue_arg.issue
2748
2749 if parsed_issue_arg.hostname:
2750 self._gerrit_host = parsed_issue_arg.hostname
2751 self._gerrit_server = 'https://%s' % self._gerrit_host
2752
tandriic2405f52016-10-10 08:13:15 -07002753 try:
2754 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002755 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002756 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002757
2758 if not parsed_issue_arg.patchset:
2759 # Use current revision by default.
2760 revision_info = detail['revisions'][detail['current_revision']]
2761 patchset = int(revision_info['_number'])
2762 else:
2763 patchset = parsed_issue_arg.patchset
2764 for revision_info in detail['revisions'].itervalues():
2765 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2766 break
2767 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002768 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002769 (parsed_issue_arg.patchset, self.GetIssue()))
2770
Aaron Gable697a91b2018-01-19 15:20:15 -08002771 remote_url = self._changelist.GetRemoteUrl()
2772 if remote_url.endswith('.git'):
2773 remote_url = remote_url[:-len('.git')]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002774 fetch_info = revision_info['fetch']['http']
Aaron Gable697a91b2018-01-19 15:20:15 -08002775
2776 if remote_url != fetch_info['url']:
2777 DieWithError('Trying to patch a change from %s but this repo appears '
2778 'to be %s.' % (fetch_info['url'], remote_url))
2779
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002780 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002781
Aaron Gable62619a32017-06-16 08:22:09 -07002782 if force:
2783 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2784 print('Checked out commit for change %i patchset %i locally' %
2785 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002786 elif nocommit:
2787 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2788 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002789 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002790 RunGit(['cherry-pick', 'FETCH_HEAD'])
2791 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002792 (parsed_issue_arg.issue, patchset))
2793 print('Note: this created a local commit which does not have '
2794 'the same hash as the one uploaded for review. This will make '
2795 'uploading changes based on top of this branch difficult.\n'
2796 'If you want to do that, use "git cl patch --force" instead.')
2797
Stefan Zagerd08043c2017-10-12 12:07:02 -07002798 if self.GetBranch():
2799 self.SetIssue(parsed_issue_arg.issue)
2800 self.SetPatchset(patchset)
2801 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2802 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2803 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2804 else:
2805 print('WARNING: You are in detached HEAD state.\n'
2806 'The patch has been applied to your checkout, but you will not be '
2807 'able to upload a new patch set to the gerrit issue.\n'
2808 'Try using the \'-b\' option if you would like to work on a '
2809 'branch and/or upload a new patch set.')
2810
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002811 return 0
2812
2813 @staticmethod
2814 def ParseIssueURL(parsed_url):
2815 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2816 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002817 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2818 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002819 # Short urls like https://domain/<issue_number> can be used, but don't allow
2820 # specifying the patchset (you'd 404), but we allow that here.
2821 if parsed_url.path == '/':
2822 part = parsed_url.fragment
2823 else:
2824 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002825 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002826 if match:
2827 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002828 issue=int(match.group(3)),
2829 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002830 hostname=parsed_url.netloc,
2831 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002832 return None
2833
tandrii16e0b4e2016-06-07 10:34:28 -07002834 def _GerritCommitMsgHookCheck(self, offer_removal):
2835 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2836 if not os.path.exists(hook):
2837 return
2838 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2839 # custom developer made one.
2840 data = gclient_utils.FileRead(hook)
2841 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2842 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002843 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002844 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002845 'and may interfere with it in subtle ways.\n'
2846 'We recommend you remove the commit-msg hook.')
2847 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002848 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002849 gclient_utils.rm_file_or_tree(hook)
2850 print('Gerrit commit-msg hook removed.')
2851 else:
2852 print('OK, will keep Gerrit commit-msg hook in place.')
2853
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002854 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002855 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002856 if options.squash and options.no_squash:
2857 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002858
2859 if not options.squash and not options.no_squash:
2860 # Load default for user, repo, squash=true, in this order.
2861 options.squash = settings.GetSquashGerritUploads()
2862 elif options.no_squash:
2863 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002864
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002865 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002866 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002867
Aaron Gableb56ad332017-01-06 15:24:31 -08002868 # This may be None; default fallback value is determined in logic below.
2869 title = options.title
2870
Dominic Battre7d1c4842017-10-27 09:17:28 +02002871 # Extract bug number from branch name.
2872 bug = options.bug
2873 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2874 if not bug and match:
2875 bug = match.group(1)
2876
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002877 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002878 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002879 if self.GetIssue():
2880 # Try to get the message from a previous upload.
2881 message = self.GetDescription()
2882 if not message:
2883 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002884 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002885 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002886 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002887 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002888 # When uploading a subsequent patchset, -m|--message is taken
2889 # as the patchset title if --title was not provided.
2890 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002891 else:
2892 default_title = RunGit(
2893 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002894 if options.force:
2895 title = default_title
2896 else:
2897 title = ask_for_data(
2898 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002899 change_id = self._GetChangeDetail()['change_id']
2900 while True:
2901 footer_change_ids = git_footers.get_footer_change_id(message)
2902 if footer_change_ids == [change_id]:
2903 break
2904 if not footer_change_ids:
2905 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002906 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002907 continue
2908 # There is already a valid footer but with different or several ids.
2909 # Doing this automatically is non-trivial as we don't want to lose
2910 # existing other footers, yet we want to append just 1 desired
2911 # Change-Id. Thus, just create a new footer, but let user verify the
2912 # new description.
2913 message = '%s\n\nChange-Id: %s' % (message, change_id)
2914 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002915 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002916 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002917 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002918 'Please, check the proposed correction to the description, '
2919 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2920 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2921 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002922 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002923 if not options.force:
2924 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002925 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002926 message = change_desc.description
2927 if not message:
2928 DieWithError("Description is empty. Aborting...")
2929 # Continue the while loop.
2930 # Sanity check of this code - we should end up with proper message
2931 # footer.
2932 assert [change_id] == git_footers.get_footer_change_id(message)
2933 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002934 else: # if not self.GetIssue()
2935 if options.message:
2936 message = options.message
2937 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002938 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002939 if options.title:
2940 message = options.title + '\n\n' + message
2941 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002942
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002943 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002944 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002945 # On first upload, patchset title is always this string, while
2946 # --title flag gets converted to first line of message.
2947 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002948 if not change_desc.description:
2949 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002950 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002951 if len(change_ids) > 1:
2952 DieWithError('too many Change-Id footers, at most 1 allowed.')
2953 if not change_ids:
2954 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002955 change_desc.set_description(git_footers.add_footer_change_id(
2956 change_desc.description,
2957 GenerateGerritChangeId(change_desc.description)))
2958 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002959 assert len(change_ids) == 1
2960 change_id = change_ids[0]
2961
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002962 if options.reviewers or options.tbrs or options.add_owners_to:
2963 change_desc.update_reviewers(options.reviewers, options.tbrs,
2964 options.add_owners_to, change)
2965
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002966 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002967 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
2968 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002969 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07002970 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
2971 desc_tempfile.write(change_desc.description)
2972 desc_tempfile.close()
2973 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2974 '-F', desc_tempfile.name]).strip()
2975 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002976 else:
2977 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002978 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002979 if not change_desc.description:
2980 DieWithError("Description is empty. Aborting...")
2981
2982 if not git_footers.get_footer_change_id(change_desc.description):
2983 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002984 change_desc.set_description(
2985 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07002986 if options.reviewers or options.tbrs or options.add_owners_to:
2987 change_desc.update_reviewers(options.reviewers, options.tbrs,
2988 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002989 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002990 # For no-squash mode, we assume the remote called "origin" is the one we
2991 # want. It is not worthwhile to support different workflows for
2992 # no-squash mode.
2993 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002994 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2995
2996 assert change_desc
2997 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2998 ref_to_push)]).splitlines()
2999 if len(commits) > 1:
3000 print('WARNING: This will upload %d commits. Run the following command '
3001 'to see which commits will be uploaded: ' % len(commits))
3002 print('git log %s..%s' % (parent, ref_to_push))
3003 print('You can also use `git squash-branch` to squash these into a '
3004 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003005 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003006
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003007 if options.reviewers or options.tbrs or options.add_owners_to:
3008 change_desc.update_reviewers(options.reviewers, options.tbrs,
3009 options.add_owners_to, change)
3010
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003011 # Extra options that can be specified at push time. Doc:
3012 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003013 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003014
Aaron Gable844cf292017-06-28 11:32:59 -07003015 # By default, new changes are started in WIP mode, and subsequent patchsets
3016 # don't send email. At any time, passing --send-mail will mark the change
3017 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003018 if options.send_mail:
3019 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003020 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003021 elif not self.GetIssue():
3022 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003023 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003024 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003025
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003026 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003027 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003028
Aaron Gable9b713dd2016-12-14 16:04:21 -08003029 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003030 # Punctuation and whitespace in |title| must be percent-encoded.
3031 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003032
agablec6787972016-09-09 16:13:34 -07003033 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003034 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003035
rmistry9eadede2016-09-19 11:22:43 -07003036 if options.topic:
3037 # Documentation on Gerrit topics is here:
3038 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003039 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003040
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003041 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003042 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003043 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003044 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003045 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3046
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003047 refspec_suffix = ''
3048 if refspec_opts:
3049 refspec_suffix = '%' + ','.join(refspec_opts)
3050 assert ' ' not in refspec_suffix, (
3051 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3052 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3053
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003054 try:
3055 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003056 ['git', 'push', self.GetRemoteUrl(), refspec],
3057 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003058 # Flush after every line: useful for seeing progress when running as
3059 # recipe.
3060 filter_fn=lambda _: sys.stdout.flush())
3061 except subprocess2.CalledProcessError:
3062 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003063 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003064 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003065 'credential problems:\n'
3066 ' git cl creds-check\n',
3067 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003068
3069 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003070 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003071 change_numbers = [m.group(1)
3072 for m in map(regex.match, push_stdout.splitlines())
3073 if m]
3074 if len(change_numbers) != 1:
3075 DieWithError(
3076 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003077 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003078 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003079 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003080
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003081 reviewers = sorted(change_desc.get_reviewers())
3082
tandrii88189772016-09-29 04:29:57 -07003083 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003084 if not options.private:
3085 cc = self.GetCCList().split(',')
3086 else:
3087 cc = []
tandrii88189772016-09-29 04:29:57 -07003088 if options.cc:
3089 cc.extend(options.cc)
3090 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003091 if change_desc.get_cced():
3092 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003093
3094 gerrit_util.AddReviewers(
3095 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3096 notify=bool(options.send_mail))
3097
Aaron Gablefd238082017-06-07 13:42:34 -07003098 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003099 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3100 score = 1
3101 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3102 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3103 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003104 gerrit_util.SetReview(
3105 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003106 msg='Self-approving for TBR',
3107 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003108
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003109 return 0
3110
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003111 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3112 change_desc):
3113 """Computes parent of the generated commit to be uploaded to Gerrit.
3114
3115 Returns revision or a ref name.
3116 """
3117 if custom_cl_base:
3118 # Try to avoid creating additional unintended CLs when uploading, unless
3119 # user wants to take this risk.
3120 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3121 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3122 local_ref_of_target_remote])
3123 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003124 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003125 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3126 'If you proceed with upload, more than 1 CL may be created by '
3127 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3128 'If you are certain that specified base `%s` has already been '
3129 'uploaded to Gerrit as another CL, you may proceed.\n' %
3130 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3131 if not force:
3132 confirm_or_exit(
3133 'Do you take responsibility for cleaning up potential mess '
3134 'resulting from proceeding with upload?',
3135 action='upload')
3136 return custom_cl_base
3137
Aaron Gablef97e33d2017-03-30 15:44:27 -07003138 if remote != '.':
3139 return self.GetCommonAncestorWithUpstream()
3140
3141 # If our upstream branch is local, we base our squashed commit on its
3142 # squashed version.
3143 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3144
Aaron Gablef97e33d2017-03-30 15:44:27 -07003145 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003146 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003147
3148 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003149 # TODO(tandrii): consider checking parent change in Gerrit and using its
3150 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3151 # the tree hash of the parent branch. The upside is less likely bogus
3152 # requests to reupload parent change just because it's uploadhash is
3153 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003154 parent = RunGit(['config',
3155 'branch.%s.gerritsquashhash' % upstream_branch_name],
3156 error_ok=True).strip()
3157 # Verify that the upstream branch has been uploaded too, otherwise
3158 # Gerrit will create additional CLs when uploading.
3159 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3160 RunGitSilent(['rev-parse', parent + ':'])):
3161 DieWithError(
3162 '\nUpload upstream branch %s first.\n'
3163 'It is likely that this branch has been rebased since its last '
3164 'upload, so you just need to upload it again.\n'
3165 '(If you uploaded it with --no-squash, then branch dependencies '
3166 'are not supported, and you should reupload with --squash.)'
3167 % upstream_branch_name,
3168 change_desc)
3169 return parent
3170
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003171 def _AddChangeIdToCommitMessage(self, options, args):
3172 """Re-commits using the current message, assumes the commit hook is in
3173 place.
3174 """
3175 log_desc = options.message or CreateDescriptionFromLog(args)
3176 git_command = ['commit', '--amend', '-m', log_desc]
3177 RunGit(git_command)
3178 new_log_desc = CreateDescriptionFromLog(args)
3179 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003180 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003181 return new_log_desc
3182 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003183 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003184
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003185 def SetCQState(self, new_state):
3186 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003187 vote_map = {
3188 _CQState.NONE: 0,
3189 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003190 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003191 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003192 labels = {'Commit-Queue': vote_map[new_state]}
3193 notify = False if new_state == _CQState.DRY_RUN else None
3194 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3195 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003196
tandriie113dfd2016-10-11 10:20:12 -07003197 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003198 try:
3199 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003200 except GerritChangeNotExists:
3201 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003202
3203 if data['status'] in ('ABANDONED', 'MERGED'):
3204 return 'CL %s is closed' % self.GetIssue()
3205
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003206 def GetTryJobProperties(self, patchset=None):
3207 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003208 data = self._GetChangeDetail(['ALL_REVISIONS'])
3209 patchset = int(patchset or self.GetPatchset())
3210 assert patchset
3211 revision_data = None # Pylint wants it to be defined.
3212 for revision_data in data['revisions'].itervalues():
3213 if int(revision_data['_number']) == patchset:
3214 break
3215 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003216 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003217 (patchset, self.GetIssue()))
3218 return {
3219 'patch_issue': self.GetIssue(),
3220 'patch_set': patchset or self.GetPatchset(),
3221 'patch_project': data['project'],
3222 'patch_storage': 'gerrit',
3223 'patch_ref': revision_data['fetch']['http']['ref'],
3224 'patch_repository_url': revision_data['fetch']['http']['url'],
3225 'patch_gerrit_url': self.GetCodereviewServer(),
3226 }
tandriie113dfd2016-10-11 10:20:12 -07003227
tandriide281ae2016-10-12 06:02:30 -07003228 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003229 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003230
Edward Lemur707d70b2018-02-07 00:50:14 +01003231 def GetReviewers(self):
3232 details = self._GetChangeDetail(['DETAILED_ACCOUNTS'])
3233 return [reviewer['email'] for reviewer in details['reviewers']['REVIEWER']]
3234
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003235
3236_CODEREVIEW_IMPLEMENTATIONS = {
3237 'rietveld': _RietveldChangelistImpl,
3238 'gerrit': _GerritChangelistImpl,
3239}
3240
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003241
iannuccie53c9352016-08-17 14:40:40 -07003242def _add_codereview_issue_select_options(parser, extra=""):
3243 _add_codereview_select_options(parser)
3244
3245 text = ('Operate on this issue number instead of the current branch\'s '
3246 'implicit issue.')
3247 if extra:
3248 text += ' '+extra
3249 parser.add_option('-i', '--issue', type=int, help=text)
3250
3251
3252def _process_codereview_issue_select_options(parser, options):
3253 _process_codereview_select_options(parser, options)
3254 if options.issue is not None and not options.forced_codereview:
3255 parser.error('--issue must be specified with either --rietveld or --gerrit')
3256
3257
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003258def _add_codereview_select_options(parser):
3259 """Appends --gerrit and --rietveld options to force specific codereview."""
3260 parser.codereview_group = optparse.OptionGroup(
3261 parser, 'EXPERIMENTAL! Codereview override options')
3262 parser.add_option_group(parser.codereview_group)
3263 parser.codereview_group.add_option(
3264 '--gerrit', action='store_true',
3265 help='Force the use of Gerrit for codereview')
3266 parser.codereview_group.add_option(
3267 '--rietveld', action='store_true',
3268 help='Force the use of Rietveld for codereview')
3269
3270
3271def _process_codereview_select_options(parser, options):
3272 if options.gerrit and options.rietveld:
3273 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3274 options.forced_codereview = None
3275 if options.gerrit:
3276 options.forced_codereview = 'gerrit'
3277 elif options.rietveld:
3278 options.forced_codereview = 'rietveld'
3279
3280
tandriif9aefb72016-07-01 09:06:51 -07003281def _get_bug_line_values(default_project, bugs):
3282 """Given default_project and comma separated list of bugs, yields bug line
3283 values.
3284
3285 Each bug can be either:
3286 * a number, which is combined with default_project
3287 * string, which is left as is.
3288
3289 This function may produce more than one line, because bugdroid expects one
3290 project per line.
3291
3292 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3293 ['v8:123', 'chromium:789']
3294 """
3295 default_bugs = []
3296 others = []
3297 for bug in bugs.split(','):
3298 bug = bug.strip()
3299 if bug:
3300 try:
3301 default_bugs.append(int(bug))
3302 except ValueError:
3303 others.append(bug)
3304
3305 if default_bugs:
3306 default_bugs = ','.join(map(str, default_bugs))
3307 if default_project:
3308 yield '%s:%s' % (default_project, default_bugs)
3309 else:
3310 yield default_bugs
3311 for other in sorted(others):
3312 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3313 yield other
3314
3315
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003316class ChangeDescription(object):
3317 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003318 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003319 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003320 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003321 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003322 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3323 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3324 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3325 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003326
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003327 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003328 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003329
agable@chromium.org42c20792013-09-12 17:34:49 +00003330 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003331 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003332 return '\n'.join(self._description_lines)
3333
3334 def set_description(self, desc):
3335 if isinstance(desc, basestring):
3336 lines = desc.splitlines()
3337 else:
3338 lines = [line.rstrip() for line in desc]
3339 while lines and not lines[0]:
3340 lines.pop(0)
3341 while lines and not lines[-1]:
3342 lines.pop(-1)
3343 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003344
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003345 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3346 """Rewrites the R=/TBR= line(s) as a single line each.
3347
3348 Args:
3349 reviewers (list(str)) - list of additional emails to use for reviewers.
3350 tbrs (list(str)) - list of additional emails to use for TBRs.
3351 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3352 the change that are missing OWNER coverage. If this is not None, you
3353 must also pass a value for `change`.
3354 change (Change) - The Change that should be used for OWNERS lookups.
3355 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003356 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003357 assert isinstance(tbrs, list), tbrs
3358
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003359 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003360 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003361
3362 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003363 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003364
3365 reviewers = set(reviewers)
3366 tbrs = set(tbrs)
3367 LOOKUP = {
3368 'TBR': tbrs,
3369 'R': reviewers,
3370 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003371
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003372 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003373 regexp = re.compile(self.R_LINE)
3374 matches = [regexp.match(line) for line in self._description_lines]
3375 new_desc = [l for i, l in enumerate(self._description_lines)
3376 if not matches[i]]
3377 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003378
agable@chromium.org42c20792013-09-12 17:34:49 +00003379 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003380
3381 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003382 for match in matches:
3383 if not match:
3384 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003385 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3386
3387 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003388 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003389 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003390 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003391 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003392 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003393 LOOKUP[add_owners_to].update(
3394 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003395
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003396 # If any folks ended up in both groups, remove them from tbrs.
3397 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003398
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003399 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3400 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003401
3402 # Put the new lines in the description where the old first R= line was.
3403 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3404 if 0 <= line_loc < len(self._description_lines):
3405 if new_tbr_line:
3406 self._description_lines.insert(line_loc, new_tbr_line)
3407 if new_r_line:
3408 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003409 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003410 if new_r_line:
3411 self.append_footer(new_r_line)
3412 if new_tbr_line:
3413 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003414
Aaron Gable3a16ed12017-03-23 10:51:55 -07003415 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003416 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003417 self.set_description([
3418 '# Enter a description of the change.',
3419 '# This will be displayed on the codereview site.',
3420 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003421 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003422 '--------------------',
3423 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003424
agable@chromium.org42c20792013-09-12 17:34:49 +00003425 regexp = re.compile(self.BUG_LINE)
3426 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003427 prefix = settings.GetBugPrefix()
3428 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003429 if git_footer:
3430 self.append_footer('Bug: %s' % ', '.join(values))
3431 else:
3432 for value in values:
3433 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003434
agable@chromium.org42c20792013-09-12 17:34:49 +00003435 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003436 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003437 if not content:
3438 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003439 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003440
Bruce Dawson2377b012018-01-11 16:46:49 -08003441 # Strip off comments and default inserted "Bug:" line.
3442 clean_lines = [line.rstrip() for line in lines if not
3443 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003444 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003445 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003446 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003447
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003448 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003449 """Adds a footer line to the description.
3450
3451 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3452 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3453 that Gerrit footers are always at the end.
3454 """
3455 parsed_footer_line = git_footers.parse_footer(line)
3456 if parsed_footer_line:
3457 # Line is a gerrit footer in the form: Footer-Key: any value.
3458 # Thus, must be appended observing Gerrit footer rules.
3459 self.set_description(
3460 git_footers.add_footer(self.description,
3461 key=parsed_footer_line[0],
3462 value=parsed_footer_line[1]))
3463 return
3464
3465 if not self._description_lines:
3466 self._description_lines.append(line)
3467 return
3468
3469 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3470 if gerrit_footers:
3471 # git_footers.split_footers ensures that there is an empty line before
3472 # actual (gerrit) footers, if any. We have to keep it that way.
3473 assert top_lines and top_lines[-1] == ''
3474 top_lines, separator = top_lines[:-1], top_lines[-1:]
3475 else:
3476 separator = [] # No need for separator if there are no gerrit_footers.
3477
3478 prev_line = top_lines[-1] if top_lines else ''
3479 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3480 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3481 top_lines.append('')
3482 top_lines.append(line)
3483 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003484
tandrii99a72f22016-08-17 14:33:24 -07003485 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003486 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003487 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003488 reviewers = [match.group(2).strip()
3489 for match in matches
3490 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003491 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003492
bradnelsond975b302016-10-23 12:20:23 -07003493 def get_cced(self):
3494 """Retrieves the list of reviewers."""
3495 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3496 cced = [match.group(2).strip() for match in matches if match]
3497 return cleanup_list(cced)
3498
Nodir Turakulov23b82142017-11-16 11:04:25 -08003499 def get_hash_tags(self):
3500 """Extracts and sanitizes a list of Gerrit hashtags."""
3501 subject = (self._description_lines or ('',))[0]
3502 subject = re.sub(
3503 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3504
3505 tags = []
3506 start = 0
3507 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3508 while True:
3509 m = bracket_exp.match(subject, start)
3510 if not m:
3511 break
3512 tags.append(self.sanitize_hash_tag(m.group(1)))
3513 start = m.end()
3514
3515 if not tags:
3516 # Try "Tag: " prefix.
3517 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3518 if m:
3519 tags.append(self.sanitize_hash_tag(m.group(1)))
3520 return tags
3521
3522 @classmethod
3523 def sanitize_hash_tag(cls, tag):
3524 """Returns a sanitized Gerrit hash tag.
3525
3526 A sanitized hashtag can be used as a git push refspec parameter value.
3527 """
3528 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3529
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003530 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3531 """Updates this commit description given the parent.
3532
3533 This is essentially what Gnumbd used to do.
3534 Consult https://goo.gl/WMmpDe for more details.
3535 """
3536 assert parent_msg # No, orphan branch creation isn't supported.
3537 assert parent_hash
3538 assert dest_ref
3539 parent_footer_map = git_footers.parse_footers(parent_msg)
3540 # This will also happily parse svn-position, which GnumbD is no longer
3541 # supporting. While we'd generate correct footers, the verifier plugin
3542 # installed in Gerrit will block such commit (ie git push below will fail).
3543 parent_position = git_footers.get_position(parent_footer_map)
3544
3545 # Cherry-picks may have last line obscuring their prior footers,
3546 # from git_footers perspective. This is also what Gnumbd did.
3547 cp_line = None
3548 if (self._description_lines and
3549 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3550 cp_line = self._description_lines.pop()
3551
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003552 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003553
3554 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3555 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003556 for i, line in enumerate(footer_lines):
3557 k, v = git_footers.parse_footer(line) or (None, None)
3558 if k and k.startswith('Cr-'):
3559 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003560
3561 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003562 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003563 if parent_position[0] == dest_ref:
3564 # Same branch as parent.
3565 number = int(parent_position[1]) + 1
3566 else:
3567 number = 1 # New branch, and extra lineage.
3568 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3569 int(parent_position[1])))
3570
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003571 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3572 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003573
3574 self._description_lines = top_lines
3575 if cp_line:
3576 self._description_lines.append(cp_line)
3577 if self._description_lines[-1] != '':
3578 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003579 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003580
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003581
Aaron Gablea1bab272017-04-11 16:38:18 -07003582def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003583 """Retrieves the reviewers that approved a CL from the issue properties with
3584 messages.
3585
3586 Note that the list may contain reviewers that are not committer, thus are not
3587 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003588
3589 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003590 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003591 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003592 return sorted(
3593 set(
3594 message['sender']
3595 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003596 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003597 )
3598 )
3599
3600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003601def FindCodereviewSettingsFile(filename='codereview.settings'):
3602 """Finds the given file starting in the cwd and going up.
3603
3604 Only looks up to the top of the repository unless an
3605 'inherit-review-settings-ok' file exists in the root of the repository.
3606 """
3607 inherit_ok_file = 'inherit-review-settings-ok'
3608 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003609 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003610 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3611 root = '/'
3612 while True:
3613 if filename in os.listdir(cwd):
3614 if os.path.isfile(os.path.join(cwd, filename)):
3615 return open(os.path.join(cwd, filename))
3616 if cwd == root:
3617 break
3618 cwd = os.path.dirname(cwd)
3619
3620
3621def LoadCodereviewSettingsFromFile(fileobj):
3622 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003623 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003624
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003625 def SetProperty(name, setting, unset_error_ok=False):
3626 fullname = 'rietveld.' + name
3627 if setting in keyvals:
3628 RunGit(['config', fullname, keyvals[setting]])
3629 else:
3630 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3631
tandrii48df5812016-10-17 03:55:37 -07003632 if not keyvals.get('GERRIT_HOST', False):
3633 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003634 # Only server setting is required. Other settings can be absent.
3635 # In that case, we ignore errors raised during option deletion attempt.
3636 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003637 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003638 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3639 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003640 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003641 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3642 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003643 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003644 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3645 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003646
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003647 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003648 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003649
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003650 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003651 RunGit(['config', 'gerrit.squash-uploads',
3652 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003653
tandrii@chromium.org28253532016-04-14 13:46:56 +00003654 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003655 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003656 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3657
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003658 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003659 # should be of the form
3660 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3661 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3663 keyvals['ORIGIN_URL_CONFIG']])
3664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003666def urlretrieve(source, destination):
3667 """urllib is broken for SSL connections via a proxy therefore we
3668 can't use urllib.urlretrieve()."""
3669 with open(destination, 'w') as f:
3670 f.write(urllib2.urlopen(source).read())
3671
3672
ukai@chromium.org712d6102013-11-27 00:52:58 +00003673def hasSheBang(fname):
3674 """Checks fname is a #! script."""
3675 with open(fname) as f:
3676 return f.read(2).startswith('#!')
3677
3678
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003679# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3680def DownloadHooks(*args, **kwargs):
3681 pass
3682
3683
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003684def DownloadGerritHook(force):
3685 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003686
3687 Args:
3688 force: True to update hooks. False to install hooks if not present.
3689 """
3690 if not settings.GetIsGerrit():
3691 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003692 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003693 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3694 if not os.access(dst, os.X_OK):
3695 if os.path.exists(dst):
3696 if not force:
3697 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003698 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003699 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003700 if not hasSheBang(dst):
3701 DieWithError('Not a script: %s\n'
3702 'You need to download from\n%s\n'
3703 'into .git/hooks/commit-msg and '
3704 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003705 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3706 except Exception:
3707 if os.path.exists(dst):
3708 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003709 DieWithError('\nFailed to download hooks.\n'
3710 'You need to download from\n%s\n'
3711 'into .git/hooks/commit-msg and '
3712 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003713
3714
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003715def GetRietveldCodereviewSettingsInteractively():
3716 """Prompt the user for settings."""
3717 server = settings.GetDefaultServerUrl(error_ok=True)
3718 prompt = 'Rietveld server (host[:port])'
3719 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3720 newserver = ask_for_data(prompt + ':')
3721 if not server and not newserver:
3722 newserver = DEFAULT_SERVER
3723 if newserver:
3724 newserver = gclient_utils.UpgradeToHttps(newserver)
3725 if newserver != server:
3726 RunGit(['config', 'rietveld.server', newserver])
3727
3728 def SetProperty(initial, caption, name, is_url):
3729 prompt = caption
3730 if initial:
3731 prompt += ' ("x" to clear) [%s]' % initial
3732 new_val = ask_for_data(prompt + ':')
3733 if new_val == 'x':
3734 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3735 elif new_val:
3736 if is_url:
3737 new_val = gclient_utils.UpgradeToHttps(new_val)
3738 if new_val != initial:
3739 RunGit(['config', 'rietveld.' + name, new_val])
3740
3741 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3742 SetProperty(settings.GetDefaultPrivateFlag(),
3743 'Private flag (rietveld only)', 'private', False)
3744 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3745 'tree-status-url', False)
3746 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3747 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3748 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3749 'run-post-upload-hook', False)
3750
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003751
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003752class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003753 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003754
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003755 _GOOGLESOURCE = 'googlesource.com'
3756
3757 def __init__(self):
3758 # Cached list of [host, identity, source], where source is either
3759 # .gitcookies or .netrc.
3760 self._all_hosts = None
3761
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003762 def ensure_configured_gitcookies(self):
3763 """Runs checks and suggests fixes to make git use .gitcookies from default
3764 path."""
3765 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3766 configured_path = RunGitSilent(
3767 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003768 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003769 if configured_path:
3770 self._ensure_default_gitcookies_path(configured_path, default)
3771 else:
3772 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003773
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003774 @staticmethod
3775 def _ensure_default_gitcookies_path(configured_path, default_path):
3776 assert configured_path
3777 if configured_path == default_path:
3778 print('git is already configured to use your .gitcookies from %s' %
3779 configured_path)
3780 return
3781
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003782 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003783 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3784 (configured_path, default_path))
3785
3786 if not os.path.exists(configured_path):
3787 print('However, your configured .gitcookies file is missing.')
3788 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3789 action='reconfigure')
3790 RunGit(['config', '--global', 'http.cookiefile', default_path])
3791 return
3792
3793 if os.path.exists(default_path):
3794 print('WARNING: default .gitcookies file already exists %s' %
3795 default_path)
3796 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3797 default_path)
3798
3799 confirm_or_exit('Move existing .gitcookies to default location?',
3800 action='move')
3801 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003802 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003803 print('Moved and reconfigured git to use .gitcookies from %s' %
3804 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003805
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003806 @staticmethod
3807 def _configure_gitcookies_path(default_path):
3808 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3809 if os.path.exists(netrc_path):
3810 print('You seem to be using outdated .netrc for git credentials: %s' %
3811 netrc_path)
3812 print('This tool will guide you through setting up recommended '
3813 '.gitcookies store for git credentials.\n'
3814 '\n'
3815 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3816 ' git config --global --unset http.cookiefile\n'
3817 ' mv %s %s.backup\n\n' % (default_path, default_path))
3818 confirm_or_exit(action='setup .gitcookies')
3819 RunGit(['config', '--global', 'http.cookiefile', default_path])
3820 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003821
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003822 def get_hosts_with_creds(self, include_netrc=False):
3823 if self._all_hosts is None:
3824 a = gerrit_util.CookiesAuthenticator()
3825 self._all_hosts = [
3826 (h, u, s)
3827 for h, u, s in itertools.chain(
3828 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3829 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3830 )
3831 if h.endswith(self._GOOGLESOURCE)
3832 ]
3833
3834 if include_netrc:
3835 return self._all_hosts
3836 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3837
3838 def print_current_creds(self, include_netrc=False):
3839 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3840 if not hosts:
3841 print('No Git/Gerrit credentials found')
3842 return
3843 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3844 header = [('Host', 'User', 'Which file'),
3845 ['=' * l for l in lengths]]
3846 for row in (header + hosts):
3847 print('\t'.join((('%%+%ds' % l) % s)
3848 for l, s in zip(lengths, row)))
3849
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003850 @staticmethod
3851 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003852 """Parses identity "git-<username>.domain" into <username> and domain."""
3853 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003854 # distinguishable from sub-domains. But we do know typical domains:
3855 if identity.endswith('.chromium.org'):
3856 domain = 'chromium.org'
3857 username = identity[:-len('.chromium.org')]
3858 else:
3859 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003860 if username.startswith('git-'):
3861 username = username[len('git-'):]
3862 return username, domain
3863
3864 def _get_usernames_of_domain(self, domain):
3865 """Returns list of usernames referenced by .gitcookies in a given domain."""
3866 identities_by_domain = {}
3867 for _, identity, _ in self.get_hosts_with_creds():
3868 username, domain = self._parse_identity(identity)
3869 identities_by_domain.setdefault(domain, []).append(username)
3870 return identities_by_domain.get(domain)
3871
3872 def _canonical_git_googlesource_host(self, host):
3873 """Normalizes Gerrit hosts (with '-review') to Git host."""
3874 assert host.endswith(self._GOOGLESOURCE)
3875 # Prefix doesn't include '.' at the end.
3876 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3877 if prefix.endswith('-review'):
3878 prefix = prefix[:-len('-review')]
3879 return prefix + '.' + self._GOOGLESOURCE
3880
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003881 def _canonical_gerrit_googlesource_host(self, host):
3882 git_host = self._canonical_git_googlesource_host(host)
3883 prefix = git_host.split('.', 1)[0]
3884 return prefix + '-review.' + self._GOOGLESOURCE
3885
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003886 def _get_counterpart_host(self, host):
3887 assert host.endswith(self._GOOGLESOURCE)
3888 git = self._canonical_git_googlesource_host(host)
3889 gerrit = self._canonical_gerrit_googlesource_host(git)
3890 return git if gerrit == host else gerrit
3891
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003892 def has_generic_host(self):
3893 """Returns whether generic .googlesource.com has been configured.
3894
3895 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3896 """
3897 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3898 if host == '.' + self._GOOGLESOURCE:
3899 return True
3900 return False
3901
3902 def _get_git_gerrit_identity_pairs(self):
3903 """Returns map from canonic host to pair of identities (Git, Gerrit).
3904
3905 One of identities might be None, meaning not configured.
3906 """
3907 host_to_identity_pairs = {}
3908 for host, identity, _ in self.get_hosts_with_creds():
3909 canonical = self._canonical_git_googlesource_host(host)
3910 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3911 idx = 0 if canonical == host else 1
3912 pair[idx] = identity
3913 return host_to_identity_pairs
3914
3915 def get_partially_configured_hosts(self):
3916 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003917 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3918 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3919 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003920
3921 def get_conflicting_hosts(self):
3922 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003923 host
3924 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003925 if None not in (i1, i2) and i1 != i2)
3926
3927 def get_duplicated_hosts(self):
3928 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3929 return set(host for host, count in counters.iteritems() if count > 1)
3930
3931 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3932 'chromium.googlesource.com': 'chromium.org',
3933 'chrome-internal.googlesource.com': 'google.com',
3934 }
3935
3936 def get_hosts_with_wrong_identities(self):
3937 """Finds hosts which **likely** reference wrong identities.
3938
3939 Note: skips hosts which have conflicting identities for Git and Gerrit.
3940 """
3941 hosts = set()
3942 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3943 pair = self._get_git_gerrit_identity_pairs().get(host)
3944 if pair and pair[0] == pair[1]:
3945 _, domain = self._parse_identity(pair[0])
3946 if domain != expected:
3947 hosts.add(host)
3948 return hosts
3949
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003950 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003951 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003952 hosts = sorted(hosts)
3953 assert hosts
3954 if extra_column_func is None:
3955 extras = [''] * len(hosts)
3956 else:
3957 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003958 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3959 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003960 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003961 lines.append(tmpl % he)
3962 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003963
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003964 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003965 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003966 yield ('.googlesource.com wildcard record detected',
3967 ['Chrome Infrastructure team recommends to list full host names '
3968 'explicitly.'],
3969 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003970
3971 dups = self.get_duplicated_hosts()
3972 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003973 yield ('The following hosts were defined twice',
3974 self._format_hosts(dups),
3975 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003976
3977 partial = self.get_partially_configured_hosts()
3978 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003979 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
3980 'These hosts are missing',
3981 self._format_hosts(partial, lambda host: 'but %s defined' %
3982 self._get_counterpart_host(host)),
3983 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003984
3985 conflicting = self.get_conflicting_hosts()
3986 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003987 yield ('The following Git hosts have differing credentials from their '
3988 'Gerrit counterparts',
3989 self._format_hosts(conflicting, lambda host: '%s vs %s' %
3990 tuple(self._get_git_gerrit_identity_pairs()[host])),
3991 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003992
3993 wrong = self.get_hosts_with_wrong_identities()
3994 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003995 yield ('These hosts likely use wrong identity',
3996 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
3997 (self._get_git_gerrit_identity_pairs()[host][0],
3998 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
3999 wrong)
4000
4001 def find_and_report_problems(self):
4002 """Returns True if there was at least one problem, else False."""
4003 found = False
4004 bad_hosts = set()
4005 for title, sublines, hosts in self._find_problems():
4006 if not found:
4007 found = True
4008 print('\n\n.gitcookies problem report:\n')
4009 bad_hosts.update(hosts or [])
4010 print(' %s%s' % (title , (':' if sublines else '')))
4011 if sublines:
4012 print()
4013 print(' %s' % '\n '.join(sublines))
4014 print()
4015
4016 if bad_hosts:
4017 assert found
4018 print(' You can manually remove corresponding lines in your %s file and '
4019 'visit the following URLs with correct account to generate '
4020 'correct credential lines:\n' %
4021 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4022 print(' %s' % '\n '.join(sorted(set(
4023 gerrit_util.CookiesAuthenticator().get_new_password_url(
4024 self._canonical_git_googlesource_host(host))
4025 for host in bad_hosts
4026 ))))
4027 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004028
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004029
4030def CMDcreds_check(parser, args):
4031 """Checks credentials and suggests changes."""
4032 _, _ = parser.parse_args(args)
4033
4034 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004035 DieWithError(
4036 'This command is not designed for GCE, are you on a bot?\n'
4037 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004038
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004039 checker = _GitCookiesChecker()
4040 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004041
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004042 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004043 checker.print_current_creds(include_netrc=True)
4044
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004045 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004046 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004047 return 0
4048 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004049
4050
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004051@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004052def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004053 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004054
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004055 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004056 # TODO(tandrii): remove this once we switch to Gerrit.
4057 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004058 parser.add_option('--activate-update', action='store_true',
4059 help='activate auto-updating [rietveld] section in '
4060 '.git/config')
4061 parser.add_option('--deactivate-update', action='store_true',
4062 help='deactivate auto-updating [rietveld] section in '
4063 '.git/config')
4064 options, args = parser.parse_args(args)
4065
4066 if options.deactivate_update:
4067 RunGit(['config', 'rietveld.autoupdate', 'false'])
4068 return
4069
4070 if options.activate_update:
4071 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4072 return
4073
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004074 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004075 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004076 return 0
4077
4078 url = args[0]
4079 if not url.endswith('codereview.settings'):
4080 url = os.path.join(url, 'codereview.settings')
4081
4082 # Load code review settings and download hooks (if available).
4083 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4084 return 0
4085
4086
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004087def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004088 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004089 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4090 branch = ShortBranchName(branchref)
4091 _, args = parser.parse_args(args)
4092 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004093 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004094 return RunGit(['config', 'branch.%s.base-url' % branch],
4095 error_ok=False).strip()
4096 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004097 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004098 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4099 error_ok=False).strip()
4100
4101
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004102def color_for_status(status):
4103 """Maps a Changelist status to color, for CMDstatus and other tools."""
4104 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004105 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004106 'waiting': Fore.BLUE,
4107 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004108 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004109 'lgtm': Fore.GREEN,
4110 'commit': Fore.MAGENTA,
4111 'closed': Fore.CYAN,
4112 'error': Fore.WHITE,
4113 }.get(status, Fore.WHITE)
4114
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004115
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004116def get_cl_statuses(changes, fine_grained, max_processes=None):
4117 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004118
4119 If fine_grained is true, this will fetch CL statuses from the server.
4120 Otherwise, simply indicate if there's a matching url for the given branches.
4121
4122 If max_processes is specified, it is used as the maximum number of processes
4123 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4124 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004125
4126 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004127 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004128 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004129 upload.verbosity = 0
4130
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004131 if not changes:
4132 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004133
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004134 if not fine_grained:
4135 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004136 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004137 for cl in changes:
4138 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004139 return
4140
4141 # First, sort out authentication issues.
4142 logging.debug('ensuring credentials exist')
4143 for cl in changes:
4144 cl.EnsureAuthenticated(force=False, refresh=True)
4145
4146 def fetch(cl):
4147 try:
4148 return (cl, cl.GetStatus())
4149 except:
4150 # See http://crbug.com/629863.
4151 logging.exception('failed to fetch status for %s:', cl)
4152 raise
4153
4154 threads_count = len(changes)
4155 if max_processes:
4156 threads_count = max(1, min(threads_count, max_processes))
4157 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4158
4159 pool = ThreadPool(threads_count)
4160 fetched_cls = set()
4161 try:
4162 it = pool.imap_unordered(fetch, changes).__iter__()
4163 while True:
4164 try:
4165 cl, status = it.next(timeout=5)
4166 except multiprocessing.TimeoutError:
4167 break
4168 fetched_cls.add(cl)
4169 yield cl, status
4170 finally:
4171 pool.close()
4172
4173 # Add any branches that failed to fetch.
4174 for cl in set(changes) - fetched_cls:
4175 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004176
rmistry@google.com2dd99862015-06-22 12:22:18 +00004177
4178def upload_branch_deps(cl, args):
4179 """Uploads CLs of local branches that are dependents of the current branch.
4180
4181 If the local branch dependency tree looks like:
4182 test1 -> test2.1 -> test3.1
4183 -> test3.2
4184 -> test2.2 -> test3.3
4185
4186 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4187 run on the dependent branches in this order:
4188 test2.1, test3.1, test3.2, test2.2, test3.3
4189
4190 Note: This function does not rebase your local dependent branches. Use it when
4191 you make a change to the parent branch that will not conflict with its
4192 dependent branches, and you would like their dependencies updated in
4193 Rietveld.
4194 """
4195 if git_common.is_dirty_git_tree('upload-branch-deps'):
4196 return 1
4197
4198 root_branch = cl.GetBranch()
4199 if root_branch is None:
4200 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4201 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004202 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004203 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4204 'patchset dependencies without an uploaded CL.')
4205
4206 branches = RunGit(['for-each-ref',
4207 '--format=%(refname:short) %(upstream:short)',
4208 'refs/heads'])
4209 if not branches:
4210 print('No local branches found.')
4211 return 0
4212
4213 # Create a dictionary of all local branches to the branches that are dependent
4214 # on it.
4215 tracked_to_dependents = collections.defaultdict(list)
4216 for b in branches.splitlines():
4217 tokens = b.split()
4218 if len(tokens) == 2:
4219 branch_name, tracked = tokens
4220 tracked_to_dependents[tracked].append(branch_name)
4221
vapiera7fbd5a2016-06-16 09:17:49 -07004222 print()
4223 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004224 dependents = []
4225 def traverse_dependents_preorder(branch, padding=''):
4226 dependents_to_process = tracked_to_dependents.get(branch, [])
4227 padding += ' '
4228 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004229 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004230 dependents.append(dependent)
4231 traverse_dependents_preorder(dependent, padding)
4232 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004233 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004234
4235 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004236 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004237 return 0
4238
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004239 confirm_or_exit('This command will checkout all dependent branches and run '
4240 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004241
andybons@chromium.org962f9462016-02-03 20:00:42 +00004242 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004243 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004244 args.extend(['-t', 'Updated patchset dependency'])
4245
rmistry@google.com2dd99862015-06-22 12:22:18 +00004246 # Record all dependents that failed to upload.
4247 failures = {}
4248 # Go through all dependents, checkout the branch and upload.
4249 try:
4250 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004251 print()
4252 print('--------------------------------------')
4253 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004254 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004255 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004256 try:
4257 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004258 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004259 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004260 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004261 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004262 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004263 finally:
4264 # Swap back to the original root branch.
4265 RunGit(['checkout', '-q', root_branch])
4266
vapiera7fbd5a2016-06-16 09:17:49 -07004267 print()
4268 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004269 for dependent_branch in dependents:
4270 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004271 print(' %s : %s' % (dependent_branch, upload_status))
4272 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004273
4274 return 0
4275
4276
kmarshall3bff56b2016-06-06 18:31:47 -07004277def CMDarchive(parser, args):
4278 """Archives and deletes branches associated with closed changelists."""
4279 parser.add_option(
4280 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004281 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004282 parser.add_option(
4283 '-f', '--force', action='store_true',
4284 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004285 parser.add_option(
4286 '-d', '--dry-run', action='store_true',
4287 help='Skip the branch tagging and removal steps.')
4288 parser.add_option(
4289 '-t', '--notags', action='store_true',
4290 help='Do not tag archived branches. '
4291 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004292
4293 auth.add_auth_options(parser)
4294 options, args = parser.parse_args(args)
4295 if args:
4296 parser.error('Unsupported args: %s' % ' '.join(args))
4297 auth_config = auth.extract_auth_config_from_options(options)
4298
4299 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4300 if not branches:
4301 return 0
4302
vapiera7fbd5a2016-06-16 09:17:49 -07004303 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004304 changes = [Changelist(branchref=b, auth_config=auth_config)
4305 for b in branches.splitlines()]
4306 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4307 statuses = get_cl_statuses(changes,
4308 fine_grained=True,
4309 max_processes=options.maxjobs)
4310 proposal = [(cl.GetBranch(),
4311 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4312 for cl, status in statuses
4313 if status == 'closed']
4314 proposal.sort()
4315
4316 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004317 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004318 return 0
4319
4320 current_branch = GetCurrentBranch()
4321
vapiera7fbd5a2016-06-16 09:17:49 -07004322 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004323 if options.notags:
4324 for next_item in proposal:
4325 print(' ' + next_item[0])
4326 else:
4327 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4328 for next_item in proposal:
4329 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004330
kmarshall9249e012016-08-23 12:02:16 -07004331 # Quit now on precondition failure or if instructed by the user, either
4332 # via an interactive prompt or by command line flags.
4333 if options.dry_run:
4334 print('\nNo changes were made (dry run).\n')
4335 return 0
4336 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004337 print('You are currently on a branch \'%s\' which is associated with a '
4338 'closed codereview issue, so archive cannot proceed. Please '
4339 'checkout another branch and run this command again.' %
4340 current_branch)
4341 return 1
kmarshall9249e012016-08-23 12:02:16 -07004342 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004343 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4344 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004345 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004346 return 1
4347
4348 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004349 if not options.notags:
4350 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004351 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004352
vapiera7fbd5a2016-06-16 09:17:49 -07004353 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004354
4355 return 0
4356
4357
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004358def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004359 """Show status of changelists.
4360
4361 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004362 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004363 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004364 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004365 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004366 - Magenta in the commit queue
4367 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004368 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004369
4370 Also see 'git cl comments'.
4371 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004372 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004373 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004374 parser.add_option('-f', '--fast', action='store_true',
4375 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004376 parser.add_option(
4377 '-j', '--maxjobs', action='store', type=int,
4378 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004379
4380 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004381 _add_codereview_issue_select_options(
4382 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004383 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004384 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004385 if args:
4386 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004387 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004388
iannuccie53c9352016-08-17 14:40:40 -07004389 if options.issue is not None and not options.field:
4390 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004391
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004392 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004393 cl = Changelist(auth_config=auth_config, issue=options.issue,
4394 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004395 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004396 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004397 elif options.field == 'id':
4398 issueid = cl.GetIssue()
4399 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004400 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004401 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004402 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004403 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004404 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004405 elif options.field == 'status':
4406 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004407 elif options.field == 'url':
4408 url = cl.GetIssueURL()
4409 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004410 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004411 return 0
4412
4413 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4414 if not branches:
4415 print('No local branch found.')
4416 return 0
4417
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004418 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004419 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004420 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004421 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004422 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004423 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004424 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004425
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004426 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004427 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4428 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4429 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004430 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004431 c, status = output.next()
4432 branch_statuses[c.GetBranch()] = status
4433 status = branch_statuses.pop(branch)
4434 url = cl.GetIssueURL()
4435 if url and (not status or status == 'error'):
4436 # The issue probably doesn't exist anymore.
4437 url += ' (broken)'
4438
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004439 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004440 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004441 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004442 color = ''
4443 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004444 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004445 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004446 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004447 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004448
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004449
4450 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004451 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004452 print('Current branch: %s' % branch)
4453 for cl in changes:
4454 if cl.GetBranch() == branch:
4455 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004456 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004457 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004458 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004459 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004460 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004461 print('Issue description:')
4462 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004463 return 0
4464
4465
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004466def colorize_CMDstatus_doc():
4467 """To be called once in main() to add colors to git cl status help."""
4468 colors = [i for i in dir(Fore) if i[0].isupper()]
4469
4470 def colorize_line(line):
4471 for color in colors:
4472 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004473 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004474 indent = len(line) - len(line.lstrip(' ')) + 1
4475 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4476 return line
4477
4478 lines = CMDstatus.__doc__.splitlines()
4479 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4480
4481
phajdan.jre328cf92016-08-22 04:12:17 -07004482def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004483 if path == '-':
4484 json.dump(contents, sys.stdout)
4485 else:
4486 with open(path, 'w') as f:
4487 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004488
4489
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004490@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004491def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004492 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004493
4494 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004495 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004496 parser.add_option('-r', '--reverse', action='store_true',
4497 help='Lookup the branch(es) for the specified issues. If '
4498 'no issues are specified, all branches with mapped '
4499 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004500 parser.add_option('--json',
4501 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004502 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004503 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004504 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004505
dnj@chromium.org406c4402015-03-03 17:22:28 +00004506 if options.reverse:
4507 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004508 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004509 # Reverse issue lookup.
4510 issue_branch_map = {}
4511 for branch in branches:
4512 cl = Changelist(branchref=branch)
4513 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4514 if not args:
4515 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004516 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004517 for issue in args:
4518 if not issue:
4519 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004520 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004521 print('Branch for issue number %s: %s' % (
4522 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004523 if options.json:
4524 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004525 return 0
4526
4527 if len(args) > 0:
4528 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4529 if not issue.valid:
4530 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4531 'or no argument to list it.\n'
4532 'Maybe you want to run git cl status?')
4533 cl = Changelist(codereview=issue.codereview)
4534 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004535 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004536 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004537 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4538 if options.json:
4539 write_json(options.json, {
4540 'issue': cl.GetIssue(),
4541 'issue_url': cl.GetIssueURL(),
4542 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004543 return 0
4544
4545
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004546def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004547 """Shows or posts review comments for any changelist."""
4548 parser.add_option('-a', '--add-comment', dest='comment',
4549 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004550 parser.add_option('-i', '--issue', dest='issue',
4551 help='review issue id (defaults to current issue). '
4552 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004553 parser.add_option('-m', '--machine-readable', dest='readable',
4554 action='store_false', default=True,
4555 help='output comments in a format compatible with '
4556 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004557 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004558 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004559 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004560 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004561 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004562 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004563 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004564
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004565 issue = None
4566 if options.issue:
4567 try:
4568 issue = int(options.issue)
4569 except ValueError:
4570 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004571 if not options.forced_codereview:
4572 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004573
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004574 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004575 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004576 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004577
4578 if options.comment:
4579 cl.AddComment(options.comment)
4580 return 0
4581
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004582 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4583 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004584 for comment in summary:
4585 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004586 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004587 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004588 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004589 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004590 color = Fore.MAGENTA
4591 else:
4592 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004593 print('\n%s%s %s%s\n%s' % (
4594 color,
4595 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4596 comment.sender,
4597 Fore.RESET,
4598 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4599
smut@google.comc85ac942015-09-15 16:34:43 +00004600 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004601 def pre_serialize(c):
4602 dct = c.__dict__.copy()
4603 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4604 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004605 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004606 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004607 return 0
4608
4609
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004610@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004611def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004612 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004613 parser.add_option('-d', '--display', action='store_true',
4614 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004615 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004616 help='New description to set for this issue (- for stdin, '
4617 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004618 parser.add_option('-f', '--force', action='store_true',
4619 help='Delete any unpublished Gerrit edits for this issue '
4620 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004621
4622 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004623 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004624 options, args = parser.parse_args(args)
4625 _process_codereview_select_options(parser, options)
4626
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004627 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004628 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004629 target_issue_arg = ParseIssueNumberArgument(args[0],
4630 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004631 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004632 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004633
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004634 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004635
martiniss6eda05f2016-06-30 10:18:35 -07004636 kwargs = {
4637 'auth_config': auth_config,
4638 'codereview': options.forced_codereview,
4639 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004640 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004641 if target_issue_arg:
4642 kwargs['issue'] = target_issue_arg.issue
4643 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004644 if target_issue_arg.codereview and not options.forced_codereview:
4645 detected_codereview_from_url = True
4646 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004647
4648 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004649 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004650 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004651 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004652
4653 if detected_codereview_from_url:
4654 logging.info('canonical issue/change URL: %s (type: %s)\n',
4655 cl.GetIssueURL(), target_issue_arg.codereview)
4656
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004657 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004658
smut@google.com34fb6b12015-07-13 20:03:26 +00004659 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004660 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004661 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004662
4663 if options.new_description:
4664 text = options.new_description
4665 if text == '-':
4666 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004667 elif text == '+':
4668 base_branch = cl.GetCommonAncestorWithUpstream()
4669 change = cl.GetChange(base_branch, None, local_description=True)
4670 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004671
4672 description.set_description(text)
4673 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004674 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004675
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004676 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004677 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004678 return 0
4679
4680
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004681def CreateDescriptionFromLog(args):
4682 """Pulls out the commit log to use as a base for the CL description."""
4683 log_args = []
4684 if len(args) == 1 and not args[0].endswith('.'):
4685 log_args = [args[0] + '..']
4686 elif len(args) == 1 and args[0].endswith('...'):
4687 log_args = [args[0][:-1]]
4688 elif len(args) == 2:
4689 log_args = [args[0] + '..' + args[1]]
4690 else:
4691 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004692 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004693
4694
thestig@chromium.org44202a22014-03-11 19:22:18 +00004695def CMDlint(parser, args):
4696 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004697 parser.add_option('--filter', action='append', metavar='-x,+y',
4698 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004699 auth.add_auth_options(parser)
4700 options, args = parser.parse_args(args)
4701 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004702
4703 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004704 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004705 try:
4706 import cpplint
4707 import cpplint_chromium
4708 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004709 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004710 return 1
4711
4712 # Change the current working directory before calling lint so that it
4713 # shows the correct base.
4714 previous_cwd = os.getcwd()
4715 os.chdir(settings.GetRoot())
4716 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004717 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004718 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4719 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004720 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004721 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004722 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004723
4724 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004725 command = args + files
4726 if options.filter:
4727 command = ['--filter=' + ','.join(options.filter)] + command
4728 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004729
4730 white_regex = re.compile(settings.GetLintRegex())
4731 black_regex = re.compile(settings.GetLintIgnoreRegex())
4732 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4733 for filename in filenames:
4734 if white_regex.match(filename):
4735 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004736 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004737 else:
4738 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4739 extra_check_functions)
4740 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004741 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004742 finally:
4743 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004744 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004745 if cpplint._cpplint_state.error_count != 0:
4746 return 1
4747 return 0
4748
4749
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004750def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004751 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004752 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004753 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004754 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004755 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004756 parser.add_option('--all', action='store_true',
4757 help='Run checks against all files, not just modified ones')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004758 auth.add_auth_options(parser)
4759 options, args = parser.parse_args(args)
4760 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004761
sbc@chromium.org71437c02015-04-09 19:29:40 +00004762 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004763 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004764 return 1
4765
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004766 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004767 if args:
4768 base_branch = args[0]
4769 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004770 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004771 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004772
Aaron Gable8076c282017-11-29 14:39:41 -08004773 if options.all:
4774 base_change = cl.GetChange(base_branch, None)
4775 files = [('M', f) for f in base_change.AllFiles()]
4776 change = presubmit_support.GitChange(
4777 base_change.Name(),
4778 base_change.FullDescriptionText(),
4779 base_change.RepositoryRoot(),
4780 files,
4781 base_change.issue,
4782 base_change.patchset,
4783 base_change.author_email,
4784 base_change._upstream)
4785 else:
4786 change = cl.GetChange(base_branch, None)
4787
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004788 cl.RunHook(
4789 committing=not options.upload,
4790 may_prompt=False,
4791 verbose=options.verbose,
Aaron Gable8076c282017-11-29 14:39:41 -08004792 change=change)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004793 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004794
4795
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004796def GenerateGerritChangeId(message):
4797 """Returns Ixxxxxx...xxx change id.
4798
4799 Works the same way as
4800 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4801 but can be called on demand on all platforms.
4802
4803 The basic idea is to generate git hash of a state of the tree, original commit
4804 message, author/committer info and timestamps.
4805 """
4806 lines = []
4807 tree_hash = RunGitSilent(['write-tree'])
4808 lines.append('tree %s' % tree_hash.strip())
4809 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4810 if code == 0:
4811 lines.append('parent %s' % parent.strip())
4812 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4813 lines.append('author %s' % author.strip())
4814 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4815 lines.append('committer %s' % committer.strip())
4816 lines.append('')
4817 # Note: Gerrit's commit-hook actually cleans message of some lines and
4818 # whitespace. This code is not doing this, but it clearly won't decrease
4819 # entropy.
4820 lines.append(message)
4821 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4822 stdin='\n'.join(lines))
4823 return 'I%s' % change_hash.strip()
4824
4825
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004826def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004827 """Computes the remote branch ref to use for the CL.
4828
4829 Args:
4830 remote (str): The git remote for the CL.
4831 remote_branch (str): The git remote branch for the CL.
4832 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004833 """
4834 if not (remote and remote_branch):
4835 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004836
wittman@chromium.org455dc922015-01-26 20:15:50 +00004837 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004838 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004839 # refs, which are then translated into the remote full symbolic refs
4840 # below.
4841 if '/' not in target_branch:
4842 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4843 else:
4844 prefix_replacements = (
4845 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4846 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4847 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4848 )
4849 match = None
4850 for regex, replacement in prefix_replacements:
4851 match = re.search(regex, target_branch)
4852 if match:
4853 remote_branch = target_branch.replace(match.group(0), replacement)
4854 break
4855 if not match:
4856 # This is a branch path but not one we recognize; use as-is.
4857 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004858 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4859 # Handle the refs that need to land in different refs.
4860 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004861
wittman@chromium.org455dc922015-01-26 20:15:50 +00004862 # Create the true path to the remote branch.
4863 # Does the following translation:
4864 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4865 # * refs/remotes/origin/master -> refs/heads/master
4866 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4867 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4868 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4869 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4870 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4871 'refs/heads/')
4872 elif remote_branch.startswith('refs/remotes/branch-heads'):
4873 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004874
wittman@chromium.org455dc922015-01-26 20:15:50 +00004875 return remote_branch
4876
4877
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004878def cleanup_list(l):
4879 """Fixes a list so that comma separated items are put as individual items.
4880
4881 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4882 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4883 """
4884 items = sum((i.split(',') for i in l), [])
4885 stripped_items = (i.strip() for i in items)
4886 return sorted(filter(None, stripped_items))
4887
4888
Aaron Gable4db38df2017-11-03 14:59:07 -07004889@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004890def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004891 """Uploads the current changelist to codereview.
4892
4893 Can skip dependency patchset uploads for a branch by running:
4894 git config branch.branch_name.skip-deps-uploads True
4895 To unset run:
4896 git config --unset branch.branch_name.skip-deps-uploads
4897 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004898
4899 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4900 a bug number, this bug number is automatically populated in the CL
4901 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004902
4903 If subject contains text in square brackets or has "<text>: " prefix, such
4904 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4905 [git-cl] add support for hashtags
4906 Foo bar: implement foo
4907 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004908 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004909 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4910 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004911 parser.add_option('--bypass-watchlists', action='store_true',
4912 dest='bypass_watchlists',
4913 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004914 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004915 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004916 parser.add_option('--message', '-m', dest='message',
4917 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004918 parser.add_option('-b', '--bug',
4919 help='pre-populate the bug number(s) for this issue. '
4920 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004921 parser.add_option('--message-file', dest='message_file',
4922 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004923 parser.add_option('--title', '-t', dest='title',
4924 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004925 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004926 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004927 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004928 parser.add_option('--tbrs',
4929 action='append', default=[],
4930 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004931 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004932 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004933 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004934 parser.add_option('--hashtag', dest='hashtags',
4935 action='append', default=[],
4936 help=('Gerrit hashtag for new CL; '
4937 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004938 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004939 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004940 parser.add_option('--emulate_svn_auto_props',
4941 '--emulate-svn-auto-props',
4942 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004943 dest="emulate_svn_auto_props",
4944 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004945 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004946 help='tell the commit queue to commit this patchset; '
4947 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004948 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004949 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004950 metavar='TARGET',
4951 help='Apply CL to remote ref TARGET. ' +
4952 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004953 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004954 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004955 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004956 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004957 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004958 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004959 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
4960 const='TBR', help='add a set of OWNERS to TBR')
4961 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
4962 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004963 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4964 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004965 help='Send the patchset to do a CQ dry run right after '
4966 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004967 parser.add_option('--dependencies', action='store_true',
4968 help='Uploads CLs of all the local branches that depend on '
4969 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004970
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004971 # TODO: remove Rietveld flags
4972 parser.add_option('--private', action='store_true',
4973 help='set the review private (rietveld only)')
4974 parser.add_option('--email', default=None,
4975 help='email address to use to connect to Rietveld')
4976
rmistry@google.com2dd99862015-06-22 12:22:18 +00004977 orig_args = args
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004978 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004979 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004980 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004981 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004982 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004983
sbc@chromium.org71437c02015-04-09 19:29:40 +00004984 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004985 return 1
4986
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004987 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004988 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004989 options.cc = cleanup_list(options.cc)
4990
tandriib80458a2016-06-23 12:20:07 -07004991 if options.message_file:
4992 if options.message:
4993 parser.error('only one of --message and --message-file allowed.')
4994 options.message = gclient_utils.FileRead(options.message_file)
4995 options.message_file = None
4996
tandrii4d0545a2016-07-06 03:56:49 -07004997 if options.cq_dry_run and options.use_commit_queue:
4998 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4999
Aaron Gableedbc4132017-09-11 13:22:28 -07005000 if options.use_commit_queue:
5001 options.send_mail = True
5002
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005003 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5004 settings.GetIsGerrit()
5005
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005006 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005007 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005008
5009
Francois Dorayd42c6812017-05-30 15:10:20 -04005010@subcommand.usage('--description=<description file>')
5011def CMDsplit(parser, args):
5012 """Splits a branch into smaller branches and uploads CLs.
5013
5014 Creates a branch and uploads a CL for each group of files modified in the
5015 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005016 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005017 the shared OWNERS file.
5018 """
5019 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005020 help="A text file containing a CL description in which "
5021 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005022 parser.add_option("-c", "--comment", dest="comment_file",
5023 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005024 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5025 default=False,
5026 help="List the files and reviewers for each CL that would "
5027 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005028 options, _ = parser.parse_args(args)
5029
5030 if not options.description_file:
5031 parser.error('No --description flag specified.')
5032
5033 def WrappedCMDupload(args):
5034 return CMDupload(OptionParser(), args)
5035
5036 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005037 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005038
5039
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005040@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005041def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005042 """DEPRECATED: Used to commit the current changelist via git-svn."""
5043 message = ('git-cl no longer supports committing to SVN repositories via '
5044 'git-svn. You probably want to use `git cl land` instead.')
5045 print(message)
5046 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005047
5048
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005049# Two special branches used by git cl land.
5050MERGE_BRANCH = 'git-cl-commit'
5051CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5052
5053
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005054@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005055def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005056 """Commits the current changelist via git.
5057
5058 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5059 upstream and closes the issue automatically and atomically.
5060
5061 Otherwise (in case of Rietveld):
5062 Squashes branch into a single commit.
5063 Updates commit message with metadata (e.g. pointer to review).
5064 Pushes the code upstream.
5065 Updates review and closes.
5066 """
5067 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5068 help='bypass upload presubmit hook')
5069 parser.add_option('-m', dest='message',
5070 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005071 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005072 help="force yes to questions (don't prompt)")
5073 parser.add_option('-c', dest='contributor',
5074 help="external contributor for patch (appended to " +
5075 "description and used as author for git). Should be " +
5076 "formatted as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005077 auth.add_auth_options(parser)
5078 (options, args) = parser.parse_args(args)
5079 auth_config = auth.extract_auth_config_from_options(options)
5080
5081 cl = Changelist(auth_config=auth_config)
5082
5083 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
5084 if cl.IsGerrit():
5085 if options.message:
5086 # This could be implemented, but it requires sending a new patch to
5087 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5088 # Besides, Gerrit has the ability to change the commit message on submit
5089 # automatically, thus there is no need to support this option (so far?).
5090 parser.error('-m MESSAGE option is not supported for Gerrit.')
5091 if options.contributor:
5092 parser.error(
5093 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5094 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5095 'the contributor\'s "name <email>". If you can\'t upload such a '
5096 'commit for review, contact your repository admin and request'
5097 '"Forge-Author" permission.')
5098 if not cl.GetIssue():
5099 DieWithError('You must upload the change first to Gerrit.\n'
5100 ' If you would rather have `git cl land` upload '
5101 'automatically for you, see http://crbug.com/642759')
5102 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
5103 options.verbose)
5104
5105 current = cl.GetBranch()
5106 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
5107 if remote == '.':
5108 print()
5109 print('Attempting to push branch %r into another local branch!' % current)
5110 print()
5111 print('Either reparent this branch on top of origin/master:')
5112 print(' git reparent-branch --root')
5113 print()
5114 print('OR run `git rebase-update` if you think the parent branch is ')
5115 print('already committed.')
5116 print()
5117 print(' Current parent: %r' % upstream_branch)
5118 return 1
5119
5120 if not args:
5121 # Default to merging against our best guess of the upstream branch.
5122 args = [cl.GetUpstreamBranch()]
5123
5124 if options.contributor:
5125 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005126 print("Please provide contributor as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005127 return 1
5128
5129 base_branch = args[0]
5130
5131 if git_common.is_dirty_git_tree('land'):
5132 return 1
5133
5134 # This rev-list syntax means "show all commits not in my branch that
5135 # are in base_branch".
5136 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
5137 base_branch]).splitlines()
5138 if upstream_commits:
5139 print('Base branch "%s" has %d commits '
5140 'not in this branch.' % (base_branch, len(upstream_commits)))
5141 print('Run "git merge %s" before attempting to land.' % base_branch)
5142 return 1
5143
5144 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
5145 if not options.bypass_hooks:
5146 author = None
5147 if options.contributor:
5148 author = re.search(r'\<(.*)\>', options.contributor).group(1)
5149 hook_results = cl.RunHook(
5150 committing=True,
5151 may_prompt=not options.force,
5152 verbose=options.verbose,
5153 change=cl.GetChange(merge_base, author))
5154 if not hook_results.should_continue():
5155 return 1
5156
5157 # Check the tree status if the tree status URL is set.
5158 status = GetTreeStatus()
5159 if 'closed' == status:
5160 print('The tree is closed. Please wait for it to reopen. Use '
5161 '"git cl land --bypass-hooks" to commit on a closed tree.')
5162 return 1
5163 elif 'unknown' == status:
5164 print('Unable to determine tree status. Please verify manually and '
5165 'use "git cl land --bypass-hooks" to commit on a closed tree.')
5166 return 1
5167
5168 change_desc = ChangeDescription(options.message)
5169 if not change_desc.description and cl.GetIssue():
5170 change_desc = ChangeDescription(cl.GetDescription())
5171
5172 if not change_desc.description:
5173 if not cl.GetIssue() and options.bypass_hooks:
5174 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
5175 else:
5176 print('No description set.')
5177 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
5178 return 1
5179
5180 # Keep a separate copy for the commit message, because the commit message
5181 # contains the link to the Rietveld issue, while the Rietveld message contains
5182 # the commit viewvc url.
5183 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07005184 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005185 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005186
5187 commit_desc = ChangeDescription(change_desc.description)
5188 if cl.GetIssue():
5189 # Xcode won't linkify this URL unless there is a non-whitespace character
5190 # after it. Add a period on a new line to circumvent this. Also add a space
5191 # before the period to make sure that Gitiles continues to correctly resolve
5192 # the URL.
5193 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
5194 if options.contributor:
5195 commit_desc.append_footer('Patch from %s.' % options.contributor)
5196
5197 print('Description:')
5198 print(commit_desc.description)
5199
5200 branches = [merge_base, cl.GetBranchRef()]
5201 if not options.force:
Aaron Gable13101a62018-02-09 13:20:41 -08005202 print_stats(branches)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005203
5204 # We want to squash all this branch's commits into one commit with the proper
5205 # description. We do this by doing a "reset --soft" to the base branch (which
5206 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005207 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005208 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
5209 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
5210 result = RunGitWithCode(showref_cmd)
5211 if result[0] == 0:
5212 RunGit(['branch', '-D', branch])
5213
5214 # We might be in a directory that's present in this branch but not in the
5215 # trunk. Move up to the top of the tree so that git commands that expect a
5216 # valid CWD won't fail after we check out the merge branch.
5217 rel_base_path = settings.GetRelativeRoot()
5218 if rel_base_path:
5219 os.chdir(rel_base_path)
5220
5221 # Stuff our change into the merge branch.
5222 # We wrap in a try...finally block so if anything goes wrong,
5223 # we clean up the branches.
5224 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005225 revision = None
5226 try:
5227 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5228 RunGit(['reset', '--soft', merge_base])
5229 if options.contributor:
5230 RunGit(
5231 [
5232 'commit', '--author', options.contributor,
5233 '-m', commit_desc.description,
5234 ])
5235 else:
5236 RunGit(['commit', '-m', commit_desc.description])
5237
5238 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5239 mirror = settings.GetGitMirror(remote)
5240 if mirror:
5241 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005242 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005243 else:
5244 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005245 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005246 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5247
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005248 retcode = PushToGitWithAutoRebase(
5249 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005250 if retcode == 0:
5251 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005252 if git_numberer_enabled:
5253 change_desc = ChangeDescription(
5254 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005255 except: # pylint: disable=bare-except
5256 if _IS_BEING_TESTED:
5257 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5258 + '-' * 30 + '8<' + '-' * 30)
5259 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5260 raise
5261 finally:
5262 # And then swap back to the original branch and clean up.
5263 RunGit(['checkout', '-q', cl.GetBranch()])
5264 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005265 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005266
5267 if not revision:
5268 print('Failed to push. If this persists, please file a bug.')
5269 return 1
5270
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005271 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005272 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005273 if viewvc_url and revision:
5274 change_desc.append_footer(
5275 'Committed: %s%s' % (viewvc_url, revision))
5276 elif revision:
5277 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005278 print('Closing issue '
5279 '(you may be prompted for your codereview password)...')
5280 cl.UpdateDescription(change_desc.description)
5281 cl.CloseIssue()
5282 props = cl.GetIssueProperties()
5283 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005284 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5285 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005286 if options.bypass_hooks:
5287 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5288 else:
5289 comment += ' (presubmit successful).'
5290 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5291
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005292 if os.path.isfile(POSTUPSTREAM_HOOK):
5293 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5294
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005295 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005296
5297
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005298def PushToGitWithAutoRebase(remote, branch, original_description,
5299 git_numberer_enabled, max_attempts=3):
5300 """Pushes current HEAD commit on top of remote's branch.
5301
5302 Attempts to fetch and autorebase on push failures.
5303 Adds git number footers on the fly.
5304
5305 Returns integer code from last command.
5306 """
5307 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5308 code = 0
5309 attempts_left = max_attempts
5310 while attempts_left:
5311 attempts_left -= 1
5312 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5313
5314 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5315 # If fetch fails, retry.
5316 print('Fetching %s/%s...' % (remote, branch))
5317 code, out = RunGitWithCode(
5318 ['retry', 'fetch', remote,
5319 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5320 if code:
5321 print('Fetch failed with exit code %d.' % code)
5322 print(out.strip())
5323 continue
5324
5325 print('Cherry-picking commit on top of latest %s' % branch)
5326 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5327 suppress_stderr=True)
5328 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5329 code, out = RunGitWithCode(['cherry-pick', cherry])
5330 if code:
5331 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5332 'the following files have merge conflicts:' %
5333 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005334 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5335 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005336 print('Please rebase your patch and try again.')
5337 RunGitWithCode(['cherry-pick', '--abort'])
5338 break
5339
5340 commit_desc = ChangeDescription(original_description)
5341 if git_numberer_enabled:
5342 logging.debug('Adding git number footers')
5343 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5344 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5345 branch)
5346 # Ensure timestamps are monotonically increasing.
5347 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5348 _get_committer_timestamp('HEAD'))
5349 _git_amend_head(commit_desc.description, timestamp)
5350
5351 code, out = RunGitWithCode(
5352 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5353 print(out)
5354 if code == 0:
5355 break
5356 if IsFatalPushFailure(out):
5357 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005358 'user.email are correct and you have push access to the repo.\n'
5359 'Hint: run command below to diangose common Git/Gerrit credential '
5360 'problems:\n'
5361 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005362 break
5363 return code
5364
5365
5366def IsFatalPushFailure(push_stdout):
5367 """True if retrying push won't help."""
5368 return '(prohibited by Gerrit)' in push_stdout
5369
5370
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005371@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005372def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005373 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005374 parser.add_option('-b', dest='newbranch',
5375 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005376 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005377 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005378 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005379 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005380 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005381 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005382 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005383 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005384 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005385 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005386
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005387
5388 group = optparse.OptionGroup(
5389 parser,
5390 'Options for continuing work on the current issue uploaded from a '
5391 'different clone (e.g. different machine). Must be used independently '
5392 'from the other options. No issue number should be specified, and the '
5393 'branch must have an issue number associated with it')
5394 group.add_option('--reapply', action='store_true', dest='reapply',
5395 help='Reset the branch and reapply the issue.\n'
5396 'CAUTION: This will undo any local changes in this '
5397 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005398
5399 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005400 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005401 parser.add_option_group(group)
5402
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005403 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005404 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005405 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005406 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005407 auth_config = auth.extract_auth_config_from_options(options)
5408
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005409 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005410 if options.newbranch:
5411 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005412 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005413 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005414
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005415 cl = Changelist(auth_config=auth_config,
5416 codereview=options.forced_codereview)
5417 if not cl.GetIssue():
5418 parser.error('current branch must have an associated issue')
5419
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005420 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005421 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005422 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005423
5424 RunGit(['reset', '--hard', upstream])
5425 if options.pull:
5426 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005427
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005428 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5429 options.directory)
5430
5431 if len(args) != 1 or not args[0]:
5432 parser.error('Must specify issue number or url')
5433
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005434 target_issue_arg = ParseIssueNumberArgument(args[0],
5435 options.forced_codereview)
5436 if not target_issue_arg.valid:
5437 parser.error('invalid codereview url or CL id')
5438
5439 cl_kwargs = {
5440 'auth_config': auth_config,
5441 'codereview_host': target_issue_arg.hostname,
5442 'codereview': options.forced_codereview,
5443 }
5444 detected_codereview_from_url = False
5445 if target_issue_arg.codereview and not options.forced_codereview:
5446 detected_codereview_from_url = True
5447 cl_kwargs['codereview'] = target_issue_arg.codereview
5448 cl_kwargs['issue'] = target_issue_arg.issue
5449
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005450 # We don't want uncommitted changes mixed up with the patch.
5451 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005452 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005453
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005454 if options.newbranch:
5455 if options.force:
5456 RunGit(['branch', '-D', options.newbranch],
5457 stderr=subprocess2.PIPE, error_ok=True)
5458 RunGit(['new-branch', options.newbranch])
5459
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005460 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005461
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005462 if cl.IsGerrit():
5463 if options.reject:
5464 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005465 if options.directory:
5466 parser.error('--directory is not supported with Gerrit codereview.')
5467
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005468 if detected_codereview_from_url:
5469 print('canonical issue/change URL: %s (type: %s)\n' %
5470 (cl.GetIssueURL(), target_issue_arg.codereview))
5471
5472 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005473 options.nocommit, options.directory,
5474 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005475
5476
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005477def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005478 """Fetches the tree status and returns either 'open', 'closed',
5479 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005480 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005481 if url:
5482 status = urllib2.urlopen(url).read().lower()
5483 if status.find('closed') != -1 or status == '0':
5484 return 'closed'
5485 elif status.find('open') != -1 or status == '1':
5486 return 'open'
5487 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005488 return 'unset'
5489
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005490
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005491def GetTreeStatusReason():
5492 """Fetches the tree status from a json url and returns the message
5493 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005494 url = settings.GetTreeStatusUrl()
5495 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005496 connection = urllib2.urlopen(json_url)
5497 status = json.loads(connection.read())
5498 connection.close()
5499 return status['message']
5500
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005501
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005502def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005503 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005504 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005505 status = GetTreeStatus()
5506 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005507 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005508 return 2
5509
vapiera7fbd5a2016-06-16 09:17:49 -07005510 print('The tree is %s' % status)
5511 print()
5512 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005513 if status != 'open':
5514 return 1
5515 return 0
5516
5517
maruel@chromium.org15192402012-09-06 12:38:29 +00005518def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005519 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005520 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005521 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005522 '-b', '--bot', action='append',
5523 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5524 'times to specify multiple builders. ex: '
5525 '"-b win_rel -b win_layout". See '
5526 'the try server waterfall for the builders name and the tests '
5527 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005528 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005529 '-B', '--bucket', default='',
5530 help=('Buildbucket bucket to send the try requests.'))
5531 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005532 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005533 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005534 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005535 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005536 help='Revision to use for the try job; default: the revision will '
5537 'be determined by the try recipe that builder runs, which usually '
5538 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005539 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005540 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005541 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005542 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005543 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005544 '--project',
5545 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005546 'in recipe to determine to which repository or directory to '
5547 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005548 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005549 '-p', '--property', dest='properties', action='append', default=[],
5550 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005551 'key2=value2 etc. The value will be treated as '
5552 'json if decodable, or as string otherwise. '
5553 'NOTE: using this may make your try job not usable for CQ, '
5554 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005555 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005556 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5557 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005558 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005559 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005560 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005561 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005562 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005563 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005564
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005565 if options.master and options.master.startswith('luci.'):
5566 parser.error(
5567 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005568 # Make sure that all properties are prop=value pairs.
5569 bad_params = [x for x in options.properties if '=' not in x]
5570 if bad_params:
5571 parser.error('Got properties with missing "=": %s' % bad_params)
5572
maruel@chromium.org15192402012-09-06 12:38:29 +00005573 if args:
5574 parser.error('Unknown arguments: %s' % args)
5575
Koji Ishii31c14782018-01-08 17:17:33 +09005576 cl = Changelist(auth_config=auth_config, issue=options.issue,
5577 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005578 if not cl.GetIssue():
5579 parser.error('Need to upload first')
5580
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005581 if cl.IsGerrit():
5582 # HACK: warm up Gerrit change detail cache to save on RPCs.
5583 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5584
tandriie113dfd2016-10-11 10:20:12 -07005585 error_message = cl.CannotTriggerTryJobReason()
5586 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005587 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005588
borenet6c0efe62016-10-19 08:13:29 -07005589 if options.bucket and options.master:
5590 parser.error('Only one of --bucket and --master may be used.')
5591
qyearsley1fdfcb62016-10-24 13:22:03 -07005592 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005593
qyearsleydd49f942016-10-28 11:57:22 -07005594 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5595 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005596 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005597 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005598 print('git cl try with no bots now defaults to CQ dry run.')
5599 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5600 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005601
borenet6c0efe62016-10-19 08:13:29 -07005602 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005603 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005604 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005605 'of bot requires an initial job from a parent (usually a builder). '
5606 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005607 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005608 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005609
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005610 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005611 # TODO(tandrii): Checking local patchset against remote patchset is only
5612 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5613 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005614 print('Warning: Codereview server has newer patchsets (%s) than most '
5615 'recent upload from local checkout (%s). Did a previous upload '
5616 'fail?\n'
5617 'By default, git cl try uses the latest patchset from '
5618 'codereview, continuing to use patchset %s.\n' %
5619 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005620
tandrii568043b2016-10-11 07:49:18 -07005621 try:
borenet6c0efe62016-10-19 08:13:29 -07005622 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5623 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)