blob: 39a46367f9f08ac86411cb28f6411ccd80738e30 [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
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000297def add_git_similarity(parser):
298 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700299 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000300 help='Sets the percentage that a pair of files need to match in order to'
301 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000302 parser.add_option(
303 '--find-copies', action='store_true',
304 help='Allows git to look for copies.')
305 parser.add_option(
306 '--no-find-copies', action='store_false', dest='find_copies',
307 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000308
309 old_parser_args = parser.parse_args
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700310
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000311 def Parse(args):
312 options, args = old_parser_args(args)
313
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000314 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700315 options.similarity = _git_get_branch_config_value(
316 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000317 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000318 print('Note: Saving similarity of %d%% in git config.'
319 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700320 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000321
iannucci@chromium.org79540052012-10-19 23:15:26 +0000322 options.similarity = max(0, min(options.similarity, 100))
323
324 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700325 options.find_copies = _git_get_branch_config_value(
326 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000327 else:
tandrii5d48c322016-08-18 16:19:37 -0700328 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000329
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000330 return options, args
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700331
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000332 parser.parse_args = Parse
333
334
machenbach@chromium.org45453142015-09-15 08:45:22 +0000335def _get_properties_from_options(options):
336 properties = dict(x.split('=', 1) for x in options.properties)
337 for key, val in properties.iteritems():
338 try:
339 properties[key] = json.loads(val)
340 except ValueError:
341 pass # If a value couldn't be evaluated, treat it as a string.
342 return properties
343
344
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000345def _prefix_master(master):
346 """Convert user-specified master name to full master name.
347
348 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
349 name, while the developers always use shortened master name
350 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
351 function does the conversion for buildbucket migration.
352 """
borenet6c0efe62016-10-19 08:13:29 -0700353 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000354 return master
borenet6c0efe62016-10-19 08:13:29 -0700355 return '%s%s' % (MASTER_PREFIX, master)
356
357
358def _unprefix_master(bucket):
359 """Convert bucket name to shortened master name.
360
361 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
362 name, while the developers always use shortened master name
363 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
364 function does the conversion for buildbucket migration.
365 """
366 if bucket.startswith(MASTER_PREFIX):
367 return bucket[len(MASTER_PREFIX):]
368 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000369
370
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000371def _buildbucket_retry(operation_name, http, *args, **kwargs):
372 """Retries requests to buildbucket service and returns parsed json content."""
373 try_count = 0
374 while True:
375 response, content = http.request(*args, **kwargs)
376 try:
377 content_json = json.loads(content)
378 except ValueError:
379 content_json = None
380
381 # Buildbucket could return an error even if status==200.
382 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000383 error = content_json.get('error')
384 if error.get('code') == 403:
385 raise BuildbucketResponseException(
386 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000387 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000388 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000389 raise BuildbucketResponseException(msg)
390
391 if response.status == 200:
392 if not content_json:
393 raise BuildbucketResponseException(
394 'Buildbucket returns invalid json content: %s.\n'
395 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
396 content)
397 return content_json
398 if response.status < 500 or try_count >= 2:
399 raise httplib2.HttpLib2Error(content)
400
401 # status >= 500 means transient failures.
402 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700403 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000404 try_count += 1
405 assert False, 'unreachable'
406
407
qyearsley1fdfcb62016-10-24 13:22:03 -0700408def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700409 """Returns a dict mapping bucket names to builders and tests,
410 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700411 """
qyearsleydd49f942016-10-28 11:57:22 -0700412 # If no bots are listed, we try to get a set of builders and tests based
413 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700414 if not options.bot:
415 change = changelist.GetChange(
416 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700417 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700418 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700419 change=change,
420 changed_files=change.LocalPaths(),
421 repository_root=settings.GetRoot(),
422 default_presubmit=None,
423 project=None,
424 verbose=options.verbose,
425 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700426 if masters is None:
427 return None
Sergiy Byelozyorov935b93f2016-11-28 20:41:56 +0100428 return {_prefix_master(m): b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700429
qyearsley1fdfcb62016-10-24 13:22:03 -0700430 if options.bucket:
431 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700432 if options.master:
433 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700434
qyearsleydd49f942016-10-28 11:57:22 -0700435 # If bots are listed but no master or bucket, then we need to find out
436 # the corresponding master for each bot.
437 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
438 if error_message:
439 option_parser.error(
440 'Tryserver master cannot be found because: %s\n'
441 'Please manually specify the tryserver master, e.g. '
442 '"-m tryserver.chromium.linux".' % error_message)
443 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700444
445
qyearsley123a4682016-10-26 09:12:17 -0700446def _get_bucket_map_for_builders(builders):
447 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700448 map_url = 'https://builders-map.appspot.com/'
449 try:
qyearsley123a4682016-10-26 09:12:17 -0700450 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700451 except urllib2.URLError as e:
452 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
453 (map_url, e))
454 except ValueError as e:
455 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700456 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700457 return None, 'Failed to build master map.'
458
qyearsley123a4682016-10-26 09:12:17 -0700459 bucket_map = {}
460 for builder in builders:
Nodir Turakulov44e01ff2018-01-25 11:12:30 -0800461 builder_info = builders_map.get(builder, {})
462 if isinstance(builder_info, list):
463 # This is a list of masters, legacy mode.
464 # TODO(nodir): remove this code path.
465 buckets = map(_prefix_master, builder_info)
466 else:
467 buckets = builder_info.get('buckets') or []
468 if not buckets:
469 return None, ('No matching bucket for builder %s.' % builder)
470 if len(buckets) > 1:
471 return None, ('The builder name %s exists in multiple buckets %s.' %
472 (builder, buckets))
473 bucket_map.setdefault(buckets[0], {})[builder] = []
qyearsley123a4682016-10-26 09:12:17 -0700474
475 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700476
477
borenet6c0efe62016-10-19 08:13:29 -0700478def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700479 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700480 """Sends a request to Buildbucket to trigger try jobs for a changelist.
481
482 Args:
483 auth_config: AuthConfig for Rietveld.
484 changelist: Changelist that the try jobs are associated with.
485 buckets: A nested dict mapping bucket names to builders to tests.
486 options: Command-line options.
487 """
tandriide281ae2016-10-12 06:02:30 -0700488 assert changelist.GetIssue(), 'CL must be uploaded first'
489 codereview_url = changelist.GetCodereviewServer()
490 assert codereview_url, 'CL must be uploaded first'
491 patchset = patchset or changelist.GetMostRecentPatchset()
492 assert patchset, 'CL must be uploaded first'
493
494 codereview_host = urlparse.urlparse(codereview_url).hostname
495 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000496 http = authenticator.authorize(httplib2.Http())
497 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700498
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000499 buildbucket_put_url = (
500 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000501 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700502 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
503 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
504 hostname=codereview_host,
505 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000506 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700507
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700508 shared_parameters_properties = changelist.GetTryJobProperties(patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700509 shared_parameters_properties['category'] = category
510 if options.clobber:
511 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700512 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700513 if extra_properties:
514 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000515
516 batch_req_body = {'builds': []}
517 print_text = []
518 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700519 for bucket, builders_and_tests in sorted(buckets.iteritems()):
520 print_text.append('Bucket: %s' % bucket)
521 master = None
522 if bucket.startswith(MASTER_PREFIX):
523 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000524 for builder, tests in sorted(builders_and_tests.iteritems()):
525 print_text.append(' %s: %s' % (builder, tests))
526 parameters = {
527 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000528 'changes': [{
Andrii Shyshkaloveadad922017-01-26 09:38:30 +0100529 'author': {'email': changelist.GetIssueOwner()},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000530 'revision': options.revision,
531 }],
tandrii8c5a3532016-11-04 07:52:02 -0700532 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000533 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000534 if 'presubmit' in builder.lower():
535 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000536 if tests:
537 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700538
539 tags = [
540 'builder:%s' % builder,
541 'buildset:%s' % buildset,
542 'user_agent:git_cl_try',
543 ]
544 if master:
545 parameters['properties']['master'] = master
546 tags.append('master:%s' % master)
547
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000548 batch_req_body['builds'].append(
549 {
550 'bucket': bucket,
551 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000552 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700553 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000554 }
555 )
556
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000557 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700558 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000559 http,
560 buildbucket_put_url,
561 'PUT',
562 body=json.dumps(batch_req_body),
563 headers={'Content-Type': 'application/json'}
564 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000565 print_text.append('To see results here, run: git cl try-results')
566 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700567 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000568
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000569
tandrii221ab252016-10-06 08:12:04 -0700570def fetch_try_jobs(auth_config, changelist, buildbucket_host,
571 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700572 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000573
qyearsley53f48a12016-09-01 10:45:13 -0700574 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000575 """
tandrii221ab252016-10-06 08:12:04 -0700576 assert buildbucket_host
577 assert changelist.GetIssue(), 'CL must be uploaded first'
578 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
579 patchset = patchset or changelist.GetMostRecentPatchset()
580 assert patchset, 'CL must be uploaded first'
581
582 codereview_url = changelist.GetCodereviewServer()
583 codereview_host = urlparse.urlparse(codereview_url).hostname
584 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000585 if authenticator.has_cached_credentials():
586 http = authenticator.authorize(httplib2.Http())
587 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700588 print('Warning: Some results might be missing because %s' %
589 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700590 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000591 http = httplib2.Http()
592
593 http.force_exception_to_status_code = True
594
tandrii221ab252016-10-06 08:12:04 -0700595 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
596 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
597 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000598 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700599 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000600 params = {'tag': 'buildset:%s' % buildset}
601
602 builds = {}
603 while True:
604 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700605 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000606 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700607 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000608 for build in content.get('builds', []):
609 builds[build['id']] = build
610 if 'next_cursor' in content:
611 params['start_cursor'] = content['next_cursor']
612 else:
613 break
614 return builds
615
616
qyearsleyeab3c042016-08-24 09:18:28 -0700617def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000618 """Prints nicely result of fetch_try_jobs."""
619 if not builds:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700620 print('No try jobs scheduled.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000621 return
622
623 # Make a copy, because we'll be modifying builds dictionary.
624 builds = builds.copy()
625 builder_names_cache = {}
626
627 def get_builder(b):
628 try:
629 return builder_names_cache[b['id']]
630 except KeyError:
631 try:
632 parameters = json.loads(b['parameters_json'])
633 name = parameters['builder_name']
634 except (ValueError, KeyError) as error:
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700635 print('WARNING: Failed to get builder name for build %s: %s' % (
vapiera7fbd5a2016-06-16 09:17:49 -0700636 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000637 name = None
638 builder_names_cache[b['id']] = name
639 return name
640
641 def get_bucket(b):
642 bucket = b['bucket']
643 if bucket.startswith('master.'):
644 return bucket[len('master.'):]
645 return bucket
646
647 if options.print_master:
648 name_fmt = '%%-%ds %%-%ds' % (
649 max(len(str(get_bucket(b))) for b in builds.itervalues()),
650 max(len(str(get_builder(b))) for b in builds.itervalues()))
651 def get_name(b):
652 return name_fmt % (get_bucket(b), get_builder(b))
653 else:
654 name_fmt = '%%-%ds' % (
655 max(len(str(get_builder(b))) for b in builds.itervalues()))
656 def get_name(b):
657 return name_fmt % get_builder(b)
658
659 def sort_key(b):
660 return b['status'], b.get('result'), get_name(b), b.get('url')
661
662 def pop(title, f, color=None, **kwargs):
663 """Pop matching builds from `builds` dict and print them."""
664
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000665 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000666 colorize = str
667 else:
668 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
669
670 result = []
671 for b in builds.values():
672 if all(b.get(k) == v for k, v in kwargs.iteritems()):
673 builds.pop(b['id'])
674 result.append(b)
675 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700676 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000677 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700678 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000679
680 total = len(builds)
681 pop(status='COMPLETED', result='SUCCESS',
682 title='Successes:', color=Fore.GREEN,
683 f=lambda b: (get_name(b), b.get('url')))
684 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
685 title='Infra Failures:', color=Fore.MAGENTA,
686 f=lambda b: (get_name(b), b.get('url')))
687 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
688 title='Failures:', color=Fore.RED,
689 f=lambda b: (get_name(b), b.get('url')))
690 pop(status='COMPLETED', result='CANCELED',
691 title='Canceled:', color=Fore.MAGENTA,
692 f=lambda b: (get_name(b),))
693 pop(status='COMPLETED', result='FAILURE',
694 failure_reason='INVALID_BUILD_DEFINITION',
695 title='Wrong master/builder name:', color=Fore.MAGENTA,
696 f=lambda b: (get_name(b),))
697 pop(status='COMPLETED', result='FAILURE',
698 title='Other failures:',
699 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
700 pop(status='COMPLETED',
701 title='Other finished:',
702 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
703 pop(status='STARTED',
704 title='Started:', color=Fore.YELLOW,
705 f=lambda b: (get_name(b), b.get('url')))
706 pop(status='SCHEDULED',
707 title='Scheduled:',
708 f=lambda b: (get_name(b), 'id=%s' % b['id']))
709 # The last section is just in case buildbucket API changes OR there is a bug.
710 pop(title='Other:',
711 f=lambda b: (get_name(b), 'id=%s' % b['id']))
712 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700713 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000714
715
qyearsley53f48a12016-09-01 10:45:13 -0700716def write_try_results_json(output_file, builds):
717 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
718
719 The input |builds| dict is assumed to be generated by Buildbucket.
720 Buildbucket documentation: http://goo.gl/G0s101
721 """
722
723 def convert_build_dict(build):
Quinten Yearsleya563d722017-12-11 16:36:54 -0800724 """Extracts some of the information from one build dict."""
725 parameters = json.loads(build.get('parameters_json', '{}')) or {}
qyearsley53f48a12016-09-01 10:45:13 -0700726 return {
727 'buildbucket_id': build.get('id'),
qyearsley53f48a12016-09-01 10:45:13 -0700728 'bucket': build.get('bucket'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800729 'builder_name': parameters.get('builder_name'),
730 'created_ts': build.get('created_ts'),
731 'experimental': build.get('experimental'),
qyearsley53f48a12016-09-01 10:45:13 -0700732 'failure_reason': build.get('failure_reason'),
Quinten Yearsleya563d722017-12-11 16:36:54 -0800733 'result': build.get('result'),
734 'status': build.get('status'),
735 'tags': build.get('tags'),
qyearsley53f48a12016-09-01 10:45:13 -0700736 'url': build.get('url'),
737 }
738
739 converted = []
740 for _, build in sorted(builds.items()):
741 converted.append(convert_build_dict(build))
742 write_json(output_file, converted)
743
744
iannucci@chromium.org79540052012-10-19 23:15:26 +0000745def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000746 """Prints statistics about the change to the user."""
747 # --no-ext-diff is broken in some versions of Git, so try to work around
748 # this by overriding the environment (but there is still a problem if the
749 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000750 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000751 if 'GIT_EXTERNAL_DIFF' in env:
752 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000753
754 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800755 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000756 else:
757 similarity_options = ['-M%s' % similarity]
758
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000759 try:
760 stdout = sys.stdout.fileno()
761 except AttributeError:
762 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000763 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000764 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000765 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000766 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000767
768
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000769class BuildbucketResponseException(Exception):
770 pass
771
772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773class Settings(object):
774 def __init__(self):
775 self.default_server = None
776 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000777 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000778 self.tree_status_url = None
779 self.viewvc_url = None
780 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000781 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000782 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000783 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000784 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000785 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000786 self.force_https_commit_url = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000787
788 def LazyUpdateIfNeeded(self):
789 """Updates the settings from a codereview.settings file, if available."""
790 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000791 # The only value that actually changes the behavior is
792 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000793 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000794 error_ok=True
795 ).strip().lower()
796
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000798 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799 LoadCodereviewSettingsFromFile(cr_settings_file)
800 self.updated = True
801
802 def GetDefaultServerUrl(self, error_ok=False):
803 if not self.default_server:
804 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000805 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000806 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000807 if error_ok:
808 return self.default_server
809 if not self.default_server:
810 error_message = ('Could not find settings file. You must configure '
811 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000812 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000813 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 return self.default_server
815
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000816 @staticmethod
817 def GetRelativeRoot():
818 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000819
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000820 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000821 if self.root is None:
822 self.root = os.path.abspath(self.GetRelativeRoot())
823 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000825 def GetGitMirror(self, remote='origin'):
826 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000827 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000828 if not os.path.isdir(local_url):
829 return None
830 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
831 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100832 # Use the /dev/null print_func to avoid terminal spew.
Andrii Shyshkalov18975322017-01-25 16:44:13 +0100833 mirror = git_cache.Mirror(remote_url, print_func=lambda *args: None)
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000834 if mirror.exists():
835 return mirror
836 return None
837
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000838 def GetTreeStatusUrl(self, error_ok=False):
839 if not self.tree_status_url:
840 error_message = ('You must configure your tree status URL by running '
841 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000842 self.tree_status_url = self._GetRietveldConfig(
843 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844 return self.tree_status_url
845
846 def GetViewVCUrl(self):
847 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000848 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 return self.viewvc_url
850
rmistry@google.com90752582014-01-14 21:04:50 +0000851 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000852 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000853
rmistry@google.com78948ed2015-07-08 23:09:57 +0000854 def GetIsSkipDependencyUpload(self, branch_name):
855 """Returns true if specified branch should skip dep uploads."""
856 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
857 error_ok=True)
858
rmistry@google.com5626a922015-02-26 14:03:30 +0000859 def GetRunPostUploadHook(self):
860 run_post_upload_hook = self._GetRietveldConfig(
861 'run-post-upload-hook', error_ok=True)
862 return run_post_upload_hook == "True"
863
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000864 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000865 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000866
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000867 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000868 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000869
ukai@chromium.orge8077812012-02-03 03:41:46 +0000870 def GetIsGerrit(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700871 """Return true if this repo is associated with gerrit code review system."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000872 if self.is_gerrit is None:
Aaron Gable9b465272017-05-12 10:53:51 -0700873 self.is_gerrit = (
874 self._GetConfig('gerrit.host', error_ok=True).lower() == 'true')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000875 return self.is_gerrit
876
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000877 def GetSquashGerritUploads(self):
878 """Return true if uploads to Gerrit should be squashed by default."""
879 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700880 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
881 if self.squash_gerrit_uploads is None:
882 # Default is squash now (http://crbug.com/611892#c23).
883 self.squash_gerrit_uploads = not (
884 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
885 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000886 return self.squash_gerrit_uploads
887
tandriia60502f2016-06-20 02:01:53 -0700888 def GetSquashGerritUploadsOverride(self):
889 """Return True or False if codereview.settings should be overridden.
890
891 Returns None if no override has been defined.
892 """
893 # See also http://crbug.com/611892#c23
894 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
895 error_ok=True).strip()
896 if result == 'true':
897 return True
898 if result == 'false':
899 return False
900 return None
901
tandrii@chromium.org28253532016-04-14 13:46:56 +0000902 def GetGerritSkipEnsureAuthenticated(self):
903 """Return True if EnsureAuthenticated should not be done for Gerrit
904 uploads."""
905 if self.gerrit_skip_ensure_authenticated is None:
906 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000907 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000908 error_ok=True).strip() == 'true')
909 return self.gerrit_skip_ensure_authenticated
910
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000911 def GetGitEditor(self):
912 """Return the editor specified in the git config, or None if none is."""
913 if self.git_editor is None:
914 self.git_editor = self._GetConfig('core.editor', error_ok=True)
915 return self.git_editor or None
916
thestig@chromium.org44202a22014-03-11 19:22:18 +0000917 def GetLintRegex(self):
918 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
919 DEFAULT_LINT_REGEX)
920
921 def GetLintIgnoreRegex(self):
922 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
923 DEFAULT_LINT_IGNORE_REGEX)
924
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000925 def GetProject(self):
926 if not self.project:
927 self.project = self._GetRietveldConfig('project', error_ok=True)
928 return self.project
929
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000930 def _GetRietveldConfig(self, param, **kwargs):
931 return self._GetConfig('rietveld.' + param, **kwargs)
932
rmistry@google.com78948ed2015-07-08 23:09:57 +0000933 def _GetBranchConfig(self, branch_name, param, **kwargs):
934 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
935
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 def _GetConfig(self, param, **kwargs):
937 self.LazyUpdateIfNeeded()
938 return RunGit(['config', param], **kwargs).strip()
939
940
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100941@contextlib.contextmanager
942def _get_gerrit_project_config_file(remote_url):
943 """Context manager to fetch and store Gerrit's project.config from
944 refs/meta/config branch and store it in temp file.
945
946 Provides a temporary filename or None if there was error.
947 """
948 error, _ = RunGitWithCode([
949 'fetch', remote_url,
950 '+refs/meta/config:refs/git_cl/meta/config'])
951 if error:
952 # Ref doesn't exist or isn't accessible to current user.
Quinten Yearsley0c62da92017-05-31 13:39:42 -0700953 print('WARNING: Failed to fetch project config for %s: %s' %
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100954 (remote_url, error))
955 yield None
956 return
957
958 error, project_config_data = RunGitWithCode(
959 ['show', 'refs/git_cl/meta/config:project.config'])
960 if error:
961 print('WARNING: project.config file not found')
962 yield None
963 return
964
965 with gclient_utils.temporary_directory() as tempdir:
966 project_config_file = os.path.join(tempdir, 'project.config')
967 gclient_utils.FileWrite(project_config_file, project_config_data)
968 yield project_config_file
969
970
971def _is_git_numberer_enabled(remote_url, remote_ref):
972 """Returns True if Git Numberer is enabled on this ref."""
973 # TODO(tandrii): this should be deleted once repos below are 100% on Gerrit.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100974 KNOWN_PROJECTS_WHITELIST = [
975 'chromium/src',
976 'external/webrtc',
977 'v8/v8',
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +0100978 'infra/experimental',
Edward Lemur32357d32017-09-11 20:22:45 +0200979 # For webrtc.googlesource.com/src.
980 'src',
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100981 ]
982
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100983 assert remote_ref and remote_ref.startswith('refs/'), remote_ref
984 url_parts = urlparse.urlparse(remote_url)
985 project_name = url_parts.path.lstrip('/').rstrip('git./')
986 for known in KNOWN_PROJECTS_WHITELIST:
987 if project_name.endswith(known):
988 break
989 else:
990 # Early exit to avoid extra fetches for repos that aren't using Git
991 # Numberer.
992 return False
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100993
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100994 with _get_gerrit_project_config_file(remote_url) as project_config_file:
995 if project_config_file is None:
996 # Failed to fetch project.config, which shouldn't happen on open source
997 # repos KNOWN_PROJECTS_WHITELIST.
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +0100998 return False
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +0100999 def get_opts(x):
1000 code, out = RunGitWithCode(
1001 ['config', '-f', project_config_file, '--get-all',
1002 'plugin.git-numberer.validate-%s-refglob' % x])
1003 if code == 0:
1004 return out.strip().splitlines()
1005 return []
1006 enabled, disabled = map(get_opts, ['enabled', 'disabled'])
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001007
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001008 logging.info('validator config enabled %s disabled %s refglobs for '
1009 '(this ref: %s)', enabled, disabled, remote_ref)
Andrii Shyshkalov351c61d2017-01-21 20:40:16 +00001010
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001011 def match_refglobs(refglobs):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001012 for refglob in refglobs:
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001013 if remote_ref == refglob or fnmatch.fnmatch(remote_ref, refglob):
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001014 return True
1015 return False
1016
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01001017 if match_refglobs(disabled):
1018 return False
1019 return match_refglobs(enabled)
Andrii Shyshkalovcd6a9362016-12-07 12:04:12 +01001020
1021
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001022def ShortBranchName(branch):
1023 """Convert a name like 'refs/heads/foo' to just 'foo'."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001024 return branch.replace('refs/heads/', '', 1)
1025
1026
1027def GetCurrentBranchRef():
1028 """Returns branch ref (e.g., refs/heads/master) or None."""
1029 return RunGit(['symbolic-ref', 'HEAD'],
1030 stderr=subprocess2.VOID, error_ok=True).strip() or None
1031
1032
1033def GetCurrentBranch():
1034 """Returns current branch or None.
1035
1036 For refs/heads/* branches, returns just last part. For others, full ref.
1037 """
1038 branchref = GetCurrentBranchRef()
1039 if branchref:
1040 return ShortBranchName(branchref)
1041 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001042
1043
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001044class _CQState(object):
1045 """Enum for states of CL with respect to Commit Queue."""
1046 NONE = 'none'
1047 DRY_RUN = 'dry_run'
1048 COMMIT = 'commit'
1049
1050 ALL_STATES = [NONE, DRY_RUN, COMMIT]
1051
1052
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001053class _ParsedIssueNumberArgument(object):
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001054 def __init__(self, issue=None, patchset=None, hostname=None, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001055 self.issue = issue
1056 self.patchset = patchset
1057 self.hostname = hostname
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02001058 assert codereview in (None, 'rietveld', 'gerrit')
1059 self.codereview = codereview
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001060
1061 @property
1062 def valid(self):
1063 return self.issue is not None
1064
1065
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001066def ParseIssueNumberArgument(arg, codereview=None):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001067 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1068 fail_result = _ParsedIssueNumberArgument()
1069
1070 if arg.isdigit():
Aaron Gableaee6c852017-06-26 12:49:01 -07001071 return _ParsedIssueNumberArgument(issue=int(arg), codereview=codereview)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001072 if not arg.startswith('http'):
1073 return fail_result
Aaron Gableaee6c852017-06-26 12:49:01 -07001074
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001075 url = gclient_utils.UpgradeToHttps(arg)
1076 try:
1077 parsed_url = urlparse.urlparse(url)
1078 except ValueError:
1079 return fail_result
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001080
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001081 if codereview is not None:
1082 parsed = _CODEREVIEW_IMPLEMENTATIONS[codereview].ParseIssueURL(parsed_url)
1083 return parsed or fail_result
1084
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001085 results = {}
1086 for name, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1087 parsed = cls.ParseIssueURL(parsed_url)
1088 if parsed is not None:
1089 results[name] = parsed
1090
1091 if not results:
1092 return fail_result
1093 if len(results) == 1:
1094 return results.values()[0]
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02001095
1096 if parsed_url.netloc and parsed_url.netloc.split('.')[0].endswith('-review'):
1097 # This is likely Gerrit.
1098 return results['gerrit']
1099 # Choose Rietveld as before if URL can parsed by either.
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02001100 return results['rietveld']
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001101
1102
Aaron Gablea45ee112016-11-22 15:14:38 -08001103class GerritChangeNotExists(Exception):
tandriic2405f52016-10-10 08:13:15 -07001104 def __init__(self, issue, url):
1105 self.issue = issue
1106 self.url = url
Aaron Gablea45ee112016-11-22 15:14:38 -08001107 super(GerritChangeNotExists, self).__init__()
tandriic2405f52016-10-10 08:13:15 -07001108
1109 def __str__(self):
Aaron Gablea45ee112016-11-22 15:14:38 -08001110 return 'change %s at %s does not exist or you have no access to it' % (
tandriic2405f52016-10-10 08:13:15 -07001111 self.issue, self.url)
1112
1113
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001114_CommentSummary = collections.namedtuple(
1115 '_CommentSummary', ['date', 'message', 'sender',
1116 # TODO(tandrii): these two aren't known in Gerrit.
1117 'approval', 'disapproval'])
1118
1119
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001121 """Changelist works with one changelist in local branch.
1122
1123 Supports two codereview backends: Rietveld or Gerrit, selected at object
1124 creation.
1125
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001126 Notes:
1127 * Not safe for concurrent multi-{thread,process} use.
1128 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001129 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001130 """
1131
1132 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1133 """Create a new ChangeList instance.
1134
1135 If issue is given, the codereview must be given too.
1136
1137 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1138 Otherwise, it's decided based on current configuration of the local branch,
1139 with default being 'rietveld' for backwards compatibility.
1140 See _load_codereview_impl for more details.
1141
1142 **kwargs will be passed directly to codereview implementation.
1143 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001145 global settings
1146 if not settings:
1147 # Happens when git_cl.py is used as a utility library.
1148 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001149
1150 if issue:
1151 assert codereview, 'codereview must be known, if issue is known'
1152
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153 self.branchref = branchref
1154 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001155 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 self.branch = ShortBranchName(self.branchref)
1157 else:
1158 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001160 self.lookedup_issue = False
1161 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 self.has_description = False
1163 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001164 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001166 self.cc = None
Daniel Cheng7227d212017-11-17 08:12:37 -08001167 self.more_cc = []
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001168 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001169
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001170 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001171 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001172 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001173 assert self._codereview_impl
1174 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001175
1176 def _load_codereview_impl(self, codereview=None, **kwargs):
1177 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001178 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1179 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1180 self._codereview = codereview
1181 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001182 return
1183
1184 # Automatic selection based on issue number set for a current branch.
1185 # Rietveld takes precedence over Gerrit.
1186 assert not self.issue
1187 # Whether we find issue or not, we are doing the lookup.
1188 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001189 if self.GetBranch():
1190 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1191 issue = _git_get_branch_config_value(
1192 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1193 if issue:
1194 self._codereview = codereview
1195 self._codereview_impl = cls(self, **kwargs)
1196 self.issue = int(issue)
1197 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001198
1199 # No issue is set for this branch, so decide based on repo-wide settings.
1200 return self._load_codereview_impl(
1201 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1202 **kwargs)
1203
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001204 def IsGerrit(self):
1205 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001206
1207 def GetCCList(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001208 """Returns the users cc'd on this CL.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001209
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001210 The return value is a string suitable for passing to git cl with the --cc
1211 flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001212 """
1213 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001214 base_cc = settings.GetDefaultCCList()
Daniel Cheng7227d212017-11-17 08:12:37 -08001215 more_cc = ','.join(self.more_cc)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001216 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1217 return self.cc
1218
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001219 def GetCCListWithoutDefault(self):
1220 """Return the users cc'd on this CL excluding default ones."""
1221 if self.cc is None:
Daniel Cheng7227d212017-11-17 08:12:37 -08001222 self.cc = ','.join(self.more_cc)
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001223 return self.cc
1224
Daniel Cheng7227d212017-11-17 08:12:37 -08001225 def ExtendCC(self, more_cc):
1226 """Extends the list of users to cc on this CL based on the changed files."""
1227 self.more_cc.extend(more_cc)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228
1229 def GetBranch(self):
1230 """Returns the short branch name, e.g. 'master'."""
1231 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001232 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001233 if not branchref:
1234 return None
1235 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001236 self.branch = ShortBranchName(self.branchref)
1237 return self.branch
1238
1239 def GetBranchRef(self):
1240 """Returns the full branch name, e.g. 'refs/heads/master'."""
1241 self.GetBranch() # Poke the lazy loader.
1242 return self.branchref
1243
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001244 def ClearBranch(self):
1245 """Clears cached branch data of this object."""
1246 self.branch = self.branchref = None
1247
tandrii5d48c322016-08-18 16:19:37 -07001248 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1249 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1250 kwargs['branch'] = self.GetBranch()
1251 return _git_get_branch_config_value(key, default, **kwargs)
1252
1253 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1254 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1255 assert self.GetBranch(), (
1256 'this CL must have an associated branch to %sset %s%s' %
1257 ('un' if value is None else '',
1258 key,
1259 '' if value is None else ' to %r' % value))
1260 kwargs['branch'] = self.GetBranch()
1261 return _git_set_branch_config_value(key, value, **kwargs)
1262
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001263 @staticmethod
1264 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001265 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 e.g. 'origin', 'refs/heads/master'
1267 """
1268 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001269 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1270
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001272 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001273 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001274 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1275 error_ok=True).strip()
1276 if upstream_branch:
1277 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001279 # Else, try to guess the origin remote.
1280 remote_branches = RunGit(['branch', '-r']).split()
1281 if 'origin/master' in remote_branches:
1282 # Fall back on origin/master if it exits.
1283 remote = 'origin'
1284 upstream_branch = 'refs/heads/master'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001285 else:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001286 DieWithError(
1287 'Unable to determine default branch to diff against.\n'
1288 'Either pass complete "git diff"-style arguments, like\n'
1289 ' git cl upload origin/master\n'
1290 'or verify this branch is set up to track another \n'
1291 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292
1293 return remote, upstream_branch
1294
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001295 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001296 upstream_branch = self.GetUpstreamBranch()
1297 if not BranchExists(upstream_branch):
1298 DieWithError('The upstream for the current branch (%s) does not exist '
1299 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001300 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001301 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001302
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 def GetUpstreamBranch(self):
1304 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001305 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001307 upstream_branch = upstream_branch.replace('refs/heads/',
1308 'refs/remotes/%s/' % remote)
1309 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1310 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 self.upstream_branch = upstream_branch
1312 return self.upstream_branch
1313
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001315 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001316 remote, branch = None, self.GetBranch()
1317 seen_branches = set()
1318 while branch not in seen_branches:
1319 seen_branches.add(branch)
1320 remote, branch = self.FetchUpstreamTuple(branch)
1321 branch = ShortBranchName(branch)
1322 if remote != '.' or branch.startswith('refs/remotes'):
1323 break
1324 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001325 remotes = RunGit(['remote'], error_ok=True).split()
1326 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001327 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001328 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001329 remote = 'origin'
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001330 logging.warn('Could not determine which remote this change is '
1331 'associated with, so defaulting to "%s".' % self._remote)
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001332 else:
1333 logging.warn('Could not determine which remote this change is '
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08001334 'associated with.')
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001335 branch = 'HEAD'
1336 if branch.startswith('refs/remotes'):
1337 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001338 elif branch.startswith('refs/branch-heads/'):
1339 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001340 else:
1341 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001342 return self._remote
1343
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001344 def GitSanityChecks(self, upstream_git_obj):
1345 """Checks git repo status and ensures diff is from local commits."""
1346
sbc@chromium.org79706062015-01-14 21:18:12 +00001347 if upstream_git_obj is None:
1348 if self.GetBranch() is None:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001349 print('ERROR: Unable to determine current branch (detached HEAD?)',
vapiera7fbd5a2016-06-16 09:17:49 -07001350 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001351 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001352 print('ERROR: No upstream branch.', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001353 return False
1354
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001355 # Verify the commit we're diffing against is in our current branch.
1356 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1357 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1358 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001359 print('ERROR: %s is not in the current branch. You may need to rebase '
1360 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001361 return False
1362
1363 # List the commits inside the diff, and verify they are all local.
1364 commits_in_diff = RunGit(
1365 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1366 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1367 remote_branch = remote_branch.strip()
1368 if code != 0:
1369 _, remote_branch = self.GetRemoteBranch()
1370
1371 commits_in_remote = RunGit(
1372 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1373
1374 common_commits = set(commits_in_diff) & set(commits_in_remote)
1375 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001376 print('ERROR: Your diff contains %d commits already in %s.\n'
1377 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1378 'the diff. If you are using a custom git flow, you can override'
1379 ' the reference used for this check with "git config '
1380 'gitcl.remotebranch <git-ref>".' % (
1381 len(common_commits), remote_branch, upstream_git_obj),
1382 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001383 return False
1384 return True
1385
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001386 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001387 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001388
1389 Returns None if it is not set.
1390 """
tandrii5d48c322016-08-18 16:19:37 -07001391 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393 def GetRemoteUrl(self):
1394 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1395
1396 Returns None if there is no remote.
1397 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001398 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001399 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1400
1401 # If URL is pointing to a local directory, it is probably a git cache.
1402 if os.path.isdir(url):
1403 url = RunGit(['config', 'remote.%s.url' % remote],
1404 error_ok=True,
1405 cwd=url).strip()
1406 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001408 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001409 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001410 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001411 self.issue = self._GitGetBranchConfigValue(
1412 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001413 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 return self.issue
1415
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416 def GetIssueURL(self):
1417 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001418 issue = self.GetIssue()
1419 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001420 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001421 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422
Andrii Shyshkalov31863012017-02-08 11:35:12 +01001423 def GetDescription(self, pretty=False, force=False):
1424 if not self.has_description or force:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425 if self.GetIssue():
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001426 self.description = self._codereview_impl.FetchDescription(force=force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427 self.has_description = True
1428 if pretty:
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001429 # Set width to 72 columns + 2 space indent.
1430 wrapper = textwrap.TextWrapper(width=74, replace_whitespace=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 wrapper.initial_indent = wrapper.subsequent_indent = ' '
Asanka Herath7c61dcb2016-12-14 13:01:58 -05001432 lines = self.description.splitlines()
1433 return '\n'.join([wrapper.fill(line) for line in lines])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 return self.description
1435
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001436 def GetDescriptionFooters(self):
1437 """Returns (non_footer_lines, footers) for the commit message.
1438
1439 Returns:
1440 non_footer_lines (list(str)) - Simple list of description lines without
1441 any footer. The lines do not contain newlines, nor does the list contain
1442 the empty line between the message and the footers.
1443 footers (list(tuple(KEY, VALUE))) - List of parsed footers, e.g.
1444 [("Change-Id", "Ideadbeef...."), ...]
1445 """
1446 raw_description = self.GetDescription()
1447 msg_lines, _, footers = git_footers.split_footers(raw_description)
1448 if footers:
1449 msg_lines = msg_lines[:len(msg_lines)-1]
1450 return msg_lines, footers
1451
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001453 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001454 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001455 self.patchset = self._GitGetBranchConfigValue(
1456 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001457 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458 return self.patchset
1459
1460 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001461 """Set this branch's patchset. If patchset=0, clears the patchset."""
1462 assert self.GetBranch()
1463 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001464 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001465 else:
1466 self.patchset = int(patchset)
1467 self._GitSetBranchConfigValue(
1468 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001469
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001470 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001471 """Set this branch's issue. If issue isn't given, clears the issue."""
1472 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001474 issue = int(issue)
1475 self._GitSetBranchConfigValue(
1476 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001477 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001478 codereview_server = self._codereview_impl.GetCodereviewServer()
1479 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001480 self._GitSetBranchConfigValue(
1481 self._codereview_impl.CodereviewServerConfigKey(),
1482 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001483 else:
tandrii5d48c322016-08-18 16:19:37 -07001484 # Reset all of these just to be clean.
1485 reset_suffixes = [
1486 'last-upload-hash',
1487 self._codereview_impl.IssueConfigKey(),
1488 self._codereview_impl.PatchsetConfigKey(),
1489 self._codereview_impl.CodereviewServerConfigKey(),
1490 ] + self._PostUnsetIssueProperties()
1491 for prop in reset_suffixes:
1492 self._GitSetBranchConfigValue(prop, None, error_ok=True)
Aaron Gableca01e2c2017-07-19 11:16:02 -07001493 msg = RunGit(['log', '-1', '--format=%B']).strip()
1494 if msg and git_footers.get_footer_change_id(msg):
1495 print('WARNING: The change patched into this branch has a Change-Id. '
1496 'Removing it.')
1497 RunGit(['commit', '--amend', '-m',
1498 git_footers.remove_footer(msg, 'Change-Id')])
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001499 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001500 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501
dnjba1b0f32016-09-02 12:37:42 -07001502 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001503 if not self.GitSanityChecks(upstream_branch):
1504 DieWithError('\nGit sanity check failure')
1505
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001506 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001507 if not root:
1508 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001509 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001510
1511 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001512 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001513 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001514 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001515 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001516 except subprocess2.CalledProcessError:
1517 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001518 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001519 'This branch probably doesn\'t exist anymore. To reset the\n'
1520 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001521 ' git branch --set-upstream-to origin/master %s\n'
1522 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001523 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001524
maruel@chromium.org52424302012-08-29 15:14:30 +00001525 issue = self.GetIssue()
1526 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001527 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001528 description = self.GetDescription()
1529 else:
1530 # If the change was never uploaded, use the log messages of all commits
1531 # up to the branch point, as git cl upload will prefill the description
1532 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001533 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1534 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001535
1536 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001537 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001538 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001539 name,
1540 description,
1541 absroot,
1542 files,
1543 issue,
1544 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001545 author,
1546 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001547
dsansomee2d6fd92016-09-08 00:10:47 -07001548 def UpdateDescription(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001549 self._codereview_impl.UpdateDescriptionRemote(description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001550 self.description = description
Andrii Shyshkalov83051152017-02-07 23:47:29 +01001551 self.has_description = True
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001552
Robert Iannucci09f1f3d2017-03-28 16:54:32 -07001553 def UpdateDescriptionFooters(self, description_lines, footers, force=False):
1554 """Sets the description for this CL remotely.
1555
1556 You can get description_lines and footers with GetDescriptionFooters.
1557
1558 Args:
1559 description_lines (list(str)) - List of CL description lines without
1560 newline characters.
1561 footers (list(tuple(KEY, VALUE))) - List of footers, as returned by
1562 GetDescriptionFooters. Key must conform to the git footers format (i.e.
1563 `List-Of-Tokens`). It will be case-normalized so that each token is
1564 title-cased.
1565 """
1566 new_description = '\n'.join(description_lines)
1567 if footers:
1568 new_description += '\n'
1569 for k, v in footers:
1570 foot = '%s: %s' % (git_footers.normalize_name(k), v)
1571 if not git_footers.FOOTER_PATTERN.match(foot):
1572 raise ValueError('Invalid footer %r' % foot)
1573 new_description += foot + '\n'
1574 self.UpdateDescription(new_description, force)
1575
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001576 def RunHook(self, committing, may_prompt, verbose, change):
1577 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1578 try:
1579 return presubmit_support.DoPresubmitChecks(change, committing,
1580 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1581 default_presubmit=None, may_prompt=may_prompt,
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001582 rietveld_obj=self._codereview_impl.GetRietveldObjForPresubmit(),
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001583 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001584 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001585 DieWithError(
1586 ('%s\nMaybe your depot_tools is out of date?\n'
1587 'If all fails, contact maruel@') % e)
1588
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001589 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1590 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001591 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1592 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001593 else:
1594 # Assume url.
1595 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1596 urlparse.urlparse(issue_arg))
1597 if not parsed_issue_arg or not parsed_issue_arg.valid:
1598 DieWithError('Failed to parse issue argument "%s". '
1599 'Must be an issue number or a valid URL.' % issue_arg)
1600 return self._codereview_impl.CMDPatchWithParsedIssue(
Aaron Gable62619a32017-06-16 08:22:09 -07001601 parsed_issue_arg, reject, nocommit, directory, False)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001602
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001603 def CMDUpload(self, options, git_diff_args, orig_args):
1604 """Uploads a change to codereview."""
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001605 custom_cl_base = None
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001606 if git_diff_args:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001607 custom_cl_base = base_branch = git_diff_args[0]
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001608 else:
1609 if self.GetBranch() is None:
1610 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1611
1612 # Default to diffing against common ancestor of upstream branch
1613 base_branch = self.GetCommonAncestorWithUpstream()
1614 git_diff_args = [base_branch, 'HEAD']
1615
Aaron Gablec4c40d12017-05-22 11:49:53 -07001616 # Warn about Rietveld deprecation for initial uploads to Rietveld.
1617 if not self.IsGerrit() and not self.GetIssue():
1618 print('=====================================')
1619 print('NOTICE: Rietveld is being deprecated. '
1620 'You can upload changes to Gerrit with')
1621 print(' git cl upload --gerrit')
1622 print('or set Gerrit to be your default code review tool with')
1623 print(' git config gerrit.host true')
1624 print('=====================================')
1625
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001626 # Fast best-effort checks to abort before running potentially
1627 # expensive hooks if uploading is likely to fail anyway. Passing these
1628 # checks does not guarantee that uploading will not fail.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001629 self._codereview_impl.EnsureAuthenticated(force=options.force)
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001630 self._codereview_impl.EnsureCanUploadPatchset(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001631
1632 # Apply watchlists on upload.
1633 change = self.GetChange(base_branch, None)
1634 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1635 files = [f.LocalPath() for f in change.AffectedFiles()]
1636 if not options.bypass_watchlists:
Daniel Cheng7227d212017-11-17 08:12:37 -08001637 self.ExtendCC(watchlist.GetWatchersForPaths(files))
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001638
1639 if not options.bypass_hooks:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001640 if options.reviewers or options.tbrs or options.add_owners_to:
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001641 # Set the reviewer list now so that presubmit checks can access it.
1642 change_description = ChangeDescription(change.FullDescriptionText())
1643 change_description.update_reviewers(options.reviewers,
Robert Iannucci6c98dc62017-04-18 11:38:00 -07001644 options.tbrs,
Robert Iannuccif2708bd2017-04-17 15:49:02 -07001645 options.add_owners_to,
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001646 change)
1647 change.SetDescriptionText(change_description.description)
1648 hook_results = self.RunHook(committing=False,
1649 may_prompt=not options.force,
1650 verbose=options.verbose,
1651 change=change)
1652 if not hook_results.should_continue():
1653 return 1
1654 if not options.reviewers and hook_results.reviewers:
1655 options.reviewers = hook_results.reviewers.split(',')
Daniel Cheng7227d212017-11-17 08:12:37 -08001656 self.ExtendCC(hook_results.more_cc)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001657
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001658 # TODO(tandrii): Checking local patchset against remote patchset is only
1659 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1660 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001661 latest_patchset = self.GetMostRecentPatchset()
1662 local_patchset = self.GetPatchset()
1663 if (latest_patchset and local_patchset and
1664 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001665 print('The last upload made from this repository was patchset #%d but '
1666 'the most recent patchset on the server is #%d.'
1667 % (local_patchset, latest_patchset))
1668 print('Uploading will still work, but if you\'ve uploaded to this '
1669 'issue from another machine or branch the patch you\'re '
1670 'uploading now might not include those changes.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01001671 confirm_or_exit(action='upload')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001672
1673 print_stats(options.similarity, options.find_copies, git_diff_args)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001674 ret = self.CMDUploadChange(options, git_diff_args, custom_cl_base, change)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001675 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001676 if options.use_commit_queue:
1677 self.SetCQState(_CQState.COMMIT)
1678 elif options.cq_dry_run:
1679 self.SetCQState(_CQState.DRY_RUN)
1680
tandrii5d48c322016-08-18 16:19:37 -07001681 _git_set_branch_config_value('last-upload-hash',
1682 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001683 # Run post upload hooks, if specified.
1684 if settings.GetRunPostUploadHook():
1685 presubmit_support.DoPostUploadExecuter(
1686 change,
1687 self,
1688 settings.GetRoot(),
1689 options.verbose,
1690 sys.stdout)
1691
1692 # Upload all dependencies if specified.
1693 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001694 print()
1695 print('--dependencies has been specified.')
1696 print('All dependent local branches will be re-uploaded.')
1697 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001698 # Remove the dependencies flag from args so that we do not end up in a
1699 # loop.
1700 orig_args.remove('--dependencies')
1701 ret = upload_branch_deps(self, orig_args)
1702 return ret
1703
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001704 def SetCQState(self, new_state):
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001705 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001706
1707 Issue must have been already uploaded and known.
1708 """
1709 assert new_state in _CQState.ALL_STATES
1710 assert self.GetIssue()
qyearsley1fdfcb62016-10-24 13:22:03 -07001711 try:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001712 self._codereview_impl.SetCQState(new_state)
qyearsley1fdfcb62016-10-24 13:22:03 -07001713 return 0
1714 except KeyboardInterrupt:
1715 raise
1716 except:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001717 print('WARNING: Failed to %s.\n'
qyearsley1fdfcb62016-10-24 13:22:03 -07001718 'Either:\n'
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07001719 ' * Your project has no CQ,\n'
1720 ' * You don\'t have permission to change the CQ state,\n'
1721 ' * There\'s a bug in this code (see stack trace below).\n'
1722 'Consider specifying which bots to trigger manually or asking your '
1723 'project owners for permissions or contacting Chrome Infra at:\n'
1724 'https://www.chromium.org/infra\n\n' %
1725 ('cancel CQ' if new_state == _CQState.NONE else 'trigger CQ'))
qyearsley1fdfcb62016-10-24 13:22:03 -07001726 # Still raise exception so that stack trace is printed.
1727 raise
1728
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001729 # Forward methods to codereview specific implementation.
1730
Aaron Gable636b13f2017-07-14 10:42:48 -07001731 def AddComment(self, message, publish=None):
1732 return self._codereview_impl.AddComment(message, publish=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001733
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001734 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001735 """Returns list of _CommentSummary for each comment.
1736
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001737 args:
1738 readable: determines whether the output is designed for a human or a machine
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001739 """
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001740 return self._codereview_impl.GetCommentsSummary(readable)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001741
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001742 def CloseIssue(self):
1743 return self._codereview_impl.CloseIssue()
1744
1745 def GetStatus(self):
1746 return self._codereview_impl.GetStatus()
1747
1748 def GetCodereviewServer(self):
1749 return self._codereview_impl.GetCodereviewServer()
1750
tandriide281ae2016-10-12 06:02:30 -07001751 def GetIssueOwner(self):
1752 """Get owner from codereview, which may differ from this checkout."""
1753 return self._codereview_impl.GetIssueOwner()
1754
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001755 def GetMostRecentPatchset(self):
1756 return self._codereview_impl.GetMostRecentPatchset()
1757
tandriide281ae2016-10-12 06:02:30 -07001758 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001759 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriide281ae2016-10-12 06:02:30 -07001760 return self._codereview_impl.CannotTriggerTryJobReason()
1761
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001762 def GetTryJobProperties(self, patchset=None):
1763 """Returns dictionary of properties to launch try job."""
1764 return self._codereview_impl.GetTryJobProperties(patchset=patchset)
tandrii8c5a3532016-11-04 07:52:02 -07001765
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001766 def __getattr__(self, attr):
1767 # This is because lots of untested code accesses Rietveld-specific stuff
1768 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001769 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001770 # Note that child method defines __getattr__ as well, and forwards it here,
1771 # because _RietveldChangelistImpl is not cleaned up yet, and given
1772 # deprecation of Rietveld, it should probably be just removed.
1773 # Until that time, avoid infinite recursion by bypassing __getattr__
1774 # of implementation class.
1775 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001776
1777
1778class _ChangelistCodereviewBase(object):
1779 """Abstract base class encapsulating codereview specifics of a changelist."""
1780 def __init__(self, changelist):
1781 self._changelist = changelist # instance of Changelist
1782
1783 def __getattr__(self, attr):
1784 # Forward methods to changelist.
1785 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1786 # _RietveldChangelistImpl to avoid this hack?
1787 return getattr(self._changelist, attr)
1788
1789 def GetStatus(self):
1790 """Apply a rough heuristic to give a simple summary of an issue's review
1791 or CQ status, assuming adherence to a common workflow.
1792
1793 Returns None if no issue for this branch, or specific string keywords.
1794 """
1795 raise NotImplementedError()
1796
1797 def GetCodereviewServer(self):
1798 """Returns server URL without end slash, like "https://codereview.com"."""
1799 raise NotImplementedError()
1800
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001801 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001802 """Fetches and returns description from the codereview server."""
1803 raise NotImplementedError()
1804
tandrii5d48c322016-08-18 16:19:37 -07001805 @classmethod
1806 def IssueConfigKey(cls):
1807 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001808 raise NotImplementedError()
1809
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001810 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001811 def PatchsetConfigKey(cls):
1812 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001813 raise NotImplementedError()
1814
tandrii5d48c322016-08-18 16:19:37 -07001815 @classmethod
1816 def CodereviewServerConfigKey(cls):
1817 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001818 raise NotImplementedError()
1819
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001820 def _PostUnsetIssueProperties(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001821 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07001822 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001823
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001824 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001825 # This is an unfortunate Rietveld-embeddedness in presubmit.
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001826 # For non-Rietveld code reviews, this probably should return a dummy object.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001827 raise NotImplementedError()
1828
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001829 def GetGerritObjForPresubmit(self):
1830 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1831 return None
1832
dsansomee2d6fd92016-09-08 00:10:47 -07001833 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001834 """Update the description on codereview site."""
1835 raise NotImplementedError()
1836
Aaron Gable636b13f2017-07-14 10:42:48 -07001837 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01001838 """Posts a comment to the codereview site."""
1839 raise NotImplementedError()
1840
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07001841 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01001842 raise NotImplementedError()
1843
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001844 def CloseIssue(self):
1845 """Closes the issue."""
1846 raise NotImplementedError()
1847
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001848 def GetMostRecentPatchset(self):
1849 """Returns the most recent patchset number from the codereview site."""
1850 raise NotImplementedError()
1851
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001852 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07001853 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001854 """Fetches and applies the issue.
1855
1856 Arguments:
1857 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1858 reject: if True, reject the failed patch instead of switching to 3-way
1859 merge. Rietveld only.
1860 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1861 only.
1862 directory: switch to directory before applying the patch. Rietveld only.
Aaron Gable62619a32017-06-16 08:22:09 -07001863 force: if true, overwrites existing local state.
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001864 """
1865 raise NotImplementedError()
1866
1867 @staticmethod
1868 def ParseIssueURL(parsed_url):
1869 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1870 failed."""
1871 raise NotImplementedError()
1872
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001873 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001874 """Best effort check that user is authenticated with codereview server.
1875
1876 Arguments:
1877 force: whether to skip confirmation questions.
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001878 refresh: whether to attempt to refresh credentials. Ignored if not
1879 applicable.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001880 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001881 raise NotImplementedError()
1882
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001883 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001884 """Best effort check that uploading isn't supposed to fail for predictable
1885 reasons.
1886
1887 This method should raise informative exception if uploading shouldn't
1888 proceed.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001889
1890 Arguments:
1891 force: whether to skip confirmation questions.
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001892 """
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001893 raise NotImplementedError()
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01001894
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02001895 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001896 """Uploads a change to codereview."""
1897 raise NotImplementedError()
1898
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001899 def SetCQState(self, new_state):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001900 """Updates the CQ state for the latest patchset.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001901
1902 Issue must have been already uploaded and known.
1903 """
1904 raise NotImplementedError()
1905
tandriie113dfd2016-10-11 10:20:12 -07001906 def CannotTriggerTryJobReason(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001907 """Returns reason (str) if unable trigger try jobs on this CL or None."""
tandriie113dfd2016-10-11 10:20:12 -07001908 raise NotImplementedError()
1909
tandriide281ae2016-10-12 06:02:30 -07001910 def GetIssueOwner(self):
1911 raise NotImplementedError()
1912
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001913 def GetTryJobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001914 raise NotImplementedError()
1915
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001916
1917class _RietveldChangelistImpl(_ChangelistCodereviewBase):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07001918
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001919 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001920 super(_RietveldChangelistImpl, self).__init__(changelist)
1921 assert settings, 'must be initialized in _ChangelistCodereviewBase'
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001922 if not codereview_host:
martiniss6eda05f2016-06-30 10:18:35 -07001923 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001924
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01001925 self._rietveld_server = codereview_host
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01001926 self._auth_config = auth_config or auth.make_auth_config()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001927 self._props = None
1928 self._rpc_server = None
1929
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001930 def GetCodereviewServer(self):
1931 if not self._rietveld_server:
1932 # If we're on a branch then get the server potentially associated
1933 # with that branch.
1934 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001935 self._rietveld_server = gclient_utils.UpgradeToHttps(
1936 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001937 if not self._rietveld_server:
1938 self._rietveld_server = settings.GetDefaultServerUrl()
1939 return self._rietveld_server
1940
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001941 def EnsureAuthenticated(self, force, refresh=False):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001942 """Best effort check that user is authenticated with Rietveld server."""
1943 if self._auth_config.use_oauth2:
1944 authenticator = auth.get_authenticator_for_host(
1945 self.GetCodereviewServer(), self._auth_config)
1946 if not authenticator.has_cached_credentials():
1947 raise auth.LoginRequiredError(self.GetCodereviewServer())
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01001948 if refresh:
1949 authenticator.get_access_token()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001950
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01001951 def EnsureCanUploadPatchset(self, force):
1952 # No checks for Rietveld because we are deprecating Rietveld.
1953 pass
1954
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001955 def FetchDescription(self, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001956 issue = self.GetIssue()
1957 assert issue
1958 try:
Kenneth Russell61e2ed42017-02-15 11:47:13 -08001959 return self.RpcServer().get_description(issue, force=force).strip()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001960 except urllib2.HTTPError as e:
1961 if e.code == 404:
1962 DieWithError(
1963 ('\nWhile fetching the description for issue %d, received a '
1964 '404 (not found)\n'
1965 'error. It is likely that you deleted this '
1966 'issue on the server. If this is the\n'
1967 'case, please run\n\n'
1968 ' git cl issue 0\n\n'
1969 'to clear the association with the deleted issue. Then run '
1970 'this command again.') % issue)
1971 else:
1972 DieWithError(
1973 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1974 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001975 print('Warning: Failed to retrieve CL description due to network '
1976 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001977 return ''
1978
1979 def GetMostRecentPatchset(self):
1980 return self.GetIssueProperties()['patchsets'][-1]
1981
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001982 def GetIssueProperties(self):
1983 if self._props is None:
1984 issue = self.GetIssue()
1985 if not issue:
1986 self._props = {}
1987 else:
1988 self._props = self.RpcServer().get_issue_properties(issue, True)
1989 return self._props
1990
tandriie113dfd2016-10-11 10:20:12 -07001991 def CannotTriggerTryJobReason(self):
1992 props = self.GetIssueProperties()
1993 if not props:
1994 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1995 if props.get('closed'):
1996 return 'CL %s is closed' % self.GetIssue()
1997 if props.get('private'):
1998 return 'CL %s is private' % self.GetIssue()
1999 return None
2000
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002001 def GetTryJobProperties(self, patchset=None):
2002 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07002003 project = (self.GetIssueProperties() or {}).get('project')
2004 return {
2005 'issue': self.GetIssue(),
2006 'patch_project': project,
2007 'patch_storage': 'rietveld',
2008 'patchset': patchset or self.GetPatchset(),
2009 'rietveld': self.GetCodereviewServer(),
2010 }
2011
tandriide281ae2016-10-12 06:02:30 -07002012 def GetIssueOwner(self):
2013 return (self.GetIssueProperties() or {}).get('owner_email')
2014
Aaron Gable636b13f2017-07-14 10:42:48 -07002015 def AddComment(self, message, publish=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002016 return self.RpcServer().add_comment(self.GetIssue(), message)
2017
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002018 def GetCommentsSummary(self, _readable=True):
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002019 summary = []
2020 for message in self.GetIssueProperties().get('messages', []):
2021 date = datetime.datetime.strptime(message['date'], '%Y-%m-%d %H:%M:%S.%f')
2022 summary.append(_CommentSummary(
2023 date=date,
2024 disapproval=bool(message['disapproval']),
2025 approval=bool(message['approval']),
2026 sender=message['sender'],
2027 message=message['text'],
2028 ))
2029 return summary
2030
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002031 def GetStatus(self):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002032 """Applies a rough heuristic to give a simple summary of an issue's review
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002033 or CQ status, assuming adherence to a common workflow.
2034
2035 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gablea1bab272017-04-11 16:38:18 -07002036 * 'error' - error from review tool (including deleted issues)
2037 * 'unsent' - not sent for review
2038 * 'waiting' - waiting for review
2039 * 'reply' - waiting for owner to reply to review
2040 * 'not lgtm' - Code-Review label has been set negatively
2041 * 'lgtm' - LGTM from at least one approved reviewer
2042 * 'commit' - in the commit queue
2043 * 'closed' - closed
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002044 """
2045 if not self.GetIssue():
2046 return None
2047
2048 try:
2049 props = self.GetIssueProperties()
2050 except urllib2.HTTPError:
2051 return 'error'
2052
2053 if props.get('closed'):
2054 # Issue is closed.
2055 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00002056 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002057 # Issue is in the commit queue.
2058 return 'commit'
2059
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002060 messages = props.get('messages') or []
Aaron Gablea1bab272017-04-11 16:38:18 -07002061 if not messages:
2062 # No message was sent.
2063 return 'unsent'
2064
2065 if get_approving_reviewers(props):
2066 return 'lgtm'
2067 elif get_approving_reviewers(props, disapproval=True):
2068 return 'not lgtm'
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002069
tandrii9d2c7a32016-06-22 03:42:45 -07002070 # Skip CQ messages that don't require owner's action.
2071 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
2072 if 'Dry run:' in messages[-1]['text']:
2073 messages.pop()
2074 elif 'The CQ bit was unchecked' in messages[-1]['text']:
2075 # This message always follows prior messages from CQ,
2076 # so skip this too.
2077 messages.pop()
2078 else:
2079 # This is probably a CQ messages warranting user attention.
2080 break
2081
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002082 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07002083 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002084 return 'reply'
2085 return 'waiting'
2086
dsansomee2d6fd92016-09-08 00:10:47 -07002087 def UpdateDescriptionRemote(self, description, force=False):
Andrii Shyshkalov83051152017-02-07 23:47:29 +01002088 self.RpcServer().update_description(self.GetIssue(), description)
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002089
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002091 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002092
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002093 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002094 return self.SetFlags({flag: value})
2095
2096 def SetFlags(self, flags):
2097 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002098 """
phajdan.jr68598232016-08-10 03:28:28 -07002099 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002100 try:
tandrii4b233bd2016-07-06 03:50:29 -07002101 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002102 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002103 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002104 if e.code == 404:
2105 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2106 if e.code == 403:
2107 DieWithError(
2108 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002109 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002110 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002112 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113 """Returns an upload.RpcServer() to access this review's rietveld instance.
2114 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002115 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002116 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002117 self.GetCodereviewServer(),
Andrii Shyshkalov98cef572017-01-25 13:57:52 +01002118 self._auth_config)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002119 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002121 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002122 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002123 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124
tandrii5d48c322016-08-18 16:19:37 -07002125 @classmethod
2126 def PatchsetConfigKey(cls):
2127 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002128
tandrii5d48c322016-08-18 16:19:37 -07002129 @classmethod
2130 def CodereviewServerConfigKey(cls):
2131 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002132
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002133 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002134 return self.RpcServer()
2135
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002136 def SetCQState(self, new_state):
2137 props = self.GetIssueProperties()
2138 if props.get('private'):
2139 DieWithError('Cannot set-commit on private issue')
2140
2141 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002142 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002143 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002144 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002145 else:
tandrii4b233bd2016-07-06 03:50:29 -07002146 assert new_state == _CQState.DRY_RUN
2147 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002148
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002149 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002150 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002151 # PatchIssue should never be called with a dirty tree. It is up to the
2152 # caller to check this, but just in case we assert here since the
2153 # consequences of the caller not checking this could be dire.
2154 assert(not git_common.is_dirty_git_tree('apply'))
2155 assert(parsed_issue_arg.valid)
2156 self._changelist.issue = parsed_issue_arg.issue
2157 if parsed_issue_arg.hostname:
2158 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2159
skobes6468b902016-10-24 08:45:10 -07002160 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2161 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2162 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002163 try:
skobes6468b902016-10-24 08:45:10 -07002164 scm_obj.apply_patch(patchset_object)
2165 except Exception as e:
2166 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002167 return 1
2168
2169 # If we had an issue, commit the current state and register the issue.
2170 if not nocommit:
Aaron Gabled343c632017-03-15 11:02:26 -07002171 self.SetIssue(self.GetIssue())
2172 self.SetPatchset(patchset)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002173 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2174 'patch from issue %(i)s at patchset '
2175 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2176 % {'i': self.GetIssue(), 'p': patchset})])
vapiera7fbd5a2016-06-16 09:17:49 -07002177 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002178 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002179 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002180 return 0
2181
2182 @staticmethod
2183 def ParseIssueURL(parsed_url):
2184 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2185 return None
wychen3c1c1722016-08-04 11:46:36 -07002186 # Rietveld patch: https://domain/<number>/#ps<patchset>
2187 match = re.match(r'/(\d+)/$', parsed_url.path)
2188 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2189 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002190 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002191 issue=int(match.group(1)),
2192 patchset=int(match2.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002193 hostname=parsed_url.netloc,
2194 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002195 # Typical url: https://domain/<issue_number>[/[other]]
2196 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2197 if match:
skobes6468b902016-10-24 08:45:10 -07002198 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002199 issue=int(match.group(1)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002200 hostname=parsed_url.netloc,
2201 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002202 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2203 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2204 if match:
skobes6468b902016-10-24 08:45:10 -07002205 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002206 issue=int(match.group(1)),
2207 patchset=int(match.group(2)),
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002208 hostname=parsed_url.netloc,
2209 codereview='rietveld')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002210 return None
2211
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002212 def CMDUploadChange(self, options, args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002213 """Upload the patch to Rietveld."""
2214 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2215 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002216 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2217 if options.emulate_svn_auto_props:
2218 upload_args.append('--emulate_svn_auto_props')
2219
2220 change_desc = None
2221
2222 if options.email is not None:
2223 upload_args.extend(['--email', options.email])
2224
2225 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002226 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002227 upload_args.extend(['--title', options.title])
2228 if options.message:
2229 upload_args.extend(['--message', options.message])
2230 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002231 print('This branch is associated with issue %s. '
2232 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002233 else:
nodirca166002016-06-27 10:59:51 -07002234 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002235 upload_args.extend(['--title', options.title])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002236 if options.message:
2237 message = options.message
2238 else:
2239 message = CreateDescriptionFromLog(args)
2240 if options.title:
2241 message = options.title + '\n\n' + message
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002242 change_desc = ChangeDescription(message)
Robert Iannuccif2708bd2017-04-17 15:49:02 -07002243 if options.reviewers or options.add_owners_to:
Robert Iannucci6c98dc62017-04-18 11:38:00 -07002244 change_desc.update_reviewers(options.reviewers, options.tbrs,
2245 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002246 if not options.force:
Aaron Gable3a16ed12017-03-23 10:51:55 -07002247 change_desc.prompt(bug=options.bug, git_footer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248
2249 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002250 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002251 return 1
2252
2253 upload_args.extend(['--message', change_desc.description])
2254 if change_desc.get_reviewers():
2255 upload_args.append('--reviewers=%s' % ','.join(
2256 change_desc.get_reviewers()))
2257 if options.send_mail:
2258 if not change_desc.get_reviewers():
Christopher Lamf732cd52017-01-24 12:40:11 +11002259 DieWithError("Must specify reviewers to send email.", change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002260 upload_args.append('--send_mail')
2261
2262 # We check this before applying rietveld.private assuming that in
2263 # rietveld.cc only addresses which we can send private CLs to are listed
2264 # if rietveld.private is set, and so we should ignore rietveld.cc only
2265 # when --private is specified explicitly on the command line.
2266 if options.private:
2267 logging.warn('rietveld.cc is ignored since private flag is specified. '
2268 'You need to review and add them manually if necessary.')
2269 cc = self.GetCCListWithoutDefault()
2270 else:
2271 cc = self.GetCCList()
2272 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002273 if change_desc.get_cced():
2274 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002275 if cc:
2276 upload_args.extend(['--cc', cc])
2277
2278 if options.private or settings.GetDefaultPrivateFlag() == "True":
2279 upload_args.append('--private')
2280
2281 upload_args.extend(['--git_similarity', str(options.similarity)])
2282 if not options.find_copies:
2283 upload_args.extend(['--git_no_find_copies'])
2284
2285 # Include the upstream repo's URL in the change -- this is useful for
2286 # projects that have their source spread across multiple repos.
2287 remote_url = self.GetGitBaseUrlFromConfig()
2288 if not remote_url:
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08002289 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2290 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2291 self.GetUpstreamBranch().split('/')[-1])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002292 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002293 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002294 target_ref = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002295 if target_ref:
2296 upload_args.extend(['--target_ref', target_ref])
2297
2298 # Look for dependent patchsets. See crbug.com/480453 for more details.
2299 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2300 upstream_branch = ShortBranchName(upstream_branch)
2301 if remote is '.':
2302 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002303 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002304 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002305 print()
2306 print('Skipping dependency patchset upload because git config '
2307 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2308 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002309 else:
2310 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002311 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002312 auth_config=auth_config)
2313 branch_cl_issue_url = branch_cl.GetIssueURL()
2314 branch_cl_issue = branch_cl.GetIssue()
2315 branch_cl_patchset = branch_cl.GetPatchset()
2316 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2317 upload_args.extend(
2318 ['--depends_on_patchset', '%s:%s' % (
2319 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002320 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002321 '\n'
2322 'The current branch (%s) is tracking a local branch (%s) with '
2323 'an associated CL.\n'
2324 'Adding %s/#ps%s as a dependency patchset.\n'
2325 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2326 branch_cl_patchset))
2327
2328 project = settings.GetProject()
2329 if project:
2330 upload_args.extend(['--project', project])
Aaron Gable665a4392017-06-29 10:53:46 -07002331 else:
2332 print()
2333 print('WARNING: Uploading without a project specified. Please ensure '
2334 'your repo\'s codereview.settings has a "PROJECT: foo" line.')
2335 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002336
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002337 try:
2338 upload_args = ['upload'] + upload_args + args
2339 logging.info('upload.RealMain(%s)', upload_args)
2340 issue, patchset = upload.RealMain(upload_args)
2341 issue = int(issue)
2342 patchset = int(patchset)
2343 except KeyboardInterrupt:
2344 sys.exit(1)
2345 except:
2346 # If we got an exception after the user typed a description for their
2347 # change, back up the description before re-raising.
2348 if change_desc:
Christopher Lamf732cd52017-01-24 12:40:11 +11002349 SaveDescriptionBackup(change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002350 raise
2351
2352 if not self.GetIssue():
2353 self.SetIssue(issue)
2354 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002355 return 0
2356
Andrii Shyshkalov18975322017-01-25 16:44:13 +01002357
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002358class _GerritChangelistImpl(_ChangelistCodereviewBase):
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002359 def __init__(self, changelist, auth_config=None, codereview_host=None):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002360 # auth_config is Rietveld thing, kept here to preserve interface only.
2361 super(_GerritChangelistImpl, self).__init__(changelist)
2362 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002363 # Lazily cached values.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002364 self._gerrit_host = None # e.g. chromium-review.googlesource.com
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002365 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002366 # Map from change number (issue) to its detail cache.
2367 self._detail_cache = {}
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002368
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002369 if codereview_host is not None:
2370 assert not codereview_host.startswith('https://'), codereview_host
2371 self._gerrit_host = codereview_host
2372 self._gerrit_server = 'https://%s' % codereview_host
2373
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002374 def _GetGerritHost(self):
2375 # Lazy load of configs.
2376 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002377 if self._gerrit_host and '.' not in self._gerrit_host:
2378 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2379 # This happens for internal stuff http://crbug.com/614312.
2380 parsed = urlparse.urlparse(self.GetRemoteUrl())
2381 if parsed.scheme == 'sso':
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002382 print('WARNING: using non-https URLs for remote is likely broken\n'
tandriie32e3ea2016-06-22 02:52:48 -07002383 ' Your current remote is: %s' % self.GetRemoteUrl())
2384 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2385 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002386 return self._gerrit_host
2387
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002388 def _GetGitHost(self):
2389 """Returns git host to be used when uploading change to Gerrit."""
2390 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2391
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002392 def GetCodereviewServer(self):
2393 if not self._gerrit_server:
2394 # If we're on a branch then get the server potentially associated
2395 # with that branch.
2396 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002397 self._gerrit_server = self._GitGetBranchConfigValue(
2398 self.CodereviewServerConfigKey())
2399 if self._gerrit_server:
2400 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002401 if not self._gerrit_server:
2402 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2403 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002404 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002405 parts[0] = parts[0] + '-review'
2406 self._gerrit_host = '.'.join(parts)
2407 self._gerrit_server = 'https://%s' % self._gerrit_host
2408 return self._gerrit_server
2409
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002410 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002411 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002412 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002413
tandrii5d48c322016-08-18 16:19:37 -07002414 @classmethod
2415 def PatchsetConfigKey(cls):
2416 return 'gerritpatchset'
2417
2418 @classmethod
2419 def CodereviewServerConfigKey(cls):
2420 return 'gerritserver'
2421
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01002422 def EnsureAuthenticated(self, force, refresh=None):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002423 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002424 if settings.GetGerritSkipEnsureAuthenticated():
2425 # For projects with unusual authentication schemes.
2426 # See http://crbug.com/603378.
2427 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002428 # Lazy-loader to identify Gerrit and Git hosts.
2429 if gerrit_util.GceAuthenticator.is_gce():
2430 return
2431 self.GetCodereviewServer()
2432 git_host = self._GetGitHost()
2433 assert self._gerrit_server and self._gerrit_host
2434 cookie_auth = gerrit_util.CookiesAuthenticator()
2435
2436 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2437 git_auth = cookie_auth.get_auth_header(git_host)
2438 if gerrit_auth and git_auth:
2439 if gerrit_auth == git_auth:
2440 return
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002441 all_gsrc = cookie_auth.get_auth_header('d0esN0tEx1st.googlesource.com')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002442 print((
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002443 'WARNING: You have different credentials for Gerrit and git hosts:\n'
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002444 ' %s\n'
2445 ' %s\n'
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002446 ' Consider running the following command:\n'
2447 ' git cl creds-check\n'
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002448 ' %s\n'
Andrii Shyshkalov8e4576f2017-05-10 15:46:53 +02002449 ' %s') %
Andrii Shyshkalov51acef92017-04-11 17:19:59 +02002450 (git_host, self._gerrit_host,
Andrii Shyshkalov354e1d22017-06-09 19:31:33 +02002451 ('Hint: delete creds for .googlesource.com' if all_gsrc else ''),
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002452 cookie_auth.get_new_password_message(git_host)))
2453 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002454 confirm_or_exit('If you know what you are doing', action='continue')
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002455 return
2456 else:
2457 missing = (
Anna Henningsen4e891442017-07-06 21:40:58 +02002458 ([] if gerrit_auth else [self._gerrit_host]) +
2459 ([] if git_auth else [git_host]))
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002460 DieWithError('Credentials for the following hosts are required:\n'
2461 ' %s\n'
2462 'These are read from %s (or legacy %s)\n'
2463 '%s' % (
2464 '\n '.join(missing),
2465 cookie_auth.get_gitcookies_path(),
2466 cookie_auth.get_netrc_path(),
2467 cookie_auth.get_new_password_message(git_host)))
2468
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002469 def EnsureCanUploadPatchset(self, force):
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002470 if not self.GetIssue():
2471 return
2472
2473 # Warm change details cache now to avoid RPCs later, reducing latency for
2474 # developers.
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002475 self._GetChangeDetail(
2476 ['DETAILED_ACCOUNTS', 'CURRENT_REVISION', 'CURRENT_COMMIT'])
Andrii Shyshkalov3e631422017-02-16 17:46:44 +01002477
2478 status = self._GetChangeDetail()['status']
2479 if status in ('MERGED', 'ABANDONED'):
2480 DieWithError('Change %s has been %s, new uploads are not allowed' %
2481 (self.GetIssueURL(),
2482 'submitted' if status == 'MERGED' else 'abandoned'))
2483
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002484 if gerrit_util.GceAuthenticator.is_gce():
2485 return
2486 cookies_user = gerrit_util.CookiesAuthenticator().get_auth_email(
2487 self._GetGerritHost())
2488 if self.GetIssueOwner() == cookies_user:
2489 return
2490 logging.debug('change %s owner is %s, cookies user is %s',
2491 self.GetIssue(), self.GetIssueOwner(), cookies_user)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002492 # Maybe user has linked accounts or something like that,
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002493 # so ask what Gerrit thinks of this user.
2494 details = gerrit_util.GetAccountDetails(self._GetGerritHost(), 'self')
2495 if details['email'] == self.GetIssueOwner():
2496 return
2497 if not force:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002498 print('WARNING: Change %s is owned by %s, but you authenticate to Gerrit '
Andrii Shyshkalovbb86fbb2017-03-24 14:59:28 +01002499 'as %s.\n'
2500 'Uploading may fail due to lack of permissions.' %
2501 (self.GetIssue(), self.GetIssueOwner(), details['email']))
2502 confirm_or_exit(action='upload')
2503
2504
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002505 def _PostUnsetIssueProperties(self):
2506 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002507 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002508
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002509 def GetRietveldObjForPresubmit(self):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002510 class ThisIsNotRietveldIssue(object):
2511 def __nonzero__(self):
2512 # This is a hack to make presubmit_support think that rietveld is not
2513 # defined, yet still ensure that calls directly result in a decent
2514 # exception message below.
2515 return False
2516
2517 def __getattr__(self, attr):
2518 print(
2519 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2520 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002521 'Please, either change your PRESUBMIT to not use rietveld_obj.%s,\n'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002522 'or use Rietveld for codereview.\n'
2523 'See also http://crbug.com/579160.' % attr)
2524 raise NotImplementedError()
2525 return ThisIsNotRietveldIssue()
2526
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002527 def GetGerritObjForPresubmit(self):
2528 return presubmit_support.GerritAccessor(self._GetGerritHost())
2529
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002530 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002531 """Apply a rough heuristic to give a simple summary of an issue's review
2532 or CQ status, assuming adherence to a common workflow.
2533
2534 Returns None if no issue for this branch, or one of the following keywords:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002535 * 'error' - error from review tool (including deleted issues)
2536 * 'unsent' - no reviewers added
2537 * 'waiting' - waiting for review
2538 * 'reply' - waiting for uploader to reply to review
2539 * 'lgtm' - Code-Review label has been set
2540 * 'commit' - in the commit queue
2541 * 'closed' - successfully submitted or abandoned
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002542 """
2543 if not self.GetIssue():
2544 return None
2545
2546 try:
Aaron Gable9ab38c62017-04-06 14:36:33 -07002547 data = self._GetChangeDetail([
2548 'DETAILED_LABELS', 'CURRENT_REVISION', 'SUBMITTABLE'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002549 except (httplib.HTTPException, GerritChangeNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002550 return 'error'
2551
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002552 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002553 return 'closed'
2554
Aaron Gable9ab38c62017-04-06 14:36:33 -07002555 if data['labels'].get('Commit-Queue', {}).get('approved'):
2556 # The section will have an "approved" subsection if anyone has voted
2557 # the maximum value on the label.
2558 return 'commit'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002559
Aaron Gable9ab38c62017-04-06 14:36:33 -07002560 if data['labels'].get('Code-Review', {}).get('approved'):
2561 return 'lgtm'
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002562
2563 if not data.get('reviewers', {}).get('REVIEWER', []):
2564 return 'unsent'
2565
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002566 owner = data['owner'].get('_account_id')
Aaron Gable9ab38c62017-04-06 14:36:33 -07002567 messages = sorted(data.get('messages', []), key=lambda m: m.get('updated'))
2568 last_message_author = messages.pop().get('author', {})
2569 while last_message_author:
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002570 if last_message_author.get('email') == COMMIT_BOT_EMAIL:
2571 # Ignore replies from CQ.
Aaron Gable9ab38c62017-04-06 14:36:33 -07002572 last_message_author = messages.pop().get('author', {})
Andrii Shyshkalov33e88a42017-01-27 14:45:30 +01002573 continue
Aaron Gable9ab38c62017-04-06 14:36:33 -07002574 if last_message_author.get('_account_id') == owner:
2575 # Most recent message was by owner.
2576 return 'waiting'
2577 else:
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002578 # Some reply from non-owner.
2579 return 'reply'
Aaron Gable9ab38c62017-04-06 14:36:33 -07002580
2581 # Somehow there are no messages even though there are reviewers.
2582 return 'unsent'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002583
2584 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002585 data = self._GetChangeDetail(['CURRENT_REVISION'])
Aaron Gablee8856ee2017-12-07 12:41:46 -08002586 patchset = data['revisions'][data['current_revision']]['_number']
2587 self.SetPatchset(patchset)
2588 return patchset
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002589
Kenneth Russell61e2ed42017-02-15 11:47:13 -08002590 def FetchDescription(self, force=False):
2591 data = self._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'],
2592 no_cache=force)
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002593 current_rev = data['current_revision']
Andrii Shyshkalov9c3a4642017-01-24 17:41:22 +01002594 return data['revisions'][current_rev]['commit']['message']
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002595
dsansomee2d6fd92016-09-08 00:10:47 -07002596 def UpdateDescriptionRemote(self, description, force=False):
2597 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2598 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002599 confirm_or_exit(
dsansomee2d6fd92016-09-08 00:10:47 -07002600 'The description cannot be modified while the issue has a pending '
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002601 'unpublished edit. Either publish the edit in the Gerrit web UI '
2602 'or delete it.\n\n', action='delete the unpublished edit')
dsansomee2d6fd92016-09-08 00:10:47 -07002603
2604 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2605 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002606 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
Andrii Shyshkalovea4fc832016-12-01 14:53:23 +01002607 description, notify='NONE')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002608
Aaron Gable636b13f2017-07-14 10:42:48 -07002609 def AddComment(self, message, publish=None):
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002610 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
Aaron Gable636b13f2017-07-14 10:42:48 -07002611 msg=message, ready=publish)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01002612
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002613 def GetCommentsSummary(self, readable=True):
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002614 # DETAILED_ACCOUNTS is to get emails in accounts.
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002615 messages = self._GetChangeDetail(
2616 options=['MESSAGES', 'DETAILED_ACCOUNTS']).get('messages', [])
2617 file_comments = gerrit_util.GetChangeComments(
2618 self._GetGerritHost(), self.GetIssue())
2619
2620 # Build dictionary of file comments for easy access and sorting later.
2621 # {author+date: {path: {patchset: {line: url+message}}}}
2622 comments = collections.defaultdict(
2623 lambda: collections.defaultdict(lambda: collections.defaultdict(dict)))
2624 for path, line_comments in file_comments.iteritems():
2625 for comment in line_comments:
2626 if comment.get('tag', '').startswith('autogenerated'):
2627 continue
2628 key = (comment['author']['email'], comment['updated'])
2629 if comment.get('side', 'REVISION') == 'PARENT':
2630 patchset = 'Base'
2631 else:
2632 patchset = 'PS%d' % comment['patch_set']
2633 line = comment.get('line', 0)
2634 url = ('https://%s/c/%s/%s/%s#%s%s' %
2635 (self._GetGerritHost(), self.GetIssue(), comment['patch_set'], path,
2636 'b' if comment.get('side') == 'PARENT' else '',
2637 str(line) if line else ''))
2638 comments[key][path][patchset][line] = (url, comment['message'])
2639
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002640 summary = []
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002641 for msg in messages:
2642 # Don't bother showing autogenerated messages.
2643 if msg.get('tag') and msg.get('tag').startswith('autogenerated'):
2644 continue
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002645 # Gerrit spits out nanoseconds.
2646 assert len(msg['date'].split('.')[-1]) == 9
2647 date = datetime.datetime.strptime(msg['date'][:-3],
2648 '%Y-%m-%d %H:%M:%S.%f')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002649 message = msg['message']
2650 key = (msg['author']['email'], msg['date'])
2651 if key in comments:
2652 message += '\n'
2653 for path, patchsets in sorted(comments.get(key, {}).items()):
2654 if readable:
2655 message += '\n%s' % path
2656 for patchset, lines in sorted(patchsets.items()):
2657 for line, (url, content) in sorted(lines.items()):
2658 if line:
2659 line_str = 'Line %d' % line
2660 path_str = '%s:%d:' % (path, line)
2661 else:
2662 line_str = 'File comment'
2663 path_str = '%s:0:' % path
2664 if readable:
2665 message += '\n %s, %s: %s' % (patchset, line_str, url)
2666 message += '\n %s\n' % content
2667 else:
2668 message += '\n%s ' % path_str
2669 message += '\n%s\n' % content
2670
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002671 summary.append(_CommentSummary(
2672 date=date,
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07002673 message=message,
Andrii Shyshkalov5a0cf202017-03-17 16:14:59 +01002674 sender=msg['author']['email'],
2675 # These could be inferred from the text messages and correlated with
2676 # Code-Review label maximum, however this is not reliable.
2677 # Leaving as is until the need arises.
2678 approval=False,
2679 disapproval=False,
2680 ))
2681 return summary
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01002682
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002683 def CloseIssue(self):
2684 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2685
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002686 def SubmitIssue(self, wait_for_merge=True):
2687 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2688 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002689
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002690 def _GetChangeDetail(self, options=None, issue=None,
2691 no_cache=False):
2692 """Returns details of the issue by querying Gerrit and caching results.
2693
2694 If fresh data is needed, set no_cache=True which will clear cache and
2695 thus new data will be fetched from Gerrit.
2696 """
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002697 options = options or []
2698 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002699 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002700
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01002701 # Optimization to avoid multiple RPCs:
2702 if (('CURRENT_REVISION' in options or 'ALL_REVISIONS' in options) and
2703 'CURRENT_COMMIT' not in options):
2704 options.append('CURRENT_COMMIT')
2705
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002706 # Normalize issue and options for consistent keys in cache.
2707 issue = str(issue)
2708 options = [o.upper() for o in options]
2709
2710 # Check in cache first unless no_cache is True.
2711 if no_cache:
2712 self._detail_cache.pop(issue, None)
2713 else:
2714 options_set = frozenset(options)
2715 for cached_options_set, data in self._detail_cache.get(issue, []):
2716 # Assumption: data fetched before with extra options is suitable
2717 # for return for a smaller set of options.
2718 # For example, if we cached data for
2719 # options=[CURRENT_REVISION, DETAILED_FOOTERS]
2720 # and request is for options=[CURRENT_REVISION],
2721 # THEN we can return prior cached data.
2722 if options_set.issubset(cached_options_set):
2723 return data
2724
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002725 try:
Aaron Gable19ee16c2017-04-18 11:56:35 -07002726 data = gerrit_util.GetChangeDetail(
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002727 self._GetGerritHost(), str(issue), options)
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002728 except gerrit_util.GerritError as e:
2729 if e.http_status == 404:
Aaron Gablea45ee112016-11-22 15:14:38 -08002730 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002731 raise
Andrii Shyshkalov258e0a62017-01-24 16:50:57 +01002732
2733 self._detail_cache.setdefault(issue, []).append((frozenset(options), data))
tandriic2405f52016-10-10 08:13:15 -07002734 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002735
agable32978d92016-11-01 12:55:02 -07002736 def _GetChangeCommit(self, issue=None):
2737 issue = issue or self.GetIssue()
2738 assert issue, 'issue is required to query Gerrit'
Aaron Gable6f5a8d92017-04-18 14:49:05 -07002739 try:
2740 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2741 except gerrit_util.GerritError as e:
2742 if e.http_status == 404:
2743 raise GerritChangeNotExists(issue, self.GetCodereviewServer())
2744 raise
agable32978d92016-11-01 12:55:02 -07002745 return data
2746
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002747 def CMDLand(self, force, bypass_hooks, verbose):
2748 if git_common.is_dirty_git_tree('land'):
2749 return 1
tandriid60367b2016-06-22 05:25:12 -07002750 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2751 if u'Commit-Queue' in detail.get('labels', {}):
2752 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002753 confirm_or_exit('\nIt seems this repository has a Commit Queue, '
2754 'which can test and land changes for you. '
2755 'Are you sure you wish to bypass it?\n',
2756 action='bypass CQ')
tandriid60367b2016-06-22 05:25:12 -07002757
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002758 differs = True
tandriic4344b52016-08-29 06:04:54 -07002759 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002760 # Note: git diff outputs nothing if there is no diff.
2761 if not last_upload or RunGit(['diff', last_upload]).strip():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002762 print('WARNING: Some changes from local branch haven\'t been uploaded.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002763 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002764 if detail['current_revision'] == last_upload:
2765 differs = False
2766 else:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002767 print('WARNING: Local branch contents differ from latest uploaded '
2768 'patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002769 if differs:
2770 if not force:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002771 confirm_or_exit(
2772 'Do you want to submit latest Gerrit patchset and bypass hooks?\n',
2773 action='submit')
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002774 print('WARNING: Bypassing hooks and submitting latest uploaded patchset.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002775 elif not bypass_hooks:
2776 hook_results = self.RunHook(
2777 committing=True,
2778 may_prompt=not force,
2779 verbose=verbose,
2780 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2781 if not hook_results.should_continue():
2782 return 1
2783
2784 self.SubmitIssue(wait_for_merge=True)
2785 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002786 links = self._GetChangeCommit().get('web_links', [])
2787 for link in links:
Aaron Gable02cdbb42016-12-13 16:24:25 -08002788 if link.get('name') == 'gitiles' and link.get('url'):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002789 print('Landed as: %s' % link.get('url'))
agable32978d92016-11-01 12:55:02 -07002790 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002791 return 0
2792
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002793 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
Aaron Gable62619a32017-06-16 08:22:09 -07002794 directory, force):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002795 assert not reject
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002796 assert not directory
2797 assert parsed_issue_arg.valid
2798
2799 self._changelist.issue = parsed_issue_arg.issue
2800
2801 if parsed_issue_arg.hostname:
2802 self._gerrit_host = parsed_issue_arg.hostname
2803 self._gerrit_server = 'https://%s' % self._gerrit_host
2804
tandriic2405f52016-10-10 08:13:15 -07002805 try:
2806 detail = self._GetChangeDetail(['ALL_REVISIONS'])
Aaron Gablea45ee112016-11-22 15:14:38 -08002807 except GerritChangeNotExists as e:
tandriic2405f52016-10-10 08:13:15 -07002808 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002809
2810 if not parsed_issue_arg.patchset:
2811 # Use current revision by default.
2812 revision_info = detail['revisions'][detail['current_revision']]
2813 patchset = int(revision_info['_number'])
2814 else:
2815 patchset = parsed_issue_arg.patchset
2816 for revision_info in detail['revisions'].itervalues():
2817 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2818 break
2819 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08002820 DieWithError('Couldn\'t find patchset %i in change %i' %
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002821 (parsed_issue_arg.patchset, self.GetIssue()))
2822
2823 fetch_info = revision_info['fetch']['http']
2824 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
Aaron Gable9387b4f2017-06-08 10:50:03 -07002825
Aaron Gable62619a32017-06-16 08:22:09 -07002826 if force:
2827 RunGit(['reset', '--hard', 'FETCH_HEAD'])
2828 print('Checked out commit for change %i patchset %i locally' %
2829 (parsed_issue_arg.issue, patchset))
Stefan Zager2d5f0392017-10-10 15:17:53 -07002830 elif nocommit:
2831 RunGit(['cherry-pick', '--no-commit', 'FETCH_HEAD'])
2832 print('Patch applied to index.')
Aaron Gable62619a32017-06-16 08:22:09 -07002833 else:
Aaron Gable9387b4f2017-06-08 10:50:03 -07002834 RunGit(['cherry-pick', 'FETCH_HEAD'])
2835 print('Committed patch for change %i patchset %i locally.' %
Aaron Gable62619a32017-06-16 08:22:09 -07002836 (parsed_issue_arg.issue, patchset))
2837 print('Note: this created a local commit which does not have '
2838 'the same hash as the one uploaded for review. This will make '
2839 'uploading changes based on top of this branch difficult.\n'
2840 'If you want to do that, use "git cl patch --force" instead.')
2841
Stefan Zagerd08043c2017-10-12 12:07:02 -07002842 if self.GetBranch():
2843 self.SetIssue(parsed_issue_arg.issue)
2844 self.SetPatchset(patchset)
2845 fetched_hash = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2846 self._GitSetBranchConfigValue('last-upload-hash', fetched_hash)
2847 self._GitSetBranchConfigValue('gerritsquashhash', fetched_hash)
2848 else:
2849 print('WARNING: You are in detached HEAD state.\n'
2850 'The patch has been applied to your checkout, but you will not be '
2851 'able to upload a new patch set to the gerrit issue.\n'
2852 'Try using the \'-b\' option if you would like to work on a '
2853 'branch and/or upload a new patch set.')
2854
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002855 return 0
2856
2857 @staticmethod
2858 def ParseIssueURL(parsed_url):
2859 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2860 return None
Aaron Gable01b91062017-08-24 17:48:40 -07002861 # Gerrit's new UI is https://domain/c/project/+/<issue_number>[/[patchset]]
2862 # But old GWT UI is https://domain/#/c/project/+/<issue_number>[/[patchset]]
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002863 # Short urls like https://domain/<issue_number> can be used, but don't allow
2864 # specifying the patchset (you'd 404), but we allow that here.
2865 if parsed_url.path == '/':
2866 part = parsed_url.fragment
2867 else:
2868 part = parsed_url.path
Aaron Gable01b91062017-08-24 17:48:40 -07002869 match = re.match('(/c(/.*/\+)?)?/(\d+)(/(\d+)?/?)?$', part)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002870 if match:
2871 return _ParsedIssueNumberArgument(
Aaron Gable01b91062017-08-24 17:48:40 -07002872 issue=int(match.group(3)),
2873 patchset=int(match.group(5)) if match.group(5) else None,
Andrii Shyshkalov90f31922017-04-10 16:10:21 +02002874 hostname=parsed_url.netloc,
2875 codereview='gerrit')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002876 return None
2877
tandrii16e0b4e2016-06-07 10:34:28 -07002878 def _GerritCommitMsgHookCheck(self, offer_removal):
2879 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2880 if not os.path.exists(hook):
2881 return
2882 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2883 # custom developer made one.
2884 data = gclient_utils.FileRead(hook)
2885 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2886 return
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002887 print('WARNING: You have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002888 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002889 'and may interfere with it in subtle ways.\n'
2890 'We recommend you remove the commit-msg hook.')
2891 if offer_removal:
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002892 if ask_for_explicit_yes('Do you want to remove it now?'):
tandrii16e0b4e2016-06-07 10:34:28 -07002893 gclient_utils.rm_file_or_tree(hook)
2894 print('Gerrit commit-msg hook removed.')
2895 else:
2896 print('OK, will keep Gerrit commit-msg hook in place.')
2897
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002898 def CMDUploadChange(self, options, git_diff_args, custom_cl_base, change):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002899 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002900 if options.squash and options.no_squash:
2901 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002902
2903 if not options.squash and not options.no_squash:
2904 # Load default for user, repo, squash=true, in this order.
2905 options.squash = settings.GetSquashGerritUploads()
2906 elif options.no_squash:
2907 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002908
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002909 remote, remote_branch = self.GetRemoteBranch()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01002910 branch = GetTargetRef(remote, remote_branch, options.target_branch)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002911
Aaron Gableb56ad332017-01-06 15:24:31 -08002912 # This may be None; default fallback value is determined in logic below.
2913 title = options.title
2914
Dominic Battre7d1c4842017-10-27 09:17:28 +02002915 # Extract bug number from branch name.
2916 bug = options.bug
2917 match = re.match(r'(?:bug|fix)[_-]?(\d+)', self.GetBranch())
2918 if not bug and match:
2919 bug = match.group(1)
2920
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002921 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002922 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002923 if self.GetIssue():
2924 # Try to get the message from a previous upload.
2925 message = self.GetDescription()
2926 if not message:
2927 DieWithError(
Aaron Gablea45ee112016-11-22 15:14:38 -08002928 'failed to fetch description from current Gerrit change %d\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002929 '%s' % (self.GetIssue(), self.GetIssueURL()))
Aaron Gableb56ad332017-01-06 15:24:31 -08002930 if not title:
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002931 if options.message:
Aaron Gable7303dcb2017-06-07 14:17:32 -07002932 # When uploading a subsequent patchset, -m|--message is taken
2933 # as the patchset title if --title was not provided.
2934 title = options.message.strip()
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02002935 else:
2936 default_title = RunGit(
2937 ['show', '-s', '--format=%s', 'HEAD']).strip()
Aaron Gable7303dcb2017-06-07 14:17:32 -07002938 if options.force:
2939 title = default_title
2940 else:
2941 title = ask_for_data(
2942 'Title for patchset [%s]: ' % default_title) or default_title
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002943 change_id = self._GetChangeDetail()['change_id']
2944 while True:
2945 footer_change_ids = git_footers.get_footer_change_id(message)
2946 if footer_change_ids == [change_id]:
2947 break
2948 if not footer_change_ids:
2949 message = git_footers.add_footer_change_id(message, change_id)
Quinten Yearsley0c62da92017-05-31 13:39:42 -07002950 print('WARNING: appended missing Change-Id to change description.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002951 continue
2952 # There is already a valid footer but with different or several ids.
2953 # Doing this automatically is non-trivial as we don't want to lose
2954 # existing other footers, yet we want to append just 1 desired
2955 # Change-Id. Thus, just create a new footer, but let user verify the
2956 # new description.
2957 message = '%s\n\nChange-Id: %s' % (message, change_id)
2958 print(
Aaron Gablea45ee112016-11-22 15:14:38 -08002959 'WARNING: change %s has Change-Id footer(s):\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002960 ' %s\n'
Aaron Gablea45ee112016-11-22 15:14:38 -08002961 'but change has Change-Id %s, according to Gerrit.\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002962 'Please, check the proposed correction to the description, '
2963 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2964 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2965 change_id))
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01002966 confirm_or_exit(action='edit')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002967 if not options.force:
2968 change_desc = ChangeDescription(message)
Dominic Battre7d1c4842017-10-27 09:17:28 +02002969 change_desc.prompt(bug=bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002970 message = change_desc.description
2971 if not message:
2972 DieWithError("Description is empty. Aborting...")
2973 # Continue the while loop.
2974 # Sanity check of this code - we should end up with proper message
2975 # footer.
2976 assert [change_id] == git_footers.get_footer_change_id(message)
2977 change_desc = ChangeDescription(message)
Aaron Gableb56ad332017-01-06 15:24:31 -08002978 else: # if not self.GetIssue()
2979 if options.message:
2980 message = options.message
2981 else:
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02002982 message = CreateDescriptionFromLog(git_diff_args)
Aaron Gableb56ad332017-01-06 15:24:31 -08002983 if options.title:
2984 message = options.title + '\n\n' + message
2985 change_desc = ChangeDescription(message)
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002986
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002987 if not options.force:
Dominic Battre7d1c4842017-10-27 09:17:28 +02002988 change_desc.prompt(bug=bug)
Aaron Gableb56ad332017-01-06 15:24:31 -08002989 # On first upload, patchset title is always this string, while
2990 # --title flag gets converted to first line of message.
2991 title = 'Initial upload'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002992 if not change_desc.description:
2993 DieWithError("Description is empty. Aborting...")
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002994 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002995 if len(change_ids) > 1:
2996 DieWithError('too many Change-Id footers, at most 1 allowed.')
2997 if not change_ids:
2998 # Generate the Change-Id automatically.
Andrii Shyshkalov8c90d032017-04-19 21:27:26 +02002999 change_desc.set_description(git_footers.add_footer_change_id(
3000 change_desc.description,
3001 GenerateGerritChangeId(change_desc.description)))
3002 change_ids = git_footers.get_footer_change_id(change_desc.description)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003003 assert len(change_ids) == 1
3004 change_id = change_ids[0]
3005
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003006 if options.reviewers or options.tbrs or options.add_owners_to:
3007 change_desc.update_reviewers(options.reviewers, options.tbrs,
3008 options.add_owners_to, change)
3009
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003010 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003011 parent = self._ComputeParent(remote, upstream_branch, custom_cl_base,
3012 options.force, change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003013 tree = RunGit(['rev-parse', 'HEAD:']).strip()
Aaron Gable9a03ae02017-11-03 11:31:07 -07003014 with tempfile.NamedTemporaryFile(delete=False) as desc_tempfile:
3015 desc_tempfile.write(change_desc.description)
3016 desc_tempfile.close()
3017 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
3018 '-F', desc_tempfile.name]).strip()
3019 os.remove(desc_tempfile.name)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003020 else:
3021 change_desc = ChangeDescription(
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003022 options.message or CreateDescriptionFromLog(git_diff_args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003023 if not change_desc.description:
3024 DieWithError("Description is empty. Aborting...")
3025
3026 if not git_footers.get_footer_change_id(change_desc.description):
3027 DownloadGerritHook(False)
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003028 change_desc.set_description(
3029 self._AddChangeIdToCommitMessage(options, git_diff_args))
Robert Iannuccidb02dd02017-04-19 12:18:20 -07003030 if options.reviewers or options.tbrs or options.add_owners_to:
3031 change_desc.update_reviewers(options.reviewers, options.tbrs,
3032 options.add_owners_to, change)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003033 ref_to_push = 'HEAD'
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003034 # For no-squash mode, we assume the remote called "origin" is the one we
3035 # want. It is not worthwhile to support different workflows for
3036 # no-squash mode.
3037 parent = 'origin/%s' % branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003038 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
3039
3040 assert change_desc
3041 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
3042 ref_to_push)]).splitlines()
3043 if len(commits) > 1:
3044 print('WARNING: This will upload %d commits. Run the following command '
3045 'to see which commits will be uploaded: ' % len(commits))
3046 print('git log %s..%s' % (parent, ref_to_push))
3047 print('You can also use `git squash-branch` to squash these into a '
3048 'single commit.')
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01003049 confirm_or_exit(action='upload')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003050
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003051 if options.reviewers or options.tbrs or options.add_owners_to:
3052 change_desc.update_reviewers(options.reviewers, options.tbrs,
3053 options.add_owners_to, change)
3054
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003055 # Extra options that can be specified at push time. Doc:
3056 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003057 refspec_opts = []
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003058
Aaron Gable844cf292017-06-28 11:32:59 -07003059 # By default, new changes are started in WIP mode, and subsequent patchsets
3060 # don't send email. At any time, passing --send-mail will mark the change
3061 # ready and send email for that particular patch.
Aaron Gableafd52772017-06-27 16:40:10 -07003062 if options.send_mail:
3063 refspec_opts.append('ready')
Aaron Gable844cf292017-06-28 11:32:59 -07003064 refspec_opts.append('notify=ALL')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003065 elif not self.GetIssue():
3066 refspec_opts.append('wip')
Aaron Gableafd52772017-06-27 16:40:10 -07003067 else:
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003068 refspec_opts.append('notify=NONE')
Aaron Gable70f4e242017-06-26 10:45:59 -07003069
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003070 # TODO(tandrii): options.message should be posted as a comment
Aaron Gablee5adf612017-07-14 10:43:58 -07003071 # if --send-mail is set on non-initial upload as Rietveld used to do it.
Andrii Shyshkalov2d8a2072017-04-10 15:02:18 +02003072
Aaron Gable9b713dd2016-12-14 16:04:21 -08003073 if title:
Nick Carter8692b182017-11-06 16:30:38 -08003074 # Punctuation and whitespace in |title| must be percent-encoded.
3075 refspec_opts.append('m=' + gerrit_util.PercentEncodeForGitRef(title))
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00003076
agablec6787972016-09-09 16:13:34 -07003077 if options.private:
Aaron Gableb02daf02017-05-23 11:53:46 -07003078 refspec_opts.append('private')
agablec6787972016-09-09 16:13:34 -07003079
rmistry9eadede2016-09-19 11:22:43 -07003080 if options.topic:
3081 # Documentation on Gerrit topics is here:
3082 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003083 refspec_opts.append('topic=%s' % options.topic)
rmistry9eadede2016-09-19 11:22:43 -07003084
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003085 # Gerrit sorts hashtags, so order is not important.
Nodir Turakulov23b82142017-11-16 11:04:25 -08003086 hashtags = {change_desc.sanitize_hash_tag(t) for t in options.hashtags}
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003087 if not self.GetIssue():
Nodir Turakulov23b82142017-11-16 11:04:25 -08003088 hashtags.update(change_desc.get_hash_tags())
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08003089 refspec_opts += ['hashtag=%s' % t for t in sorted(hashtags)]
3090
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003091 refspec_suffix = ''
3092 if refspec_opts:
3093 refspec_suffix = '%' + ','.join(refspec_opts)
3094 assert ' ' not in refspec_suffix, (
3095 'spaces not allowed in refspec: "%s"' % refspec_suffix)
3096 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
3097
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003098 try:
3099 push_stdout = gclient_utils.CheckCallAndFilter(
Andrii Shyshkalovfebbae92017-04-05 15:05:20 +00003100 ['git', 'push', self.GetRemoteUrl(), refspec],
3101 print_stdout=True,
Andrii Shyshkalov7d518832016-12-15 20:48:21 +01003102 # Flush after every line: useful for seeing progress when running as
3103 # recipe.
3104 filter_fn=lambda _: sys.stdout.flush())
3105 except subprocess2.CalledProcessError:
3106 DieWithError('Failed to create a change. Please examine output above '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003107 'for the reason of the failure.\n'
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003108 'Hint: run command below to diagnose common Git/Gerrit '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02003109 'credential problems:\n'
3110 ' git cl creds-check\n',
3111 change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003112
3113 if options.squash:
Aaron Gable289b4312017-09-13 14:06:16 -07003114 regex = re.compile(r'remote:\s+https?://[\w\-\.\+\/#]*/(\d+)\s.*')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003115 change_numbers = [m.group(1)
3116 for m in map(regex.match, push_stdout.splitlines())
3117 if m]
3118 if len(change_numbers) != 1:
3119 DieWithError(
3120 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
Christopher Lamf732cd52017-01-24 12:40:11 +11003121 'Change-Id: %s') % (len(change_numbers), change_id), change_desc)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003122 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07003123 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07003124
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003125 reviewers = sorted(change_desc.get_reviewers())
3126
tandrii88189772016-09-29 04:29:57 -07003127 # Add cc's from the CC_LIST and --cc flag (if any).
Aaron Gabled1052492017-05-15 15:05:34 -07003128 if not options.private:
3129 cc = self.GetCCList().split(',')
3130 else:
3131 cc = []
tandrii88189772016-09-29 04:29:57 -07003132 if options.cc:
3133 cc.extend(options.cc)
3134 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07003135 if change_desc.get_cced():
3136 cc.extend(change_desc.get_cced())
Aaron Gable6dadfbf2017-05-09 14:27:58 -07003137
3138 gerrit_util.AddReviewers(
3139 self._GetGerritHost(), self.GetIssue(), reviewers, cc,
3140 notify=bool(options.send_mail))
3141
Aaron Gablefd238082017-06-07 13:42:34 -07003142 if change_desc.get_reviewers(tbr_only=True):
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003143 labels = self._GetChangeDetail(['LABELS']).get('labels', {})
3144 score = 1
3145 if 'Code-Review' in labels and 'values' in labels['Code-Review']:
3146 score = max([int(x) for x in labels['Code-Review']['values'].keys()])
3147 print('Adding self-LGTM (Code-Review +%d) because of TBRs.' % score)
Aaron Gablefd238082017-06-07 13:42:34 -07003148 gerrit_util.SetReview(
3149 self._GetGerritHost(), self.GetIssue(),
Yoshisato Yanagisawa81e3ff52017-09-26 15:33:34 +09003150 msg='Self-approving for TBR',
3151 labels={'Code-Review': score})
Aaron Gablefd238082017-06-07 13:42:34 -07003152
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003153 return 0
3154
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003155 def _ComputeParent(self, remote, upstream_branch, custom_cl_base, force,
3156 change_desc):
3157 """Computes parent of the generated commit to be uploaded to Gerrit.
3158
3159 Returns revision or a ref name.
3160 """
3161 if custom_cl_base:
3162 # Try to avoid creating additional unintended CLs when uploading, unless
3163 # user wants to take this risk.
3164 local_ref_of_target_remote = self.GetRemoteBranch()[1]
3165 code, _ = RunGitWithCode(['merge-base', '--is-ancestor', custom_cl_base,
3166 local_ref_of_target_remote])
3167 if code == 1:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003168 print('\nWARNING: Manually specified base of this CL `%s` '
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003169 'doesn\'t seem to belong to target remote branch `%s`.\n\n'
3170 'If you proceed with upload, more than 1 CL may be created by '
3171 'Gerrit as a result, in turn confusing or crashing git cl.\n\n'
3172 'If you are certain that specified base `%s` has already been '
3173 'uploaded to Gerrit as another CL, you may proceed.\n' %
3174 (custom_cl_base, local_ref_of_target_remote, custom_cl_base))
3175 if not force:
3176 confirm_or_exit(
3177 'Do you take responsibility for cleaning up potential mess '
3178 'resulting from proceeding with upload?',
3179 action='upload')
3180 return custom_cl_base
3181
Aaron Gablef97e33d2017-03-30 15:44:27 -07003182 if remote != '.':
3183 return self.GetCommonAncestorWithUpstream()
3184
3185 # If our upstream branch is local, we base our squashed commit on its
3186 # squashed version.
3187 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
3188
Aaron Gablef97e33d2017-03-30 15:44:27 -07003189 if upstream_branch_name == 'master':
Aaron Gable0bbd1c22017-05-08 14:37:08 -07003190 return self.GetCommonAncestorWithUpstream()
Aaron Gablef97e33d2017-03-30 15:44:27 -07003191
3192 # Check the squashed hash of the parent.
Andrii Shyshkalov550e9242017-04-12 17:14:49 +02003193 # TODO(tandrii): consider checking parent change in Gerrit and using its
3194 # hash if tree hash of latest parent revision (patchset) in Gerrit matches
3195 # the tree hash of the parent branch. The upside is less likely bogus
3196 # requests to reupload parent change just because it's uploadhash is
3197 # missing, yet the downside likely exists, too (albeit unknown to me yet).
Aaron Gablef97e33d2017-03-30 15:44:27 -07003198 parent = RunGit(['config',
3199 'branch.%s.gerritsquashhash' % upstream_branch_name],
3200 error_ok=True).strip()
3201 # Verify that the upstream branch has been uploaded too, otherwise
3202 # Gerrit will create additional CLs when uploading.
3203 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
3204 RunGitSilent(['rev-parse', parent + ':'])):
3205 DieWithError(
3206 '\nUpload upstream branch %s first.\n'
3207 'It is likely that this branch has been rebased since its last '
3208 'upload, so you just need to upload it again.\n'
3209 '(If you uploaded it with --no-squash, then branch dependencies '
3210 'are not supported, and you should reupload with --squash.)'
3211 % upstream_branch_name,
3212 change_desc)
3213 return parent
3214
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003215 def _AddChangeIdToCommitMessage(self, options, args):
3216 """Re-commits using the current message, assumes the commit hook is in
3217 place.
3218 """
3219 log_desc = options.message or CreateDescriptionFromLog(args)
3220 git_command = ['commit', '--amend', '-m', log_desc]
3221 RunGit(git_command)
3222 new_log_desc = CreateDescriptionFromLog(args)
3223 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07003224 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003225 return new_log_desc
3226 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00003227 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00003228
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003229 def SetCQState(self, new_state):
3230 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003231 vote_map = {
3232 _CQState.NONE: 0,
3233 _CQState.DRY_RUN: 1,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003234 _CQState.COMMIT: 2,
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003235 }
Aaron Gablefc62f762017-07-17 11:12:07 -07003236 labels = {'Commit-Queue': vote_map[new_state]}
3237 notify = False if new_state == _CQState.DRY_RUN else None
3238 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
3239 labels=labels, notify=notify)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00003240
tandriie113dfd2016-10-11 10:20:12 -07003241 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07003242 try:
3243 data = self._GetChangeDetail()
Aaron Gablea45ee112016-11-22 15:14:38 -08003244 except GerritChangeNotExists:
3245 return 'Gerrit doesn\'t know about your change %s' % self.GetIssue()
tandrii8c5a3532016-11-04 07:52:02 -07003246
3247 if data['status'] in ('ABANDONED', 'MERGED'):
3248 return 'CL %s is closed' % self.GetIssue()
3249
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003250 def GetTryJobProperties(self, patchset=None):
3251 """Returns dictionary of properties to launch try job."""
tandrii8c5a3532016-11-04 07:52:02 -07003252 data = self._GetChangeDetail(['ALL_REVISIONS'])
3253 patchset = int(patchset or self.GetPatchset())
3254 assert patchset
3255 revision_data = None # Pylint wants it to be defined.
3256 for revision_data in data['revisions'].itervalues():
3257 if int(revision_data['_number']) == patchset:
3258 break
3259 else:
Aaron Gablea45ee112016-11-22 15:14:38 -08003260 raise Exception('Patchset %d is not known in Gerrit change %d' %
tandrii8c5a3532016-11-04 07:52:02 -07003261 (patchset, self.GetIssue()))
3262 return {
3263 'patch_issue': self.GetIssue(),
3264 'patch_set': patchset or self.GetPatchset(),
3265 'patch_project': data['project'],
3266 'patch_storage': 'gerrit',
3267 'patch_ref': revision_data['fetch']['http']['ref'],
3268 'patch_repository_url': revision_data['fetch']['http']['url'],
3269 'patch_gerrit_url': self.GetCodereviewServer(),
3270 }
tandriie113dfd2016-10-11 10:20:12 -07003271
tandriide281ae2016-10-12 06:02:30 -07003272 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07003273 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07003274
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00003275
3276_CODEREVIEW_IMPLEMENTATIONS = {
3277 'rietveld': _RietveldChangelistImpl,
3278 'gerrit': _GerritChangelistImpl,
3279}
3280
tandrii@chromium.org013a2802016-03-29 09:52:33 +00003281
iannuccie53c9352016-08-17 14:40:40 -07003282def _add_codereview_issue_select_options(parser, extra=""):
3283 _add_codereview_select_options(parser)
3284
3285 text = ('Operate on this issue number instead of the current branch\'s '
3286 'implicit issue.')
3287 if extra:
3288 text += ' '+extra
3289 parser.add_option('-i', '--issue', type=int, help=text)
3290
3291
3292def _process_codereview_issue_select_options(parser, options):
3293 _process_codereview_select_options(parser, options)
3294 if options.issue is not None and not options.forced_codereview:
3295 parser.error('--issue must be specified with either --rietveld or --gerrit')
3296
3297
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003298def _add_codereview_select_options(parser):
3299 """Appends --gerrit and --rietveld options to force specific codereview."""
3300 parser.codereview_group = optparse.OptionGroup(
3301 parser, 'EXPERIMENTAL! Codereview override options')
3302 parser.add_option_group(parser.codereview_group)
3303 parser.codereview_group.add_option(
3304 '--gerrit', action='store_true',
3305 help='Force the use of Gerrit for codereview')
3306 parser.codereview_group.add_option(
3307 '--rietveld', action='store_true',
3308 help='Force the use of Rietveld for codereview')
3309
3310
3311def _process_codereview_select_options(parser, options):
3312 if options.gerrit and options.rietveld:
3313 parser.error('Options --gerrit and --rietveld are mutually exclusive')
3314 options.forced_codereview = None
3315 if options.gerrit:
3316 options.forced_codereview = 'gerrit'
3317 elif options.rietveld:
3318 options.forced_codereview = 'rietveld'
3319
3320
tandriif9aefb72016-07-01 09:06:51 -07003321def _get_bug_line_values(default_project, bugs):
3322 """Given default_project and comma separated list of bugs, yields bug line
3323 values.
3324
3325 Each bug can be either:
3326 * a number, which is combined with default_project
3327 * string, which is left as is.
3328
3329 This function may produce more than one line, because bugdroid expects one
3330 project per line.
3331
3332 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
3333 ['v8:123', 'chromium:789']
3334 """
3335 default_bugs = []
3336 others = []
3337 for bug in bugs.split(','):
3338 bug = bug.strip()
3339 if bug:
3340 try:
3341 default_bugs.append(int(bug))
3342 except ValueError:
3343 others.append(bug)
3344
3345 if default_bugs:
3346 default_bugs = ','.join(map(str, default_bugs))
3347 if default_project:
3348 yield '%s:%s' % (default_project, default_bugs)
3349 else:
3350 yield default_bugs
3351 for other in sorted(others):
3352 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
3353 yield other
3354
3355
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003356class ChangeDescription(object):
3357 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00003358 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003359 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
Aaron Gable3a16ed12017-03-23 10:51:55 -07003360 BUG_LINE = r'^[ \t]*(?:(BUG)[ \t]*=|Bug:)[ \t]*(.*?)[ \t]*$'
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003361 CHERRY_PICK_LINE = r'^\(cherry picked from commit [a-fA-F0-9]{40}\)$'
Nodir Turakulov23b82142017-11-16 11:04:25 -08003362 STRIP_HASH_TAG_PREFIX = r'^(\s*(revert|reland)( "|:)?\s*)*'
3363 BRACKET_HASH_TAG = r'\s*\[([^\[\]]+)\]'
3364 COLON_SEPARATED_HASH_TAG = r'^([a-zA-Z0-9_\- ]+):'
3365 BAD_HASH_TAG_CHUNK = r'[^a-zA-Z0-9]+'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003366
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003367 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003368 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003369
agable@chromium.org42c20792013-09-12 17:34:49 +00003370 @property # www.logilab.org/ticket/89786
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08003371 def description(self): # pylint: disable=method-hidden
agable@chromium.org42c20792013-09-12 17:34:49 +00003372 return '\n'.join(self._description_lines)
3373
3374 def set_description(self, desc):
3375 if isinstance(desc, basestring):
3376 lines = desc.splitlines()
3377 else:
3378 lines = [line.rstrip() for line in desc]
3379 while lines and not lines[0]:
3380 lines.pop(0)
3381 while lines and not lines[-1]:
3382 lines.pop(-1)
3383 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003384
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003385 def update_reviewers(self, reviewers, tbrs, add_owners_to=None, change=None):
3386 """Rewrites the R=/TBR= line(s) as a single line each.
3387
3388 Args:
3389 reviewers (list(str)) - list of additional emails to use for reviewers.
3390 tbrs (list(str)) - list of additional emails to use for TBRs.
3391 add_owners_to (None|'R'|'TBR') - Pass to do an OWNERS lookup for files in
3392 the change that are missing OWNER coverage. If this is not None, you
3393 must also pass a value for `change`.
3394 change (Change) - The Change that should be used for OWNERS lookups.
3395 """
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003396 assert isinstance(reviewers, list), reviewers
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003397 assert isinstance(tbrs, list), tbrs
3398
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003399 assert add_owners_to in (None, 'TBR', 'R'), add_owners_to
Robert Iannuccia28592e2017-04-18 13:14:49 -07003400 assert not add_owners_to or change, add_owners_to
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003401
3402 if not reviewers and not tbrs and not add_owners_to:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003403 return
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003404
3405 reviewers = set(reviewers)
3406 tbrs = set(tbrs)
3407 LOOKUP = {
3408 'TBR': tbrs,
3409 'R': reviewers,
3410 }
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003411
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003412 # Get the set of R= and TBR= lines and remove them from the description.
agable@chromium.org42c20792013-09-12 17:34:49 +00003413 regexp = re.compile(self.R_LINE)
3414 matches = [regexp.match(line) for line in self._description_lines]
3415 new_desc = [l for i, l in enumerate(self._description_lines)
3416 if not matches[i]]
3417 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003418
agable@chromium.org42c20792013-09-12 17:34:49 +00003419 # Construct new unified R= and TBR= lines.
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003420
3421 # First, update tbrs/reviewers with names from the R=/TBR= lines (if any).
agable@chromium.org42c20792013-09-12 17:34:49 +00003422 for match in matches:
3423 if not match:
3424 continue
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003425 LOOKUP[match.group(1)].update(cleanup_list([match.group(2).strip()]))
3426
3427 # Next, maybe fill in OWNERS coverage gaps to either tbrs/reviewers.
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003428 if add_owners_to:
piman@chromium.org336f9122014-09-04 02:16:55 +00003429 owners_db = owners.Database(change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02003430 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003431 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
Robert Iannucci100aa212017-04-18 17:28:26 -07003432 (tbrs | reviewers))
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003433 LOOKUP[add_owners_to].update(
3434 owners_db.reviewers_for(missing_files, change.author_email))
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003435
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003436 # If any folks ended up in both groups, remove them from tbrs.
3437 tbrs -= reviewers
Robert Iannuccif2708bd2017-04-17 15:49:02 -07003438
Robert Iannucci6c98dc62017-04-18 11:38:00 -07003439 new_r_line = 'R=' + ', '.join(sorted(reviewers)) if reviewers else None
3440 new_tbr_line = 'TBR=' + ', '.join(sorted(tbrs)) if tbrs else None
agable@chromium.org42c20792013-09-12 17:34:49 +00003441
3442 # Put the new lines in the description where the old first R= line was.
3443 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3444 if 0 <= line_loc < len(self._description_lines):
3445 if new_tbr_line:
3446 self._description_lines.insert(line_loc, new_tbr_line)
3447 if new_r_line:
3448 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003449 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003450 if new_r_line:
3451 self.append_footer(new_r_line)
3452 if new_tbr_line:
3453 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003454
Aaron Gable3a16ed12017-03-23 10:51:55 -07003455 def prompt(self, bug=None, git_footer=True):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003456 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003457 self.set_description([
3458 '# Enter a description of the change.',
3459 '# This will be displayed on the codereview site.',
3460 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003461 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003462 '--------------------',
3463 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003464
agable@chromium.org42c20792013-09-12 17:34:49 +00003465 regexp = re.compile(self.BUG_LINE)
3466 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003467 prefix = settings.GetBugPrefix()
3468 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
Aaron Gable3a16ed12017-03-23 10:51:55 -07003469 if git_footer:
3470 self.append_footer('Bug: %s' % ', '.join(values))
3471 else:
3472 for value in values:
3473 self.append_footer('BUG=%s' % value)
tandriif9aefb72016-07-01 09:06:51 -07003474
agable@chromium.org42c20792013-09-12 17:34:49 +00003475 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003476 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003477 if not content:
3478 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003479 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003480
Bruce Dawson2377b012018-01-11 16:46:49 -08003481 # Strip off comments and default inserted "Bug:" line.
3482 clean_lines = [line.rstrip() for line in lines if not
3483 (line.startswith('#') or line.rstrip() == "Bug:")]
agable@chromium.org42c20792013-09-12 17:34:49 +00003484 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003485 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003486 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003487
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003488 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003489 """Adds a footer line to the description.
3490
3491 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3492 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3493 that Gerrit footers are always at the end.
3494 """
3495 parsed_footer_line = git_footers.parse_footer(line)
3496 if parsed_footer_line:
3497 # Line is a gerrit footer in the form: Footer-Key: any value.
3498 # Thus, must be appended observing Gerrit footer rules.
3499 self.set_description(
3500 git_footers.add_footer(self.description,
3501 key=parsed_footer_line[0],
3502 value=parsed_footer_line[1]))
3503 return
3504
3505 if not self._description_lines:
3506 self._description_lines.append(line)
3507 return
3508
3509 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3510 if gerrit_footers:
3511 # git_footers.split_footers ensures that there is an empty line before
3512 # actual (gerrit) footers, if any. We have to keep it that way.
3513 assert top_lines and top_lines[-1] == ''
3514 top_lines, separator = top_lines[:-1], top_lines[-1:]
3515 else:
3516 separator = [] # No need for separator if there are no gerrit_footers.
3517
3518 prev_line = top_lines[-1] if top_lines else ''
3519 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3520 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3521 top_lines.append('')
3522 top_lines.append(line)
3523 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003524
tandrii99a72f22016-08-17 14:33:24 -07003525 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003526 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003527 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003528 reviewers = [match.group(2).strip()
3529 for match in matches
3530 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003531 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003532
bradnelsond975b302016-10-23 12:20:23 -07003533 def get_cced(self):
3534 """Retrieves the list of reviewers."""
3535 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3536 cced = [match.group(2).strip() for match in matches if match]
3537 return cleanup_list(cced)
3538
Nodir Turakulov23b82142017-11-16 11:04:25 -08003539 def get_hash_tags(self):
3540 """Extracts and sanitizes a list of Gerrit hashtags."""
3541 subject = (self._description_lines or ('',))[0]
3542 subject = re.sub(
3543 self.STRIP_HASH_TAG_PREFIX, '', subject, flags=re.IGNORECASE)
3544
3545 tags = []
3546 start = 0
3547 bracket_exp = re.compile(self.BRACKET_HASH_TAG)
3548 while True:
3549 m = bracket_exp.match(subject, start)
3550 if not m:
3551 break
3552 tags.append(self.sanitize_hash_tag(m.group(1)))
3553 start = m.end()
3554
3555 if not tags:
3556 # Try "Tag: " prefix.
3557 m = re.match(self.COLON_SEPARATED_HASH_TAG, subject)
3558 if m:
3559 tags.append(self.sanitize_hash_tag(m.group(1)))
3560 return tags
3561
3562 @classmethod
3563 def sanitize_hash_tag(cls, tag):
3564 """Returns a sanitized Gerrit hash tag.
3565
3566 A sanitized hashtag can be used as a git push refspec parameter value.
3567 """
3568 return re.sub(cls.BAD_HASH_TAG_CHUNK, '-', tag).strip('-').lower()
3569
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003570 def update_with_git_number_footers(self, parent_hash, parent_msg, dest_ref):
3571 """Updates this commit description given the parent.
3572
3573 This is essentially what Gnumbd used to do.
3574 Consult https://goo.gl/WMmpDe for more details.
3575 """
3576 assert parent_msg # No, orphan branch creation isn't supported.
3577 assert parent_hash
3578 assert dest_ref
3579 parent_footer_map = git_footers.parse_footers(parent_msg)
3580 # This will also happily parse svn-position, which GnumbD is no longer
3581 # supporting. While we'd generate correct footers, the verifier plugin
3582 # installed in Gerrit will block such commit (ie git push below will fail).
3583 parent_position = git_footers.get_position(parent_footer_map)
3584
3585 # Cherry-picks may have last line obscuring their prior footers,
3586 # from git_footers perspective. This is also what Gnumbd did.
3587 cp_line = None
3588 if (self._description_lines and
3589 re.match(self.CHERRY_PICK_LINE, self._description_lines[-1])):
3590 cp_line = self._description_lines.pop()
3591
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003592 top_lines, footer_lines, _ = git_footers.split_footers(self.description)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003593
3594 # Original-ify all Cr- footers, to avoid re-lands, cherry-picks, or just
3595 # user interference with actual footers we'd insert below.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003596 for i, line in enumerate(footer_lines):
3597 k, v = git_footers.parse_footer(line) or (None, None)
3598 if k and k.startswith('Cr-'):
3599 footer_lines[i] = '%s: %s' % ('Cr-Original-' + k[len('Cr-'):], v)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003600
3601 # Add Position and Lineage footers based on the parent.
Andrii Shyshkalovb5effa12016-12-14 19:35:12 +01003602 lineage = list(reversed(parent_footer_map.get('Cr-Branched-From', [])))
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003603 if parent_position[0] == dest_ref:
3604 # Same branch as parent.
3605 number = int(parent_position[1]) + 1
3606 else:
3607 number = 1 # New branch, and extra lineage.
3608 lineage.insert(0, '%s-%s@{#%d}' % (parent_hash, parent_position[0],
3609 int(parent_position[1])))
3610
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003611 footer_lines.append('Cr-Commit-Position: %s@{#%d}' % (dest_ref, number))
3612 footer_lines.extend('Cr-Branched-From: %s' % v for v in lineage)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003613
3614 self._description_lines = top_lines
3615 if cp_line:
3616 self._description_lines.append(cp_line)
3617 if self._description_lines[-1] != '':
3618 self._description_lines.append('') # Ensure footer separator.
Andrii Shyshkalovde37c012017-07-06 21:06:50 +02003619 self._description_lines.extend(footer_lines)
Andrii Shyshkalov15e50cc2016-12-02 14:34:08 +01003620
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003621
Aaron Gablea1bab272017-04-11 16:38:18 -07003622def get_approving_reviewers(props, disapproval=False):
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003623 """Retrieves the reviewers that approved a CL from the issue properties with
3624 messages.
3625
3626 Note that the list may contain reviewers that are not committer, thus are not
3627 considered by the CQ.
Aaron Gablea1bab272017-04-11 16:38:18 -07003628
3629 If disapproval is True, instead returns reviewers who 'not lgtm'd the CL.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003630 """
Aaron Gablea1bab272017-04-11 16:38:18 -07003631 approval_type = 'disapproval' if disapproval else 'approval'
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003632 return sorted(
3633 set(
3634 message['sender']
3635 for message in props['messages']
Aaron Gablea1bab272017-04-11 16:38:18 -07003636 if message[approval_type] and message['sender'] in props['reviewers']
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003637 )
3638 )
3639
3640
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003641def FindCodereviewSettingsFile(filename='codereview.settings'):
3642 """Finds the given file starting in the cwd and going up.
3643
3644 Only looks up to the top of the repository unless an
3645 'inherit-review-settings-ok' file exists in the root of the repository.
3646 """
3647 inherit_ok_file = 'inherit-review-settings-ok'
3648 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003649 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003650 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3651 root = '/'
3652 while True:
3653 if filename in os.listdir(cwd):
3654 if os.path.isfile(os.path.join(cwd, filename)):
3655 return open(os.path.join(cwd, filename))
3656 if cwd == root:
3657 break
3658 cwd = os.path.dirname(cwd)
3659
3660
3661def LoadCodereviewSettingsFromFile(fileobj):
3662 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003663 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003664
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665 def SetProperty(name, setting, unset_error_ok=False):
3666 fullname = 'rietveld.' + name
3667 if setting in keyvals:
3668 RunGit(['config', fullname, keyvals[setting]])
3669 else:
3670 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3671
tandrii48df5812016-10-17 03:55:37 -07003672 if not keyvals.get('GERRIT_HOST', False):
3673 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003674 # Only server setting is required. Other settings can be absent.
3675 # In that case, we ignore errors raised during option deletion attempt.
3676 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003677 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003678 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3679 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003680 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003681 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
3682 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003683 SetProperty('project', 'PROJECT', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003684 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3685 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003686
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003687 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003688 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003689
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003690 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003691 RunGit(['config', 'gerrit.squash-uploads',
3692 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003693
tandrii@chromium.org28253532016-04-14 13:46:56 +00003694 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003695 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003696 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003698 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003699 # should be of the form
3700 # PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3701 # ORIGIN_URL_CONFIG: http://src.chromium.org/git
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003702 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3703 keyvals['ORIGIN_URL_CONFIG']])
3704
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003705
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003706def urlretrieve(source, destination):
3707 """urllib is broken for SSL connections via a proxy therefore we
3708 can't use urllib.urlretrieve()."""
3709 with open(destination, 'w') as f:
3710 f.write(urllib2.urlopen(source).read())
3711
3712
ukai@chromium.org712d6102013-11-27 00:52:58 +00003713def hasSheBang(fname):
3714 """Checks fname is a #! script."""
3715 with open(fname) as f:
3716 return f.read(2).startswith('#!')
3717
3718
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003719# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3720def DownloadHooks(*args, **kwargs):
3721 pass
3722
3723
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003724def DownloadGerritHook(force):
3725 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003726
3727 Args:
3728 force: True to update hooks. False to install hooks if not present.
3729 """
3730 if not settings.GetIsGerrit():
3731 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003732 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003733 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3734 if not os.access(dst, os.X_OK):
3735 if os.path.exists(dst):
3736 if not force:
3737 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003738 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003739 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003740 if not hasSheBang(dst):
3741 DieWithError('Not a script: %s\n'
3742 'You need to download from\n%s\n'
3743 'into .git/hooks/commit-msg and '
3744 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003745 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3746 except Exception:
3747 if os.path.exists(dst):
3748 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003749 DieWithError('\nFailed to download hooks.\n'
3750 'You need to download from\n%s\n'
3751 'into .git/hooks/commit-msg and '
3752 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003753
3754
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003755def GetRietveldCodereviewSettingsInteractively():
3756 """Prompt the user for settings."""
3757 server = settings.GetDefaultServerUrl(error_ok=True)
3758 prompt = 'Rietveld server (host[:port])'
3759 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3760 newserver = ask_for_data(prompt + ':')
3761 if not server and not newserver:
3762 newserver = DEFAULT_SERVER
3763 if newserver:
3764 newserver = gclient_utils.UpgradeToHttps(newserver)
3765 if newserver != server:
3766 RunGit(['config', 'rietveld.server', newserver])
3767
3768 def SetProperty(initial, caption, name, is_url):
3769 prompt = caption
3770 if initial:
3771 prompt += ' ("x" to clear) [%s]' % initial
3772 new_val = ask_for_data(prompt + ':')
3773 if new_val == 'x':
3774 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3775 elif new_val:
3776 if is_url:
3777 new_val = gclient_utils.UpgradeToHttps(new_val)
3778 if new_val != initial:
3779 RunGit(['config', 'rietveld.' + name, new_val])
3780
3781 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3782 SetProperty(settings.GetDefaultPrivateFlag(),
3783 'Private flag (rietveld only)', 'private', False)
3784 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3785 'tree-status-url', False)
3786 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3787 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3788 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3789 'run-post-upload-hook', False)
3790
Andrii Shyshkalov18975322017-01-25 16:44:13 +01003791
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003792class _GitCookiesChecker(object):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003793 """Provides facilities for validating and suggesting fixes to .gitcookies."""
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003794
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003795 _GOOGLESOURCE = 'googlesource.com'
3796
3797 def __init__(self):
3798 # Cached list of [host, identity, source], where source is either
3799 # .gitcookies or .netrc.
3800 self._all_hosts = None
3801
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003802 def ensure_configured_gitcookies(self):
3803 """Runs checks and suggests fixes to make git use .gitcookies from default
3804 path."""
3805 default = gerrit_util.CookiesAuthenticator.get_gitcookies_path()
3806 configured_path = RunGitSilent(
3807 ['config', '--global', 'http.cookiefile']).strip()
Andrii Shyshkalov1e250cd2017-05-10 15:39:31 +02003808 configured_path = os.path.expanduser(configured_path)
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003809 if configured_path:
3810 self._ensure_default_gitcookies_path(configured_path, default)
3811 else:
3812 self._configure_gitcookies_path(default)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003813
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003814 @staticmethod
3815 def _ensure_default_gitcookies_path(configured_path, default_path):
3816 assert configured_path
3817 if configured_path == default_path:
3818 print('git is already configured to use your .gitcookies from %s' %
3819 configured_path)
3820 return
3821
Quinten Yearsley0c62da92017-05-31 13:39:42 -07003822 print('WARNING: You have configured custom path to .gitcookies: %s\n'
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003823 'Gerrit and other depot_tools expect .gitcookies at %s\n' %
3824 (configured_path, default_path))
3825
3826 if not os.path.exists(configured_path):
3827 print('However, your configured .gitcookies file is missing.')
3828 confirm_or_exit('Reconfigure git to use default .gitcookies?',
3829 action='reconfigure')
3830 RunGit(['config', '--global', 'http.cookiefile', default_path])
3831 return
3832
3833 if os.path.exists(default_path):
3834 print('WARNING: default .gitcookies file already exists %s' %
3835 default_path)
3836 DieWithError('Please delete %s manually and re-run git cl creds-check' %
3837 default_path)
3838
3839 confirm_or_exit('Move existing .gitcookies to default location?',
3840 action='move')
3841 shutil.move(configured_path, default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003842 RunGit(['config', '--global', 'http.cookiefile', default_path])
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003843 print('Moved and reconfigured git to use .gitcookies from %s' %
3844 default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003845
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01003846 @staticmethod
3847 def _configure_gitcookies_path(default_path):
3848 netrc_path = gerrit_util.CookiesAuthenticator.get_netrc_path()
3849 if os.path.exists(netrc_path):
3850 print('You seem to be using outdated .netrc for git credentials: %s' %
3851 netrc_path)
3852 print('This tool will guide you through setting up recommended '
3853 '.gitcookies store for git credentials.\n'
3854 '\n'
3855 'IMPORTANT: If something goes wrong and you decide to go back, do:\n'
3856 ' git config --global --unset http.cookiefile\n'
3857 ' mv %s %s.backup\n\n' % (default_path, default_path))
3858 confirm_or_exit(action='setup .gitcookies')
3859 RunGit(['config', '--global', 'http.cookiefile', default_path])
3860 print('Configured git to use .gitcookies from %s' % default_path)
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01003861
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01003862 def get_hosts_with_creds(self, include_netrc=False):
3863 if self._all_hosts is None:
3864 a = gerrit_util.CookiesAuthenticator()
3865 self._all_hosts = [
3866 (h, u, s)
3867 for h, u, s in itertools.chain(
3868 ((h, u, '.netrc') for h, (u, _, _) in a.netrc.hosts.iteritems()),
3869 ((h, u, '.gitcookies') for h, (u, _) in a.gitcookies.iteritems())
3870 )
3871 if h.endswith(self._GOOGLESOURCE)
3872 ]
3873
3874 if include_netrc:
3875 return self._all_hosts
3876 return [(h, u, s) for h, u, s in self._all_hosts if s != '.netrc']
3877
3878 def print_current_creds(self, include_netrc=False):
3879 hosts = sorted(self.get_hosts_with_creds(include_netrc=include_netrc))
3880 if not hosts:
3881 print('No Git/Gerrit credentials found')
3882 return
3883 lengths = [max(map(len, (row[i] for row in hosts))) for i in xrange(3)]
3884 header = [('Host', 'User', 'Which file'),
3885 ['=' * l for l in lengths]]
3886 for row in (header + hosts):
3887 print('\t'.join((('%%+%ds' % l) % s)
3888 for l, s in zip(lengths, row)))
3889
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003890 @staticmethod
3891 def _parse_identity(identity):
Lei Zhangd3f769a2017-12-15 15:16:14 -08003892 """Parses identity "git-<username>.domain" into <username> and domain."""
3893 # Special case: usernames that contain ".", which are generally not
Andrii Shyshkalov0d2dea02017-07-17 15:17:55 +02003894 # distinguishable from sub-domains. But we do know typical domains:
3895 if identity.endswith('.chromium.org'):
3896 domain = 'chromium.org'
3897 username = identity[:-len('.chromium.org')]
3898 else:
3899 username, domain = identity.split('.', 1)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003900 if username.startswith('git-'):
3901 username = username[len('git-'):]
3902 return username, domain
3903
3904 def _get_usernames_of_domain(self, domain):
3905 """Returns list of usernames referenced by .gitcookies in a given domain."""
3906 identities_by_domain = {}
3907 for _, identity, _ in self.get_hosts_with_creds():
3908 username, domain = self._parse_identity(identity)
3909 identities_by_domain.setdefault(domain, []).append(username)
3910 return identities_by_domain.get(domain)
3911
3912 def _canonical_git_googlesource_host(self, host):
3913 """Normalizes Gerrit hosts (with '-review') to Git host."""
3914 assert host.endswith(self._GOOGLESOURCE)
3915 # Prefix doesn't include '.' at the end.
3916 prefix = host[:-(1 + len(self._GOOGLESOURCE))]
3917 if prefix.endswith('-review'):
3918 prefix = prefix[:-len('-review')]
3919 return prefix + '.' + self._GOOGLESOURCE
3920
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003921 def _canonical_gerrit_googlesource_host(self, host):
3922 git_host = self._canonical_git_googlesource_host(host)
3923 prefix = git_host.split('.', 1)[0]
3924 return prefix + '-review.' + self._GOOGLESOURCE
3925
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003926 def _get_counterpart_host(self, host):
3927 assert host.endswith(self._GOOGLESOURCE)
3928 git = self._canonical_git_googlesource_host(host)
3929 gerrit = self._canonical_gerrit_googlesource_host(git)
3930 return git if gerrit == host else gerrit
3931
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003932 def has_generic_host(self):
3933 """Returns whether generic .googlesource.com has been configured.
3934
3935 Chrome Infra recommends to use explicit ${host}.googlesource.com instead.
3936 """
3937 for host, _, _ in self.get_hosts_with_creds(include_netrc=False):
3938 if host == '.' + self._GOOGLESOURCE:
3939 return True
3940 return False
3941
3942 def _get_git_gerrit_identity_pairs(self):
3943 """Returns map from canonic host to pair of identities (Git, Gerrit).
3944
3945 One of identities might be None, meaning not configured.
3946 """
3947 host_to_identity_pairs = {}
3948 for host, identity, _ in self.get_hosts_with_creds():
3949 canonical = self._canonical_git_googlesource_host(host)
3950 pair = host_to_identity_pairs.setdefault(canonical, [None, None])
3951 idx = 0 if canonical == host else 1
3952 pair[idx] = identity
3953 return host_to_identity_pairs
3954
3955 def get_partially_configured_hosts(self):
3956 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003957 (host if i1 else self._canonical_gerrit_googlesource_host(host))
3958 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
3959 if None in (i1, i2) and host != '.' + self._GOOGLESOURCE)
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003960
3961 def get_conflicting_hosts(self):
3962 return set(
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003963 host
3964 for host, (i1, i2) in self._get_git_gerrit_identity_pairs().iteritems()
Andrii Shyshkalov97800502017-03-16 16:04:32 +01003965 if None not in (i1, i2) and i1 != i2)
3966
3967 def get_duplicated_hosts(self):
3968 counters = collections.Counter(h for h, _, _ in self.get_hosts_with_creds())
3969 return set(host for host, count in counters.iteritems() if count > 1)
3970
3971 _EXPECTED_HOST_IDENTITY_DOMAINS = {
3972 'chromium.googlesource.com': 'chromium.org',
3973 'chrome-internal.googlesource.com': 'google.com',
3974 }
3975
3976 def get_hosts_with_wrong_identities(self):
3977 """Finds hosts which **likely** reference wrong identities.
3978
3979 Note: skips hosts which have conflicting identities for Git and Gerrit.
3980 """
3981 hosts = set()
3982 for host, expected in self._EXPECTED_HOST_IDENTITY_DOMAINS.iteritems():
3983 pair = self._get_git_gerrit_identity_pairs().get(host)
3984 if pair and pair[0] == pair[1]:
3985 _, domain = self._parse_identity(pair[0])
3986 if domain != expected:
3987 hosts.add(host)
3988 return hosts
3989
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003990 @staticmethod
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003991 def _format_hosts(hosts, extra_column_func=None):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01003992 hosts = sorted(hosts)
3993 assert hosts
3994 if extra_column_func is None:
3995 extras = [''] * len(hosts)
3996 else:
3997 extras = [extra_column_func(host) for host in hosts]
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02003998 tmpl = '%%-%ds %%-%ds' % (max(map(len, hosts)), max(map(len, extras)))
3999 lines = []
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004000 for he in zip(hosts, extras):
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004001 lines.append(tmpl % he)
4002 return lines
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004003
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004004 def _find_problems(self):
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004005 if self.has_generic_host():
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004006 yield ('.googlesource.com wildcard record detected',
4007 ['Chrome Infrastructure team recommends to list full host names '
4008 'explicitly.'],
4009 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004010
4011 dups = self.get_duplicated_hosts()
4012 if dups:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004013 yield ('The following hosts were defined twice',
4014 self._format_hosts(dups),
4015 None)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004016
4017 partial = self.get_partially_configured_hosts()
4018 if partial:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004019 yield ('Credentials should come in pairs for Git and Gerrit hosts. '
4020 'These hosts are missing',
4021 self._format_hosts(partial, lambda host: 'but %s defined' %
4022 self._get_counterpart_host(host)),
4023 partial)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004024
4025 conflicting = self.get_conflicting_hosts()
4026 if conflicting:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004027 yield ('The following Git hosts have differing credentials from their '
4028 'Gerrit counterparts',
4029 self._format_hosts(conflicting, lambda host: '%s vs %s' %
4030 tuple(self._get_git_gerrit_identity_pairs()[host])),
4031 conflicting)
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004032
4033 wrong = self.get_hosts_with_wrong_identities()
4034 if wrong:
Andrii Shyshkalovc8173822017-07-10 12:10:53 +02004035 yield ('These hosts likely use wrong identity',
4036 self._format_hosts(wrong, lambda host: '%s but %s recommended' %
4037 (self._get_git_gerrit_identity_pairs()[host][0],
4038 self._EXPECTED_HOST_IDENTITY_DOMAINS[host])),
4039 wrong)
4040
4041 def find_and_report_problems(self):
4042 """Returns True if there was at least one problem, else False."""
4043 found = False
4044 bad_hosts = set()
4045 for title, sublines, hosts in self._find_problems():
4046 if not found:
4047 found = True
4048 print('\n\n.gitcookies problem report:\n')
4049 bad_hosts.update(hosts or [])
4050 print(' %s%s' % (title , (':' if sublines else '')))
4051 if sublines:
4052 print()
4053 print(' %s' % '\n '.join(sublines))
4054 print()
4055
4056 if bad_hosts:
4057 assert found
4058 print(' You can manually remove corresponding lines in your %s file and '
4059 'visit the following URLs with correct account to generate '
4060 'correct credential lines:\n' %
4061 gerrit_util.CookiesAuthenticator.get_gitcookies_path())
4062 print(' %s' % '\n '.join(sorted(set(
4063 gerrit_util.CookiesAuthenticator().get_new_password_url(
4064 self._canonical_git_googlesource_host(host))
4065 for host in bad_hosts
4066 ))))
4067 return found
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004068
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004069
4070def CMDcreds_check(parser, args):
4071 """Checks credentials and suggests changes."""
4072 _, _ = parser.parse_args(args)
4073
4074 if gerrit_util.GceAuthenticator.is_gce():
Aaron Gabled10ca0e2017-09-11 11:24:10 -07004075 DieWithError(
4076 'This command is not designed for GCE, are you on a bot?\n'
4077 'If you need to run this, export SKIP_GCE_AUTH_FOR_GIT=1 in your env.')
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004078
Andrii Shyshkalov517948b2017-03-15 15:51:59 +01004079 checker = _GitCookiesChecker()
4080 checker.ensure_configured_gitcookies()
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004081
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004082 print('Your .netrc and .gitcookies have credentials for these hosts:')
Andrii Shyshkalov34924cd2017-03-15 17:08:32 +01004083 checker.print_current_creds(include_netrc=True)
4084
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004085 if not checker.find_and_report_problems():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004086 print('\nNo problems detected in your .gitcookies file.')
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01004087 return 0
4088 return 1
Andrii Shyshkalov353637c2017-03-14 16:52:18 +01004089
4090
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004091@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004092def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004093 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004094
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004095 print('WARNING: git cl config works for Rietveld only.')
tandrii5d0a0422016-09-14 06:24:35 -07004096 # TODO(tandrii): remove this once we switch to Gerrit.
4097 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00004098 parser.add_option('--activate-update', action='store_true',
4099 help='activate auto-updating [rietveld] section in '
4100 '.git/config')
4101 parser.add_option('--deactivate-update', action='store_true',
4102 help='deactivate auto-updating [rietveld] section in '
4103 '.git/config')
4104 options, args = parser.parse_args(args)
4105
4106 if options.deactivate_update:
4107 RunGit(['config', 'rietveld.autoupdate', 'false'])
4108 return
4109
4110 if options.activate_update:
4111 RunGit(['config', '--unset', 'rietveld.autoupdate'])
4112 return
4113
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004114 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00004115 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004116 return 0
4117
4118 url = args[0]
4119 if not url.endswith('codereview.settings'):
4120 url = os.path.join(url, 'codereview.settings')
4121
4122 # Load code review settings and download hooks (if available).
4123 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
4124 return 0
4125
4126
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004127def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004128 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004129 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
4130 branch = ShortBranchName(branchref)
4131 _, args = parser.parse_args(args)
4132 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07004133 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004134 return RunGit(['config', 'branch.%s.base-url' % branch],
4135 error_ok=False).strip()
4136 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004137 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00004138 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
4139 error_ok=False).strip()
4140
4141
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004142def color_for_status(status):
4143 """Maps a Changelist status to color, for CMDstatus and other tools."""
4144 return {
Aaron Gable9ab38c62017-04-06 14:36:33 -07004145 'unsent': Fore.YELLOW,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004146 'waiting': Fore.BLUE,
4147 'reply': Fore.YELLOW,
Aaron Gable9ab38c62017-04-06 14:36:33 -07004148 'not lgtm': Fore.RED,
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004149 'lgtm': Fore.GREEN,
4150 'commit': Fore.MAGENTA,
4151 'closed': Fore.CYAN,
4152 'error': Fore.WHITE,
4153 }.get(status, Fore.WHITE)
4154
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00004155
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004156def get_cl_statuses(changes, fine_grained, max_processes=None):
4157 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004158
4159 If fine_grained is true, this will fetch CL statuses from the server.
4160 Otherwise, simply indicate if there's a matching url for the given branches.
4161
4162 If max_processes is specified, it is used as the maximum number of processes
4163 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
4164 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004165
4166 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004167 """
qyearsley12fa6ff2016-08-24 09:18:40 -07004168 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004169 upload.verbosity = 0
4170
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004171 if not changes:
4172 raise StopIteration()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00004173
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004174 if not fine_grained:
4175 # Fast path which doesn't involve querying codereview servers.
Aaron Gablea1bab272017-04-11 16:38:18 -07004176 # Do not use get_approving_reviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004177 for cl in changes:
4178 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
Andrii Shyshkalov2f8e9242017-01-23 19:20:19 +01004179 return
4180
4181 # First, sort out authentication issues.
4182 logging.debug('ensuring credentials exist')
4183 for cl in changes:
4184 cl.EnsureAuthenticated(force=False, refresh=True)
4185
4186 def fetch(cl):
4187 try:
4188 return (cl, cl.GetStatus())
4189 except:
4190 # See http://crbug.com/629863.
4191 logging.exception('failed to fetch status for %s:', cl)
4192 raise
4193
4194 threads_count = len(changes)
4195 if max_processes:
4196 threads_count = max(1, min(threads_count, max_processes))
4197 logging.debug('querying %d CLs using %d threads', len(changes), threads_count)
4198
4199 pool = ThreadPool(threads_count)
4200 fetched_cls = set()
4201 try:
4202 it = pool.imap_unordered(fetch, changes).__iter__()
4203 while True:
4204 try:
4205 cl, status = it.next(timeout=5)
4206 except multiprocessing.TimeoutError:
4207 break
4208 fetched_cls.add(cl)
4209 yield cl, status
4210 finally:
4211 pool.close()
4212
4213 # Add any branches that failed to fetch.
4214 for cl in set(changes) - fetched_cls:
4215 yield (cl, 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00004216
rmistry@google.com2dd99862015-06-22 12:22:18 +00004217
4218def upload_branch_deps(cl, args):
4219 """Uploads CLs of local branches that are dependents of the current branch.
4220
4221 If the local branch dependency tree looks like:
4222 test1 -> test2.1 -> test3.1
4223 -> test3.2
4224 -> test2.2 -> test3.3
4225
4226 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
4227 run on the dependent branches in this order:
4228 test2.1, test3.1, test3.2, test2.2, test3.3
4229
4230 Note: This function does not rebase your local dependent branches. Use it when
4231 you make a change to the parent branch that will not conflict with its
4232 dependent branches, and you would like their dependencies updated in
4233 Rietveld.
4234 """
4235 if git_common.is_dirty_git_tree('upload-branch-deps'):
4236 return 1
4237
4238 root_branch = cl.GetBranch()
4239 if root_branch is None:
4240 DieWithError('Can\'t find dependent branches from detached HEAD state. '
4241 'Get on a branch!')
Andrii Shyshkalov1090fd52017-01-26 09:37:54 +01004242 if not cl.GetIssue() or (not cl.IsGerrit() and not cl.GetPatchset()):
rmistry@google.com2dd99862015-06-22 12:22:18 +00004243 DieWithError('Current branch does not have an uploaded CL. We cannot set '
4244 'patchset dependencies without an uploaded CL.')
4245
4246 branches = RunGit(['for-each-ref',
4247 '--format=%(refname:short) %(upstream:short)',
4248 'refs/heads'])
4249 if not branches:
4250 print('No local branches found.')
4251 return 0
4252
4253 # Create a dictionary of all local branches to the branches that are dependent
4254 # on it.
4255 tracked_to_dependents = collections.defaultdict(list)
4256 for b in branches.splitlines():
4257 tokens = b.split()
4258 if len(tokens) == 2:
4259 branch_name, tracked = tokens
4260 tracked_to_dependents[tracked].append(branch_name)
4261
vapiera7fbd5a2016-06-16 09:17:49 -07004262 print()
4263 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004264 dependents = []
4265 def traverse_dependents_preorder(branch, padding=''):
4266 dependents_to_process = tracked_to_dependents.get(branch, [])
4267 padding += ' '
4268 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07004269 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00004270 dependents.append(dependent)
4271 traverse_dependents_preorder(dependent, padding)
4272 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07004273 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004274
4275 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004276 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004277 return 0
4278
Andrii Shyshkalovabc26ac2017-03-14 14:49:38 +01004279 confirm_or_exit('This command will checkout all dependent branches and run '
4280 '"git cl upload".', action='continue')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004281
andybons@chromium.org962f9462016-02-03 20:00:42 +00004282 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00004283 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00004284 args.extend(['-t', 'Updated patchset dependency'])
4285
rmistry@google.com2dd99862015-06-22 12:22:18 +00004286 # Record all dependents that failed to upload.
4287 failures = {}
4288 # Go through all dependents, checkout the branch and upload.
4289 try:
4290 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07004291 print()
4292 print('--------------------------------------')
4293 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004294 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07004295 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004296 try:
4297 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07004298 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00004299 failures[dependent_branch] = 1
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004300 except: # pylint: disable=bare-except
rmistry@google.com2dd99862015-06-22 12:22:18 +00004301 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07004302 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004303 finally:
4304 # Swap back to the original root branch.
4305 RunGit(['checkout', '-q', root_branch])
4306
vapiera7fbd5a2016-06-16 09:17:49 -07004307 print()
4308 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004309 for dependent_branch in dependents:
4310 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07004311 print(' %s : %s' % (dependent_branch, upload_status))
4312 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00004313
4314 return 0
4315
4316
kmarshall3bff56b2016-06-06 18:31:47 -07004317def CMDarchive(parser, args):
4318 """Archives and deletes branches associated with closed changelists."""
4319 parser.add_option(
4320 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07004321 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07004322 parser.add_option(
4323 '-f', '--force', action='store_true',
4324 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07004325 parser.add_option(
4326 '-d', '--dry-run', action='store_true',
4327 help='Skip the branch tagging and removal steps.')
4328 parser.add_option(
4329 '-t', '--notags', action='store_true',
4330 help='Do not tag archived branches. '
4331 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07004332
4333 auth.add_auth_options(parser)
4334 options, args = parser.parse_args(args)
4335 if args:
4336 parser.error('Unsupported args: %s' % ' '.join(args))
4337 auth_config = auth.extract_auth_config_from_options(options)
4338
4339 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4340 if not branches:
4341 return 0
4342
vapiera7fbd5a2016-06-16 09:17:49 -07004343 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07004344 changes = [Changelist(branchref=b, auth_config=auth_config)
4345 for b in branches.splitlines()]
4346 alignment = max(5, max(len(c.GetBranch()) for c in changes))
4347 statuses = get_cl_statuses(changes,
4348 fine_grained=True,
4349 max_processes=options.maxjobs)
4350 proposal = [(cl.GetBranch(),
4351 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
4352 for cl, status in statuses
4353 if status == 'closed']
4354 proposal.sort()
4355
4356 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07004357 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07004358 return 0
4359
4360 current_branch = GetCurrentBranch()
4361
vapiera7fbd5a2016-06-16 09:17:49 -07004362 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07004363 if options.notags:
4364 for next_item in proposal:
4365 print(' ' + next_item[0])
4366 else:
4367 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
4368 for next_item in proposal:
4369 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07004370
kmarshall9249e012016-08-23 12:02:16 -07004371 # Quit now on precondition failure or if instructed by the user, either
4372 # via an interactive prompt or by command line flags.
4373 if options.dry_run:
4374 print('\nNo changes were made (dry run).\n')
4375 return 0
4376 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07004377 print('You are currently on a branch \'%s\' which is associated with a '
4378 'closed codereview issue, so archive cannot proceed. Please '
4379 'checkout another branch and run this command again.' %
4380 current_branch)
4381 return 1
kmarshall9249e012016-08-23 12:02:16 -07004382 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07004383 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
4384 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07004385 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07004386 return 1
4387
4388 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07004389 if not options.notags:
4390 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07004391 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07004392
vapiera7fbd5a2016-06-16 09:17:49 -07004393 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07004394
4395 return 0
4396
4397
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004398def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004399 """Show status of changelists.
4400
4401 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004402 - Blue waiting for review
Aaron Gable9ab38c62017-04-06 14:36:33 -07004403 - Yellow waiting for you to reply to review, or not yet sent
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004404 - Green LGTM'ed
Aaron Gable9ab38c62017-04-06 14:36:33 -07004405 - Red 'not LGTM'ed
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00004406 - Magenta in the commit queue
4407 - Cyan was committed, branch can be deleted
Aaron Gable9ab38c62017-04-06 14:36:33 -07004408 - White error, or unknown status
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004409
4410 Also see 'git cl comments'.
4411 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004412 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07004413 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004414 parser.add_option('-f', '--fast', action='store_true',
4415 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004416 parser.add_option(
4417 '-j', '--maxjobs', action='store', type=int,
4418 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004419
4420 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07004421 _add_codereview_issue_select_options(
4422 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004423 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07004424 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004425 if args:
4426 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004427 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004428
iannuccie53c9352016-08-17 14:40:40 -07004429 if options.issue is not None and not options.field:
4430 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07004431
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004432 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07004433 cl = Changelist(auth_config=auth_config, issue=options.issue,
4434 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004435 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07004436 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004437 elif options.field == 'id':
4438 issueid = cl.GetIssue()
4439 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07004440 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004441 elif options.field == 'patch':
Aaron Gablee8856ee2017-12-07 12:41:46 -08004442 patchset = cl.GetMostRecentPatchset()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004443 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07004444 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07004445 elif options.field == 'status':
4446 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004447 elif options.field == 'url':
4448 url = cl.GetIssueURL()
4449 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07004450 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00004451 return 0
4452
4453 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
4454 if not branches:
4455 print('No local branch found.')
4456 return 0
4457
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004458 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004459 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004460 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07004461 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004462 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004463 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004464 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004465
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004466 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004467 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
4468 for cl in sorted(changes, key=lambda c: c.GetBranch()):
4469 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00004470 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004471 c, status = output.next()
4472 branch_statuses[c.GetBranch()] = status
4473 status = branch_statuses.pop(branch)
4474 url = cl.GetIssueURL()
4475 if url and (not status or status == 'error'):
4476 # The issue probably doesn't exist anymore.
4477 url += ' (broken)'
4478
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004479 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00004480 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00004481 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00004482 color = ''
4483 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00004484 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07004485 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00004486 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07004487 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004488
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004489
4490 branch = GetCurrentBranch()
vapiera7fbd5a2016-06-16 09:17:49 -07004491 print()
Andrii Shyshkalovd0e1d9d2017-01-24 17:10:51 +01004492 print('Current branch: %s' % branch)
4493 for cl in changes:
4494 if cl.GetBranch() == branch:
4495 break
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004496 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07004497 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00004498 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07004499 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00004500 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07004501 print('Issue description:')
4502 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004503 return 0
4504
4505
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004506def colorize_CMDstatus_doc():
4507 """To be called once in main() to add colors to git cl status help."""
4508 colors = [i for i in dir(Fore) if i[0].isupper()]
4509
4510 def colorize_line(line):
4511 for color in colors:
4512 if color in line.upper():
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004513 # Extract whitespace first and the leading '-'.
maruel@chromium.org39c0b222013-08-17 16:57:01 +00004514 indent = len(line) - len(line.lstrip(' ')) + 1
4515 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
4516 return line
4517
4518 lines = CMDstatus.__doc__.splitlines()
4519 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
4520
4521
phajdan.jre328cf92016-08-22 04:12:17 -07004522def write_json(path, contents):
Stefan Zager1306bd02017-06-22 19:26:46 -07004523 if path == '-':
4524 json.dump(contents, sys.stdout)
4525 else:
4526 with open(path, 'w') as f:
4527 json.dump(contents, f)
phajdan.jre328cf92016-08-22 04:12:17 -07004528
4529
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004530@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004531def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004532 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004533
4534 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004535 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00004536 parser.add_option('-r', '--reverse', action='store_true',
4537 help='Lookup the branch(es) for the specified issues. If '
4538 'no issues are specified, all branches with mapped '
4539 'issues will be listed.')
Stefan Zager1306bd02017-06-22 19:26:46 -07004540 parser.add_option('--json',
4541 help='Path to JSON output file, or "-" for stdout.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004542 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004543 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004544 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004545
dnj@chromium.org406c4402015-03-03 17:22:28 +00004546 if options.reverse:
4547 branches = RunGit(['for-each-ref', 'refs/heads',
Aaron Gablead64abd2017-12-04 09:49:13 -08004548 '--format=%(refname)']).splitlines()
dnj@chromium.org406c4402015-03-03 17:22:28 +00004549 # Reverse issue lookup.
4550 issue_branch_map = {}
4551 for branch in branches:
4552 cl = Changelist(branchref=branch)
4553 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
4554 if not args:
4555 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07004556 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00004557 for issue in args:
4558 if not issue:
4559 continue
phajdan.jre328cf92016-08-22 04:12:17 -07004560 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07004561 print('Branch for issue number %s: %s' % (
4562 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07004563 if options.json:
4564 write_json(options.json, result)
Aaron Gable78753da2017-06-15 10:35:49 -07004565 return 0
4566
4567 if len(args) > 0:
4568 issue = ParseIssueNumberArgument(args[0], options.forced_codereview)
4569 if not issue.valid:
4570 DieWithError('Pass a url or number to set the issue, 0 to unset it, '
4571 'or no argument to list it.\n'
4572 'Maybe you want to run git cl status?')
4573 cl = Changelist(codereview=issue.codereview)
4574 cl.SetIssue(issue.issue)
dnj@chromium.org406c4402015-03-03 17:22:28 +00004575 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004576 cl = Changelist(codereview=options.forced_codereview)
Aaron Gable78753da2017-06-15 10:35:49 -07004577 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
4578 if options.json:
4579 write_json(options.json, {
4580 'issue': cl.GetIssue(),
4581 'issue_url': cl.GetIssueURL(),
4582 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004583 return 0
4584
4585
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004586def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004587 """Shows or posts review comments for any changelist."""
4588 parser.add_option('-a', '--add-comment', dest='comment',
4589 help='comment to add to an issue')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004590 parser.add_option('-i', '--issue', dest='issue',
4591 help='review issue id (defaults to current issue). '
4592 'If given, requires --rietveld or --gerrit')
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004593 parser.add_option('-m', '--machine-readable', dest='readable',
4594 action='store_false', default=True,
4595 help='output comments in a format compatible with '
4596 'editor parsing')
smut@google.comc85ac942015-09-15 16:34:43 +00004597 parser.add_option('-j', '--json-file',
Stefan Zager1306bd02017-06-22 19:26:46 -07004598 help='File to write JSON summary to, or "-" for stdout')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004599 auth.add_auth_options(parser)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004600 _add_codereview_select_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004601 options, args = parser.parse_args(args)
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004602 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004603 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004604
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004605 issue = None
4606 if options.issue:
4607 try:
4608 issue = int(options.issue)
4609 except ValueError:
4610 DieWithError('A review issue id is expected to be a number')
Andrii Shyshkalov0d6b46e2017-03-17 22:23:22 +01004611 if not options.forced_codereview:
4612 parser.error('--gerrit or --rietveld is required if --issue is specified')
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004613
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004614 cl = Changelist(issue=issue,
Andrii Shyshkalov70909e12017-04-10 14:38:32 +02004615 codereview=options.forced_codereview,
Andrii Shyshkalov625986d2017-03-16 00:24:37 +01004616 auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004617
4618 if options.comment:
4619 cl.AddComment(options.comment)
4620 return 0
4621
Aaron Gable0ffdf2d2017-06-05 13:01:17 -07004622 summary = sorted(cl.GetCommentsSummary(readable=options.readable),
4623 key=lambda c: c.date)
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004624 for comment in summary:
4625 if comment.disapproval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004626 color = Fore.RED
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004627 elif comment.approval:
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004628 color = Fore.GREEN
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004629 elif comment.sender == cl.GetIssueOwner():
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00004630 color = Fore.MAGENTA
4631 else:
4632 color = Fore.BLUE
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004633 print('\n%s%s %s%s\n%s' % (
4634 color,
4635 comment.date.strftime('%Y-%m-%d %H:%M:%S UTC'),
4636 comment.sender,
4637 Fore.RESET,
4638 '\n'.join(' ' + l for l in comment.message.strip().splitlines())))
4639
smut@google.comc85ac942015-09-15 16:34:43 +00004640 if options.json_file:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004641 def pre_serialize(c):
4642 dct = c.__dict__.copy()
4643 dct['date'] = dct['date'].strftime('%Y-%m-%d %H:%M:%S.%f')
4644 return dct
smut@google.comc85ac942015-09-15 16:34:43 +00004645 with open(options.json_file, 'wb') as f:
Andrii Shyshkalovd8aa49f2017-03-17 16:05:49 +01004646 json.dump(map(pre_serialize, summary), f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00004647 return 0
4648
4649
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004650@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004651def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004652 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00004653 parser.add_option('-d', '--display', action='store_true',
4654 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004655 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07004656 help='New description to set for this issue (- for stdin, '
4657 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07004658 parser.add_option('-f', '--force', action='store_true',
4659 help='Delete any unpublished Gerrit edits for this issue '
4660 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004661
4662 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004663 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004664 options, args = parser.parse_args(args)
4665 _process_codereview_select_options(parser, options)
4666
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004667 target_issue_arg = None
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004668 if len(args) > 0:
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004669 target_issue_arg = ParseIssueNumberArgument(args[0],
4670 options.forced_codereview)
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004671 if not target_issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02004672 parser.error('invalid codereview url or CL id')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004673
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004674 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00004675
martiniss6eda05f2016-06-30 10:18:35 -07004676 kwargs = {
4677 'auth_config': auth_config,
4678 'codereview': options.forced_codereview,
4679 }
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004680 detected_codereview_from_url = False
Andrii Shyshkalov8039be72017-01-26 09:38:18 +01004681 if target_issue_arg:
4682 kwargs['issue'] = target_issue_arg.issue
4683 kwargs['codereview_host'] = target_issue_arg.hostname
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004684 if target_issue_arg.codereview and not options.forced_codereview:
4685 detected_codereview_from_url = True
4686 kwargs['codereview'] = target_issue_arg.codereview
martiniss6eda05f2016-06-30 10:18:35 -07004687
4688 cl = Changelist(**kwargs)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004689 if not cl.GetIssue():
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004690 assert not detected_codereview_from_url
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004691 DieWithError('This branch has no associated changelist.')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02004692
4693 if detected_codereview_from_url:
4694 logging.info('canonical issue/change URL: %s (type: %s)\n',
4695 cl.GetIssueURL(), target_issue_arg.codereview)
4696
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004697 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004698
smut@google.com34fb6b12015-07-13 20:03:26 +00004699 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07004700 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00004701 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004702
4703 if options.new_description:
4704 text = options.new_description
4705 if text == '-':
4706 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07004707 elif text == '+':
4708 base_branch = cl.GetCommonAncestorWithUpstream()
4709 change = cl.GetChange(base_branch, None, local_description=True)
4710 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004711
4712 description.set_description(text)
4713 else:
Aaron Gable3a16ed12017-03-23 10:51:55 -07004714 description.prompt(git_footer=cl.IsGerrit())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00004715
Andrii Shyshkalov680253d2017-03-15 21:07:36 +01004716 if cl.GetDescription().strip() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07004717 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00004718 return 0
4719
4720
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004721def CreateDescriptionFromLog(args):
4722 """Pulls out the commit log to use as a base for the CL description."""
4723 log_args = []
4724 if len(args) == 1 and not args[0].endswith('.'):
4725 log_args = [args[0] + '..']
4726 elif len(args) == 1 and args[0].endswith('...'):
4727 log_args = [args[0][:-1]]
4728 elif len(args) == 2:
4729 log_args = [args[0] + '..' + args[1]]
4730 else:
4731 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00004732 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004733
4734
thestig@chromium.org44202a22014-03-11 19:22:18 +00004735def CMDlint(parser, args):
4736 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004737 parser.add_option('--filter', action='append', metavar='-x,+y',
4738 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004739 auth.add_auth_options(parser)
4740 options, args = parser.parse_args(args)
4741 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004742
4743 # Access to a protected member _XX of a client class
Quinten Yearsleyb2cc4a92016-12-15 13:53:26 -08004744 # pylint: disable=protected-access
thestig@chromium.org44202a22014-03-11 19:22:18 +00004745 try:
4746 import cpplint
4747 import cpplint_chromium
4748 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07004749 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00004750 return 1
4751
4752 # Change the current working directory before calling lint so that it
4753 # shows the correct base.
4754 previous_cwd = os.getcwd()
4755 os.chdir(settings.GetRoot())
4756 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004757 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004758 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
4759 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004760 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07004761 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00004762 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00004763
4764 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00004765 command = args + files
4766 if options.filter:
4767 command = ['--filter=' + ','.join(options.filter)] + command
4768 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004769
4770 white_regex = re.compile(settings.GetLintRegex())
4771 black_regex = re.compile(settings.GetLintIgnoreRegex())
4772 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
4773 for filename in filenames:
4774 if white_regex.match(filename):
4775 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07004776 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004777 else:
4778 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
4779 extra_check_functions)
4780 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004781 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004782 finally:
4783 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07004784 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00004785 if cpplint._cpplint_state.error_count != 0:
4786 return 1
4787 return 0
4788
4789
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004790def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004791 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004792 parser.add_option('-u', '--upload', action='store_true',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004793 help='Run upload hook instead of the push hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00004794 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004795 help='Run checks even if tree is dirty')
Aaron Gable8076c282017-11-29 14:39:41 -08004796 parser.add_option('--all', action='store_true',
4797 help='Run checks against all files, not just modified ones')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004798 auth.add_auth_options(parser)
4799 options, args = parser.parse_args(args)
4800 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004801
sbc@chromium.org71437c02015-04-09 19:29:40 +00004802 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004803 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004804 return 1
4805
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004806 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004807 if args:
4808 base_branch = args[0]
4809 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004810 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004811 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004812
Aaron Gable8076c282017-11-29 14:39:41 -08004813 if options.all:
4814 base_change = cl.GetChange(base_branch, None)
4815 files = [('M', f) for f in base_change.AllFiles()]
4816 change = presubmit_support.GitChange(
4817 base_change.Name(),
4818 base_change.FullDescriptionText(),
4819 base_change.RepositoryRoot(),
4820 files,
4821 base_change.issue,
4822 base_change.patchset,
4823 base_change.author_email,
4824 base_change._upstream)
4825 else:
4826 change = cl.GetChange(base_branch, None)
4827
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004828 cl.RunHook(
4829 committing=not options.upload,
4830 may_prompt=False,
4831 verbose=options.verbose,
Aaron Gable8076c282017-11-29 14:39:41 -08004832 change=change)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004833 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004834
4835
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004836def GenerateGerritChangeId(message):
4837 """Returns Ixxxxxx...xxx change id.
4838
4839 Works the same way as
4840 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4841 but can be called on demand on all platforms.
4842
4843 The basic idea is to generate git hash of a state of the tree, original commit
4844 message, author/committer info and timestamps.
4845 """
4846 lines = []
4847 tree_hash = RunGitSilent(['write-tree'])
4848 lines.append('tree %s' % tree_hash.strip())
4849 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4850 if code == 0:
4851 lines.append('parent %s' % parent.strip())
4852 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4853 lines.append('author %s' % author.strip())
4854 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4855 lines.append('committer %s' % committer.strip())
4856 lines.append('')
4857 # Note: Gerrit's commit-hook actually cleans message of some lines and
4858 # whitespace. This code is not doing this, but it clearly won't decrease
4859 # entropy.
4860 lines.append(message)
4861 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4862 stdin='\n'.join(lines))
4863 return 'I%s' % change_hash.strip()
4864
4865
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01004866def GetTargetRef(remote, remote_branch, target_branch):
wittman@chromium.org455dc922015-01-26 20:15:50 +00004867 """Computes the remote branch ref to use for the CL.
4868
4869 Args:
4870 remote (str): The git remote for the CL.
4871 remote_branch (str): The git remote branch for the CL.
4872 target_branch (str): The target branch specified by the user.
wittman@chromium.org455dc922015-01-26 20:15:50 +00004873 """
4874 if not (remote and remote_branch):
4875 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004876
wittman@chromium.org455dc922015-01-26 20:15:50 +00004877 if target_branch:
Quinten Yearsley0c62da92017-05-31 13:39:42 -07004878 # Canonicalize branch references to the equivalent local full symbolic
wittman@chromium.org455dc922015-01-26 20:15:50 +00004879 # refs, which are then translated into the remote full symbolic refs
4880 # below.
4881 if '/' not in target_branch:
4882 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4883 else:
4884 prefix_replacements = (
4885 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4886 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4887 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4888 )
4889 match = None
4890 for regex, replacement in prefix_replacements:
4891 match = re.search(regex, target_branch)
4892 if match:
4893 remote_branch = target_branch.replace(match.group(0), replacement)
4894 break
4895 if not match:
4896 # This is a branch path but not one we recognize; use as-is.
4897 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004898 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4899 # Handle the refs that need to land in different refs.
4900 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004901
wittman@chromium.org455dc922015-01-26 20:15:50 +00004902 # Create the true path to the remote branch.
4903 # Does the following translation:
4904 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4905 # * refs/remotes/origin/master -> refs/heads/master
4906 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4907 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4908 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4909 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4910 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4911 'refs/heads/')
4912 elif remote_branch.startswith('refs/remotes/branch-heads'):
4913 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
Andrii Shyshkalov768f1d82016-12-08 15:10:13 +01004914
wittman@chromium.org455dc922015-01-26 20:15:50 +00004915 return remote_branch
4916
4917
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004918def cleanup_list(l):
4919 """Fixes a list so that comma separated items are put as individual items.
4920
4921 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4922 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4923 """
4924 items = sum((i.split(',') for i in l), [])
4925 stripped_items = (i.strip() for i in items)
4926 return sorted(filter(None, stripped_items))
4927
4928
Aaron Gable4db38df2017-11-03 14:59:07 -07004929@subcommand.usage('[flags]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004930def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004931 """Uploads the current changelist to codereview.
4932
4933 Can skip dependency patchset uploads for a branch by running:
4934 git config branch.branch_name.skip-deps-uploads True
4935 To unset run:
4936 git config --unset branch.branch_name.skip-deps-uploads
4937 Can also set the above globally by using the --global flag.
Dominic Battre7d1c4842017-10-27 09:17:28 +02004938
4939 If the name of the checked out branch starts with "bug-" or "fix-" followed by
4940 a bug number, this bug number is automatically populated in the CL
4941 description.
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004942
4943 If subject contains text in square brackets or has "<text>: " prefix, such
4944 text(s) is treated as Gerrit hashtags. For example, CLs with subjects
4945 [git-cl] add support for hashtags
4946 Foo bar: implement foo
4947 will be hashtagged with "git-cl" and "foo-bar" respectively.
rmistry@google.com78948ed2015-07-08 23:09:57 +00004948 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004949 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4950 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004951 parser.add_option('--bypass-watchlists', action='store_true',
4952 dest='bypass_watchlists',
4953 help='bypass watchlists auto CC-ing reviewers')
Aaron Gablef7543cd2017-07-20 14:26:31 -07004954 parser.add_option('-f', '--force', action='store_true', dest='force',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004955 help="force yes to questions (don't prompt)")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004956 parser.add_option('--message', '-m', dest='message',
4957 help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004958 parser.add_option('-b', '--bug',
4959 help='pre-populate the bug number(s) for this issue. '
4960 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004961 parser.add_option('--message-file', dest='message_file',
4962 help='file which contains message for patchset')
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08004963 parser.add_option('--title', '-t', dest='title',
4964 help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004965 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004966 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004967 help='reviewer email addresses')
Robert Iannucci6c98dc62017-04-18 11:38:00 -07004968 parser.add_option('--tbrs',
4969 action='append', default=[],
4970 help='TBR email addresses')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004971 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004972 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004973 help='cc email addresses')
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004974 parser.add_option('--hashtag', dest='hashtags',
4975 action='append', default=[],
4976 help=('Gerrit hashtag for new CL; '
4977 'can be applied multiple times'))
adamk@chromium.org36f47302013-04-05 01:08:31 +00004978 parser.add_option('-s', '--send-mail', action='store_true',
Aaron Gable59f48512017-01-12 10:54:46 -08004979 help='send email to reviewer(s) and cc(s) immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004980 parser.add_option('--emulate_svn_auto_props',
4981 '--emulate-svn-auto-props',
4982 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004983 dest="emulate_svn_auto_props",
4984 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004985 parser.add_option('-c', '--use-commit-queue', action='store_true',
Aaron Gableedbc4132017-09-11 13:22:28 -07004986 help='tell the commit queue to commit this patchset; '
4987 'implies --send-mail')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004988 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004989 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004990 metavar='TARGET',
4991 help='Apply CL to remote ref TARGET. ' +
4992 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004993 parser.add_option('--squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004994 help='Squash multiple commits into one')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004995 parser.add_option('--no-squash', action='store_true',
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004996 help='Don\'t squash multiple commits into one')
rmistry9eadede2016-09-19 11:22:43 -07004997 parser.add_option('--topic', default=None,
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08004998 help='Topic to specify when uploading')
Robert Iannuccif2708bd2017-04-17 15:49:02 -07004999 parser.add_option('--tbr-owners', dest='add_owners_to', action='store_const',
5000 const='TBR', help='add a set of OWNERS to TBR')
5001 parser.add_option('--r-owners', dest='add_owners_to', action='store_const',
5002 const='R', help='add a set of OWNERS to R')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00005003 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
5004 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00005005 help='Send the patchset to do a CQ dry run right after '
5006 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00005007 parser.add_option('--dependencies', action='store_true',
5008 help='Uploads CLs of all the local branches that depend on '
5009 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00005010
Nodir Turakulovd0e2cd22017-11-15 10:22:06 -08005011 # TODO: remove Rietveld flags
5012 parser.add_option('--private', action='store_true',
5013 help='set the review private (rietveld only)')
5014 parser.add_option('--email', default=None,
5015 help='email address to use to connect to Rietveld')
5016
rmistry@google.com2dd99862015-06-22 12:22:18 +00005017 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00005018 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005019 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005020 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005021 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005022 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005023 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005024
sbc@chromium.org71437c02015-04-09 19:29:40 +00005025 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00005026 return 1
5027
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005028 options.reviewers = cleanup_list(options.reviewers)
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005029 options.tbrs = cleanup_list(options.tbrs)
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00005030 options.cc = cleanup_list(options.cc)
5031
tandriib80458a2016-06-23 12:20:07 -07005032 if options.message_file:
5033 if options.message:
5034 parser.error('only one of --message and --message-file allowed.')
5035 options.message = gclient_utils.FileRead(options.message_file)
5036 options.message_file = None
5037
tandrii4d0545a2016-07-06 03:56:49 -07005038 if options.cq_dry_run and options.use_commit_queue:
5039 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
5040
Aaron Gableedbc4132017-09-11 13:22:28 -07005041 if options.use_commit_queue:
5042 options.send_mail = True
5043
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00005044 # For sanity of test expectations, do this otherwise lazy-loading *now*.
5045 settings.GetIsGerrit()
5046
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005047 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00005048 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00005049
5050
Francois Dorayd42c6812017-05-30 15:10:20 -04005051@subcommand.usage('--description=<description file>')
5052def CMDsplit(parser, args):
5053 """Splits a branch into smaller branches and uploads CLs.
5054
5055 Creates a branch and uploads a CL for each group of files modified in the
5056 current branch that share a common OWNERS file. In the CL description and
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005057 comment, the string '$directory', is replaced with the directory containing
Francois Dorayd42c6812017-05-30 15:10:20 -04005058 the shared OWNERS file.
5059 """
5060 parser.add_option("-d", "--description", dest="description_file",
Gabriel Charette02b5ee82017-11-08 16:36:05 -05005061 help="A text file containing a CL description in which "
5062 "$directory will be replaced by each CL's directory.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005063 parser.add_option("-c", "--comment", dest="comment_file",
5064 help="A text file containing a CL comment.")
Chris Watkinsba28e462017-12-13 11:22:17 +11005065 parser.add_option("-n", "--dry-run", dest="dry_run", action='store_true',
5066 default=False,
5067 help="List the files and reviewers for each CL that would "
5068 "be created, but don't create branches or CLs.")
Francois Dorayd42c6812017-05-30 15:10:20 -04005069 options, _ = parser.parse_args(args)
5070
5071 if not options.description_file:
5072 parser.error('No --description flag specified.')
5073
5074 def WrappedCMDupload(args):
5075 return CMDupload(OptionParser(), args)
5076
5077 return split_cl.SplitCl(options.description_file, options.comment_file,
Chris Watkinsba28e462017-12-13 11:22:17 +11005078 Changelist, WrappedCMDupload, options.dry_run)
Francois Dorayd42c6812017-05-30 15:10:20 -04005079
5080
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005081@subcommand.usage('DEPRECATED')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005082def CMDdcommit(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005083 """DEPRECATED: Used to commit the current changelist via git-svn."""
5084 message = ('git-cl no longer supports committing to SVN repositories via '
5085 'git-svn. You probably want to use `git cl land` instead.')
5086 print(message)
5087 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005088
5089
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005090# Two special branches used by git cl land.
5091MERGE_BRANCH = 'git-cl-commit'
5092CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
5093
5094
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005095@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00005096def CMDland(parser, args):
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005097 """Commits the current changelist via git.
5098
5099 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
5100 upstream and closes the issue automatically and atomically.
5101
5102 Otherwise (in case of Rietveld):
5103 Squashes branch into a single commit.
5104 Updates commit message with metadata (e.g. pointer to review).
5105 Pushes the code upstream.
5106 Updates review and closes.
5107 """
5108 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
5109 help='bypass upload presubmit hook')
5110 parser.add_option('-m', dest='message',
5111 help="override review description")
Aaron Gablef7543cd2017-07-20 14:26:31 -07005112 parser.add_option('-f', '--force', action='store_true', dest='force',
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005113 help="force yes to questions (don't prompt)")
5114 parser.add_option('-c', dest='contributor',
5115 help="external contributor for patch (appended to " +
5116 "description and used as author for git). Should be " +
5117 "formatted as 'First Last <email@example.com>'")
5118 add_git_similarity(parser)
5119 auth.add_auth_options(parser)
5120 (options, args) = parser.parse_args(args)
5121 auth_config = auth.extract_auth_config_from_options(options)
5122
5123 cl = Changelist(auth_config=auth_config)
5124
5125 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
5126 if cl.IsGerrit():
5127 if options.message:
5128 # This could be implemented, but it requires sending a new patch to
5129 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
5130 # Besides, Gerrit has the ability to change the commit message on submit
5131 # automatically, thus there is no need to support this option (so far?).
5132 parser.error('-m MESSAGE option is not supported for Gerrit.')
5133 if options.contributor:
5134 parser.error(
5135 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
5136 'Before uploading a commit to Gerrit, ensure it\'s author field is '
5137 'the contributor\'s "name <email>". If you can\'t upload such a '
5138 'commit for review, contact your repository admin and request'
5139 '"Forge-Author" permission.')
5140 if not cl.GetIssue():
5141 DieWithError('You must upload the change first to Gerrit.\n'
5142 ' If you would rather have `git cl land` upload '
5143 'automatically for you, see http://crbug.com/642759')
5144 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
5145 options.verbose)
5146
5147 current = cl.GetBranch()
5148 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
5149 if remote == '.':
5150 print()
5151 print('Attempting to push branch %r into another local branch!' % current)
5152 print()
5153 print('Either reparent this branch on top of origin/master:')
5154 print(' git reparent-branch --root')
5155 print()
5156 print('OR run `git rebase-update` if you think the parent branch is ')
5157 print('already committed.')
5158 print()
5159 print(' Current parent: %r' % upstream_branch)
5160 return 1
5161
5162 if not args:
5163 # Default to merging against our best guess of the upstream branch.
5164 args = [cl.GetUpstreamBranch()]
5165
5166 if options.contributor:
5167 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
Quinten Yearsley0c62da92017-05-31 13:39:42 -07005168 print("Please provide contributor as 'First Last <email@example.com>'")
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005169 return 1
5170
5171 base_branch = args[0]
5172
5173 if git_common.is_dirty_git_tree('land'):
5174 return 1
5175
5176 # This rev-list syntax means "show all commits not in my branch that
5177 # are in base_branch".
5178 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
5179 base_branch]).splitlines()
5180 if upstream_commits:
5181 print('Base branch "%s" has %d commits '
5182 'not in this branch.' % (base_branch, len(upstream_commits)))
5183 print('Run "git merge %s" before attempting to land.' % base_branch)
5184 return 1
5185
5186 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
5187 if not options.bypass_hooks:
5188 author = None
5189 if options.contributor:
5190 author = re.search(r'\<(.*)\>', options.contributor).group(1)
5191 hook_results = cl.RunHook(
5192 committing=True,
5193 may_prompt=not options.force,
5194 verbose=options.verbose,
5195 change=cl.GetChange(merge_base, author))
5196 if not hook_results.should_continue():
5197 return 1
5198
5199 # Check the tree status if the tree status URL is set.
5200 status = GetTreeStatus()
5201 if 'closed' == status:
5202 print('The tree is closed. Please wait for it to reopen. Use '
5203 '"git cl land --bypass-hooks" to commit on a closed tree.')
5204 return 1
5205 elif 'unknown' == status:
5206 print('Unable to determine tree status. Please verify manually and '
5207 'use "git cl land --bypass-hooks" to commit on a closed tree.')
5208 return 1
5209
5210 change_desc = ChangeDescription(options.message)
5211 if not change_desc.description and cl.GetIssue():
5212 change_desc = ChangeDescription(cl.GetDescription())
5213
5214 if not change_desc.description:
5215 if not cl.GetIssue() and options.bypass_hooks:
5216 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
5217 else:
5218 print('No description set.')
5219 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
5220 return 1
5221
5222 # Keep a separate copy for the commit message, because the commit message
5223 # contains the link to the Rietveld issue, while the Rietveld message contains
5224 # the commit viewvc url.
5225 if cl.GetIssue():
Aaron Gablea1bab272017-04-11 16:38:18 -07005226 change_desc.update_reviewers(
Robert Iannucci6c98dc62017-04-18 11:38:00 -07005227 get_approving_reviewers(cl.GetIssueProperties()), [])
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005228
5229 commit_desc = ChangeDescription(change_desc.description)
5230 if cl.GetIssue():
5231 # Xcode won't linkify this URL unless there is a non-whitespace character
5232 # after it. Add a period on a new line to circumvent this. Also add a space
5233 # before the period to make sure that Gitiles continues to correctly resolve
5234 # the URL.
5235 commit_desc.append_footer('Review-Url: %s .' % cl.GetIssueURL())
5236 if options.contributor:
5237 commit_desc.append_footer('Patch from %s.' % options.contributor)
5238
5239 print('Description:')
5240 print(commit_desc.description)
5241
5242 branches = [merge_base, cl.GetBranchRef()]
5243 if not options.force:
5244 print_stats(options.similarity, options.find_copies, branches)
5245
5246 # We want to squash all this branch's commits into one commit with the proper
5247 # description. We do this by doing a "reset --soft" to the base branch (which
5248 # keeps the working copy the same), then landing that.
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005249 # Delete the special branches if they exist.
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005250 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
5251 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
5252 result = RunGitWithCode(showref_cmd)
5253 if result[0] == 0:
5254 RunGit(['branch', '-D', branch])
5255
5256 # We might be in a directory that's present in this branch but not in the
5257 # trunk. Move up to the top of the tree so that git commands that expect a
5258 # valid CWD won't fail after we check out the merge branch.
5259 rel_base_path = settings.GetRelativeRoot()
5260 if rel_base_path:
5261 os.chdir(rel_base_path)
5262
5263 # Stuff our change into the merge branch.
5264 # We wrap in a try...finally block so if anything goes wrong,
5265 # we clean up the branches.
5266 retcode = -1
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005267 revision = None
5268 try:
5269 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
5270 RunGit(['reset', '--soft', merge_base])
5271 if options.contributor:
5272 RunGit(
5273 [
5274 'commit', '--author', options.contributor,
5275 '-m', commit_desc.description,
5276 ])
5277 else:
5278 RunGit(['commit', '-m', commit_desc.description])
5279
5280 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
5281 mirror = settings.GetGitMirror(remote)
5282 if mirror:
5283 pushurl = mirror.url
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005284 git_numberer_enabled = _is_git_numberer_enabled(pushurl, branch)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005285 else:
5286 pushurl = remote # Usually, this is 'origin'.
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005287 git_numberer_enabled = _is_git_numberer_enabled(
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005288 RunGit(['config', 'remote.%s.url' % remote]).strip(), branch)
5289
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005290 retcode = PushToGitWithAutoRebase(
5291 pushurl, branch, commit_desc.description, git_numberer_enabled)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005292 if retcode == 0:
5293 revision = RunGit(['rev-parse', 'HEAD']).strip()
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005294 if git_numberer_enabled:
5295 change_desc = ChangeDescription(
5296 RunGit(['show', '-s', '--format=%B', 'HEAD']).strip())
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005297 except: # pylint: disable=bare-except
5298 if _IS_BEING_TESTED:
5299 logging.exception('this is likely your ACTUAL cause of test failure.\n'
5300 + '-' * 30 + '8<' + '-' * 30)
5301 logging.error('\n' + '-' * 30 + '8<' + '-' * 30 + '\n\n\n')
5302 raise
5303 finally:
5304 # And then swap back to the original branch and clean up.
5305 RunGit(['checkout', '-q', cl.GetBranch()])
5306 RunGit(['branch', '-D', MERGE_BRANCH])
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005307 RunGit(['branch', '-D', CHERRY_PICK_BRANCH], error_ok=True)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005308
5309 if not revision:
5310 print('Failed to push. If this persists, please file a bug.')
5311 return 1
5312
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005313 if cl.GetIssue():
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005314 viewvc_url = settings.GetViewVCUrl()
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005315 if viewvc_url and revision:
5316 change_desc.append_footer(
5317 'Committed: %s%s' % (viewvc_url, revision))
5318 elif revision:
5319 change_desc.append_footer('Committed: %s' % (revision,))
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005320 print('Closing issue '
5321 '(you may be prompted for your codereview password)...')
5322 cl.UpdateDescription(change_desc.description)
5323 cl.CloseIssue()
5324 props = cl.GetIssueProperties()
5325 patch_num = len(props['patchsets'])
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005326 comment = "Committed patchset #%d (id:%d) manually as %s" % (
5327 patch_num, props['patchsets'][-1], revision)
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005328 if options.bypass_hooks:
5329 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
5330 else:
5331 comment += ' (presubmit successful).'
5332 cl.RpcServer().add_comment(cl.GetIssue(), comment)
5333
Aaron Gable1bc7bfe2016-12-19 10:08:14 -08005334 if os.path.isfile(POSTUPSTREAM_HOOK):
5335 RunCommand([POSTUPSTREAM_HOOK, merge_base], error_ok=True)
5336
Andrii Shyshkalovf3a20ae2017-01-24 21:23:57 +01005337 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005338
5339
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005340def PushToGitWithAutoRebase(remote, branch, original_description,
5341 git_numberer_enabled, max_attempts=3):
5342 """Pushes current HEAD commit on top of remote's branch.
5343
5344 Attempts to fetch and autorebase on push failures.
5345 Adds git number footers on the fly.
5346
5347 Returns integer code from last command.
5348 """
5349 cherry = RunGit(['rev-parse', 'HEAD']).strip()
5350 code = 0
5351 attempts_left = max_attempts
5352 while attempts_left:
5353 attempts_left -= 1
5354 print('Attempt %d of %d' % (max_attempts - attempts_left, max_attempts))
5355
5356 # Fetch remote/branch into local cherry_pick_branch, overriding the latter.
5357 # If fetch fails, retry.
5358 print('Fetching %s/%s...' % (remote, branch))
5359 code, out = RunGitWithCode(
5360 ['retry', 'fetch', remote,
5361 '+%s:refs/heads/%s' % (branch, CHERRY_PICK_BRANCH)])
5362 if code:
5363 print('Fetch failed with exit code %d.' % code)
5364 print(out.strip())
5365 continue
5366
5367 print('Cherry-picking commit on top of latest %s' % branch)
5368 RunGitWithCode(['checkout', 'refs/heads/%s' % CHERRY_PICK_BRANCH],
5369 suppress_stderr=True)
5370 parent_hash = RunGit(['rev-parse', 'HEAD']).strip()
5371 code, out = RunGitWithCode(['cherry-pick', cherry])
5372 if code:
5373 print('Your patch doesn\'t apply cleanly to \'%s\' HEAD @ %s, '
5374 'the following files have merge conflicts:' %
5375 (branch, parent_hash))
Aaron Gable7817f022017-12-12 09:43:17 -08005376 print(RunGit(['-c', 'core.quotePath=false', 'diff',
5377 '--name-status', '--diff-filter=U']).strip())
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005378 print('Please rebase your patch and try again.')
5379 RunGitWithCode(['cherry-pick', '--abort'])
5380 break
5381
5382 commit_desc = ChangeDescription(original_description)
5383 if git_numberer_enabled:
5384 logging.debug('Adding git number footers')
5385 parent_msg = RunGit(['show', '-s', '--format=%B', parent_hash]).strip()
5386 commit_desc.update_with_git_number_footers(parent_hash, parent_msg,
5387 branch)
5388 # Ensure timestamps are monotonically increasing.
5389 timestamp = max(1 + _get_committer_timestamp(parent_hash),
5390 _get_committer_timestamp('HEAD'))
5391 _git_amend_head(commit_desc.description, timestamp)
5392
5393 code, out = RunGitWithCode(
5394 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
5395 print(out)
5396 if code == 0:
5397 break
5398 if IsFatalPushFailure(out):
5399 print('Fatal push error. Make sure your .netrc credentials and git '
Andrii Shyshkalov9d932212017-04-10 14:28:23 +02005400 'user.email are correct and you have push access to the repo.\n'
5401 'Hint: run command below to diangose common Git/Gerrit credential '
5402 'problems:\n'
5403 ' git cl creds-check\n')
Andrii Shyshkalovaa31b972017-03-24 16:16:33 +01005404 break
5405 return code
5406
5407
5408def IsFatalPushFailure(push_stdout):
5409 """True if retrying push won't help."""
5410 return '(prohibited by Gerrit)' in push_stdout
5411
5412
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005413@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005414def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00005415 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005416 parser.add_option('-b', dest='newbranch',
5417 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005418 parser.add_option('-f', '--force', action='store_true',
Aaron Gable62619a32017-06-16 08:22:09 -07005419 help='overwrite state on the current or chosen branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005420 parser.add_option('-d', '--directory', action='store', metavar='DIR',
Aaron Gable62619a32017-06-16 08:22:09 -07005421 help='change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005422 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00005423 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00005424 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005425 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005426 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005427 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005428
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005429
5430 group = optparse.OptionGroup(
5431 parser,
5432 'Options for continuing work on the current issue uploaded from a '
5433 'different clone (e.g. different machine). Must be used independently '
5434 'from the other options. No issue number should be specified, and the '
5435 'branch must have an issue number associated with it')
5436 group.add_option('--reapply', action='store_true', dest='reapply',
5437 help='Reset the branch and reapply the issue.\n'
5438 'CAUTION: This will undo any local changes in this '
5439 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005440
5441 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005442 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005443 parser.add_option_group(group)
5444
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005445 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005446 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005447 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00005448 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005449 auth_config = auth.extract_auth_config_from_options(options)
5450
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005451 if options.reapply:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005452 if options.newbranch:
5453 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005454 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005455 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005456
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005457 cl = Changelist(auth_config=auth_config,
5458 codereview=options.forced_codereview)
5459 if not cl.GetIssue():
5460 parser.error('current branch must have an associated issue')
5461
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005462 upstream = cl.GetUpstreamBranch()
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005463 if upstream is None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005464 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005465
5466 RunGit(['reset', '--hard', upstream])
5467 if options.pull:
5468 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00005469
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005470 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
5471 options.directory)
5472
5473 if len(args) != 1 or not args[0]:
5474 parser.error('Must specify issue number or url')
5475
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005476 target_issue_arg = ParseIssueNumberArgument(args[0],
5477 options.forced_codereview)
5478 if not target_issue_arg.valid:
5479 parser.error('invalid codereview url or CL id')
5480
5481 cl_kwargs = {
5482 'auth_config': auth_config,
5483 'codereview_host': target_issue_arg.hostname,
5484 'codereview': options.forced_codereview,
5485 }
5486 detected_codereview_from_url = False
5487 if target_issue_arg.codereview and not options.forced_codereview:
5488 detected_codereview_from_url = True
5489 cl_kwargs['codereview'] = target_issue_arg.codereview
5490 cl_kwargs['issue'] = target_issue_arg.issue
5491
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005492 # We don't want uncommitted changes mixed up with the patch.
5493 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00005494 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005495
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005496 if options.newbranch:
5497 if options.force:
5498 RunGit(['branch', '-D', options.newbranch],
5499 stderr=subprocess2.PIPE, error_ok=True)
5500 RunGit(['new-branch', options.newbranch])
5501
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005502 cl = Changelist(**cl_kwargs)
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00005503
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005504 if cl.IsGerrit():
5505 if options.reject:
5506 parser.error('--reject is not supported with Gerrit codereview.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005507 if options.directory:
5508 parser.error('--directory is not supported with Gerrit codereview.')
5509
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02005510 if detected_codereview_from_url:
5511 print('canonical issue/change URL: %s (type: %s)\n' %
5512 (cl.GetIssueURL(), target_issue_arg.codereview))
5513
5514 return cl.CMDPatchWithParsedIssue(target_issue_arg, options.reject,
Aaron Gable62619a32017-06-16 08:22:09 -07005515 options.nocommit, options.directory,
5516 options.force)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005517
5518
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005519def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005520 """Fetches the tree status and returns either 'open', 'closed',
5521 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00005522 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005523 if url:
5524 status = urllib2.urlopen(url).read().lower()
5525 if status.find('closed') != -1 or status == '0':
5526 return 'closed'
5527 elif status.find('open') != -1 or status == '1':
5528 return 'open'
5529 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005530 return 'unset'
5531
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005532
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005533def GetTreeStatusReason():
5534 """Fetches the tree status from a json url and returns the message
5535 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00005536 url = settings.GetTreeStatusUrl()
5537 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005538 connection = urllib2.urlopen(json_url)
5539 status = json.loads(connection.read())
5540 connection.close()
5541 return status['message']
5542
dpranke@chromium.org970c5222011-03-12 00:32:24 +00005543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005544def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005545 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005546 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005547 status = GetTreeStatus()
5548 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07005549 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005550 return 2
5551
vapiera7fbd5a2016-06-16 09:17:49 -07005552 print('The tree is %s' % status)
5553 print()
5554 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005555 if status != 'open':
5556 return 1
5557 return 0
5558
5559
maruel@chromium.org15192402012-09-06 12:38:29 +00005560def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07005561 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07005562 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00005563 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005564 '-b', '--bot', action='append',
5565 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
5566 'times to specify multiple builders. ex: '
5567 '"-b win_rel -b win_layout". See '
5568 'the try server waterfall for the builders name and the tests '
5569 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00005570 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07005571 '-B', '--bucket', default='',
5572 help=('Buildbucket bucket to send the try requests.'))
5573 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005574 '-m', '--master', default='',
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005575 help=('DEPRECATED, use -B. The try master where to run the builds.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005576 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005577 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07005578 help='Revision to use for the try job; default: the revision will '
5579 'be determined by the try recipe that builder runs, which usually '
5580 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00005581 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005582 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07005583 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07005584 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00005585 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005586 '--project',
5587 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07005588 'in recipe to determine to which repository or directory to '
5589 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00005590 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005591 '-p', '--property', dest='properties', action='append', default=[],
5592 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07005593 'key2=value2 etc. The value will be treated as '
5594 'json if decodable, or as string otherwise. '
5595 'NOTE: using this may make your try job not usable for CQ, '
5596 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00005597 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005598 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5599 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00005600 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005601 auth.add_auth_options(parser)
Koji Ishii31c14782018-01-08 17:17:33 +09005602 _add_codereview_issue_select_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00005603 options, args = parser.parse_args(args)
Koji Ishii31c14782018-01-08 17:17:33 +09005604 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005605 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00005606
Nodir Turakulovf6929a12017-10-09 12:34:44 -07005607 if options.master and options.master.startswith('luci.'):
5608 parser.error(
5609 '-m option does not support LUCI. Please pass -B %s' % options.master)
machenbach@chromium.org45453142015-09-15 08:45:22 +00005610 # Make sure that all properties are prop=value pairs.
5611 bad_params = [x for x in options.properties if '=' not in x]
5612 if bad_params:
5613 parser.error('Got properties with missing "=": %s' % bad_params)
5614
maruel@chromium.org15192402012-09-06 12:38:29 +00005615 if args:
5616 parser.error('Unknown arguments: %s' % args)
5617
Koji Ishii31c14782018-01-08 17:17:33 +09005618 cl = Changelist(auth_config=auth_config, issue=options.issue,
5619 codereview=options.forced_codereview)
maruel@chromium.org15192402012-09-06 12:38:29 +00005620 if not cl.GetIssue():
5621 parser.error('Need to upload first')
5622
Andrii Shyshkaloveadad922017-01-26 09:38:30 +01005623 if cl.IsGerrit():
5624 # HACK: warm up Gerrit change detail cache to save on RPCs.
5625 cl._codereview_impl._GetChangeDetail(['DETAILED_ACCOUNTS', 'ALL_REVISIONS'])
5626
tandriie113dfd2016-10-11 10:20:12 -07005627 error_message = cl.CannotTriggerTryJobReason()
5628 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07005629 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00005630
borenet6c0efe62016-10-19 08:13:29 -07005631 if options.bucket and options.master:
5632 parser.error('Only one of --bucket and --master may be used.')
5633
qyearsley1fdfcb62016-10-24 13:22:03 -07005634 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00005635
qyearsleydd49f942016-10-28 11:57:22 -07005636 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
5637 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07005638 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07005639 if options.verbose:
Quinten Yearsleyfc5fd922017-05-31 11:50:52 -07005640 print('git cl try with no bots now defaults to CQ dry run.')
5641 print('Scheduling CQ dry run on: %s' % cl.GetIssueURL())
5642 return cl.SetCQState(_CQState.DRY_RUN)
stip@chromium.org43064fd2013-12-18 20:07:44 +00005643
borenet6c0efe62016-10-19 08:13:29 -07005644 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005645 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07005646 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07005647 'of bot requires an initial job from a parent (usually a builder). '
5648 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07005649 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00005650 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00005651
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00005652 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005653 # TODO(tandrii): Checking local patchset against remote patchset is only
5654 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5655 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07005656 print('Warning: Codereview server has newer patchsets (%s) than most '
5657 'recent upload from local checkout (%s). Did a previous upload '
5658 'fail?\n'
5659 'By default, git cl try uses the latest patchset from '
5660 'codereview, continuing to use patchset %s.\n' %
5661 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07005662
tandrii568043b2016-10-11 07:49:18 -07005663 try:
borenet6c0efe62016-10-19 08:13:29 -07005664 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
5665 patchset)
tandrii568043b2016-10-11 07:49:18 -07005666 except BuildbucketResponseException as ex:
5667 print('ERROR: %s' % ex)
5668 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00005669 return 0
5670
5671
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005672def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07005673 """Prints info about try jobs associated with current CL."""
5674 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005675 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005676 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005677 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005678 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00005679 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005680 '--color', action='store_true', default=setup_color.IS_TTY,
5681 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005682 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07005683 '--buildbucket-host', default='cr-buildbucket.appspot.com',
5684 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07005685 group.add_option(
Stefan Zager1306bd02017-06-22 19:26:46 -07005686 '--json', help=('Path of JSON output file to write try job results to,'
5687 'or "-" for stdout.'))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005688 parser.add_option_group(group)
5689 auth.add_auth_options(parser)
Stefan Zager27db3f22017-10-10 15:15:01 -07005690 _add_codereview_issue_select_options(parser)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005691 options, args = parser.parse_args(args)
Stefan Zager27db3f22017-10-10 15:15:01 -07005692 _process_codereview_issue_select_options(parser, options)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005693 if args:
5694 parser.error('Unrecognized args: %s' % ' '.join(args))
5695
5696 auth_config = auth.extract_auth_config_from_options(options)
Stefan Zager27db3f22017-10-10 15:15:01 -07005697 cl = Changelist(
5698 issue=options.issue, codereview=options.forced_codereview,
5699 auth_config=auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005700 if not cl.GetIssue():
5701 parser.error('Need to upload first')
5702
tandrii221ab252016-10-06 08:12:04 -07005703 patchset = options.patchset
5704 if not patchset:
5705 patchset = cl.GetMostRecentPatchset()
5706 if not patchset:
5707 parser.error('Codereview doesn\'t know about issue %s. '
5708 'No access to issue or wrong issue number?\n'
Andrii Shyshkalov0a0b0672017-03-16 16:27:48 +01005709 'Either upload first, or pass --patchset explicitly' %
tandrii221ab252016-10-06 08:12:04 -07005710 cl.GetIssue())
5711
Ravi Mistryfda50ca2016-11-14 10:19:18 -05005712 # TODO(tandrii): Checking local patchset against remote patchset is only
5713 # supported for Rietveld. Extend it to Gerrit or remove it completely.
5714 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07005715 print('Warning: Codereview server has newer patchsets (%s) than most '
5716 'recent upload from local checkout (%s). Did a previous upload '
5717 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07005718 'By default, git cl try-results uses the latest patchset from '
5719 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07005720 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005721 try:
tandrii221ab252016-10-06 08:12:04 -07005722 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005723 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07005724 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005725 return 1
qyearsley53f48a12016-09-01 10:45:13 -07005726 if options.json:
5727 write_try_results_json(options.json, jobs)
5728 else:
5729 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00005730 return 0
5731
5732
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005733@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005734def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005735 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00005736 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005737 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005738 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005739
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005740 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005741 if args:
5742 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005743 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07005744 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005745 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07005746 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00005747
5748 # Clear configured merge-base, if there is one.
5749 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00005750 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005751 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005752 return 0
5753
5754
thestig@chromium.org00858c82013-12-02 23:08:03 +00005755def CMDweb(parser, args):
5756 """Opens the current CL in the web browser."""
5757 _, args = parser.parse_args(args)
5758 if args:
5759 parser.error('Unrecognized args: %s' % ' '.join(args))
5760
5761 issue_url = Changelist().GetIssueURL()
5762 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005763 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005764 return 1
5765
5766 webbrowser.open(issue_url)
5767 return 0
5768
5769
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005770def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005771 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005772 parser.add_option('-d', '--dry-run', action='store_true',
5773 help='trigger in dry run mode')
5774 parser.add_option('-c', '--clear', action='store_true',
5775 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005776 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005777 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005778 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005779 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005780 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005781 if args:
5782 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005783 if options.dry_run and options.clear:
5784 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5785
iannuccie53c9352016-08-17 14:40:40 -07005786 cl = Changelist(auth_config=auth_config, issue=options.issue,
5787 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005788 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005789 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005790 elif options.dry_run:
5791 state = _CQState.DRY_RUN
5792 else:
5793 state = _CQState.COMMIT
5794 if not cl.GetIssue():
5795 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005796 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005797 return 0
5798
5799
groby@chromium.org411034a2013-02-26 15:12:01 +00005800def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005801 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005802 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005803 auth.add_auth_options(parser)
5804 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005805 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005806 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005807 if args:
5808 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005809 cl = Changelist(auth_config=auth_config, issue=options.issue,
5810 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005811 # Ensure there actually is an issue to close.
Aaron Gable7139a4e2017-09-05 17:53:09 -07005812 if not cl.GetIssue():
5813 DieWithError('ERROR No issue to close')
groby@chromium.org411034a2013-02-26 15:12:01 +00005814 cl.CloseIssue()
5815 return 0
5816
5817
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005818def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005819 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005820 parser.add_option(
5821 '--stat',
5822 action='store_true',
5823 dest='stat',
5824 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005825 auth.add_auth_options(parser)
5826 options, args = parser.parse_args(args)
5827 auth_config = auth.extract_auth_config_from_options(options)
5828 if args:
5829 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005830
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005831 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005832 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005833 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005834 if not issue:
5835 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005836
Aaron Gablea718c3e2017-08-28 17:47:28 -07005837 base = cl._GitGetBranchConfigValue('last-upload-hash')
5838 if not base:
5839 base = cl._GitGetBranchConfigValue('gerritsquashhash')
5840 if not base:
5841 detail = cl._GetChangeDetail(['CURRENT_REVISION', 'CURRENT_COMMIT'])
5842 revision_info = detail['revisions'][detail['current_revision']]
5843 fetch_info = revision_info['fetch']['http']
5844 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
5845 base = 'FETCH_HEAD'
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005846
Aaron Gablea718c3e2017-08-28 17:47:28 -07005847 cmd = ['git', 'diff']
5848 if options.stat:
5849 cmd.append('--stat')
5850 cmd.append(base)
5851 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005852
5853 return 0
5854
5855
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005856def CMDowners(parser, args):
Dirk Prankebf980882017-09-02 15:08:00 -07005857 """Finds potential owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005858 parser.add_option(
5859 '--no-color',
5860 action='store_true',
5861 help='Use this option to disable color output')
Dirk Prankebf980882017-09-02 15:08:00 -07005862 parser.add_option(
5863 '--batch',
5864 action='store_true',
5865 help='Do not run interactively, just suggest some')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005866 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005867 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005868 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005869
5870 author = RunGit(['config', 'user.email']).strip() or None
5871
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005872 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005873
5874 if args:
5875 if len(args) > 1:
5876 parser.error('Unknown args')
5877 base_branch = args[0]
5878 else:
5879 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005880 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005881
5882 change = cl.GetChange(base_branch, None)
Dirk Prankebf980882017-09-02 15:08:00 -07005883 affected_files = [f.LocalPath() for f in change.AffectedFiles()]
5884
5885 if options.batch:
5886 db = owners.Database(change.RepositoryRoot(), file, os.path)
5887 print('\n'.join(db.reviewers_for(affected_files, author)))
5888 return 0
5889
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005890 return owners_finder.OwnersFinder(
Dirk Prankebf980882017-09-02 15:08:00 -07005891 affected_files,
Jochen Eisinger72606f82017-04-04 10:44:18 +02005892 change.RepositoryRoot(),
Jochen Eisinger72606f82017-04-04 10:44:18 +02005893 author, fopen=file, os_path=os.path,
Jochen Eisingerd0573ec2017-04-13 10:55:06 +02005894 disable_color=options.no_color,
5895 override_files=change.OriginalOwnersFiles()).run()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005896
5897
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005898def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005899 """Generates a diff command."""
5900 # Generate diff for the current branch's changes.
Aaron Gablef4068aa2017-12-12 15:14:09 -08005901 diff_cmd = ['-c', 'core.quotePath=false', 'diff',
5902 '--no-ext-diff', '--no-prefix', diff_type,
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005903 upstream_commit, '--']
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005904
5905 if args:
5906 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005907 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005908 diff_cmd.append(arg)
5909 else:
5910 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005911
5912 return diff_cmd
5913
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005914
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005915def MatchingFileType(file_name, extensions):
5916 """Returns true if the file name ends with one of the given extensions."""
5917 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005918
Andrii Shyshkalov18975322017-01-25 16:44:13 +01005919
enne@chromium.org555cfe42014-01-29 18:21:39 +00005920@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005921def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005922 """Runs auto-formatting tools (clang-format etc.) on the diff."""
Christopher Lamc5ba6922017-01-24 11:19:14 +11005923 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005924 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005925 parser.add_option('--full', action='store_true',
5926 help='Reformat the full content of all touched files')
5927 parser.add_option('--dry-run', action='store_true',
5928 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005929 parser.add_option('--python', action='store_true',
5930 help='Format python code with yapf (experimental).')
Christopher Lamc5ba6922017-01-24 11:19:14 +11005931 parser.add_option('--js', action='store_true',
5932 help='Format javascript code with clang-format.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005933 parser.add_option('--diff', action='store_true',
5934 help='Print diff to stdout rather than modifying files.')
Ilya Shermane081cbe2017-08-15 17:51:04 -07005935 parser.add_option('--presubmit', action='store_true',
5936 help='Used when running the script from a presubmit.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005937 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005938
Daniel Chengc55eecf2016-12-30 03:11:02 -08005939 # Normalize any remaining args against the current path, so paths relative to
5940 # the current directory are still resolved as expected.
5941 args = [os.path.join(os.getcwd(), arg) for arg in args]
5942
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005943 # git diff generates paths against the root of the repository. Change
5944 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005945 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005946 if rel_base_path:
5947 os.chdir(rel_base_path)
5948
digit@chromium.org29e47272013-05-17 17:01:46 +00005949 # Grab the merge-base commit, i.e. the upstream commit of the current
5950 # branch when it was created or the last time it was rebased. This is
5951 # to cover the case where the user may have called "git fetch origin",
5952 # moving the origin branch to a newer commit, but hasn't rebased yet.
5953 upstream_commit = None
5954 cl = Changelist()
5955 upstream_branch = cl.GetUpstreamBranch()
5956 if upstream_branch:
5957 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5958 upstream_commit = upstream_commit.strip()
5959
5960 if not upstream_commit:
5961 DieWithError('Could not find base commit for this branch. '
5962 'Are you in detached state?')
5963
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005964 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5965 diff_output = RunGit(changed_files_cmd)
5966 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005967 # Filter out files deleted by this CL
5968 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005969
Christopher Lamc5ba6922017-01-24 11:19:14 +11005970 if opts.js:
5971 CLANG_EXTS.append('.js')
5972
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005973 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5974 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5975 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005976 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005977
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005978 top_dir = os.path.normpath(
5979 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5980
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005981 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5982 # formatted. This is used to block during the presubmit.
5983 return_value = 0
5984
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005985 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005986 # Locate the clang-format binary in the checkout
5987 try:
5988 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005989 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005990 DieWithError(e)
5991
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005992 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005993 cmd = [clang_format_tool]
5994 if not opts.dry_run and not opts.diff:
5995 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005996 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005997 if opts.diff:
5998 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005999 else:
6000 env = os.environ.copy()
6001 env['PATH'] = str(os.path.dirname(clang_format_tool))
6002 try:
6003 script = clang_format.FindClangFormatScriptInChromiumTree(
6004 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07006005 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006006 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006007
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006008 cmd = [sys.executable, script, '-p0']
6009 if not opts.dry_run and not opts.diff:
6010 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00006011
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006012 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
6013 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006014
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00006015 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
6016 if opts.diff:
6017 sys.stdout.write(stdout)
6018 if opts.dry_run and len(stdout) > 0:
6019 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006020
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006021 # Similar code to above, but using yapf on .py files rather than clang-format
6022 # on C/C++ files
6023 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006024 yapf_tool = gclient_utils.FindExecutable('yapf')
6025 if yapf_tool is None:
6026 DieWithError('yapf not found in PATH')
6027
6028 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006029 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006030 cmd = [yapf_tool]
6031 if not opts.dry_run and not opts.diff:
6032 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006033 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00006034 if opts.diff:
6035 sys.stdout.write(stdout)
6036 else:
6037 # TODO(sbc): yapf --lines mode still has some issues.
6038 # https://github.com/google/yapf/issues/154
6039 DieWithError('--python currently only works with --full')
6040
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006041 # Dart's formatter does not have the nice property of only operating on
6042 # modified chunks, so hard code full.
6043 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006044 try:
6045 command = [dart_format.FindDartFmtToolInChromiumTree()]
6046 if not opts.dry_run and not opts.diff:
6047 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00006048 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006049
ppi@chromium.org6593d932016-03-03 15:41:15 +00006050 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006051 if opts.dry_run and stdout:
6052 return_value = 2
6053 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07006054 print('Warning: Unable to check Dart code formatting. Dart SDK not '
6055 'found in this checkout. Files in other languages are still '
6056 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006057
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006058 # Format GN build files. Always run on full build files for canonical form.
6059 if gn_diff_files:
Andrii Shyshkalov18975322017-01-25 16:44:13 +01006060 cmd = ['gn', 'format']
brettw4b8ed592016-08-05 16:19:12 -07006061 if opts.dry_run or opts.diff:
6062 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006063 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07006064 gn_ret = subprocess2.call(cmd + [gn_diff_file],
6065 shell=sys.platform == 'win32',
6066 cwd=top_dir)
6067 if opts.dry_run and gn_ret == 2:
6068 return_value = 2 # Not formatted.
6069 elif opts.diff and gn_ret == 2:
6070 # TODO this should compute and print the actual diff.
6071 print("This change has GN build file diff for " + gn_diff_file)
6072 elif gn_ret != 0:
6073 # For non-dry run cases (and non-2 return values for dry-run), a
6074 # nonzero error code indicates a failure, probably because the file
6075 # doesn't parse.
6076 DieWithError("gn format failed on " + gn_diff_file +
6077 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00006078
Ilya Shermane081cbe2017-08-15 17:51:04 -07006079 # Skip the metrics formatting from the global presubmit hook. These files have
6080 # a separate presubmit hook that issues an error if the files need formatting,
6081 # whereas the top-level presubmit script merely issues a warning. Formatting
6082 # these files is somewhat slow, so it's important not to duplicate the work.
6083 if not opts.presubmit:
6084 for xml_dir in GetDirtyMetricsDirs(diff_files):
6085 tool_dir = os.path.join(top_dir, xml_dir)
6086 cmd = [os.path.join(tool_dir, 'pretty_print.py'), '--non-interactive']
6087 if opts.dry_run or opts.diff:
6088 cmd.append('--diff')
Ilya Sherman235b70d2017-08-22 17:46:38 -07006089 stdout = RunCommand(cmd, cwd=top_dir)
Ilya Shermane081cbe2017-08-15 17:51:04 -07006090 if opts.diff:
6091 sys.stdout.write(stdout)
6092 if opts.dry_run and stdout:
6093 return_value = 2 # Not formatted.
Alexei Svitkinef3cac412017-02-06 11:08:50 -05006094
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00006095 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006096
Steven Holte2e664bf2017-04-21 13:10:47 -07006097def GetDirtyMetricsDirs(diff_files):
6098 xml_diff_files = [x for x in diff_files if MatchingFileType(x, ['.xml'])]
6099 metrics_xml_dirs = [
6100 os.path.join('tools', 'metrics', 'actions'),
6101 os.path.join('tools', 'metrics', 'histograms'),
6102 os.path.join('tools', 'metrics', 'rappor'),
6103 os.path.join('tools', 'metrics', 'ukm')]
6104 for xml_dir in metrics_xml_dirs:
6105 if any(file.startswith(xml_dir) for file in xml_diff_files):
6106 yield xml_dir
6107
agable@chromium.orgfab8f822013-05-06 17:43:09 +00006108
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006109@subcommand.usage('<codereview url or issue id>')
6110def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006111 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006112 _, args = parser.parse_args(args)
6113
6114 if len(args) != 1:
6115 parser.print_help()
6116 return 1
6117
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00006118 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00006119 if not issue_arg.valid:
Andrii Shyshkalov28d840e2017-04-10 15:45:09 +02006120 parser.error('invalid codereview url or CL id')
Andrii Shyshkalovc9712392017-04-11 13:35:21 +02006121
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00006122 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006123
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006124 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00006125 output = RunGit(['config', '--local', '--get-regexp',
6126 r'branch\..*\.%s' % issueprefix],
6127 error_ok=True)
6128 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006129 if issue == target_issue:
6130 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006131
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00006132 branches = []
6133 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07006134 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006135 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07006136 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006137 return 1
6138 if len(branches) == 1:
6139 RunGit(['checkout', branches[0]])
6140 else:
vapiera7fbd5a2016-06-16 09:17:49 -07006141 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006142 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07006143 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006144 which = raw_input('Choose by index: ')
6145 try:
6146 RunGit(['checkout', branches[int(which)]])
6147 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07006148 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00006149 return 1
6150
6151 return 0
6152
6153
maruel@chromium.org29404b52014-09-08 22:58:00 +00006154def CMDlol(parser, args):
6155 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07006156 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00006157 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
6158 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
6159 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07006160 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00006161 return 0
6162
6163
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006164class OptionParser(optparse.OptionParser):
6165 """Creates the option parse and add --verbose support."""
6166 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006167 optparse.OptionParser.__init__(
6168 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006169 self.add_option(
6170 '-v', '--verbose', action='count', default=0,
6171 help='Use 2 times for more debugging info')
6172
6173 def parse_args(self, args=None, values=None):
6174 options, args = optparse.OptionParser.parse_args(self, args, values)
6175 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
Andrii Shyshkalov5b04a572017-01-23 17:44:41 +01006176 logging.basicConfig(
6177 level=levels[min(options.verbose, len(levels) - 1)],
6178 format='[%(levelname).1s%(asctime)s %(process)d %(thread)d '
6179 '%(filename)s] %(message)s')
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006180 return options, args
6181
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00006182
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006183def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006184 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07006185 print('\nYour python version %s is unsupported, please upgrade.\n' %
6186 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00006187 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006188
maruel@chromium.orgddd59412011-11-30 14:20:38 +00006189 # Reload settings.
6190 global settings
6191 settings = Settings()
6192
maruel@chromium.org39c0b222013-08-17 16:57:01 +00006193 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006194 dispatcher = subcommand.CommandDispatcher(__name__)
6195 try:
6196 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00006197 except auth.AuthenticationError as e:
6198 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07006199 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00006200 if e.code != 500:
6201 raise
6202 DieWithError(
6203 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
6204 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00006205 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006206
6207
6208if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00006209 # These affect sys.stdout so do it outside of main() to simplify mocks in
6210 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00006211 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00006212 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00006213 try:
6214 sys.exit(main(sys.argv[1:]))
6215 except KeyboardInterrupt:
6216 sys.stderr.write('interrupted\n')
6217 sys.exit(1)