blob: 1227565e842e95982561abc5b1385d98e4a980aa [file] [log] [blame]
iannucci@chromium.org405b87e2015-11-12 18:08:34 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 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
sheyang@google.com6ebaf782015-05-12 19:17:54 +000016import httplib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000017import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import logging
calamity@chromium.orgcf197482016-04-29 20:15:53 +000019import multiprocessing
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import optparse
21import os
22import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000023import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025import textwrap
sheyang@google.com6ebaf782015-05-12 19:17:54 +000026import traceback
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000027import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000029import urlparse
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +000030import uuid
thestig@chromium.org00858c82013-12-02 23:08:03 +000031import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000032import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000033
34try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000035 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000036except ImportError:
37 pass
38
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000039from third_party import colorama
sheyang@google.com6ebaf782015-05-12 19:17:54 +000040from third_party import httplib2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041from third_party import upload
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +000042import auth
skobes6468b902016-10-24 08:45:10 -070043import checkout
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000044import clang_format
tandrii@chromium.org71184c02016-01-13 15:18:44 +000045import commit_queue
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +000046import dart_format
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +000047import setup_color
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000048import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000049import gclient_utils
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +000050import gerrit_util
szager@chromium.org151ebcf2016-03-09 01:08:25 +000051import git_cache
iannucci@chromium.org9e849272014-04-04 00:31:55 +000052import git_common
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +000053import git_footers
piman@chromium.org336f9122014-09-04 02:16:55 +000054import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000055import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000056import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000057import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000058import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000059import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000061import watchlists
62
tandrii7400cf02016-06-21 08:48:07 -070063__version__ = '2.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000064
tandrii9d2c7a32016-06-22 03:42:45 -070065COMMIT_BOT_EMAIL = 'commit-bot@chromium.org'
iannuccie7f68952016-08-15 17:45:29 -070066DEFAULT_SERVER = 'https://codereview.chromium.org'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000067POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000069GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
rmistry@google.comc68112d2015-03-03 12:48:06 +000070REFS_THAT_ALIAS_TO_OTHER_REFS = {
71 'refs/remotes/origin/lkgr': 'refs/remotes/origin/master',
72 'refs/remotes/origin/lkcr': 'refs/remotes/origin/master',
73}
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
thestig@chromium.org44202a22014-03-11 19:22:18 +000075# Valid extensions for files we want to lint.
76DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
77DEFAULT_LINT_IGNORE_REGEX = r"$^"
78
borenet6c0efe62016-10-19 08:13:29 -070079# Buildbucket master name prefix.
80MASTER_PREFIX = 'master.'
81
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000082# Shortcut since it quickly becomes redundant.
83Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000084
maruel@chromium.orgddd59412011-11-30 14:20:38 +000085# Initialized in main()
86settings = None
87
88
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089def DieWithError(message):
vapiera7fbd5a2016-06-16 09:17:49 -070090 print(message, file=sys.stderr)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091 sys.exit(1)
92
93
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000094def GetNoGitPagerEnv():
95 env = os.environ.copy()
96 # 'cat' is a magical git string that disables pagers on all platforms.
97 env['GIT_PAGER'] = 'cat'
98 return env
99
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000100
bsep@chromium.org627d9002016-04-29 00:00:52 +0000101def RunCommand(args, error_ok=False, error_message=None, shell=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102 try:
bsep@chromium.org627d9002016-04-29 00:00:52 +0000103 return subprocess2.check_output(args, shell=shell, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000104 except subprocess2.CalledProcessError as e:
105 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000106 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000107 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000108 'Command "%s" failed.\n%s' % (
109 ' '.join(args), error_message or e.stdout or ''))
110 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
113def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000114 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000115 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000116
117
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000118def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000119 """Returns return code and stdout."""
tandrii5d48c322016-08-18 16:19:37 -0700120 if suppress_stderr:
121 stderr = subprocess2.VOID
122 else:
123 stderr = sys.stderr
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000124 try:
tandrii5d48c322016-08-18 16:19:37 -0700125 (out, _), code = subprocess2.communicate(['git'] + args,
126 env=GetNoGitPagerEnv(),
127 stdout=subprocess2.PIPE,
128 stderr=stderr)
129 return code, out
130 except subprocess2.CalledProcessError as e:
131 logging.debug('Failed running %s', args)
132 return e.returncode, e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000133
134
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000135def RunGitSilent(args):
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +0000136 """Returns stdout, suppresses stderr and ignores the return code."""
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000137 return RunGitWithCode(args, suppress_stderr=True)[1]
138
139
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000140def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000141 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000142 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000143 return (version.startswith(prefix) and
144 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000145
146
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +0000147def BranchExists(branch):
148 """Return True if specified branch exists."""
149 code, _ = RunGitWithCode(['rev-parse', '--verify', branch],
150 suppress_stderr=True)
151 return not code
152
153
tandrii2a16b952016-10-19 07:09:44 -0700154def time_sleep(seconds):
155 # Use this so that it can be mocked in tests without interfering with python
156 # system machinery.
157 import time # Local import to discourage others from importing time globally.
158 return time.sleep(seconds)
159
160
maruel@chromium.org90541732011-04-01 17:54:18 +0000161def ask_for_data(prompt):
162 try:
163 return raw_input(prompt)
164 except KeyboardInterrupt:
165 # Hide the exception.
166 sys.exit(1)
167
168
tandrii5d48c322016-08-18 16:19:37 -0700169def _git_branch_config_key(branch, key):
170 """Helper method to return Git config key for a branch."""
171 assert branch, 'branch name is required to set git config for it'
172 return 'branch.%s.%s' % (branch, key)
173
174
175def _git_get_branch_config_value(key, default=None, value_type=str,
176 branch=False):
177 """Returns git config value of given or current branch if any.
178
179 Returns default in all other cases.
180 """
181 assert value_type in (int, str, bool)
182 if branch is False: # Distinguishing default arg value from None.
183 branch = GetCurrentBranch()
184
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000185 if not branch:
tandrii5d48c322016-08-18 16:19:37 -0700186 return default
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000187
tandrii5d48c322016-08-18 16:19:37 -0700188 args = ['config']
tandrii33a46ff2016-08-23 05:53:40 -0700189 if value_type == bool:
tandrii5d48c322016-08-18 16:19:37 -0700190 args.append('--bool')
tandrii33a46ff2016-08-23 05:53:40 -0700191 # git config also has --int, but apparently git config suffers from integer
192 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700193 args.append(_git_branch_config_key(branch, key))
194 code, out = RunGitWithCode(args)
195 if code == 0:
196 value = out.strip()
197 if value_type == int:
198 return int(value)
199 if value_type == bool:
200 return bool(value.lower() == 'true')
201 return value
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202 return default
203
204
tandrii5d48c322016-08-18 16:19:37 -0700205def _git_set_branch_config_value(key, value, branch=None, **kwargs):
206 """Sets the value or unsets if it's None of a git branch config.
207
208 Valid, though not necessarily existing, branch must be provided,
209 otherwise currently checked out branch is used.
210 """
211 if not branch:
212 branch = GetCurrentBranch()
213 assert branch, 'a branch name OR currently checked out branch is required'
214 args = ['config']
qyearsley12fa6ff2016-08-24 09:18:40 -0700215 # Check for boolean first, because bool is int, but int is not bool.
tandrii5d48c322016-08-18 16:19:37 -0700216 if value is None:
217 args.append('--unset')
218 elif isinstance(value, bool):
219 args.append('--bool')
220 value = str(value).lower()
tandrii5d48c322016-08-18 16:19:37 -0700221 else:
tandrii33a46ff2016-08-23 05:53:40 -0700222 # git config also has --int, but apparently git config suffers from integer
223 # overflows (http://crbug.com/640115), so don't use it.
tandrii5d48c322016-08-18 16:19:37 -0700224 value = str(value)
225 args.append(_git_branch_config_key(branch, key))
226 if value is not None:
227 args.append(value)
228 RunGit(args, **kwargs)
229
230
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000231def add_git_similarity(parser):
232 parser.add_option(
tandrii5d48c322016-08-18 16:19:37 -0700233 '--similarity', metavar='SIM', type=int, action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000234 help='Sets the percentage that a pair of files need to match in order to'
235 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000236 parser.add_option(
237 '--find-copies', action='store_true',
238 help='Allows git to look for copies.')
239 parser.add_option(
240 '--no-find-copies', action='store_false', dest='find_copies',
241 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000242
243 old_parser_args = parser.parse_args
244 def Parse(args):
245 options, args = old_parser_args(args)
246
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000247 if options.similarity is None:
tandrii5d48c322016-08-18 16:19:37 -0700248 options.similarity = _git_get_branch_config_value(
249 'git-cl-similarity', default=50, value_type=int)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000250 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000251 print('Note: Saving similarity of %d%% in git config.'
252 % options.similarity)
tandrii5d48c322016-08-18 16:19:37 -0700253 _git_set_branch_config_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000254
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255 options.similarity = max(0, min(options.similarity, 100))
256
257 if options.find_copies is None:
tandrii5d48c322016-08-18 16:19:37 -0700258 options.find_copies = _git_get_branch_config_value(
259 'git-find-copies', default=True, value_type=bool)
iannucci@chromium.org79540052012-10-19 23:15:26 +0000260 else:
tandrii5d48c322016-08-18 16:19:37 -0700261 _git_set_branch_config_value('git-find-copies', bool(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000262
263 print('Using %d%% similarity for rename/copy detection. '
264 'Override with --similarity.' % options.similarity)
265
266 return options, args
267 parser.parse_args = Parse
268
269
machenbach@chromium.org45453142015-09-15 08:45:22 +0000270def _get_properties_from_options(options):
271 properties = dict(x.split('=', 1) for x in options.properties)
272 for key, val in properties.iteritems():
273 try:
274 properties[key] = json.loads(val)
275 except ValueError:
276 pass # If a value couldn't be evaluated, treat it as a string.
277 return properties
278
279
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000280def _prefix_master(master):
281 """Convert user-specified master name to full master name.
282
283 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
284 name, while the developers always use shortened master name
285 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
286 function does the conversion for buildbucket migration.
287 """
borenet6c0efe62016-10-19 08:13:29 -0700288 if master.startswith(MASTER_PREFIX):
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000289 return master
borenet6c0efe62016-10-19 08:13:29 -0700290 return '%s%s' % (MASTER_PREFIX, master)
291
292
293def _unprefix_master(bucket):
294 """Convert bucket name to shortened master name.
295
296 Buildbucket uses full master name(master.tryserver.chromium.linux) as bucket
297 name, while the developers always use shortened master name
298 (tryserver.chromium.linux) by stripping off the prefix 'master.'. This
299 function does the conversion for buildbucket migration.
300 """
301 if bucket.startswith(MASTER_PREFIX):
302 return bucket[len(MASTER_PREFIX):]
303 return bucket
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000304
305
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000306def _buildbucket_retry(operation_name, http, *args, **kwargs):
307 """Retries requests to buildbucket service and returns parsed json content."""
308 try_count = 0
309 while True:
310 response, content = http.request(*args, **kwargs)
311 try:
312 content_json = json.loads(content)
313 except ValueError:
314 content_json = None
315
316 # Buildbucket could return an error even if status==200.
317 if content_json and content_json.get('error'):
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000318 error = content_json.get('error')
319 if error.get('code') == 403:
320 raise BuildbucketResponseException(
321 'Access denied: %s' % error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000322 msg = 'Error in response. Reason: %s. Message: %s.' % (
nodir@chromium.orgbaff4e12016-03-08 00:33:57 +0000323 error.get('reason', ''), error.get('message', ''))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000324 raise BuildbucketResponseException(msg)
325
326 if response.status == 200:
327 if not content_json:
328 raise BuildbucketResponseException(
329 'Buildbucket returns invalid json content: %s.\n'
330 'Please file bugs at http://crbug.com, label "Infra-BuildBucket".' %
331 content)
332 return content_json
333 if response.status < 500 or try_count >= 2:
334 raise httplib2.HttpLib2Error(content)
335
336 # status >= 500 means transient failures.
337 logging.debug('Transient errors when %s. Will retry.', operation_name)
tandrii2a16b952016-10-19 07:09:44 -0700338 time_sleep(0.5 + 1.5*try_count)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000339 try_count += 1
340 assert False, 'unreachable'
341
342
qyearsley1fdfcb62016-10-24 13:22:03 -0700343def _get_bucket_map(changelist, options, option_parser):
qyearsleydd49f942016-10-28 11:57:22 -0700344 """Returns a dict mapping bucket names to builders and tests,
345 for triggering try jobs.
qyearsley1fdfcb62016-10-24 13:22:03 -0700346 """
qyearsleydd49f942016-10-28 11:57:22 -0700347 # If no bots are listed, we try to get a set of builders and tests based
348 # on GetPreferredTryMasters functions in PRESUBMIT.py files.
qyearsley1fdfcb62016-10-24 13:22:03 -0700349 if not options.bot:
350 change = changelist.GetChange(
351 changelist.GetCommonAncestorWithUpstream(), None)
qyearsley136b49f2016-10-31 09:02:26 -0700352 # Get try masters from PRESUBMIT.py files.
nodire4f0fe02016-11-04 16:23:30 -0700353 masters = presubmit_support.DoGetTryMasters(
qyearsley1fdfcb62016-10-24 13:22:03 -0700354 change=change,
355 changed_files=change.LocalPaths(),
356 repository_root=settings.GetRoot(),
357 default_presubmit=None,
358 project=None,
359 verbose=options.verbose,
360 output_stream=sys.stdout)
nodire4f0fe02016-11-04 16:23:30 -0700361 if masters is None:
362 return None
363 return {MASTER_PREFIX + m: b for m, b in masters.iteritems()}
qyearsley1fdfcb62016-10-24 13:22:03 -0700364
qyearsley1fdfcb62016-10-24 13:22:03 -0700365 if options.bucket:
366 return {options.bucket: {b: [] for b in options.bot}}
qyearsleydd49f942016-10-28 11:57:22 -0700367 if options.master:
368 return {_prefix_master(options.master): {b: [] for b in options.bot}}
qyearsley1fdfcb62016-10-24 13:22:03 -0700369
qyearsleydd49f942016-10-28 11:57:22 -0700370 # If bots are listed but no master or bucket, then we need to find out
371 # the corresponding master for each bot.
372 bucket_map, error_message = _get_bucket_map_for_builders(options.bot)
373 if error_message:
374 option_parser.error(
375 'Tryserver master cannot be found because: %s\n'
376 'Please manually specify the tryserver master, e.g. '
377 '"-m tryserver.chromium.linux".' % error_message)
378 return bucket_map
qyearsley1fdfcb62016-10-24 13:22:03 -0700379
380
qyearsley123a4682016-10-26 09:12:17 -0700381def _get_bucket_map_for_builders(builders):
382 """Returns a map of buckets to builders for the given builders."""
qyearsley1fdfcb62016-10-24 13:22:03 -0700383 map_url = 'https://builders-map.appspot.com/'
384 try:
qyearsley123a4682016-10-26 09:12:17 -0700385 builders_map = json.load(urllib2.urlopen(map_url))
qyearsley1fdfcb62016-10-24 13:22:03 -0700386 except urllib2.URLError as e:
387 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
388 (map_url, e))
389 except ValueError as e:
390 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
qyearsley123a4682016-10-26 09:12:17 -0700391 if not builders_map:
qyearsley1fdfcb62016-10-24 13:22:03 -0700392 return None, 'Failed to build master map.'
393
qyearsley123a4682016-10-26 09:12:17 -0700394 bucket_map = {}
395 for builder in builders:
qyearsley123a4682016-10-26 09:12:17 -0700396 masters = builders_map.get(builder, [])
397 if not masters:
qyearsley1fdfcb62016-10-24 13:22:03 -0700398 return None, ('No matching master for builder %s.' % builder)
qyearsley123a4682016-10-26 09:12:17 -0700399 if len(masters) > 1:
qyearsley1fdfcb62016-10-24 13:22:03 -0700400 return None, ('The builder name %s exists in multiple masters %s.' %
qyearsley123a4682016-10-26 09:12:17 -0700401 (builder, masters))
402 bucket = _prefix_master(masters[0])
403 bucket_map.setdefault(bucket, {})[builder] = []
404
405 return bucket_map, None
qyearsley1fdfcb62016-10-24 13:22:03 -0700406
407
borenet6c0efe62016-10-19 08:13:29 -0700408def _trigger_try_jobs(auth_config, changelist, buckets, options,
tandriide281ae2016-10-12 06:02:30 -0700409 category='git_cl_try', patchset=None):
qyearsley1fdfcb62016-10-24 13:22:03 -0700410 """Sends a request to Buildbucket to trigger try jobs for a changelist.
411
412 Args:
413 auth_config: AuthConfig for Rietveld.
414 changelist: Changelist that the try jobs are associated with.
415 buckets: A nested dict mapping bucket names to builders to tests.
416 options: Command-line options.
417 """
tandriide281ae2016-10-12 06:02:30 -0700418 assert changelist.GetIssue(), 'CL must be uploaded first'
419 codereview_url = changelist.GetCodereviewServer()
420 assert codereview_url, 'CL must be uploaded first'
421 patchset = patchset or changelist.GetMostRecentPatchset()
422 assert patchset, 'CL must be uploaded first'
423
424 codereview_host = urlparse.urlparse(codereview_url).hostname
425 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000426 http = authenticator.authorize(httplib2.Http())
427 http.force_exception_to_status_code = True
tandriide281ae2016-10-12 06:02:30 -0700428
429 # TODO(tandrii): consider caching Gerrit CL details just like
430 # _RietveldChangelistImpl does, then caching values in these two variables
431 # won't be necessary.
432 owner_email = changelist.GetIssueOwner()
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000433
434 buildbucket_put_url = (
435 'https://{hostname}/_ah/api/buildbucket/v1/builds/batch'.format(
sheyang@chromium.orgdb375572015-08-17 19:22:23 +0000436 hostname=options.buildbucket_host))
tandriide281ae2016-10-12 06:02:30 -0700437 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
438 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
439 hostname=codereview_host,
440 issue=changelist.GetIssue(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000441 patch=patchset)
tandrii8c5a3532016-11-04 07:52:02 -0700442
443 shared_parameters_properties = changelist.GetTryjobProperties(patchset)
444 shared_parameters_properties['category'] = category
445 if options.clobber:
446 shared_parameters_properties['clobber'] = True
tandriide281ae2016-10-12 06:02:30 -0700447 extra_properties = _get_properties_from_options(options)
tandrii8c5a3532016-11-04 07:52:02 -0700448 if extra_properties:
449 shared_parameters_properties.update(extra_properties)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000450
451 batch_req_body = {'builds': []}
452 print_text = []
453 print_text.append('Tried jobs on:')
borenet6c0efe62016-10-19 08:13:29 -0700454 for bucket, builders_and_tests in sorted(buckets.iteritems()):
455 print_text.append('Bucket: %s' % bucket)
456 master = None
457 if bucket.startswith(MASTER_PREFIX):
458 master = _unprefix_master(bucket)
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000459 for builder, tests in sorted(builders_and_tests.iteritems()):
460 print_text.append(' %s: %s' % (builder, tests))
461 parameters = {
462 'builder_name': builder,
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000463 'changes': [{
tandriide281ae2016-10-12 06:02:30 -0700464 'author': {'email': owner_email},
nodir@chromium.orgd2217312015-09-21 15:51:21 +0000465 'revision': options.revision,
466 }],
tandrii8c5a3532016-11-04 07:52:02 -0700467 'properties': shared_parameters_properties.copy(),
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000468 }
machenbach@chromium.org2403e802016-04-29 12:34:42 +0000469 if 'presubmit' in builder.lower():
470 parameters['properties']['dry_run'] = 'true'
tandrii@chromium.org3764fa22015-10-21 16:40:40 +0000471 if tests:
472 parameters['properties']['testfilter'] = tests
borenet6c0efe62016-10-19 08:13:29 -0700473
474 tags = [
475 'builder:%s' % builder,
476 'buildset:%s' % buildset,
477 'user_agent:git_cl_try',
478 ]
479 if master:
480 parameters['properties']['master'] = master
481 tags.append('master:%s' % master)
482
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000483 batch_req_body['builds'].append(
484 {
485 'bucket': bucket,
486 'parameters_json': json.dumps(parameters),
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000487 'client_operation_id': str(uuid.uuid4()),
borenet6c0efe62016-10-19 08:13:29 -0700488 'tags': tags,
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000489 }
490 )
491
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000492 _buildbucket_retry(
qyearsleyeab3c042016-08-24 09:18:28 -0700493 'triggering try jobs',
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000494 http,
495 buildbucket_put_url,
496 'PUT',
497 body=json.dumps(batch_req_body),
498 headers={'Content-Type': 'application/json'}
499 )
tandrii@chromium.org35c61452016-02-26 15:24:57 +0000500 print_text.append('To see results here, run: git cl try-results')
501 print_text.append('To see results in browser, run: git cl web')
vapiera7fbd5a2016-06-16 09:17:49 -0700502 print('\n'.join(print_text))
kjellander@chromium.org44424542015-06-02 18:35:29 +0000503
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000504
tandrii221ab252016-10-06 08:12:04 -0700505def fetch_try_jobs(auth_config, changelist, buildbucket_host,
506 patchset=None):
qyearsleyeab3c042016-08-24 09:18:28 -0700507 """Fetches try jobs from buildbucket.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000508
qyearsley53f48a12016-09-01 10:45:13 -0700509 Returns a map from build id to build info as a dictionary.
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000510 """
tandrii221ab252016-10-06 08:12:04 -0700511 assert buildbucket_host
512 assert changelist.GetIssue(), 'CL must be uploaded first'
513 assert changelist.GetCodereviewServer(), 'CL must be uploaded first'
514 patchset = patchset or changelist.GetMostRecentPatchset()
515 assert patchset, 'CL must be uploaded first'
516
517 codereview_url = changelist.GetCodereviewServer()
518 codereview_host = urlparse.urlparse(codereview_url).hostname
519 authenticator = auth.get_authenticator_for_host(codereview_host, auth_config)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000520 if authenticator.has_cached_credentials():
521 http = authenticator.authorize(httplib2.Http())
522 else:
vapiera7fbd5a2016-06-16 09:17:49 -0700523 print('Warning: Some results might be missing because %s' %
524 # Get the message on how to login.
tandrii221ab252016-10-06 08:12:04 -0700525 (auth.LoginRequiredError(codereview_host).message,))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000526 http = httplib2.Http()
527
528 http.force_exception_to_status_code = True
529
tandrii221ab252016-10-06 08:12:04 -0700530 buildset = 'patch/{codereview}/{hostname}/{issue}/{patch}'.format(
531 codereview='gerrit' if changelist.IsGerrit() else 'rietveld',
532 hostname=codereview_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000533 issue=changelist.GetIssue(),
tandrii221ab252016-10-06 08:12:04 -0700534 patch=patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000535 params = {'tag': 'buildset:%s' % buildset}
536
537 builds = {}
538 while True:
539 url = 'https://{hostname}/_ah/api/buildbucket/v1/search?{params}'.format(
tandrii221ab252016-10-06 08:12:04 -0700540 hostname=buildbucket_host,
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000541 params=urllib.urlencode(params))
qyearsleyeab3c042016-08-24 09:18:28 -0700542 content = _buildbucket_retry('fetching try jobs', http, url, 'GET')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000543 for build in content.get('builds', []):
544 builds[build['id']] = build
545 if 'next_cursor' in content:
546 params['start_cursor'] = content['next_cursor']
547 else:
548 break
549 return builds
550
551
qyearsleyeab3c042016-08-24 09:18:28 -0700552def print_try_jobs(options, builds):
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000553 """Prints nicely result of fetch_try_jobs."""
554 if not builds:
qyearsleyeab3c042016-08-24 09:18:28 -0700555 print('No try jobs scheduled')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000556 return
557
558 # Make a copy, because we'll be modifying builds dictionary.
559 builds = builds.copy()
560 builder_names_cache = {}
561
562 def get_builder(b):
563 try:
564 return builder_names_cache[b['id']]
565 except KeyError:
566 try:
567 parameters = json.loads(b['parameters_json'])
568 name = parameters['builder_name']
569 except (ValueError, KeyError) as error:
vapiera7fbd5a2016-06-16 09:17:49 -0700570 print('WARNING: failed to get builder name for build %s: %s' % (
571 b['id'], error))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000572 name = None
573 builder_names_cache[b['id']] = name
574 return name
575
576 def get_bucket(b):
577 bucket = b['bucket']
578 if bucket.startswith('master.'):
579 return bucket[len('master.'):]
580 return bucket
581
582 if options.print_master:
583 name_fmt = '%%-%ds %%-%ds' % (
584 max(len(str(get_bucket(b))) for b in builds.itervalues()),
585 max(len(str(get_builder(b))) for b in builds.itervalues()))
586 def get_name(b):
587 return name_fmt % (get_bucket(b), get_builder(b))
588 else:
589 name_fmt = '%%-%ds' % (
590 max(len(str(get_builder(b))) for b in builds.itervalues()))
591 def get_name(b):
592 return name_fmt % get_builder(b)
593
594 def sort_key(b):
595 return b['status'], b.get('result'), get_name(b), b.get('url')
596
597 def pop(title, f, color=None, **kwargs):
598 """Pop matching builds from `builds` dict and print them."""
599
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +0000600 if not options.color or color is None:
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000601 colorize = str
602 else:
603 colorize = lambda x: '%s%s%s' % (color, x, Fore.RESET)
604
605 result = []
606 for b in builds.values():
607 if all(b.get(k) == v for k, v in kwargs.iteritems()):
608 builds.pop(b['id'])
609 result.append(b)
610 if result:
vapiera7fbd5a2016-06-16 09:17:49 -0700611 print(colorize(title))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000612 for b in sorted(result, key=sort_key):
vapiera7fbd5a2016-06-16 09:17:49 -0700613 print(' ', colorize('\t'.join(map(str, f(b)))))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000614
615 total = len(builds)
616 pop(status='COMPLETED', result='SUCCESS',
617 title='Successes:', color=Fore.GREEN,
618 f=lambda b: (get_name(b), b.get('url')))
619 pop(status='COMPLETED', result='FAILURE', failure_reason='INFRA_FAILURE',
620 title='Infra Failures:', color=Fore.MAGENTA,
621 f=lambda b: (get_name(b), b.get('url')))
622 pop(status='COMPLETED', result='FAILURE', failure_reason='BUILD_FAILURE',
623 title='Failures:', color=Fore.RED,
624 f=lambda b: (get_name(b), b.get('url')))
625 pop(status='COMPLETED', result='CANCELED',
626 title='Canceled:', color=Fore.MAGENTA,
627 f=lambda b: (get_name(b),))
628 pop(status='COMPLETED', result='FAILURE',
629 failure_reason='INVALID_BUILD_DEFINITION',
630 title='Wrong master/builder name:', color=Fore.MAGENTA,
631 f=lambda b: (get_name(b),))
632 pop(status='COMPLETED', result='FAILURE',
633 title='Other failures:',
634 f=lambda b: (get_name(b), b.get('failure_reason'), b.get('url')))
635 pop(status='COMPLETED',
636 title='Other finished:',
637 f=lambda b: (get_name(b), b.get('result'), b.get('url')))
638 pop(status='STARTED',
639 title='Started:', color=Fore.YELLOW,
640 f=lambda b: (get_name(b), b.get('url')))
641 pop(status='SCHEDULED',
642 title='Scheduled:',
643 f=lambda b: (get_name(b), 'id=%s' % b['id']))
644 # The last section is just in case buildbucket API changes OR there is a bug.
645 pop(title='Other:',
646 f=lambda b: (get_name(b), 'id=%s' % b['id']))
647 assert len(builds) == 0
qyearsleyeab3c042016-08-24 09:18:28 -0700648 print('Total: %d try jobs' % total)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +0000649
650
qyearsley53f48a12016-09-01 10:45:13 -0700651def write_try_results_json(output_file, builds):
652 """Writes a subset of the data from fetch_try_jobs to a file as JSON.
653
654 The input |builds| dict is assumed to be generated by Buildbucket.
655 Buildbucket documentation: http://goo.gl/G0s101
656 """
657
658 def convert_build_dict(build):
659 return {
660 'buildbucket_id': build.get('id'),
661 'status': build.get('status'),
662 'result': build.get('result'),
663 'bucket': build.get('bucket'),
664 'builder_name': json.loads(
665 build.get('parameters_json', '{}')).get('builder_name'),
666 'failure_reason': build.get('failure_reason'),
667 'url': build.get('url'),
668 }
669
670 converted = []
671 for _, build in sorted(builds.items()):
672 converted.append(convert_build_dict(build))
673 write_json(output_file, converted)
674
675
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000676def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
677 """Return the corresponding git ref if |base_url| together with |glob_spec|
678 matches the full |url|.
679
680 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
681 """
682 fetch_suburl, as_ref = glob_spec.split(':')
683 if allow_wildcards:
684 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
685 if glob_match:
686 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
687 # "branches/{472,597,648}/src:refs/remotes/svn/*".
688 branch_re = re.escape(base_url)
689 if glob_match.group(1):
690 branch_re += '/' + re.escape(glob_match.group(1))
691 wildcard = glob_match.group(2)
692 if wildcard == '*':
693 branch_re += '([^/]*)'
694 else:
695 # Escape and replace surrounding braces with parentheses and commas
696 # with pipe symbols.
697 wildcard = re.escape(wildcard)
698 wildcard = re.sub('^\\\\{', '(', wildcard)
699 wildcard = re.sub('\\\\,', '|', wildcard)
700 wildcard = re.sub('\\\\}$', ')', wildcard)
701 branch_re += wildcard
702 if glob_match.group(3):
703 branch_re += re.escape(glob_match.group(3))
704 match = re.match(branch_re, url)
705 if match:
706 return re.sub('\*$', match.group(1), as_ref)
707
708 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
709 if fetch_suburl:
710 full_url = base_url + '/' + fetch_suburl
711 else:
712 full_url = base_url
713 if full_url == url:
714 return as_ref
715 return None
716
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000717
iannucci@chromium.org79540052012-10-19 23:15:26 +0000718def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000719 """Prints statistics about the change to the user."""
720 # --no-ext-diff is broken in some versions of Git, so try to work around
721 # this by overriding the environment (but there is still a problem if the
722 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000723 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000724 if 'GIT_EXTERNAL_DIFF' in env:
725 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000726
727 if find_copies:
scottmgb84b5e32016-11-10 09:25:33 -0800728 similarity_options = ['-l100000', '-C%s' % similarity]
iannucci@chromium.org79540052012-10-19 23:15:26 +0000729 else:
730 similarity_options = ['-M%s' % similarity]
731
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000732 try:
733 stdout = sys.stdout.fileno()
734 except AttributeError:
735 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000736 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000737 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000738 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000739 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000740
741
sheyang@google.com6ebaf782015-05-12 19:17:54 +0000742class BuildbucketResponseException(Exception):
743 pass
744
745
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746class Settings(object):
747 def __init__(self):
748 self.default_server = None
749 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000750 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751 self.is_git_svn = None
752 self.svn_branch = None
753 self.tree_status_url = None
754 self.viewvc_url = None
755 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000756 self.is_gerrit = None
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000757 self.squash_gerrit_uploads = None
tandrii@chromium.org28253532016-04-14 13:46:56 +0000758 self.gerrit_skip_ensure_authenticated = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000759 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000760 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000761 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000762 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763
764 def LazyUpdateIfNeeded(self):
765 """Updates the settings from a codereview.settings file, if available."""
766 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000767 # The only value that actually changes the behavior is
768 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000769 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000770 error_ok=True
771 ).strip().lower()
772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000773 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000774 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 LoadCodereviewSettingsFromFile(cr_settings_file)
776 self.updated = True
777
778 def GetDefaultServerUrl(self, error_ok=False):
779 if not self.default_server:
780 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000781 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000782 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783 if error_ok:
784 return self.default_server
785 if not self.default_server:
786 error_message = ('Could not find settings file. You must configure '
787 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000788 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000789 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 return self.default_server
791
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000792 @staticmethod
793 def GetRelativeRoot():
794 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000795
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000797 if self.root is None:
798 self.root = os.path.abspath(self.GetRelativeRoot())
799 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000801 def GetGitMirror(self, remote='origin'):
802 """If this checkout is from a local git mirror, return a Mirror object."""
szager@chromium.org81593742016-03-09 20:27:58 +0000803 local_url = RunGit(['config', '--get', 'remote.%s.url' % remote]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +0000804 if not os.path.isdir(local_url):
805 return None
806 git_cache.Mirror.SetCachePath(os.path.dirname(local_url))
807 remote_url = git_cache.Mirror.CacheDirToUrl(local_url)
808 # Use the /dev/null print_func to avoid terminal spew in WaitForRealCommit.
809 mirror = git_cache.Mirror(remote_url, print_func = lambda *args: None)
810 if mirror.exists():
811 return mirror
812 return None
813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 def GetIsGitSvn(self):
815 """Return true if this repo looks like it's using git-svn."""
816 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000817 if self.GetPendingRefPrefix():
818 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
819 self.is_git_svn = False
820 else:
821 # If you have any "svn-remote.*" config keys, we think you're using svn.
822 self.is_git_svn = RunGitWithCode(
823 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 return self.is_git_svn
825
826 def GetSVNBranch(self):
827 if self.svn_branch is None:
828 if not self.GetIsGitSvn():
829 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
830
831 # Try to figure out which remote branch we're based on.
832 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000833 # 1) iterate through our branch history and find the svn URL.
834 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
836 # regexp matching the git-svn line that contains the URL.
837 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
838
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000839 # We don't want to go through all of history, so read a line from the
840 # pipe at a time.
841 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000842 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000843 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
844 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000845 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000846 for line in proc.stdout:
847 match = git_svn_re.match(line)
848 if match:
849 url = match.group(1)
850 proc.stdout.close() # Cut pipe.
851 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000853 if url:
854 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
855 remotes = RunGit(['config', '--get-regexp',
856 r'^svn-remote\..*\.url']).splitlines()
857 for remote in remotes:
858 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000860 remote = match.group(1)
861 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000862 rewrite_root = RunGit(
863 ['config', 'svn-remote.%s.rewriteRoot' % remote],
864 error_ok=True).strip()
865 if rewrite_root:
866 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000867 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000868 ['config', 'svn-remote.%s.fetch' % remote],
869 error_ok=True).strip()
870 if fetch_spec:
871 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
872 if self.svn_branch:
873 break
874 branch_spec = RunGit(
875 ['config', 'svn-remote.%s.branches' % remote],
876 error_ok=True).strip()
877 if branch_spec:
878 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
879 if self.svn_branch:
880 break
881 tag_spec = RunGit(
882 ['config', 'svn-remote.%s.tags' % remote],
883 error_ok=True).strip()
884 if tag_spec:
885 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
886 if self.svn_branch:
887 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888
889 if not self.svn_branch:
890 DieWithError('Can\'t guess svn branch -- try specifying it on the '
891 'command line')
892
893 return self.svn_branch
894
895 def GetTreeStatusUrl(self, error_ok=False):
896 if not self.tree_status_url:
897 error_message = ('You must configure your tree status URL by running '
898 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000899 self.tree_status_url = self._GetRietveldConfig(
900 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000901 return self.tree_status_url
902
903 def GetViewVCUrl(self):
904 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000905 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000906 return self.viewvc_url
907
rmistry@google.com90752582014-01-14 21:04:50 +0000908 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000909 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000910
rmistry@google.com78948ed2015-07-08 23:09:57 +0000911 def GetIsSkipDependencyUpload(self, branch_name):
912 """Returns true if specified branch should skip dep uploads."""
913 return self._GetBranchConfig(branch_name, 'skip-deps-uploads',
914 error_ok=True)
915
rmistry@google.com5626a922015-02-26 14:03:30 +0000916 def GetRunPostUploadHook(self):
917 run_post_upload_hook = self._GetRietveldConfig(
918 'run-post-upload-hook', error_ok=True)
919 return run_post_upload_hook == "True"
920
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000921 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000922 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000923
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000924 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000925 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000926
ukai@chromium.orge8077812012-02-03 03:41:46 +0000927 def GetIsGerrit(self):
928 """Return true if this repo is assosiated with gerrit code review system."""
929 if self.is_gerrit is None:
930 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
931 return self.is_gerrit
932
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000933 def GetSquashGerritUploads(self):
934 """Return true if uploads to Gerrit should be squashed by default."""
935 if self.squash_gerrit_uploads is None:
tandriia60502f2016-06-20 02:01:53 -0700936 self.squash_gerrit_uploads = self.GetSquashGerritUploadsOverride()
937 if self.squash_gerrit_uploads is None:
938 # Default is squash now (http://crbug.com/611892#c23).
939 self.squash_gerrit_uploads = not (
940 RunGit(['config', '--bool', 'gerrit.squash-uploads'],
941 error_ok=True).strip() == 'false')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +0000942 return self.squash_gerrit_uploads
943
tandriia60502f2016-06-20 02:01:53 -0700944 def GetSquashGerritUploadsOverride(self):
945 """Return True or False if codereview.settings should be overridden.
946
947 Returns None if no override has been defined.
948 """
949 # See also http://crbug.com/611892#c23
950 result = RunGit(['config', '--bool', 'gerrit.override-squash-uploads'],
951 error_ok=True).strip()
952 if result == 'true':
953 return True
954 if result == 'false':
955 return False
956 return None
957
tandrii@chromium.org28253532016-04-14 13:46:56 +0000958 def GetGerritSkipEnsureAuthenticated(self):
959 """Return True if EnsureAuthenticated should not be done for Gerrit
960 uploads."""
961 if self.gerrit_skip_ensure_authenticated is None:
962 self.gerrit_skip_ensure_authenticated = (
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +0000963 RunGit(['config', '--bool', 'gerrit.skip-ensure-authenticated'],
tandrii@chromium.org28253532016-04-14 13:46:56 +0000964 error_ok=True).strip() == 'true')
965 return self.gerrit_skip_ensure_authenticated
966
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000967 def GetGitEditor(self):
968 """Return the editor specified in the git config, or None if none is."""
969 if self.git_editor is None:
970 self.git_editor = self._GetConfig('core.editor', error_ok=True)
971 return self.git_editor or None
972
thestig@chromium.org44202a22014-03-11 19:22:18 +0000973 def GetLintRegex(self):
974 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
975 DEFAULT_LINT_REGEX)
976
977 def GetLintIgnoreRegex(self):
978 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
979 DEFAULT_LINT_IGNORE_REGEX)
980
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000981 def GetProject(self):
982 if not self.project:
983 self.project = self._GetRietveldConfig('project', error_ok=True)
984 return self.project
985
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000986 def GetForceHttpsCommitUrl(self):
987 if not self.force_https_commit_url:
988 self.force_https_commit_url = self._GetRietveldConfig(
989 'force-https-commit-url', error_ok=True)
990 return self.force_https_commit_url
991
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000992 def GetPendingRefPrefix(self):
993 if not self.pending_ref_prefix:
994 self.pending_ref_prefix = self._GetRietveldConfig(
995 'pending-ref-prefix', error_ok=True)
996 return self.pending_ref_prefix
997
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000998 def _GetRietveldConfig(self, param, **kwargs):
999 return self._GetConfig('rietveld.' + param, **kwargs)
1000
rmistry@google.com78948ed2015-07-08 23:09:57 +00001001 def _GetBranchConfig(self, branch_name, param, **kwargs):
1002 return self._GetConfig('branch.' + branch_name + '.' + param, **kwargs)
1003
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004 def _GetConfig(self, param, **kwargs):
1005 self.LazyUpdateIfNeeded()
1006 return RunGit(['config', param], **kwargs).strip()
1007
1008
Andrii Shyshkalov5fb47742016-11-24 17:09:40 +01001009def ShouldGenerateGitNumberFooters():
1010 """Decides depending on codereview.settings file in the current checkout HEAD.
1011 """
1012 # TODO(tandrii): this has to be removed after Rietveld is read-only.
1013 # see also bugs http://crbug.com/642493 and http://crbug.com/600469.
1014 cr_settings_file = FindCodereviewSettingsFile()
1015 if not cr_settings_file:
1016 return False
1017 keyvals = gclient_utils.ParseCodereviewSettingsContent(
1018 cr_settings_file.read())
Andrii Shyshkalovb8c535f2016-11-24 18:01:52 +01001019 return keyvals.get('GENERATE_GIT_NUMBER_FOOTERS', '').lower() == 'true'
Andrii Shyshkalov5fb47742016-11-24 17:09:40 +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):
1054 def __init__(self, issue=None, patchset=None, hostname=None):
1055 self.issue = issue
1056 self.patchset = patchset
1057 self.hostname = hostname
1058
1059 @property
1060 def valid(self):
1061 return self.issue is not None
1062
1063
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001064def ParseIssueNumberArgument(arg):
1065 """Parses the issue argument and returns _ParsedIssueNumberArgument."""
1066 fail_result = _ParsedIssueNumberArgument()
1067
1068 if arg.isdigit():
1069 return _ParsedIssueNumberArgument(issue=int(arg))
1070 if not arg.startswith('http'):
1071 return fail_result
1072 url = gclient_utils.UpgradeToHttps(arg)
1073 try:
1074 parsed_url = urlparse.urlparse(url)
1075 except ValueError:
1076 return fail_result
1077 for cls in _CODEREVIEW_IMPLEMENTATIONS.itervalues():
1078 tmp = cls.ParseIssueURL(parsed_url)
1079 if tmp is not None:
1080 return tmp
1081 return fail_result
1082
1083
tandriic2405f52016-10-10 08:13:15 -07001084class GerritIssueNotExists(Exception):
1085 def __init__(self, issue, url):
1086 self.issue = issue
1087 self.url = url
1088 super(GerritIssueNotExists, self).__init__()
1089
1090 def __str__(self):
1091 return 'issue %s at %s does not exist or you have no access to it' % (
1092 self.issue, self.url)
1093
1094
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095class Changelist(object):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001096 """Changelist works with one changelist in local branch.
1097
1098 Supports two codereview backends: Rietveld or Gerrit, selected at object
1099 creation.
1100
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00001101 Notes:
1102 * Not safe for concurrent multi-{thread,process} use.
1103 * Caches values from current branch. Therefore, re-use after branch change
tandrii5d48c322016-08-18 16:19:37 -07001104 with great care.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001105 """
1106
1107 def __init__(self, branchref=None, issue=None, codereview=None, **kwargs):
1108 """Create a new ChangeList instance.
1109
1110 If issue is given, the codereview must be given too.
1111
1112 If `codereview` is given, it must be 'rietveld' or 'gerrit'.
1113 Otherwise, it's decided based on current configuration of the local branch,
1114 with default being 'rietveld' for backwards compatibility.
1115 See _load_codereview_impl for more details.
1116
1117 **kwargs will be passed directly to codereview implementation.
1118 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +00001120 global settings
1121 if not settings:
1122 # Happens when git_cl.py is used as a utility library.
1123 settings = Settings()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001124
1125 if issue:
1126 assert codereview, 'codereview must be known, if issue is known'
1127
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 self.branchref = branchref
1129 if self.branchref:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001130 assert branchref.startswith('refs/heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 self.branch = ShortBranchName(self.branchref)
1132 else:
1133 self.branch = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001135 self.lookedup_issue = False
1136 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 self.has_description = False
1138 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001139 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 self.patchset = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001141 self.cc = None
1142 self.watchers = ()
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001143 self._remote = None
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00001144
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001145 self._codereview_impl = None
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001146 self._codereview = None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001147 self._load_codereview_impl(codereview, **kwargs)
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001148 assert self._codereview_impl
1149 assert self._codereview in _CODEREVIEW_IMPLEMENTATIONS
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001150
1151 def _load_codereview_impl(self, codereview=None, **kwargs):
1152 if codereview:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001153 assert codereview in _CODEREVIEW_IMPLEMENTATIONS
1154 cls = _CODEREVIEW_IMPLEMENTATIONS[codereview]
1155 self._codereview = codereview
1156 self._codereview_impl = cls(self, **kwargs)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001157 return
1158
1159 # Automatic selection based on issue number set for a current branch.
1160 # Rietveld takes precedence over Gerrit.
1161 assert not self.issue
1162 # Whether we find issue or not, we are doing the lookup.
1163 self.lookedup_issue = True
tandrii5d48c322016-08-18 16:19:37 -07001164 if self.GetBranch():
1165 for codereview, cls in _CODEREVIEW_IMPLEMENTATIONS.iteritems():
1166 issue = _git_get_branch_config_value(
1167 cls.IssueConfigKey(), value_type=int, branch=self.GetBranch())
1168 if issue:
1169 self._codereview = codereview
1170 self._codereview_impl = cls(self, **kwargs)
1171 self.issue = int(issue)
1172 return
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001173
1174 # No issue is set for this branch, so decide based on repo-wide settings.
1175 return self._load_codereview_impl(
1176 codereview='gerrit' if settings.GetIsGerrit() else 'rietveld',
1177 **kwargs)
1178
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00001179 def IsGerrit(self):
1180 return self._codereview == 'gerrit'
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001181
1182 def GetCCList(self):
1183 """Return the users cc'd on this CL.
1184
agable92bec4f2016-08-24 09:27:27 -07001185 Return is a string suitable for passing to git cl with the --cc flag.
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001186 """
1187 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001188 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001189 more_cc = ','.join(self.watchers)
1190 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
1191 return self.cc
1192
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001193 def GetCCListWithoutDefault(self):
1194 """Return the users cc'd on this CL excluding default ones."""
1195 if self.cc is None:
1196 self.cc = ','.join(self.watchers)
1197 return self.cc
1198
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001199 def SetWatchers(self, watchers):
1200 """Set the list of email addresses that should be cc'd based on the changed
1201 files in this CL.
1202 """
1203 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204
1205 def GetBranch(self):
1206 """Returns the short branch name, e.g. 'master'."""
1207 if not self.branch:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001208 branchref = GetCurrentBranchRef()
szager@chromium.orgd62c61f2014-10-20 22:33:21 +00001209 if not branchref:
1210 return None
1211 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 self.branch = ShortBranchName(self.branchref)
1213 return self.branch
1214
1215 def GetBranchRef(self):
1216 """Returns the full branch name, e.g. 'refs/heads/master'."""
1217 self.GetBranch() # Poke the lazy loader.
1218 return self.branchref
1219
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00001220 def ClearBranch(self):
1221 """Clears cached branch data of this object."""
1222 self.branch = self.branchref = None
1223
tandrii5d48c322016-08-18 16:19:37 -07001224 def _GitGetBranchConfigValue(self, key, default=None, **kwargs):
1225 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1226 kwargs['branch'] = self.GetBranch()
1227 return _git_get_branch_config_value(key, default, **kwargs)
1228
1229 def _GitSetBranchConfigValue(self, key, value, **kwargs):
1230 assert 'branch' not in kwargs, 'this CL branch is used automatically'
1231 assert self.GetBranch(), (
1232 'this CL must have an associated branch to %sset %s%s' %
1233 ('un' if value is None else '',
1234 key,
1235 '' if value is None else ' to %r' % value))
1236 kwargs['branch'] = self.GetBranch()
1237 return _git_set_branch_config_value(key, value, **kwargs)
1238
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001239 @staticmethod
1240 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001241 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 e.g. 'origin', 'refs/heads/master'
1243 """
1244 remote = '.'
tandrii5d48c322016-08-18 16:19:37 -07001245 upstream_branch = _git_get_branch_config_value('merge', branch=branch)
1246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 if upstream_branch:
tandrii5d48c322016-08-18 16:19:37 -07001248 remote = _git_get_branch_config_value('remote', branch=branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001250 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
1251 error_ok=True).strip()
1252 if upstream_branch:
1253 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001255 # Fall back on trying a git-svn upstream branch.
1256 if settings.GetIsGitSvn():
1257 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001258 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +00001259 # Else, try to guess the origin remote.
1260 remote_branches = RunGit(['branch', '-r']).split()
1261 if 'origin/master' in remote_branches:
1262 # Fall back on origin/master if it exits.
1263 remote = 'origin'
1264 upstream_branch = 'refs/heads/master'
1265 elif 'origin/trunk' in remote_branches:
1266 # Fall back on origin/trunk if it exists. Generally a shared
1267 # git-svn clone
1268 remote = 'origin'
1269 upstream_branch = 'refs/heads/trunk'
1270 else:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001271 DieWithError(
1272 'Unable to determine default branch to diff against.\n'
1273 'Either pass complete "git diff"-style arguments, like\n'
1274 ' git cl upload origin/master\n'
1275 'or verify this branch is set up to track another \n'
1276 '(via the --track argument to "git checkout -b ...").')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277
1278 return remote, upstream_branch
1279
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001280 def GetCommonAncestorWithUpstream(self):
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001281 upstream_branch = self.GetUpstreamBranch()
1282 if not BranchExists(upstream_branch):
1283 DieWithError('The upstream for the current branch (%s) does not exist '
1284 'anymore.\nPlease fix it and try again.' % self.GetBranch())
iannucci@chromium.org9e849272014-04-04 00:31:55 +00001285 return git_common.get_or_create_merge_base(self.GetBranch(),
pgervais@chromium.org8ba38ff2015-06-11 21:41:25 +00001286 upstream_branch)
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001287
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288 def GetUpstreamBranch(self):
1289 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001290 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001292 upstream_branch = upstream_branch.replace('refs/heads/',
1293 'refs/remotes/%s/' % remote)
1294 upstream_branch = upstream_branch.replace('refs/branch-heads/',
1295 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296 self.upstream_branch = upstream_branch
1297 return self.upstream_branch
1298
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001299 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001300 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001301 remote, branch = None, self.GetBranch()
1302 seen_branches = set()
1303 while branch not in seen_branches:
1304 seen_branches.add(branch)
1305 remote, branch = self.FetchUpstreamTuple(branch)
1306 branch = ShortBranchName(branch)
1307 if remote != '.' or branch.startswith('refs/remotes'):
1308 break
1309 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001310 remotes = RunGit(['remote'], error_ok=True).split()
1311 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001312 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001313 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001314 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001315 logging.warning('Could not determine which remote this change is '
1316 'associated with, so defaulting to "%s". This may '
1317 'not be what you want. You may prevent this message '
1318 'by running "git svn info" as documented here: %s',
1319 self._remote,
1320 GIT_INSTRUCTIONS_URL)
1321 else:
1322 logging.warn('Could not determine which remote this change is '
1323 'associated with. You may prevent this message by '
1324 'running "git svn info" as documented here: %s',
1325 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001326 branch = 'HEAD'
1327 if branch.startswith('refs/remotes'):
1328 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +00001329 elif branch.startswith('refs/branch-heads/'):
1330 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001331 else:
1332 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001333 return self._remote
1334
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001335 def GitSanityChecks(self, upstream_git_obj):
1336 """Checks git repo status and ensures diff is from local commits."""
1337
sbc@chromium.org79706062015-01-14 21:18:12 +00001338 if upstream_git_obj is None:
1339 if self.GetBranch() is None:
vapiera7fbd5a2016-06-16 09:17:49 -07001340 print('ERROR: unable to determine current branch (detached HEAD?)',
1341 file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001342 else:
vapiera7fbd5a2016-06-16 09:17:49 -07001343 print('ERROR: no upstream branch', file=sys.stderr)
sbc@chromium.org79706062015-01-14 21:18:12 +00001344 return False
1345
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001346 # Verify the commit we're diffing against is in our current branch.
1347 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
1348 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
1349 if upstream_sha != common_ancestor:
vapiera7fbd5a2016-06-16 09:17:49 -07001350 print('ERROR: %s is not in the current branch. You may need to rebase '
1351 'your tracking branch' % upstream_sha, file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001352 return False
1353
1354 # List the commits inside the diff, and verify they are all local.
1355 commits_in_diff = RunGit(
1356 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
1357 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
1358 remote_branch = remote_branch.strip()
1359 if code != 0:
1360 _, remote_branch = self.GetRemoteBranch()
1361
1362 commits_in_remote = RunGit(
1363 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
1364
1365 common_commits = set(commits_in_diff) & set(commits_in_remote)
1366 if common_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07001367 print('ERROR: Your diff contains %d commits already in %s.\n'
1368 'Run "git log --oneline %s..HEAD" to get a list of commits in '
1369 'the diff. If you are using a custom git flow, you can override'
1370 ' the reference used for this check with "git config '
1371 'gitcl.remotebranch <git-ref>".' % (
1372 len(common_commits), remote_branch, upstream_git_obj),
1373 file=sys.stderr)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001374 return False
1375 return True
1376
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001377 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +00001378 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001379
1380 Returns None if it is not set.
1381 """
tandrii5d48c322016-08-18 16:19:37 -07001382 return self._GitGetBranchConfigValue('base-url')
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +00001383
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001384 def GetGitSvnRemoteUrl(self):
1385 """Return the configured git-svn remote URL parsed from git svn info.
1386
1387 Returns None if it is not set.
1388 """
1389 # URL is dependent on the current directory.
1390 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1391 if data:
1392 keys = dict(line.split(': ', 1) for line in data.splitlines()
1393 if ': ' in line)
1394 return keys.get('URL', None)
1395 return None
1396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397 def GetRemoteUrl(self):
1398 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
1399
1400 Returns None if there is no remote.
1401 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001402 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +00001403 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
1404
1405 # If URL is pointing to a local directory, it is probably a git cache.
1406 if os.path.isdir(url):
1407 url = RunGit(['config', 'remote.%s.url' % remote],
1408 error_ok=True,
1409 cwd=url).strip()
1410 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001412 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001413 """Returns the issue number as a int or None if not set."""
tandrii@chromium.org87985d22016-03-24 17:33:33 +00001414 if self.issue is None and not self.lookedup_issue:
tandrii5d48c322016-08-18 16:19:37 -07001415 self.issue = self._GitGetBranchConfigValue(
1416 self._codereview_impl.IssueConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001417 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 return self.issue
1419
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 def GetIssueURL(self):
1421 """Get the URL for a particular issue."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001422 issue = self.GetIssue()
1423 if not issue:
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001424 return None
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001425 return '%s/%s' % (self._codereview_impl.GetCodereviewServer(), issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426
1427 def GetDescription(self, pretty=False):
1428 if not self.has_description:
1429 if self.GetIssue():
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001430 self.description = self._codereview_impl.FetchDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 self.has_description = True
1432 if pretty:
1433 wrapper = textwrap.TextWrapper()
1434 wrapper.initial_indent = wrapper.subsequent_indent = ' '
1435 return wrapper.fill(self.description)
1436 return self.description
1437
1438 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +00001439 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001440 if self.patchset is None and not self.lookedup_patchset:
tandrii5d48c322016-08-18 16:19:37 -07001441 self.patchset = self._GitGetBranchConfigValue(
1442 self._codereview_impl.PatchsetConfigKey(), value_type=int)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001443 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001444 return self.patchset
1445
1446 def SetPatchset(self, patchset):
tandrii5d48c322016-08-18 16:19:37 -07001447 """Set this branch's patchset. If patchset=0, clears the patchset."""
1448 assert self.GetBranch()
1449 if not patchset:
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001450 self.patchset = None
tandrii5d48c322016-08-18 16:19:37 -07001451 else:
1452 self.patchset = int(patchset)
1453 self._GitSetBranchConfigValue(
1454 self._codereview_impl.PatchsetConfigKey(), self.patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455
tandrii@chromium.orga342c922016-03-16 07:08:25 +00001456 def SetIssue(self, issue=None):
tandrii5d48c322016-08-18 16:19:37 -07001457 """Set this branch's issue. If issue isn't given, clears the issue."""
1458 assert self.GetBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459 if issue:
tandrii5d48c322016-08-18 16:19:37 -07001460 issue = int(issue)
1461 self._GitSetBranchConfigValue(
1462 self._codereview_impl.IssueConfigKey(), issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001463 self.issue = issue
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001464 codereview_server = self._codereview_impl.GetCodereviewServer()
1465 if codereview_server:
tandrii5d48c322016-08-18 16:19:37 -07001466 self._GitSetBranchConfigValue(
1467 self._codereview_impl.CodereviewServerConfigKey(),
1468 codereview_server)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001469 else:
tandrii5d48c322016-08-18 16:19:37 -07001470 # Reset all of these just to be clean.
1471 reset_suffixes = [
1472 'last-upload-hash',
1473 self._codereview_impl.IssueConfigKey(),
1474 self._codereview_impl.PatchsetConfigKey(),
1475 self._codereview_impl.CodereviewServerConfigKey(),
1476 ] + self._PostUnsetIssueProperties()
1477 for prop in reset_suffixes:
1478 self._GitSetBranchConfigValue(prop, None, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001479 self.issue = None
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001480 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001481
dnjba1b0f32016-09-02 12:37:42 -07001482 def GetChange(self, upstream_branch, author, local_description=False):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001483 if not self.GitSanityChecks(upstream_branch):
1484 DieWithError('\nGit sanity check failure')
1485
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001486 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +00001487 if not root:
1488 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001489 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001490
1491 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001492 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +00001493 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001494 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +00001495 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001496 except subprocess2.CalledProcessError:
1497 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +00001498 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001499 'This branch probably doesn\'t exist anymore. To reset the\n'
1500 'tracking branch, please run\n'
stip7a3dd352016-09-22 17:32:28 -07001501 ' git branch --set-upstream-to origin/master %s\n'
1502 'or replace origin/master with the relevant branch') %
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +00001503 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001504
maruel@chromium.org52424302012-08-29 15:14:30 +00001505 issue = self.GetIssue()
1506 patchset = self.GetPatchset()
dnjba1b0f32016-09-02 12:37:42 -07001507 if issue and not local_description:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001508 description = self.GetDescription()
1509 else:
1510 # If the change was never uploaded, use the log messages of all commits
1511 # up to the branch point, as git cl upload will prefill the description
1512 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001513 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
1514 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001515
1516 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001517 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +00001518 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001519 name,
1520 description,
1521 absroot,
1522 files,
1523 issue,
1524 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +00001525 author,
1526 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001527
dsansomee2d6fd92016-09-08 00:10:47 -07001528 def UpdateDescription(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001529 self.description = description
dsansomee2d6fd92016-09-08 00:10:47 -07001530 return self._codereview_impl.UpdateDescriptionRemote(
1531 description, force=force)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001532
1533 def RunHook(self, committing, may_prompt, verbose, change):
1534 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
1535 try:
1536 return presubmit_support.DoPresubmitChecks(change, committing,
1537 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
1538 default_presubmit=None, may_prompt=may_prompt,
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001539 rietveld_obj=self._codereview_impl.GetRieveldObjForPresubmit(),
1540 gerrit_obj=self._codereview_impl.GetGerritObjForPresubmit())
vapierfd77ac72016-06-16 08:33:57 -07001541 except presubmit_support.PresubmitFailure as e:
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001542 DieWithError(
1543 ('%s\nMaybe your depot_tools is out of date?\n'
1544 'If all fails, contact maruel@') % e)
1545
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001546 def CMDPatchIssue(self, issue_arg, reject, nocommit, directory):
1547 """Fetches and applies the issue patch from codereview to local branch."""
tandrii@chromium.orgef7c68c2016-04-07 09:39:39 +00001548 if isinstance(issue_arg, (int, long)) or issue_arg.isdigit():
1549 parsed_issue_arg = _ParsedIssueNumberArgument(int(issue_arg))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001550 else:
1551 # Assume url.
1552 parsed_issue_arg = self._codereview_impl.ParseIssueURL(
1553 urlparse.urlparse(issue_arg))
1554 if not parsed_issue_arg or not parsed_issue_arg.valid:
1555 DieWithError('Failed to parse issue argument "%s". '
1556 'Must be an issue number or a valid URL.' % issue_arg)
1557 return self._codereview_impl.CMDPatchWithParsedIssue(
1558 parsed_issue_arg, reject, nocommit, directory)
1559
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001560 def CMDUpload(self, options, git_diff_args, orig_args):
1561 """Uploads a change to codereview."""
1562 if git_diff_args:
1563 # TODO(ukai): is it ok for gerrit case?
1564 base_branch = git_diff_args[0]
1565 else:
1566 if self.GetBranch() is None:
1567 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1568
1569 # Default to diffing against common ancestor of upstream branch
1570 base_branch = self.GetCommonAncestorWithUpstream()
1571 git_diff_args = [base_branch, 'HEAD']
1572
1573 # Make sure authenticated to codereview before running potentially expensive
1574 # hooks. It is a fast, best efforts check. Codereview still can reject the
1575 # authentication during the actual upload.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001576 self._codereview_impl.EnsureAuthenticated(force=options.force)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001577
1578 # Apply watchlists on upload.
1579 change = self.GetChange(base_branch, None)
1580 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1581 files = [f.LocalPath() for f in change.AffectedFiles()]
1582 if not options.bypass_watchlists:
1583 self.SetWatchers(watchlist.GetWatchersForPaths(files))
1584
1585 if not options.bypass_hooks:
1586 if options.reviewers or options.tbr_owners:
1587 # Set the reviewer list now so that presubmit checks can access it.
1588 change_description = ChangeDescription(change.FullDescriptionText())
1589 change_description.update_reviewers(options.reviewers,
1590 options.tbr_owners,
1591 change)
1592 change.SetDescriptionText(change_description.description)
1593 hook_results = self.RunHook(committing=False,
1594 may_prompt=not options.force,
1595 verbose=options.verbose,
1596 change=change)
1597 if not hook_results.should_continue():
1598 return 1
1599 if not options.reviewers and hook_results.reviewers:
1600 options.reviewers = hook_results.reviewers.split(',')
1601
Ravi Mistryfda50ca2016-11-14 10:19:18 -05001602 # TODO(tandrii): Checking local patchset against remote patchset is only
1603 # supported for Rietveld. Extend it to Gerrit or remove it completely.
1604 if self.GetIssue() and not self.IsGerrit():
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001605 latest_patchset = self.GetMostRecentPatchset()
1606 local_patchset = self.GetPatchset()
1607 if (latest_patchset and local_patchset and
1608 local_patchset != latest_patchset):
vapiera7fbd5a2016-06-16 09:17:49 -07001609 print('The last upload made from this repository was patchset #%d but '
1610 'the most recent patchset on the server is #%d.'
1611 % (local_patchset, latest_patchset))
1612 print('Uploading will still work, but if you\'ve uploaded to this '
1613 'issue from another machine or branch the patch you\'re '
1614 'uploading now might not include those changes.')
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001615 ask_for_data('About to upload; enter to confirm.')
1616
1617 print_stats(options.similarity, options.find_copies, git_diff_args)
1618 ret = self.CMDUploadChange(options, git_diff_args, change)
1619 if not ret:
tandrii4d0545a2016-07-06 03:56:49 -07001620 if options.use_commit_queue:
1621 self.SetCQState(_CQState.COMMIT)
1622 elif options.cq_dry_run:
1623 self.SetCQState(_CQState.DRY_RUN)
1624
tandrii5d48c322016-08-18 16:19:37 -07001625 _git_set_branch_config_value('last-upload-hash',
1626 RunGit(['rev-parse', 'HEAD']).strip())
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001627 # Run post upload hooks, if specified.
1628 if settings.GetRunPostUploadHook():
1629 presubmit_support.DoPostUploadExecuter(
1630 change,
1631 self,
1632 settings.GetRoot(),
1633 options.verbose,
1634 sys.stdout)
1635
1636 # Upload all dependencies if specified.
1637 if options.dependencies:
vapiera7fbd5a2016-06-16 09:17:49 -07001638 print()
1639 print('--dependencies has been specified.')
1640 print('All dependent local branches will be re-uploaded.')
1641 print()
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001642 # Remove the dependencies flag from args so that we do not end up in a
1643 # loop.
1644 orig_args.remove('--dependencies')
1645 ret = upload_branch_deps(self, orig_args)
1646 return ret
1647
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001648 def SetCQState(self, new_state):
1649 """Update the CQ state for latest patchset.
1650
1651 Issue must have been already uploaded and known.
1652 """
1653 assert new_state in _CQState.ALL_STATES
1654 assert self.GetIssue()
1655 return self._codereview_impl.SetCQState(new_state)
1656
qyearsley1fdfcb62016-10-24 13:22:03 -07001657 def TriggerDryRun(self):
1658 """Triggers a dry run and prints a warning on failure."""
1659 # TODO(qyearsley): Either re-use this method in CMDset_commit
1660 # and CMDupload, or change CMDtry to trigger dry runs with
1661 # just SetCQState, and catch keyboard interrupt and other
1662 # errors in that method.
1663 try:
1664 self.SetCQState(_CQState.DRY_RUN)
1665 print('scheduled CQ Dry Run on %s' % self.GetIssueURL())
1666 return 0
1667 except KeyboardInterrupt:
1668 raise
1669 except:
1670 print('WARNING: failed to trigger CQ Dry Run.\n'
1671 'Either:\n'
1672 ' * your project has no CQ\n'
1673 ' * you don\'t have permission to trigger Dry Run\n'
1674 ' * bug in this code (see stack trace below).\n'
1675 'Consider specifying which bots to trigger manually '
1676 'or asking your project owners for permissions '
1677 'or contacting Chrome Infrastructure team at '
1678 'https://www.chromium.org/infra\n\n')
1679 # Still raise exception so that stack trace is printed.
1680 raise
1681
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001682 # Forward methods to codereview specific implementation.
1683
1684 def CloseIssue(self):
1685 return self._codereview_impl.CloseIssue()
1686
1687 def GetStatus(self):
1688 return self._codereview_impl.GetStatus()
1689
1690 def GetCodereviewServer(self):
1691 return self._codereview_impl.GetCodereviewServer()
1692
tandriide281ae2016-10-12 06:02:30 -07001693 def GetIssueOwner(self):
1694 """Get owner from codereview, which may differ from this checkout."""
1695 return self._codereview_impl.GetIssueOwner()
1696
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001697 def GetApprovingReviewers(self):
1698 return self._codereview_impl.GetApprovingReviewers()
1699
1700 def GetMostRecentPatchset(self):
1701 return self._codereview_impl.GetMostRecentPatchset()
1702
tandriide281ae2016-10-12 06:02:30 -07001703 def CannotTriggerTryJobReason(self):
1704 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1705 return self._codereview_impl.CannotTriggerTryJobReason()
1706
tandrii8c5a3532016-11-04 07:52:02 -07001707 def GetTryjobProperties(self, patchset=None):
1708 """Returns dictionary of properties to launch tryjob."""
1709 return self._codereview_impl.GetTryjobProperties(patchset=patchset)
1710
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001711 def __getattr__(self, attr):
1712 # This is because lots of untested code accesses Rietveld-specific stuff
1713 # directly, and it's hard to fix for sure. So, just let it work, and fix
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00001714 # on a case by case basis.
tandrii4d895502016-08-18 08:26:19 -07001715 # Note that child method defines __getattr__ as well, and forwards it here,
1716 # because _RietveldChangelistImpl is not cleaned up yet, and given
1717 # deprecation of Rietveld, it should probably be just removed.
1718 # Until that time, avoid infinite recursion by bypassing __getattr__
1719 # of implementation class.
1720 return self._codereview_impl.__getattribute__(attr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001721
1722
1723class _ChangelistCodereviewBase(object):
1724 """Abstract base class encapsulating codereview specifics of a changelist."""
1725 def __init__(self, changelist):
1726 self._changelist = changelist # instance of Changelist
1727
1728 def __getattr__(self, attr):
1729 # Forward methods to changelist.
1730 # TODO(tandrii): maybe clean up _GerritChangelistImpl and
1731 # _RietveldChangelistImpl to avoid this hack?
1732 return getattr(self._changelist, attr)
1733
1734 def GetStatus(self):
1735 """Apply a rough heuristic to give a simple summary of an issue's review
1736 or CQ status, assuming adherence to a common workflow.
1737
1738 Returns None if no issue for this branch, or specific string keywords.
1739 """
1740 raise NotImplementedError()
1741
1742 def GetCodereviewServer(self):
1743 """Returns server URL without end slash, like "https://codereview.com"."""
1744 raise NotImplementedError()
1745
1746 def FetchDescription(self):
1747 """Fetches and returns description from the codereview server."""
1748 raise NotImplementedError()
1749
tandrii5d48c322016-08-18 16:19:37 -07001750 @classmethod
1751 def IssueConfigKey(cls):
1752 """Returns branch setting storing issue number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001753 raise NotImplementedError()
1754
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00001755 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07001756 def PatchsetConfigKey(cls):
1757 """Returns branch setting storing patchset number."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001758 raise NotImplementedError()
1759
tandrii5d48c322016-08-18 16:19:37 -07001760 @classmethod
1761 def CodereviewServerConfigKey(cls):
1762 """Returns branch setting storing codereview server."""
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001763 raise NotImplementedError()
1764
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001765 def _PostUnsetIssueProperties(self):
1766 """Which branch-specific properties to erase when unsettin issue."""
tandrii5d48c322016-08-18 16:19:37 -07001767 return []
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00001768
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001769 def GetRieveldObjForPresubmit(self):
1770 # This is an unfortunate Rietveld-embeddedness in presubmit.
1771 # For non-Rietveld codereviews, this probably should return a dummy object.
1772 raise NotImplementedError()
1773
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00001774 def GetGerritObjForPresubmit(self):
1775 # None is valid return value, otherwise presubmit_support.GerritAccessor.
1776 return None
1777
dsansomee2d6fd92016-09-08 00:10:47 -07001778 def UpdateDescriptionRemote(self, description, force=False):
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001779 """Update the description on codereview site."""
1780 raise NotImplementedError()
1781
1782 def CloseIssue(self):
1783 """Closes the issue."""
1784 raise NotImplementedError()
1785
1786 def GetApprovingReviewers(self):
1787 """Returns a list of reviewers approving the change.
1788
1789 Note: not necessarily committers.
1790 """
1791 raise NotImplementedError()
1792
1793 def GetMostRecentPatchset(self):
1794 """Returns the most recent patchset number from the codereview site."""
1795 raise NotImplementedError()
1796
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00001797 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
1798 directory):
1799 """Fetches and applies the issue.
1800
1801 Arguments:
1802 parsed_issue_arg: instance of _ParsedIssueNumberArgument.
1803 reject: if True, reject the failed patch instead of switching to 3-way
1804 merge. Rietveld only.
1805 nocommit: do not commit the patch, thus leave the tree dirty. Rietveld
1806 only.
1807 directory: switch to directory before applying the patch. Rietveld only.
1808 """
1809 raise NotImplementedError()
1810
1811 @staticmethod
1812 def ParseIssueURL(parsed_url):
1813 """Parses url and returns instance of _ParsedIssueNumberArgument or None if
1814 failed."""
1815 raise NotImplementedError()
1816
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001817 def EnsureAuthenticated(self, force):
1818 """Best effort check that user is authenticated with codereview server.
1819
1820 Arguments:
1821 force: whether to skip confirmation questions.
1822 """
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001823 raise NotImplementedError()
1824
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00001825 def CMDUploadChange(self, options, args, change):
1826 """Uploads a change to codereview."""
1827 raise NotImplementedError()
1828
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00001829 def SetCQState(self, new_state):
1830 """Update the CQ state for latest patchset.
1831
1832 Issue must have been already uploaded and known.
1833 """
1834 raise NotImplementedError()
1835
tandriie113dfd2016-10-11 10:20:12 -07001836 def CannotTriggerTryJobReason(self):
1837 """Returns reason (str) if unable trigger tryjobs on this CL or None."""
1838 raise NotImplementedError()
1839
tandriide281ae2016-10-12 06:02:30 -07001840 def GetIssueOwner(self):
1841 raise NotImplementedError()
1842
tandrii8c5a3532016-11-04 07:52:02 -07001843 def GetTryjobProperties(self, patchset=None):
tandriide281ae2016-10-12 06:02:30 -07001844 raise NotImplementedError()
1845
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001846
1847class _RietveldChangelistImpl(_ChangelistCodereviewBase):
1848 def __init__(self, changelist, auth_config=None, rietveld_server=None):
1849 super(_RietveldChangelistImpl, self).__init__(changelist)
1850 assert settings, 'must be initialized in _ChangelistCodereviewBase'
martiniss6eda05f2016-06-30 10:18:35 -07001851 if not rietveld_server:
1852 settings.GetDefaultServerUrl()
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001853
1854 self._rietveld_server = rietveld_server
1855 self._auth_config = auth_config
1856 self._props = None
1857 self._rpc_server = None
1858
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001859 def GetCodereviewServer(self):
1860 if not self._rietveld_server:
1861 # If we're on a branch then get the server potentially associated
1862 # with that branch.
1863 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07001864 self._rietveld_server = gclient_utils.UpgradeToHttps(
1865 self._GitGetBranchConfigValue(self.CodereviewServerConfigKey()))
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001866 if not self._rietveld_server:
1867 self._rietveld_server = settings.GetDefaultServerUrl()
1868 return self._rietveld_server
1869
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00001870 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00001871 """Best effort check that user is authenticated with Rietveld server."""
1872 if self._auth_config.use_oauth2:
1873 authenticator = auth.get_authenticator_for_host(
1874 self.GetCodereviewServer(), self._auth_config)
1875 if not authenticator.has_cached_credentials():
1876 raise auth.LoginRequiredError(self.GetCodereviewServer())
1877
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001878 def FetchDescription(self):
1879 issue = self.GetIssue()
1880 assert issue
1881 try:
1882 return self.RpcServer().get_description(issue).strip()
1883 except urllib2.HTTPError as e:
1884 if e.code == 404:
1885 DieWithError(
1886 ('\nWhile fetching the description for issue %d, received a '
1887 '404 (not found)\n'
1888 'error. It is likely that you deleted this '
1889 'issue on the server. If this is the\n'
1890 'case, please run\n\n'
1891 ' git cl issue 0\n\n'
1892 'to clear the association with the deleted issue. Then run '
1893 'this command again.') % issue)
1894 else:
1895 DieWithError(
1896 '\nFailed to fetch issue description. HTTP error %d' % e.code)
1897 except urllib2.URLError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07001898 print('Warning: Failed to retrieve CL description due to network '
1899 'failure.', file=sys.stderr)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001900 return ''
1901
1902 def GetMostRecentPatchset(self):
1903 return self.GetIssueProperties()['patchsets'][-1]
1904
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001905 def GetIssueProperties(self):
1906 if self._props is None:
1907 issue = self.GetIssue()
1908 if not issue:
1909 self._props = {}
1910 else:
1911 self._props = self.RpcServer().get_issue_properties(issue, True)
1912 return self._props
1913
tandriie113dfd2016-10-11 10:20:12 -07001914 def CannotTriggerTryJobReason(self):
1915 props = self.GetIssueProperties()
1916 if not props:
1917 return 'Rietveld doesn\'t know about your issue %s' % self.GetIssue()
1918 if props.get('closed'):
1919 return 'CL %s is closed' % self.GetIssue()
1920 if props.get('private'):
1921 return 'CL %s is private' % self.GetIssue()
1922 return None
1923
tandrii8c5a3532016-11-04 07:52:02 -07001924 def GetTryjobProperties(self, patchset=None):
1925 """Returns dictionary of properties to launch tryjob."""
1926 project = (self.GetIssueProperties() or {}).get('project')
1927 return {
1928 'issue': self.GetIssue(),
1929 'patch_project': project,
1930 'patch_storage': 'rietveld',
1931 'patchset': patchset or self.GetPatchset(),
1932 'rietveld': self.GetCodereviewServer(),
1933 }
1934
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001935 def GetApprovingReviewers(self):
1936 return get_approving_reviewers(self.GetIssueProperties())
1937
tandriide281ae2016-10-12 06:02:30 -07001938 def GetIssueOwner(self):
1939 return (self.GetIssueProperties() or {}).get('owner_email')
1940
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00001941 def AddComment(self, message):
1942 return self.RpcServer().add_comment(self.GetIssue(), message)
1943
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001944 def GetStatus(self):
1945 """Apply a rough heuristic to give a simple summary of an issue's review
1946 or CQ status, assuming adherence to a common workflow.
1947
1948 Returns None if no issue for this branch, or one of the following keywords:
1949 * 'error' - error from review tool (including deleted issues)
1950 * 'unsent' - not sent for review
1951 * 'waiting' - waiting for review
1952 * 'reply' - waiting for owner to reply to review
1953 * 'lgtm' - LGTM from at least one approved reviewer
1954 * 'commit' - in the commit queue
1955 * 'closed' - closed
1956 """
1957 if not self.GetIssue():
1958 return None
1959
1960 try:
1961 props = self.GetIssueProperties()
1962 except urllib2.HTTPError:
1963 return 'error'
1964
1965 if props.get('closed'):
1966 # Issue is closed.
1967 return 'closed'
tandrii@chromium.orgb4f6a222016-03-03 01:11:04 +00001968 if props.get('commit') and not props.get('cq_dry_run', False):
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001969 # Issue is in the commit queue.
1970 return 'commit'
1971
1972 try:
1973 reviewers = self.GetApprovingReviewers()
1974 except urllib2.HTTPError:
1975 return 'error'
1976
1977 if reviewers:
1978 # Was LGTM'ed.
1979 return 'lgtm'
1980
1981 messages = props.get('messages') or []
1982
tandrii9d2c7a32016-06-22 03:42:45 -07001983 # Skip CQ messages that don't require owner's action.
1984 while messages and messages[-1]['sender'] == COMMIT_BOT_EMAIL:
1985 if 'Dry run:' in messages[-1]['text']:
1986 messages.pop()
1987 elif 'The CQ bit was unchecked' in messages[-1]['text']:
1988 # This message always follows prior messages from CQ,
1989 # so skip this too.
1990 messages.pop()
1991 else:
1992 # This is probably a CQ messages warranting user attention.
1993 break
1994
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001995 if not messages:
1996 # No message was sent.
1997 return 'unsent'
1998 if messages[-1]['sender'] != props.get('owner_email'):
tandrii9d2c7a32016-06-22 03:42:45 -07001999 # Non-LGTM reply from non-owner and not CQ bot.
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00002000 return 'reply'
2001 return 'waiting'
2002
dsansomee2d6fd92016-09-08 00:10:47 -07002003 def UpdateDescriptionRemote(self, description, force=False):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002004 return self.RpcServer().update_description(
2005 self.GetIssue(), self.description)
2006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007 def CloseIssue(self):
maruel@chromium.orgb021b322013-04-08 17:57:29 +00002008 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002010 def SetFlag(self, flag, value):
tandrii4b233bd2016-07-06 03:50:29 -07002011 return self.SetFlags({flag: value})
2012
2013 def SetFlags(self, flags):
2014 """Sets flags on this CL/patchset in Rietveld.
tandrii4b233bd2016-07-06 03:50:29 -07002015 """
phajdan.jr68598232016-08-10 03:28:28 -07002016 patchset = self.GetPatchset() or self.GetMostRecentPatchset()
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002017 try:
tandrii4b233bd2016-07-06 03:50:29 -07002018 return self.RpcServer().set_flags(
phajdan.jr68598232016-08-10 03:28:28 -07002019 self.GetIssue(), patchset, flags)
vapierfd77ac72016-06-16 08:33:57 -07002020 except urllib2.HTTPError as e:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002021 if e.code == 404:
2022 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
2023 if e.code == 403:
2024 DieWithError(
2025 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
phajdan.jr68598232016-08-10 03:28:28 -07002026 'match?') % (self.GetIssue(), patchset))
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002027 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028
maruel@chromium.orgcab38e92011-04-09 00:30:51 +00002029 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030 """Returns an upload.RpcServer() to access this review's rietveld instance.
2031 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002032 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +00002033 self._rpc_server = rietveld.CachingRietveld(
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002034 self.GetCodereviewServer(),
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00002035 self._auth_config or auth.make_auth_config())
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002036 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002037
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002038 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002039 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002040 return 'rietveldissue'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041
tandrii5d48c322016-08-18 16:19:37 -07002042 @classmethod
2043 def PatchsetConfigKey(cls):
2044 return 'rietveldpatchset'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045
tandrii5d48c322016-08-18 16:19:37 -07002046 @classmethod
2047 def CodereviewServerConfigKey(cls):
2048 return 'rietveldserver'
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002049
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002050 def GetRieveldObjForPresubmit(self):
2051 return self.RpcServer()
2052
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002053 def SetCQState(self, new_state):
2054 props = self.GetIssueProperties()
2055 if props.get('private'):
2056 DieWithError('Cannot set-commit on private issue')
2057
2058 if new_state == _CQState.COMMIT:
tandrii4d843592016-07-27 08:22:56 -07002059 self.SetFlags({'commit': '1', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002060 elif new_state == _CQState.NONE:
tandrii4b233bd2016-07-06 03:50:29 -07002061 self.SetFlags({'commit': '0', 'cq_dry_run': '0'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002062 else:
tandrii4b233bd2016-07-06 03:50:29 -07002063 assert new_state == _CQState.DRY_RUN
2064 self.SetFlags({'commit': '1', 'cq_dry_run': '1'})
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002065
2066
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002067 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2068 directory):
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002069 # PatchIssue should never be called with a dirty tree. It is up to the
2070 # caller to check this, but just in case we assert here since the
2071 # consequences of the caller not checking this could be dire.
2072 assert(not git_common.is_dirty_git_tree('apply'))
2073 assert(parsed_issue_arg.valid)
2074 self._changelist.issue = parsed_issue_arg.issue
2075 if parsed_issue_arg.hostname:
2076 self._rietveld_server = 'https://%s' % parsed_issue_arg.hostname
2077
skobes6468b902016-10-24 08:45:10 -07002078 patchset = parsed_issue_arg.patchset or self.GetMostRecentPatchset()
2079 patchset_object = self.RpcServer().get_patch(self.GetIssue(), patchset)
2080 scm_obj = checkout.GitCheckout(settings.GetRoot(), None, None, None, None)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002081 try:
skobes6468b902016-10-24 08:45:10 -07002082 scm_obj.apply_patch(patchset_object)
2083 except Exception as e:
2084 print(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002085 return 1
2086
2087 # If we had an issue, commit the current state and register the issue.
2088 if not nocommit:
2089 RunGit(['commit', '-m', (self.GetDescription() + '\n\n' +
2090 'patch from issue %(i)s at patchset '
2091 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2092 % {'i': self.GetIssue(), 'p': patchset})])
2093 self.SetIssue(self.GetIssue())
2094 self.SetPatchset(patchset)
vapiera7fbd5a2016-06-16 09:17:49 -07002095 print('Committed patch locally.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002096 else:
vapiera7fbd5a2016-06-16 09:17:49 -07002097 print('Patch applied to index.')
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002098 return 0
2099
2100 @staticmethod
2101 def ParseIssueURL(parsed_url):
2102 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2103 return None
wychen3c1c1722016-08-04 11:46:36 -07002104 # Rietveld patch: https://domain/<number>/#ps<patchset>
2105 match = re.match(r'/(\d+)/$', parsed_url.path)
2106 match2 = re.match(r'ps(\d+)$', parsed_url.fragment)
2107 if match and match2:
skobes6468b902016-10-24 08:45:10 -07002108 return _ParsedIssueNumberArgument(
wychen3c1c1722016-08-04 11:46:36 -07002109 issue=int(match.group(1)),
2110 patchset=int(match2.group(1)),
2111 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002112 # Typical url: https://domain/<issue_number>[/[other]]
2113 match = re.match('/(\d+)(/.*)?$', parsed_url.path)
2114 if match:
skobes6468b902016-10-24 08:45:10 -07002115 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002116 issue=int(match.group(1)),
2117 hostname=parsed_url.netloc)
2118 # Rietveld patch: https://domain/download/issue<number>_<patchset>.diff
2119 match = re.match(r'/download/issue(\d+)_(\d+).diff$', parsed_url.path)
2120 if match:
skobes6468b902016-10-24 08:45:10 -07002121 return _ParsedIssueNumberArgument(
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002122 issue=int(match.group(1)),
2123 patchset=int(match.group(2)),
skobes6468b902016-10-24 08:45:10 -07002124 hostname=parsed_url.netloc)
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002125 return None
2126
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002127 def CMDUploadChange(self, options, args, change):
2128 """Upload the patch to Rietveld."""
2129 upload_args = ['--assume_yes'] # Don't ask about untracked files.
2130 upload_args.extend(['--server', self.GetCodereviewServer()])
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002131 upload_args.extend(auth.auth_config_to_command_options(self._auth_config))
2132 if options.emulate_svn_auto_props:
2133 upload_args.append('--emulate_svn_auto_props')
2134
2135 change_desc = None
2136
2137 if options.email is not None:
2138 upload_args.extend(['--email', options.email])
2139
2140 if self.GetIssue():
nodirca166002016-06-27 10:59:51 -07002141 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002142 upload_args.extend(['--title', options.title])
2143 if options.message:
2144 upload_args.extend(['--message', options.message])
2145 upload_args.extend(['--issue', str(self.GetIssue())])
vapiera7fbd5a2016-06-16 09:17:49 -07002146 print('This branch is associated with issue %s. '
2147 'Adding patch to that issue.' % self.GetIssue())
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002148 else:
nodirca166002016-06-27 10:59:51 -07002149 if options.title is not None:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002150 upload_args.extend(['--title', options.title])
2151 message = (options.title or options.message or
2152 CreateDescriptionFromLog(args))
2153 change_desc = ChangeDescription(message)
2154 if options.reviewers or options.tbr_owners:
2155 change_desc.update_reviewers(options.reviewers,
2156 options.tbr_owners,
2157 change)
2158 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002159 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002160
2161 if not change_desc.description:
vapiera7fbd5a2016-06-16 09:17:49 -07002162 print('Description is empty; aborting.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002163 return 1
2164
2165 upload_args.extend(['--message', change_desc.description])
2166 if change_desc.get_reviewers():
2167 upload_args.append('--reviewers=%s' % ','.join(
2168 change_desc.get_reviewers()))
2169 if options.send_mail:
2170 if not change_desc.get_reviewers():
2171 DieWithError("Must specify reviewers to send email.")
2172 upload_args.append('--send_mail')
2173
2174 # We check this before applying rietveld.private assuming that in
2175 # rietveld.cc only addresses which we can send private CLs to are listed
2176 # if rietveld.private is set, and so we should ignore rietveld.cc only
2177 # when --private is specified explicitly on the command line.
2178 if options.private:
2179 logging.warn('rietveld.cc is ignored since private flag is specified. '
2180 'You need to review and add them manually if necessary.')
2181 cc = self.GetCCListWithoutDefault()
2182 else:
2183 cc = self.GetCCList()
2184 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
bradnelsond975b302016-10-23 12:20:23 -07002185 if change_desc.get_cced():
2186 cc = ','.join(filter(None, (cc, ','.join(change_desc.get_cced()))))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002187 if cc:
2188 upload_args.extend(['--cc', cc])
2189
2190 if options.private or settings.GetDefaultPrivateFlag() == "True":
2191 upload_args.append('--private')
2192
2193 upload_args.extend(['--git_similarity', str(options.similarity)])
2194 if not options.find_copies:
2195 upload_args.extend(['--git_no_find_copies'])
2196
2197 # Include the upstream repo's URL in the change -- this is useful for
2198 # projects that have their source spread across multiple repos.
2199 remote_url = self.GetGitBaseUrlFromConfig()
2200 if not remote_url:
2201 if settings.GetIsGitSvn():
2202 remote_url = self.GetGitSvnRemoteUrl()
2203 else:
2204 if self.GetRemoteUrl() and '/' in self.GetUpstreamBranch():
2205 remote_url = '%s@%s' % (self.GetRemoteUrl(),
2206 self.GetUpstreamBranch().split('/')[-1])
2207 if remote_url:
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002208 remote, remote_branch = self.GetRemoteBranch()
2209 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
2210 settings.GetPendingRefPrefix())
2211 if target_ref:
2212 upload_args.extend(['--target_ref', target_ref])
2213
2214 # Look for dependent patchsets. See crbug.com/480453 for more details.
2215 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2216 upstream_branch = ShortBranchName(upstream_branch)
2217 if remote is '.':
2218 # A local branch is being tracked.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002219 local_branch = upstream_branch
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002220 if settings.GetIsSkipDependencyUpload(local_branch):
vapiera7fbd5a2016-06-16 09:17:49 -07002221 print()
2222 print('Skipping dependency patchset upload because git config '
2223 'branch.%s.skip-deps-uploads is set to True.' % local_branch)
2224 print()
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002225 else:
2226 auth_config = auth.extract_auth_config_from_options(options)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00002227 branch_cl = Changelist(branchref='refs/heads/'+local_branch,
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002228 auth_config=auth_config)
2229 branch_cl_issue_url = branch_cl.GetIssueURL()
2230 branch_cl_issue = branch_cl.GetIssue()
2231 branch_cl_patchset = branch_cl.GetPatchset()
2232 if branch_cl_issue_url and branch_cl_issue and branch_cl_patchset:
2233 upload_args.extend(
2234 ['--depends_on_patchset', '%s:%s' % (
2235 branch_cl_issue, branch_cl_patchset)])
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002236 print(
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002237 '\n'
2238 'The current branch (%s) is tracking a local branch (%s) with '
2239 'an associated CL.\n'
2240 'Adding %s/#ps%s as a dependency patchset.\n'
2241 '\n' % (self.GetBranch(), local_branch, branch_cl_issue_url,
2242 branch_cl_patchset))
2243
2244 project = settings.GetProject()
2245 if project:
2246 upload_args.extend(['--project', project])
2247
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002248 try:
2249 upload_args = ['upload'] + upload_args + args
2250 logging.info('upload.RealMain(%s)', upload_args)
2251 issue, patchset = upload.RealMain(upload_args)
2252 issue = int(issue)
2253 patchset = int(patchset)
2254 except KeyboardInterrupt:
2255 sys.exit(1)
2256 except:
2257 # If we got an exception after the user typed a description for their
2258 # change, back up the description before re-raising.
2259 if change_desc:
2260 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
2261 print('\nGot exception while uploading -- saving description to %s\n' %
2262 backup_path)
2263 backup_file = open(backup_path, 'w')
2264 backup_file.write(change_desc.description)
2265 backup_file.close()
2266 raise
2267
2268 if not self.GetIssue():
2269 self.SetIssue(issue)
2270 self.SetPatchset(patchset)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002271 return 0
2272
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002273
2274class _GerritChangelistImpl(_ChangelistCodereviewBase):
2275 def __init__(self, changelist, auth_config=None):
2276 # auth_config is Rietveld thing, kept here to preserve interface only.
2277 super(_GerritChangelistImpl, self).__init__(changelist)
2278 self._change_id = None
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002279 # Lazily cached values.
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002280 self._gerrit_server = None # e.g. https://chromium-review.googlesource.com
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002281 self._gerrit_host = None # e.g. chromium-review.googlesource.com
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002282
2283 def _GetGerritHost(self):
2284 # Lazy load of configs.
2285 self.GetCodereviewServer()
tandriie32e3ea2016-06-22 02:52:48 -07002286 if self._gerrit_host and '.' not in self._gerrit_host:
2287 # Abbreviated domain like "chromium" instead of chromium.googlesource.com.
2288 # This happens for internal stuff http://crbug.com/614312.
2289 parsed = urlparse.urlparse(self.GetRemoteUrl())
2290 if parsed.scheme == 'sso':
2291 print('WARNING: using non https URLs for remote is likely broken\n'
2292 ' Your current remote is: %s' % self.GetRemoteUrl())
2293 self._gerrit_host = '%s.googlesource.com' % self._gerrit_host
2294 self._gerrit_server = 'https://%s' % self._gerrit_host
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002295 return self._gerrit_host
2296
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002297 def _GetGitHost(self):
2298 """Returns git host to be used when uploading change to Gerrit."""
2299 return urlparse.urlparse(self.GetRemoteUrl()).netloc
2300
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002301 def GetCodereviewServer(self):
2302 if not self._gerrit_server:
2303 # If we're on a branch then get the server potentially associated
2304 # with that branch.
2305 if self.GetIssue():
tandrii5d48c322016-08-18 16:19:37 -07002306 self._gerrit_server = self._GitGetBranchConfigValue(
2307 self.CodereviewServerConfigKey())
2308 if self._gerrit_server:
2309 self._gerrit_host = urlparse.urlparse(self._gerrit_server).netloc
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002310 if not self._gerrit_server:
2311 # We assume repo to be hosted on Gerrit, and hence Gerrit server
2312 # has "-review" suffix for lowest level subdomain.
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002313 parts = self._GetGitHost().split('.')
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002314 parts[0] = parts[0] + '-review'
2315 self._gerrit_host = '.'.join(parts)
2316 self._gerrit_server = 'https://%s' % self._gerrit_host
2317 return self._gerrit_server
2318
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002319 @classmethod
tandrii5d48c322016-08-18 16:19:37 -07002320 def IssueConfigKey(cls):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00002321 return 'gerritissue'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002322
tandrii5d48c322016-08-18 16:19:37 -07002323 @classmethod
2324 def PatchsetConfigKey(cls):
2325 return 'gerritpatchset'
2326
2327 @classmethod
2328 def CodereviewServerConfigKey(cls):
2329 return 'gerritserver'
2330
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002331 def EnsureAuthenticated(self, force):
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002332 """Best effort check that user is authenticated with Gerrit server."""
tandrii@chromium.org28253532016-04-14 13:46:56 +00002333 if settings.GetGerritSkipEnsureAuthenticated():
2334 # For projects with unusual authentication schemes.
2335 # See http://crbug.com/603378.
2336 return
tandrii@chromium.orgfe30f182016-04-13 12:15:04 +00002337 # Lazy-loader to identify Gerrit and Git hosts.
2338 if gerrit_util.GceAuthenticator.is_gce():
2339 return
2340 self.GetCodereviewServer()
2341 git_host = self._GetGitHost()
2342 assert self._gerrit_server and self._gerrit_host
2343 cookie_auth = gerrit_util.CookiesAuthenticator()
2344
2345 gerrit_auth = cookie_auth.get_auth_header(self._gerrit_host)
2346 git_auth = cookie_auth.get_auth_header(git_host)
2347 if gerrit_auth and git_auth:
2348 if gerrit_auth == git_auth:
2349 return
2350 print((
2351 'WARNING: you have different credentials for Gerrit and git hosts.\n'
2352 ' Check your %s or %s file for credentials of hosts:\n'
2353 ' %s\n'
2354 ' %s\n'
2355 ' %s') %
2356 (cookie_auth.get_gitcookies_path(), cookie_auth.get_netrc_path(),
2357 git_host, self._gerrit_host,
2358 cookie_auth.get_new_password_message(git_host)))
2359 if not force:
2360 ask_for_data('If you know what you are doing, press Enter to continue, '
2361 'Ctrl+C to abort.')
2362 return
2363 else:
2364 missing = (
2365 [] if gerrit_auth else [self._gerrit_host] +
2366 [] if git_auth else [git_host])
2367 DieWithError('Credentials for the following hosts are required:\n'
2368 ' %s\n'
2369 'These are read from %s (or legacy %s)\n'
2370 '%s' % (
2371 '\n '.join(missing),
2372 cookie_auth.get_gitcookies_path(),
2373 cookie_auth.get_netrc_path(),
2374 cookie_auth.get_new_password_message(git_host)))
2375
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002376 def _PostUnsetIssueProperties(self):
2377 """Which branch-specific properties to erase when unsetting issue."""
tandrii5d48c322016-08-18 16:19:37 -07002378 return ['gerritsquashhash']
tandrii@chromium.org9b7fd712016-06-01 13:45:20 +00002379
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002380 def GetRieveldObjForPresubmit(self):
2381 class ThisIsNotRietveldIssue(object):
2382 def __nonzero__(self):
2383 # This is a hack to make presubmit_support think that rietveld is not
2384 # defined, yet still ensure that calls directly result in a decent
2385 # exception message below.
2386 return False
2387
2388 def __getattr__(self, attr):
2389 print(
2390 'You aren\'t using Rietveld at the moment, but Gerrit.\n'
2391 'Using Rietveld in your PRESUBMIT scripts won\'t work.\n'
2392 'Please, either change your PRESUBIT to not use rietveld_obj.%s,\n'
2393 'or use Rietveld for codereview.\n'
2394 'See also http://crbug.com/579160.' % attr)
2395 raise NotImplementedError()
2396 return ThisIsNotRietveldIssue()
2397
tandrii@chromium.org37b07a72016-04-29 16:42:28 +00002398 def GetGerritObjForPresubmit(self):
2399 return presubmit_support.GerritAccessor(self._GetGerritHost())
2400
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002401 def GetStatus(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002402 """Apply a rough heuristic to give a simple summary of an issue's review
2403 or CQ status, assuming adherence to a common workflow.
2404
2405 Returns None if no issue for this branch, or one of the following keywords:
2406 * 'error' - error from review tool (including deleted issues)
2407 * 'unsent' - no reviewers added
2408 * 'waiting' - waiting for review
2409 * 'reply' - waiting for owner to reply to review
tandriic2405f52016-10-10 08:13:15 -07002410 * 'not lgtm' - Code-Review disaproval from at least one valid reviewer
2411 * 'lgtm' - Code-Review approval from at least one valid reviewer
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002412 * 'commit' - in the commit queue
2413 * 'closed' - abandoned
2414 """
2415 if not self.GetIssue():
2416 return None
2417
2418 try:
2419 data = self._GetChangeDetail(['DETAILED_LABELS', 'CURRENT_REVISION'])
tandriic2405f52016-10-10 08:13:15 -07002420 except (httplib.HTTPException, GerritIssueNotExists):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002421 return 'error'
2422
tandrii@chromium.org5e1bf382016-05-17 08:43:24 +00002423 if data['status'] in ('ABANDONED', 'MERGED'):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002424 return 'closed'
2425
2426 cq_label = data['labels'].get('Commit-Queue', {})
2427 if cq_label:
rmistryc9ebbd22016-10-14 12:35:54 -07002428 votes = cq_label.get('all', [])
2429 highest_vote = 0
2430 for v in votes:
2431 highest_vote = max(highest_vote, v.get('value', 0))
2432 vote_value = str(highest_vote)
2433 if vote_value != '0':
2434 # Add a '+' if the value is not 0 to match the values in the label.
2435 # The cq_label does not have negatives.
2436 vote_value = '+' + vote_value
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002437 vote_text = cq_label.get('values', {}).get(vote_value, '')
2438 if vote_text.lower() == 'commit':
2439 return 'commit'
2440
2441 lgtm_label = data['labels'].get('Code-Review', {})
2442 if lgtm_label:
2443 if 'rejected' in lgtm_label:
2444 return 'not lgtm'
2445 if 'approved' in lgtm_label:
2446 return 'lgtm'
2447
2448 if not data.get('reviewers', {}).get('REVIEWER', []):
2449 return 'unsent'
2450
2451 messages = data.get('messages', [])
2452 if messages:
2453 owner = data['owner'].get('_account_id')
2454 last_message_author = messages[-1].get('author', {}).get('_account_id')
2455 if owner != last_message_author:
2456 # Some reply from non-owner.
2457 return 'reply'
2458
2459 return 'waiting'
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002460
2461 def GetMostRecentPatchset(self):
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002462 data = self._GetChangeDetail(['CURRENT_REVISION'])
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002463 return data['revisions'][data['current_revision']]['_number']
2464
2465 def FetchDescription(self):
tandrii@chromium.org2d3da632016-04-25 19:23:27 +00002466 data = self._GetChangeDetail(['CURRENT_REVISION'])
2467 current_rev = data['current_revision']
2468 url = data['revisions'][current_rev]['fetch']['http']['url']
2469 return gerrit_util.GetChangeDescriptionFromGitiles(url, current_rev)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002470
dsansomee2d6fd92016-09-08 00:10:47 -07002471 def UpdateDescriptionRemote(self, description, force=False):
2472 if gerrit_util.HasPendingChangeEdit(self._GetGerritHost(), self.GetIssue()):
2473 if not force:
2474 ask_for_data(
2475 'The description cannot be modified while the issue has a pending '
2476 'unpublished edit. Either publish the edit in the Gerrit web UI '
2477 'or delete it.\n\n'
2478 'Press Enter to delete the unpublished edit, Ctrl+C to abort.')
2479
2480 gerrit_util.DeletePendingChangeEdit(self._GetGerritHost(),
2481 self.GetIssue())
scottmg@chromium.org6d1266e2016-04-26 11:12:26 +00002482 gerrit_util.SetCommitMessage(self._GetGerritHost(), self.GetIssue(),
2483 description)
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00002484
2485 def CloseIssue(self):
2486 gerrit_util.AbandonChange(self._GetGerritHost(), self.GetIssue(), msg='')
2487
tandrii@chromium.org600b4922016-04-26 10:57:52 +00002488 def GetApprovingReviewers(self):
2489 """Returns a list of reviewers approving the change.
2490
2491 Note: not necessarily committers.
2492 """
2493 raise NotImplementedError()
2494
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002495 def SubmitIssue(self, wait_for_merge=True):
2496 gerrit_util.SubmitChange(self._GetGerritHost(), self.GetIssue(),
2497 wait_for_merge=wait_for_merge)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002498
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002499 def _GetChangeDetail(self, options=None, issue=None):
2500 options = options or []
2501 issue = issue or self.GetIssue()
tandriic2405f52016-10-10 08:13:15 -07002502 assert issue, 'issue is required to query Gerrit'
Andrii Shyshkalovc6c8b4c2016-11-09 20:51:20 +01002503 try:
2504 data = gerrit_util.GetChangeDetail(self._GetGerritHost(), str(issue),
2505 options, ignore_404=False)
2506 except gerrit_util.GerritError as e:
2507 if e.http_status == 404:
2508 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2509 raise
tandriic2405f52016-10-10 08:13:15 -07002510 return data
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002511
agable32978d92016-11-01 12:55:02 -07002512 def _GetChangeCommit(self, issue=None):
2513 issue = issue or self.GetIssue()
2514 assert issue, 'issue is required to query Gerrit'
2515 data = gerrit_util.GetChangeCommit(self._GetGerritHost(), str(issue))
2516 if not data:
2517 raise GerritIssueNotExists(issue, self.GetCodereviewServer())
2518 return data
2519
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002520 def CMDLand(self, force, bypass_hooks, verbose):
2521 if git_common.is_dirty_git_tree('land'):
2522 return 1
tandriid60367b2016-06-22 05:25:12 -07002523 detail = self._GetChangeDetail(['CURRENT_REVISION', 'LABELS'])
2524 if u'Commit-Queue' in detail.get('labels', {}):
2525 if not force:
2526 ask_for_data('\nIt seems this repository has a Commit Queue, '
2527 'which can test and land changes for you. '
2528 'Are you sure you wish to bypass it?\n'
2529 'Press Enter to continue, Ctrl+C to abort.')
2530
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002531 differs = True
tandriic4344b52016-08-29 06:04:54 -07002532 last_upload = self._GitGetBranchConfigValue('gerritsquashhash')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002533 # Note: git diff outputs nothing if there is no diff.
2534 if not last_upload or RunGit(['diff', last_upload]).strip():
2535 print('WARNING: some changes from local branch haven\'t been uploaded')
2536 else:
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002537 if detail['current_revision'] == last_upload:
2538 differs = False
2539 else:
2540 print('WARNING: local branch contents differ from latest uploaded '
2541 'patchset')
2542 if differs:
2543 if not force:
2544 ask_for_data(
Andrii Shyshkalov3aac7752016-11-16 15:23:13 +01002545 'Do you want to submit latest Gerrit patchset and bypass hooks?\n'
2546 'Press Enter to continue, Ctrl+C to abort.')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002547 print('WARNING: bypassing hooks and submitting latest uploaded patchset')
2548 elif not bypass_hooks:
2549 hook_results = self.RunHook(
2550 committing=True,
2551 may_prompt=not force,
2552 verbose=verbose,
2553 change=self.GetChange(self.GetCommonAncestorWithUpstream(), None))
2554 if not hook_results.should_continue():
2555 return 1
2556
2557 self.SubmitIssue(wait_for_merge=True)
2558 print('Issue %s has been submitted.' % self.GetIssueURL())
agable32978d92016-11-01 12:55:02 -07002559 links = self._GetChangeCommit().get('web_links', [])
2560 for link in links:
2561 if link.get('name') == 'gerrit' and link.get('url'):
2562 print('Landed as %s' % link.get('url'))
2563 break
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002564 return 0
2565
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002566 def CMDPatchWithParsedIssue(self, parsed_issue_arg, reject, nocommit,
2567 directory):
2568 assert not reject
2569 assert not nocommit
2570 assert not directory
2571 assert parsed_issue_arg.valid
2572
2573 self._changelist.issue = parsed_issue_arg.issue
2574
2575 if parsed_issue_arg.hostname:
2576 self._gerrit_host = parsed_issue_arg.hostname
2577 self._gerrit_server = 'https://%s' % self._gerrit_host
2578
tandriic2405f52016-10-10 08:13:15 -07002579 try:
2580 detail = self._GetChangeDetail(['ALL_REVISIONS'])
2581 except GerritIssueNotExists as e:
2582 DieWithError(str(e))
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00002583
2584 if not parsed_issue_arg.patchset:
2585 # Use current revision by default.
2586 revision_info = detail['revisions'][detail['current_revision']]
2587 patchset = int(revision_info['_number'])
2588 else:
2589 patchset = parsed_issue_arg.patchset
2590 for revision_info in detail['revisions'].itervalues():
2591 if int(revision_info['_number']) == parsed_issue_arg.patchset:
2592 break
2593 else:
2594 DieWithError('Couldn\'t find patchset %i in issue %i' %
2595 (parsed_issue_arg.patchset, self.GetIssue()))
2596
2597 fetch_info = revision_info['fetch']['http']
2598 RunGit(['fetch', fetch_info['url'], fetch_info['ref']])
2599 RunGit(['cherry-pick', 'FETCH_HEAD'])
2600 self.SetIssue(self.GetIssue())
2601 self.SetPatchset(patchset)
2602 print('Committed patch for issue %i pathset %i locally' %
2603 (self.GetIssue(), self.GetPatchset()))
2604 return 0
2605
2606 @staticmethod
2607 def ParseIssueURL(parsed_url):
2608 if not parsed_url.scheme or not parsed_url.scheme.startswith('http'):
2609 return None
2610 # Gerrit's new UI is https://domain/c/<issue_number>[/[patchset]]
2611 # But current GWT UI is https://domain/#/c/<issue_number>[/[patchset]]
2612 # Short urls like https://domain/<issue_number> can be used, but don't allow
2613 # specifying the patchset (you'd 404), but we allow that here.
2614 if parsed_url.path == '/':
2615 part = parsed_url.fragment
2616 else:
2617 part = parsed_url.path
2618 match = re.match('(/c)?/(\d+)(/(\d+)?/?)?$', part)
2619 if match:
2620 return _ParsedIssueNumberArgument(
2621 issue=int(match.group(2)),
2622 patchset=int(match.group(4)) if match.group(4) else None,
2623 hostname=parsed_url.netloc)
2624 return None
2625
tandrii16e0b4e2016-06-07 10:34:28 -07002626 def _GerritCommitMsgHookCheck(self, offer_removal):
2627 hook = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
2628 if not os.path.exists(hook):
2629 return
2630 # Crude attempt to distinguish Gerrit Codereview hook from potentially
2631 # custom developer made one.
2632 data = gclient_utils.FileRead(hook)
2633 if not('From Gerrit Code Review' in data and 'add_ChangeId()' in data):
2634 return
2635 print('Warning: you have Gerrit commit-msg hook installed.\n'
qyearsley12fa6ff2016-08-24 09:18:40 -07002636 'It is not necessary for uploading with git cl in squash mode, '
tandrii16e0b4e2016-06-07 10:34:28 -07002637 'and may interfere with it in subtle ways.\n'
2638 'We recommend you remove the commit-msg hook.')
2639 if offer_removal:
2640 reply = ask_for_data('Do you want to remove it now? [Yes/No]')
2641 if reply.lower().startswith('y'):
2642 gclient_utils.rm_file_or_tree(hook)
2643 print('Gerrit commit-msg hook removed.')
2644 else:
2645 print('OK, will keep Gerrit commit-msg hook in place.')
2646
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002647 def CMDUploadChange(self, options, args, change):
2648 """Upload the current branch to Gerrit."""
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00002649 if options.squash and options.no_squash:
2650 DieWithError('Can only use one of --squash or --no-squash')
tandriia60502f2016-06-20 02:01:53 -07002651
2652 if not options.squash and not options.no_squash:
2653 # Load default for user, repo, squash=true, in this order.
2654 options.squash = settings.GetSquashGerritUploads()
2655 elif options.no_squash:
2656 options.squash = False
tandrii26f3e4e2016-06-10 08:37:04 -07002657
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002658 # We assume the remote called "origin" is the one we want.
2659 # It is probably not worthwhile to support different workflows.
2660 gerrit_remote = 'origin'
2661
2662 remote, remote_branch = self.GetRemoteBranch()
2663 branch = GetTargetRef(remote, remote_branch, options.target_branch,
2664 pending_prefix='')
2665
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002666 if options.squash:
tandrii16e0b4e2016-06-07 10:34:28 -07002667 self._GerritCommitMsgHookCheck(offer_removal=not options.force)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002668 if self.GetIssue():
2669 # Try to get the message from a previous upload.
2670 message = self.GetDescription()
2671 if not message:
2672 DieWithError(
2673 'failed to fetch description from current Gerrit issue %d\n'
2674 '%s' % (self.GetIssue(), self.GetIssueURL()))
2675 change_id = self._GetChangeDetail()['change_id']
2676 while True:
2677 footer_change_ids = git_footers.get_footer_change_id(message)
2678 if footer_change_ids == [change_id]:
2679 break
2680 if not footer_change_ids:
2681 message = git_footers.add_footer_change_id(message, change_id)
2682 print('WARNING: appended missing Change-Id to issue description')
2683 continue
2684 # There is already a valid footer but with different or several ids.
2685 # Doing this automatically is non-trivial as we don't want to lose
2686 # existing other footers, yet we want to append just 1 desired
2687 # Change-Id. Thus, just create a new footer, but let user verify the
2688 # new description.
2689 message = '%s\n\nChange-Id: %s' % (message, change_id)
2690 print(
2691 'WARNING: issue %s has Change-Id footer(s):\n'
2692 ' %s\n'
2693 'but issue has Change-Id %s, according to Gerrit.\n'
2694 'Please, check the proposed correction to the description, '
2695 'and edit it if necessary but keep the "Change-Id: %s" footer\n'
2696 % (self.GetIssue(), '\n '.join(footer_change_ids), change_id,
2697 change_id))
2698 ask_for_data('Press enter to edit now, Ctrl+C to abort')
2699 if not options.force:
2700 change_desc = ChangeDescription(message)
tandriif9aefb72016-07-01 09:06:51 -07002701 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002702 message = change_desc.description
2703 if not message:
2704 DieWithError("Description is empty. Aborting...")
2705 # Continue the while loop.
2706 # Sanity check of this code - we should end up with proper message
2707 # footer.
2708 assert [change_id] == git_footers.get_footer_change_id(message)
2709 change_desc = ChangeDescription(message)
2710 else:
2711 change_desc = ChangeDescription(
2712 options.message or CreateDescriptionFromLog(args))
2713 if not options.force:
tandriif9aefb72016-07-01 09:06:51 -07002714 change_desc.prompt(bug=options.bug)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002715 if not change_desc.description:
2716 DieWithError("Description is empty. Aborting...")
2717 message = change_desc.description
2718 change_ids = git_footers.get_footer_change_id(message)
2719 if len(change_ids) > 1:
2720 DieWithError('too many Change-Id footers, at most 1 allowed.')
2721 if not change_ids:
2722 # Generate the Change-Id automatically.
2723 message = git_footers.add_footer_change_id(
2724 message, GenerateGerritChangeId(message))
2725 change_desc.set_description(message)
2726 change_ids = git_footers.get_footer_change_id(message)
2727 assert len(change_ids) == 1
2728 change_id = change_ids[0]
2729
2730 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
2731 if remote is '.':
2732 # If our upstream branch is local, we base our squashed commit on its
2733 # squashed version.
2734 upstream_branch_name = scm.GIT.ShortBranchName(upstream_branch)
2735 # Check the squashed hash of the parent.
2736 parent = RunGit(['config',
2737 'branch.%s.gerritsquashhash' % upstream_branch_name],
2738 error_ok=True).strip()
2739 # Verify that the upstream branch has been uploaded too, otherwise
2740 # Gerrit will create additional CLs when uploading.
2741 if not parent or (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
2742 RunGitSilent(['rev-parse', parent + ':'])):
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002743 DieWithError(
2744 'Upload upstream branch %s first.\n'
tandrii2bdadf12016-07-12 12:27:54 -07002745 'Note: maybe you\'ve uploaded it with --no-squash. '
2746 'If so, then re-upload it with:\n'
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002747 ' git cl upload --squash\n' % upstream_branch_name)
2748 else:
2749 parent = self.GetCommonAncestorWithUpstream()
2750
2751 tree = RunGit(['rev-parse', 'HEAD:']).strip()
2752 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
2753 '-m', message]).strip()
2754 else:
2755 change_desc = ChangeDescription(
2756 options.message or CreateDescriptionFromLog(args))
2757 if not change_desc.description:
2758 DieWithError("Description is empty. Aborting...")
2759
2760 if not git_footers.get_footer_change_id(change_desc.description):
2761 DownloadGerritHook(False)
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002762 change_desc.set_description(self._AddChangeIdToCommitMessage(options,
2763 args))
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002764 ref_to_push = 'HEAD'
2765 parent = '%s/%s' % (gerrit_remote, branch)
2766 change_id = git_footers.get_footer_change_id(change_desc.description)[0]
2767
2768 assert change_desc
2769 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
2770 ref_to_push)]).splitlines()
2771 if len(commits) > 1:
2772 print('WARNING: This will upload %d commits. Run the following command '
2773 'to see which commits will be uploaded: ' % len(commits))
2774 print('git log %s..%s' % (parent, ref_to_push))
2775 print('You can also use `git squash-branch` to squash these into a '
2776 'single commit.')
2777 ask_for_data('About to upload; enter to confirm.')
2778
2779 if options.reviewers or options.tbr_owners:
2780 change_desc.update_reviewers(options.reviewers, options.tbr_owners,
2781 change)
2782
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002783 # Extra options that can be specified at push time. Doc:
2784 # https://gerrit-review.googlesource.com/Documentation/user-upload.html
2785 refspec_opts = []
tandrii99a72f22016-08-17 14:33:24 -07002786 if change_desc.get_reviewers(tbr_only=True):
2787 print('Adding self-LGTM (Code-Review +1) because of TBRs')
2788 refspec_opts.append('l=Code-Review+1')
2789
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002790 if options.title:
tandriieefe8322016-08-17 10:12:24 -07002791 if not re.match(r'^[\w ]+$', options.title):
2792 options.title = re.sub(r'[^\w ]', '', options.title)
2793 print('WARNING: Patchset title may only contain alphanumeric chars '
2794 'and spaces. Cleaned up title:\n%s' % options.title)
2795 if not options.force:
2796 ask_for_data('Press enter to continue, Ctrl+C to abort')
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002797 # Per doc, spaces must be converted to underscores, and Gerrit will do the
2798 # reverse on its side.
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002799 refspec_opts.append('m=' + options.title.replace(' ', '_'))
2800
tandrii@chromium.org8da45402016-05-24 23:11:03 +00002801 if options.send_mail:
2802 if not change_desc.get_reviewers():
2803 DieWithError('Must specify reviewers to send email.')
2804 refspec_opts.append('notify=ALL')
2805 else:
2806 refspec_opts.append('notify=NONE')
2807
tandrii99a72f22016-08-17 14:33:24 -07002808 reviewers = change_desc.get_reviewers()
2809 if reviewers:
2810 refspec_opts.extend('r=' + email.strip() for email in reviewers)
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002811
agablec6787972016-09-09 16:13:34 -07002812 if options.private:
2813 refspec_opts.append('draft')
2814
rmistry9eadede2016-09-19 11:22:43 -07002815 if options.topic:
2816 # Documentation on Gerrit topics is here:
2817 # https://gerrit-review.googlesource.com/Documentation/user-upload.html#topic
2818 refspec_opts.append('topic=%s' % options.topic)
2819
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002820 refspec_suffix = ''
2821 if refspec_opts:
2822 refspec_suffix = '%' + ','.join(refspec_opts)
2823 assert ' ' not in refspec_suffix, (
2824 'spaces not allowed in refspec: "%s"' % refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002825 refspec = '%s:refs/for/%s%s' % (ref_to_push, branch, refspec_suffix)
tandrii@chromium.orgbf766ba2016-04-13 12:51:23 +00002826
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002827 push_stdout = gclient_utils.CheckCallAndFilter(
tandrii@chromium.org8acd8332016-04-13 12:56:03 +00002828 ['git', 'push', gerrit_remote, refspec],
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002829 print_stdout=True,
2830 # Flush after every line: useful for seeing progress when running as
2831 # recipe.
2832 filter_fn=lambda _: sys.stdout.flush())
2833
2834 if options.squash:
2835 regex = re.compile(r'remote:\s+https?://[\w\-\.\/]*/(\d+)\s.*')
2836 change_numbers = [m.group(1)
2837 for m in map(regex.match, push_stdout.splitlines())
2838 if m]
2839 if len(change_numbers) != 1:
2840 DieWithError(
2841 ('Created|Updated %d issues on Gerrit, but only 1 expected.\n'
2842 'Change-Id: %s') % (len(change_numbers), change_id))
2843 self.SetIssue(change_numbers[0])
tandrii5d48c322016-08-18 16:19:37 -07002844 self._GitSetBranchConfigValue('gerritsquashhash', ref_to_push)
tandrii88189772016-09-29 04:29:57 -07002845
2846 # Add cc's from the CC_LIST and --cc flag (if any).
2847 cc = self.GetCCList().split(',')
2848 if options.cc:
2849 cc.extend(options.cc)
2850 cc = filter(None, [email.strip() for email in cc])
bradnelsond975b302016-10-23 12:20:23 -07002851 if change_desc.get_cced():
2852 cc.extend(change_desc.get_cced())
tandrii88189772016-09-29 04:29:57 -07002853 if cc:
Aaron Gable5db82092016-11-17 10:52:08 -08002854 gerrit_util.AddReviewers(
tandrii88189772016-09-29 04:29:57 -07002855 self._GetGerritHost(), self.GetIssue(), cc, is_reviewer=False)
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002856 return 0
2857
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002858 def _AddChangeIdToCommitMessage(self, options, args):
2859 """Re-commits using the current message, assumes the commit hook is in
2860 place.
2861 """
2862 log_desc = options.message or CreateDescriptionFromLog(args)
2863 git_command = ['commit', '--amend', '-m', log_desc]
2864 RunGit(git_command)
2865 new_log_desc = CreateDescriptionFromLog(args)
2866 if git_footers.get_footer_change_id(new_log_desc):
vapiera7fbd5a2016-06-16 09:17:49 -07002867 print('git-cl: Added Change-Id to commit message.')
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00002868 return new_log_desc
2869 else:
tandrii@chromium.orgb067ec52016-05-31 15:24:44 +00002870 DieWithError('ERROR: Gerrit commit-msg hook not installed.')
tandrii@chromium.orgaa6235b2016-04-11 21:35:29 +00002871
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002872 def SetCQState(self, new_state):
2873 """Sets the Commit-Queue label assuming canonical CQ config for Gerrit."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00002874 vote_map = {
2875 _CQState.NONE: 0,
2876 _CQState.DRY_RUN: 1,
2877 _CQState.COMMIT : 2,
2878 }
2879 gerrit_util.SetReview(self._GetGerritHost(), self.GetIssue(),
2880 labels={'Commit-Queue': vote_map[new_state]})
2881
tandriie113dfd2016-10-11 10:20:12 -07002882 def CannotTriggerTryJobReason(self):
tandrii8c5a3532016-11-04 07:52:02 -07002883 try:
2884 data = self._GetChangeDetail()
2885 except GerritIssueNotExists:
2886 return 'Gerrit doesn\'t know about your issue %s' % self.GetIssue()
2887
2888 if data['status'] in ('ABANDONED', 'MERGED'):
2889 return 'CL %s is closed' % self.GetIssue()
2890
2891 def GetTryjobProperties(self, patchset=None):
2892 """Returns dictionary of properties to launch tryjob."""
2893 data = self._GetChangeDetail(['ALL_REVISIONS'])
2894 patchset = int(patchset or self.GetPatchset())
2895 assert patchset
2896 revision_data = None # Pylint wants it to be defined.
2897 for revision_data in data['revisions'].itervalues():
2898 if int(revision_data['_number']) == patchset:
2899 break
2900 else:
2901 raise Exception('Patchset %d is not known in Gerrit issue %d' %
2902 (patchset, self.GetIssue()))
2903 return {
2904 'patch_issue': self.GetIssue(),
2905 'patch_set': patchset or self.GetPatchset(),
2906 'patch_project': data['project'],
2907 'patch_storage': 'gerrit',
2908 'patch_ref': revision_data['fetch']['http']['ref'],
2909 'patch_repository_url': revision_data['fetch']['http']['url'],
2910 'patch_gerrit_url': self.GetCodereviewServer(),
2911 }
tandriie113dfd2016-10-11 10:20:12 -07002912
tandriide281ae2016-10-12 06:02:30 -07002913 def GetIssueOwner(self):
tandrii8c5a3532016-11-04 07:52:02 -07002914 return self._GetChangeDetail(['DETAILED_ACCOUNTS'])['owner']['email']
tandriide281ae2016-10-12 06:02:30 -07002915
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00002916
2917_CODEREVIEW_IMPLEMENTATIONS = {
2918 'rietveld': _RietveldChangelistImpl,
2919 'gerrit': _GerritChangelistImpl,
2920}
2921
tandrii@chromium.org013a2802016-03-29 09:52:33 +00002922
iannuccie53c9352016-08-17 14:40:40 -07002923def _add_codereview_issue_select_options(parser, extra=""):
2924 _add_codereview_select_options(parser)
2925
2926 text = ('Operate on this issue number instead of the current branch\'s '
2927 'implicit issue.')
2928 if extra:
2929 text += ' '+extra
2930 parser.add_option('-i', '--issue', type=int, help=text)
2931
2932
2933def _process_codereview_issue_select_options(parser, options):
2934 _process_codereview_select_options(parser, options)
2935 if options.issue is not None and not options.forced_codereview:
2936 parser.error('--issue must be specified with either --rietveld or --gerrit')
2937
2938
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00002939def _add_codereview_select_options(parser):
2940 """Appends --gerrit and --rietveld options to force specific codereview."""
2941 parser.codereview_group = optparse.OptionGroup(
2942 parser, 'EXPERIMENTAL! Codereview override options')
2943 parser.add_option_group(parser.codereview_group)
2944 parser.codereview_group.add_option(
2945 '--gerrit', action='store_true',
2946 help='Force the use of Gerrit for codereview')
2947 parser.codereview_group.add_option(
2948 '--rietveld', action='store_true',
2949 help='Force the use of Rietveld for codereview')
2950
2951
2952def _process_codereview_select_options(parser, options):
2953 if options.gerrit and options.rietveld:
2954 parser.error('Options --gerrit and --rietveld are mutually exclusive')
2955 options.forced_codereview = None
2956 if options.gerrit:
2957 options.forced_codereview = 'gerrit'
2958 elif options.rietveld:
2959 options.forced_codereview = 'rietveld'
2960
2961
tandriif9aefb72016-07-01 09:06:51 -07002962def _get_bug_line_values(default_project, bugs):
2963 """Given default_project and comma separated list of bugs, yields bug line
2964 values.
2965
2966 Each bug can be either:
2967 * a number, which is combined with default_project
2968 * string, which is left as is.
2969
2970 This function may produce more than one line, because bugdroid expects one
2971 project per line.
2972
2973 >>> list(_get_bug_line_values('v8', '123,chromium:789'))
2974 ['v8:123', 'chromium:789']
2975 """
2976 default_bugs = []
2977 others = []
2978 for bug in bugs.split(','):
2979 bug = bug.strip()
2980 if bug:
2981 try:
2982 default_bugs.append(int(bug))
2983 except ValueError:
2984 others.append(bug)
2985
2986 if default_bugs:
2987 default_bugs = ','.join(map(str, default_bugs))
2988 if default_project:
2989 yield '%s:%s' % (default_project, default_bugs)
2990 else:
2991 yield default_bugs
2992 for other in sorted(others):
2993 # Don't bother finding common prefixes, CLs with >2 bugs are very very rare.
2994 yield other
2995
2996
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00002997class ChangeDescription(object):
2998 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00002999 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
bradnelsond975b302016-10-23 12:20:23 -07003000 CC_LINE = r'^[ \t]*(CC)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00003001 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003002
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003003 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00003004 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003005
agable@chromium.org42c20792013-09-12 17:34:49 +00003006 @property # www.logilab.org/ticket/89786
3007 def description(self): # pylint: disable=E0202
3008 return '\n'.join(self._description_lines)
3009
3010 def set_description(self, desc):
3011 if isinstance(desc, basestring):
3012 lines = desc.splitlines()
3013 else:
3014 lines = [line.rstrip() for line in desc]
3015 while lines and not lines[0]:
3016 lines.pop(0)
3017 while lines and not lines[-1]:
3018 lines.pop(-1)
3019 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003020
piman@chromium.org336f9122014-09-04 02:16:55 +00003021 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00003022 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003023 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00003024 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003025 return
agable@chromium.org42c20792013-09-12 17:34:49 +00003026 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003027
agable@chromium.org42c20792013-09-12 17:34:49 +00003028 # Get the set of R= and TBR= lines and remove them from the desciption.
3029 regexp = re.compile(self.R_LINE)
3030 matches = [regexp.match(line) for line in self._description_lines]
3031 new_desc = [l for i, l in enumerate(self._description_lines)
3032 if not matches[i]]
3033 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003034
agable@chromium.org42c20792013-09-12 17:34:49 +00003035 # Construct new unified R= and TBR= lines.
3036 r_names = []
3037 tbr_names = []
3038 for match in matches:
3039 if not match:
3040 continue
3041 people = cleanup_list([match.group(2).strip()])
3042 if match.group(1) == 'TBR':
3043 tbr_names.extend(people)
3044 else:
3045 r_names.extend(people)
3046 for name in r_names:
3047 if name not in reviewers:
3048 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00003049 if add_owners_tbr:
3050 owners_db = owners.Database(change.RepositoryRoot(),
dtu944b6052016-07-14 14:48:21 -07003051 fopen=file, os_path=os.path)
piman@chromium.org336f9122014-09-04 02:16:55 +00003052 all_reviewers = set(tbr_names + reviewers)
3053 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
3054 all_reviewers)
3055 tbr_names.extend(owners_db.reviewers_for(missing_files,
3056 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00003057 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
3058 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
3059
3060 # Put the new lines in the description where the old first R= line was.
3061 line_loc = next((i for i, match in enumerate(matches) if match), -1)
3062 if 0 <= line_loc < len(self._description_lines):
3063 if new_tbr_line:
3064 self._description_lines.insert(line_loc, new_tbr_line)
3065 if new_r_line:
3066 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003067 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00003068 if new_r_line:
3069 self.append_footer(new_r_line)
3070 if new_tbr_line:
3071 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003072
tandriif9aefb72016-07-01 09:06:51 -07003073 def prompt(self, bug=None):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003074 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003075 self.set_description([
3076 '# Enter a description of the change.',
3077 '# This will be displayed on the codereview site.',
3078 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00003079 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00003080 '--------------------',
3081 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003082
agable@chromium.org42c20792013-09-12 17:34:49 +00003083 regexp = re.compile(self.BUG_LINE)
3084 if not any((regexp.match(line) for line in self._description_lines)):
tandriif9aefb72016-07-01 09:06:51 -07003085 prefix = settings.GetBugPrefix()
3086 values = list(_get_bug_line_values(prefix, bug or '')) or [prefix]
3087 for value in values:
3088 # TODO(tandrii): change this to 'Bug: xxx' to be a proper Gerrit footer.
3089 self.append_footer('BUG=%s' % value)
3090
agable@chromium.org42c20792013-09-12 17:34:49 +00003091 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00003092 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003093 if not content:
3094 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00003095 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003096
3097 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00003098 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
3099 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00003100 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00003101 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003102
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003103 def append_footer(self, line):
tandrii@chromium.org601e1d12016-06-03 13:03:54 +00003104 """Adds a footer line to the description.
3105
3106 Differentiates legacy "KEY=xxx" footers (used to be called tags) and
3107 Gerrit's footers in the form of "Footer-Key: footer any value" and ensures
3108 that Gerrit footers are always at the end.
3109 """
3110 parsed_footer_line = git_footers.parse_footer(line)
3111 if parsed_footer_line:
3112 # Line is a gerrit footer in the form: Footer-Key: any value.
3113 # Thus, must be appended observing Gerrit footer rules.
3114 self.set_description(
3115 git_footers.add_footer(self.description,
3116 key=parsed_footer_line[0],
3117 value=parsed_footer_line[1]))
3118 return
3119
3120 if not self._description_lines:
3121 self._description_lines.append(line)
3122 return
3123
3124 top_lines, gerrit_footers, _ = git_footers.split_footers(self.description)
3125 if gerrit_footers:
3126 # git_footers.split_footers ensures that there is an empty line before
3127 # actual (gerrit) footers, if any. We have to keep it that way.
3128 assert top_lines and top_lines[-1] == ''
3129 top_lines, separator = top_lines[:-1], top_lines[-1:]
3130 else:
3131 separator = [] # No need for separator if there are no gerrit_footers.
3132
3133 prev_line = top_lines[-1] if top_lines else ''
3134 if (not presubmit_support.Change.TAG_LINE_RE.match(prev_line) or
3135 not presubmit_support.Change.TAG_LINE_RE.match(line)):
3136 top_lines.append('')
3137 top_lines.append(line)
3138 self._description_lines = top_lines + separator + gerrit_footers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003139
tandrii99a72f22016-08-17 14:33:24 -07003140 def get_reviewers(self, tbr_only=False):
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003141 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00003142 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
tandrii99a72f22016-08-17 14:33:24 -07003143 reviewers = [match.group(2).strip()
3144 for match in matches
3145 if match and (not tbr_only or match.group(1).upper() == 'TBR')]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00003146 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003147
bradnelsond975b302016-10-23 12:20:23 -07003148 def get_cced(self):
3149 """Retrieves the list of reviewers."""
3150 matches = [re.match(self.CC_LINE, line) for line in self._description_lines]
3151 cced = [match.group(2).strip() for match in matches if match]
3152 return cleanup_list(cced)
3153
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00003154
maruel@chromium.orge52678e2013-04-26 18:34:44 +00003155def get_approving_reviewers(props):
3156 """Retrieves the reviewers that approved a CL from the issue properties with
3157 messages.
3158
3159 Note that the list may contain reviewers that are not committer, thus are not
3160 considered by the CQ.
3161 """
3162 return sorted(
3163 set(
3164 message['sender']
3165 for message in props['messages']
3166 if message['approval'] and message['sender'] in props['reviewers']
3167 )
3168 )
3169
3170
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003171def FindCodereviewSettingsFile(filename='codereview.settings'):
3172 """Finds the given file starting in the cwd and going up.
3173
3174 Only looks up to the top of the repository unless an
3175 'inherit-review-settings-ok' file exists in the root of the repository.
3176 """
3177 inherit_ok_file = 'inherit-review-settings-ok'
3178 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00003179 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003180 if os.path.isfile(os.path.join(root, inherit_ok_file)):
3181 root = '/'
3182 while True:
3183 if filename in os.listdir(cwd):
3184 if os.path.isfile(os.path.join(cwd, filename)):
3185 return open(os.path.join(cwd, filename))
3186 if cwd == root:
3187 break
3188 cwd = os.path.dirname(cwd)
3189
3190
3191def LoadCodereviewSettingsFromFile(fileobj):
3192 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00003193 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003194
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003195 def SetProperty(name, setting, unset_error_ok=False):
3196 fullname = 'rietveld.' + name
3197 if setting in keyvals:
3198 RunGit(['config', fullname, keyvals[setting]])
3199 else:
3200 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
3201
tandrii48df5812016-10-17 03:55:37 -07003202 if not keyvals.get('GERRIT_HOST', False):
3203 SetProperty('server', 'CODE_REVIEW_SERVER')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003204 # Only server setting is required. Other settings can be absent.
3205 # In that case, we ignore errors raised during option deletion attempt.
3206 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00003207 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003208 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
3209 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00003210 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003211 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00003212 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
3213 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003214 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00003215 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00003216 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
rmistry@google.com5626a922015-02-26 14:03:30 +00003217 SetProperty('run-post-upload-hook', 'RUN_POST_UPLOAD_HOOK',
3218 unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003219
ukai@chromium.org7044efc2013-11-28 01:51:21 +00003220 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00003221 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00003222
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003223 if 'GERRIT_SQUASH_UPLOADS' in keyvals:
tandrii8dd81ea2016-06-16 13:24:23 -07003224 RunGit(['config', 'gerrit.squash-uploads',
3225 keyvals['GERRIT_SQUASH_UPLOADS']])
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00003226
tandrii@chromium.org28253532016-04-14 13:46:56 +00003227 if 'GERRIT_SKIP_ENSURE_AUTHENTICATED' in keyvals:
shinyak@chromium.org00dbccd2016-04-15 07:24:43 +00003228 RunGit(['config', 'gerrit.skip-ensure-authenticated',
tandrii@chromium.org28253532016-04-14 13:46:56 +00003229 keyvals['GERRIT_SKIP_ENSURE_AUTHENTICATED']])
3230
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003231 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
3232 #should be of the form
3233 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
3234 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
3235 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
3236 keyvals['ORIGIN_URL_CONFIG']])
3237
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003238
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003239def urlretrieve(source, destination):
3240 """urllib is broken for SSL connections via a proxy therefore we
3241 can't use urllib.urlretrieve()."""
3242 with open(destination, 'w') as f:
3243 f.write(urllib2.urlopen(source).read())
3244
3245
ukai@chromium.org712d6102013-11-27 00:52:58 +00003246def hasSheBang(fname):
3247 """Checks fname is a #! script."""
3248 with open(fname) as f:
3249 return f.read(2).startswith('#!')
3250
3251
bpastene@chromium.org917f0ff2016-04-05 00:45:30 +00003252# TODO(bpastene) Remove once a cleaner fix to crbug.com/600473 presents itself.
3253def DownloadHooks(*args, **kwargs):
3254 pass
3255
3256
tandrii@chromium.org18630d62016-03-04 12:06:02 +00003257def DownloadGerritHook(force):
3258 """Download and install Gerrit commit-msg hook.
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003259
3260 Args:
3261 force: True to update hooks. False to install hooks if not present.
3262 """
3263 if not settings.GetIsGerrit():
3264 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00003265 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003266 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
3267 if not os.access(dst, os.X_OK):
3268 if os.path.exists(dst):
3269 if not force:
3270 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003271 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00003272 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003273 if not hasSheBang(dst):
3274 DieWithError('Not a script: %s\n'
3275 'You need to download from\n%s\n'
3276 'into .git/hooks/commit-msg and '
3277 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003278 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
3279 except Exception:
3280 if os.path.exists(dst):
3281 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00003282 DieWithError('\nFailed to download hooks.\n'
3283 'You need to download from\n%s\n'
3284 'into .git/hooks/commit-msg and '
3285 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00003286
3287
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003288
3289def GetRietveldCodereviewSettingsInteractively():
3290 """Prompt the user for settings."""
3291 server = settings.GetDefaultServerUrl(error_ok=True)
3292 prompt = 'Rietveld server (host[:port])'
3293 prompt += ' [%s]' % (server or DEFAULT_SERVER)
3294 newserver = ask_for_data(prompt + ':')
3295 if not server and not newserver:
3296 newserver = DEFAULT_SERVER
3297 if newserver:
3298 newserver = gclient_utils.UpgradeToHttps(newserver)
3299 if newserver != server:
3300 RunGit(['config', 'rietveld.server', newserver])
3301
3302 def SetProperty(initial, caption, name, is_url):
3303 prompt = caption
3304 if initial:
3305 prompt += ' ("x" to clear) [%s]' % initial
3306 new_val = ask_for_data(prompt + ':')
3307 if new_val == 'x':
3308 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
3309 elif new_val:
3310 if is_url:
3311 new_val = gclient_utils.UpgradeToHttps(new_val)
3312 if new_val != initial:
3313 RunGit(['config', 'rietveld.' + name, new_val])
3314
3315 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
3316 SetProperty(settings.GetDefaultPrivateFlag(),
3317 'Private flag (rietveld only)', 'private', False)
3318 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
3319 'tree-status-url', False)
3320 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
3321 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
3322 SetProperty(settings.GetRunPostUploadHook(), 'Run Post Upload Hook',
3323 'run-post-upload-hook', False)
3324
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003325@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003326def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003327 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003328
tandrii5d0a0422016-09-14 06:24:35 -07003329 print('WARNING: git cl config works for Rietveld only')
3330 # TODO(tandrii): remove this once we switch to Gerrit.
3331 # See bugs http://crbug.com/637561 and http://crbug.com/600469.
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00003332 parser.add_option('--activate-update', action='store_true',
3333 help='activate auto-updating [rietveld] section in '
3334 '.git/config')
3335 parser.add_option('--deactivate-update', action='store_true',
3336 help='deactivate auto-updating [rietveld] section in '
3337 '.git/config')
3338 options, args = parser.parse_args(args)
3339
3340 if options.deactivate_update:
3341 RunGit(['config', 'rietveld.autoupdate', 'false'])
3342 return
3343
3344 if options.activate_update:
3345 RunGit(['config', '--unset', 'rietveld.autoupdate'])
3346 return
3347
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003348 if len(args) == 0:
tandrii@chromium.orge7d3d162016-03-15 14:15:57 +00003349 GetRietveldCodereviewSettingsInteractively()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003350 return 0
3351
3352 url = args[0]
3353 if not url.endswith('codereview.settings'):
3354 url = os.path.join(url, 'codereview.settings')
3355
3356 # Load code review settings and download hooks (if available).
3357 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
3358 return 0
3359
3360
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003361def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003362 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003363 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
3364 branch = ShortBranchName(branchref)
3365 _, args = parser.parse_args(args)
3366 if not args:
vapiera7fbd5a2016-06-16 09:17:49 -07003367 print('Current base-url:')
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003368 return RunGit(['config', 'branch.%s.base-url' % branch],
3369 error_ok=False).strip()
3370 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003371 print('Setting base-url to %s' % args[0])
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00003372 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
3373 error_ok=False).strip()
3374
3375
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003376def color_for_status(status):
3377 """Maps a Changelist status to color, for CMDstatus and other tools."""
3378 return {
3379 'unsent': Fore.RED,
3380 'waiting': Fore.BLUE,
3381 'reply': Fore.YELLOW,
3382 'lgtm': Fore.GREEN,
3383 'commit': Fore.MAGENTA,
3384 'closed': Fore.CYAN,
3385 'error': Fore.WHITE,
3386 }.get(status, Fore.WHITE)
3387
tandrii@chromium.org04ea8462016-04-25 19:51:21 +00003388
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003389def get_cl_statuses(changes, fine_grained, max_processes=None):
3390 """Returns a blocking iterable of (cl, status) for given branches.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003391
3392 If fine_grained is true, this will fetch CL statuses from the server.
3393 Otherwise, simply indicate if there's a matching url for the given branches.
3394
3395 If max_processes is specified, it is used as the maximum number of processes
3396 to spawn to fetch CL status from the server. Otherwise 1 process per branch is
3397 spawned.
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003398
3399 See GetStatus() for a list of possible statuses.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003400 """
qyearsley12fa6ff2016-08-24 09:18:40 -07003401 # Silence upload.py otherwise it becomes unwieldy.
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003402 upload.verbosity = 0
3403
3404 if fine_grained:
3405 # Process one branch synchronously to work through authentication, then
3406 # spawn processes to process all the other branches in parallel.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003407 if changes:
tandriiea9514a2016-08-17 12:32:37 -07003408 def fetch(cl):
3409 try:
3410 return (cl, cl.GetStatus())
3411 except:
3412 # See http://crbug.com/629863.
3413 logging.exception('failed to fetch status for %s:', cl)
3414 raise
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003415 yield fetch(changes[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003416
tandriiea9514a2016-08-17 12:32:37 -07003417 changes_to_fetch = changes[1:]
3418 if not changes_to_fetch:
kmarshall3bff56b2016-06-06 18:31:47 -07003419 # Exit early if there was only one branch to fetch.
3420 return
3421
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003422 pool = ThreadPool(
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003423 min(max_processes, len(changes_to_fetch))
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003424 if max_processes is not None
dsinclair99d30172016-08-09 10:48:58 -07003425 else max(len(changes_to_fetch), 1))
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003426
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003427 fetched_cls = set()
3428 it = pool.imap_unordered(fetch, changes_to_fetch).__iter__()
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003429 while True:
3430 try:
3431 row = it.next(timeout=5)
3432 except multiprocessing.TimeoutError:
3433 break
3434
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003435 fetched_cls.add(row[0])
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003436 yield row
3437
3438 # Add any branches that failed to fetch.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003439 for cl in set(changes_to_fetch) - fetched_cls:
3440 yield (cl, 'error')
calamity@chromium.orgcf197482016-04-29 20:15:53 +00003441
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003442 else:
3443 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003444 for cl in changes:
3445 yield (cl, 'waiting' if cl.GetIssueURL() else 'error')
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00003446
rmistry@google.com2dd99862015-06-22 12:22:18 +00003447
3448def upload_branch_deps(cl, args):
3449 """Uploads CLs of local branches that are dependents of the current branch.
3450
3451 If the local branch dependency tree looks like:
3452 test1 -> test2.1 -> test3.1
3453 -> test3.2
3454 -> test2.2 -> test3.3
3455
3456 and you run "git cl upload --dependencies" from test1 then "git cl upload" is
3457 run on the dependent branches in this order:
3458 test2.1, test3.1, test3.2, test2.2, test3.3
3459
3460 Note: This function does not rebase your local dependent branches. Use it when
3461 you make a change to the parent branch that will not conflict with its
3462 dependent branches, and you would like their dependencies updated in
3463 Rietveld.
3464 """
3465 if git_common.is_dirty_git_tree('upload-branch-deps'):
3466 return 1
3467
3468 root_branch = cl.GetBranch()
3469 if root_branch is None:
3470 DieWithError('Can\'t find dependent branches from detached HEAD state. '
3471 'Get on a branch!')
3472 if not cl.GetIssue() or not cl.GetPatchset():
3473 DieWithError('Current branch does not have an uploaded CL. We cannot set '
3474 'patchset dependencies without an uploaded CL.')
3475
3476 branches = RunGit(['for-each-ref',
3477 '--format=%(refname:short) %(upstream:short)',
3478 'refs/heads'])
3479 if not branches:
3480 print('No local branches found.')
3481 return 0
3482
3483 # Create a dictionary of all local branches to the branches that are dependent
3484 # on it.
3485 tracked_to_dependents = collections.defaultdict(list)
3486 for b in branches.splitlines():
3487 tokens = b.split()
3488 if len(tokens) == 2:
3489 branch_name, tracked = tokens
3490 tracked_to_dependents[tracked].append(branch_name)
3491
vapiera7fbd5a2016-06-16 09:17:49 -07003492 print()
3493 print('The dependent local branches of %s are:' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003494 dependents = []
3495 def traverse_dependents_preorder(branch, padding=''):
3496 dependents_to_process = tracked_to_dependents.get(branch, [])
3497 padding += ' '
3498 for dependent in dependents_to_process:
vapiera7fbd5a2016-06-16 09:17:49 -07003499 print('%s%s' % (padding, dependent))
rmistry@google.com2dd99862015-06-22 12:22:18 +00003500 dependents.append(dependent)
3501 traverse_dependents_preorder(dependent, padding)
3502 traverse_dependents_preorder(root_branch)
vapiera7fbd5a2016-06-16 09:17:49 -07003503 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003504
3505 if not dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003506 print('There are no dependent local branches for %s' % root_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003507 return 0
3508
vapiera7fbd5a2016-06-16 09:17:49 -07003509 print('This command will checkout all dependent branches and run '
3510 '"git cl upload".')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003511 ask_for_data('[Press enter to continue or ctrl-C to quit]')
3512
andybons@chromium.org962f9462016-02-03 20:00:42 +00003513 # Add a default patchset title to all upload calls in Rietveld.
tandrii@chromium.org4c72b082016-03-31 22:26:35 +00003514 if not cl.IsGerrit():
andybons@chromium.org962f9462016-02-03 20:00:42 +00003515 args.extend(['-t', 'Updated patchset dependency'])
3516
rmistry@google.com2dd99862015-06-22 12:22:18 +00003517 # Record all dependents that failed to upload.
3518 failures = {}
3519 # Go through all dependents, checkout the branch and upload.
3520 try:
3521 for dependent_branch in dependents:
vapiera7fbd5a2016-06-16 09:17:49 -07003522 print()
3523 print('--------------------------------------')
3524 print('Running "git cl upload" from %s:' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003525 RunGit(['checkout', '-q', dependent_branch])
vapiera7fbd5a2016-06-16 09:17:49 -07003526 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003527 try:
3528 if CMDupload(OptionParser(), args) != 0:
vapiera7fbd5a2016-06-16 09:17:49 -07003529 print('Upload failed for %s!' % dependent_branch)
rmistry@google.com2dd99862015-06-22 12:22:18 +00003530 failures[dependent_branch] = 1
3531 except: # pylint: disable=W0702
3532 failures[dependent_branch] = 1
vapiera7fbd5a2016-06-16 09:17:49 -07003533 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003534 finally:
3535 # Swap back to the original root branch.
3536 RunGit(['checkout', '-q', root_branch])
3537
vapiera7fbd5a2016-06-16 09:17:49 -07003538 print()
3539 print('Upload complete for dependent branches!')
rmistry@google.com2dd99862015-06-22 12:22:18 +00003540 for dependent_branch in dependents:
3541 upload_status = 'failed' if failures.get(dependent_branch) else 'succeeded'
vapiera7fbd5a2016-06-16 09:17:49 -07003542 print(' %s : %s' % (dependent_branch, upload_status))
3543 print()
rmistry@google.com2dd99862015-06-22 12:22:18 +00003544
3545 return 0
3546
3547
kmarshall3bff56b2016-06-06 18:31:47 -07003548def CMDarchive(parser, args):
3549 """Archives and deletes branches associated with closed changelists."""
3550 parser.add_option(
3551 '-j', '--maxjobs', action='store', type=int,
kmarshall9249e012016-08-23 12:02:16 -07003552 help='The maximum number of jobs to use when retrieving review status.')
kmarshall3bff56b2016-06-06 18:31:47 -07003553 parser.add_option(
3554 '-f', '--force', action='store_true',
3555 help='Bypasses the confirmation prompt.')
kmarshall9249e012016-08-23 12:02:16 -07003556 parser.add_option(
3557 '-d', '--dry-run', action='store_true',
3558 help='Skip the branch tagging and removal steps.')
3559 parser.add_option(
3560 '-t', '--notags', action='store_true',
3561 help='Do not tag archived branches. '
3562 'Note: local commit history may be lost.')
kmarshall3bff56b2016-06-06 18:31:47 -07003563
3564 auth.add_auth_options(parser)
3565 options, args = parser.parse_args(args)
3566 if args:
3567 parser.error('Unsupported args: %s' % ' '.join(args))
3568 auth_config = auth.extract_auth_config_from_options(options)
3569
3570 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3571 if not branches:
3572 return 0
3573
vapiera7fbd5a2016-06-16 09:17:49 -07003574 print('Finding all branches associated with closed issues...')
kmarshall3bff56b2016-06-06 18:31:47 -07003575 changes = [Changelist(branchref=b, auth_config=auth_config)
3576 for b in branches.splitlines()]
3577 alignment = max(5, max(len(c.GetBranch()) for c in changes))
3578 statuses = get_cl_statuses(changes,
3579 fine_grained=True,
3580 max_processes=options.maxjobs)
3581 proposal = [(cl.GetBranch(),
3582 'git-cl-archived-%s-%s' % (cl.GetIssue(), cl.GetBranch()))
3583 for cl, status in statuses
3584 if status == 'closed']
3585 proposal.sort()
3586
3587 if not proposal:
vapiera7fbd5a2016-06-16 09:17:49 -07003588 print('No branches with closed codereview issues found.')
kmarshall3bff56b2016-06-06 18:31:47 -07003589 return 0
3590
3591 current_branch = GetCurrentBranch()
3592
vapiera7fbd5a2016-06-16 09:17:49 -07003593 print('\nBranches with closed issues that will be archived:\n')
kmarshall9249e012016-08-23 12:02:16 -07003594 if options.notags:
3595 for next_item in proposal:
3596 print(' ' + next_item[0])
3597 else:
3598 print('%*s | %s' % (alignment, 'Branch name', 'Archival tag name'))
3599 for next_item in proposal:
3600 print('%*s %s' % (alignment, next_item[0], next_item[1]))
kmarshall3bff56b2016-06-06 18:31:47 -07003601
kmarshall9249e012016-08-23 12:02:16 -07003602 # Quit now on precondition failure or if instructed by the user, either
3603 # via an interactive prompt or by command line flags.
3604 if options.dry_run:
3605 print('\nNo changes were made (dry run).\n')
3606 return 0
3607 elif any(branch == current_branch for branch, _ in proposal):
kmarshall3bff56b2016-06-06 18:31:47 -07003608 print('You are currently on a branch \'%s\' which is associated with a '
3609 'closed codereview issue, so archive cannot proceed. Please '
3610 'checkout another branch and run this command again.' %
3611 current_branch)
3612 return 1
kmarshall9249e012016-08-23 12:02:16 -07003613 elif not options.force:
sergiyb4a5ecbe2016-06-20 09:46:00 -07003614 answer = ask_for_data('\nProceed with deletion (Y/n)? ').lower()
3615 if answer not in ('y', ''):
vapiera7fbd5a2016-06-16 09:17:49 -07003616 print('Aborted.')
kmarshall3bff56b2016-06-06 18:31:47 -07003617 return 1
3618
3619 for branch, tagname in proposal:
kmarshall9249e012016-08-23 12:02:16 -07003620 if not options.notags:
3621 RunGit(['tag', tagname, branch])
kmarshall3bff56b2016-06-06 18:31:47 -07003622 RunGit(['branch', '-D', branch])
kmarshall9249e012016-08-23 12:02:16 -07003623
vapiera7fbd5a2016-06-16 09:17:49 -07003624 print('\nJob\'s done!')
kmarshall3bff56b2016-06-06 18:31:47 -07003625
3626 return 0
3627
3628
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003629def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003630 """Show status of changelists.
3631
3632 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00003633 - Red not sent for review or broken
3634 - Blue waiting for review
3635 - Yellow waiting for you to reply to review
3636 - Green LGTM'ed
3637 - Magenta in the commit queue
3638 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003639
3640 Also see 'git cl comments'.
3641 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003642 parser.add_option('--field',
phajdan.jr289d03e2016-08-16 08:21:06 -07003643 help='print only specific field (desc|id|patch|status|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003644 parser.add_option('-f', '--fast', action='store_true',
3645 help='Do not retrieve review status')
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003646 parser.add_option(
3647 '-j', '--maxjobs', action='store', type=int,
3648 help='The maximum number of jobs to use when retrieving review status')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003649
3650 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07003651 _add_codereview_issue_select_options(
3652 parser, 'Must be in conjunction with --field.')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003653 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07003654 _process_codereview_issue_select_options(parser, options)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003655 if args:
3656 parser.error('Unsupported args: %s' % args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003657 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003658
iannuccie53c9352016-08-17 14:40:40 -07003659 if options.issue is not None and not options.field:
3660 parser.error('--field must be specified with --issue')
iannucci3c972b92016-08-17 13:24:10 -07003661
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003662 if options.field:
iannucci3c972b92016-08-17 13:24:10 -07003663 cl = Changelist(auth_config=auth_config, issue=options.issue,
3664 codereview=options.forced_codereview)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003665 if options.field.startswith('desc'):
vapiera7fbd5a2016-06-16 09:17:49 -07003666 print(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003667 elif options.field == 'id':
3668 issueid = cl.GetIssue()
3669 if issueid:
vapiera7fbd5a2016-06-16 09:17:49 -07003670 print(issueid)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003671 elif options.field == 'patch':
3672 patchset = cl.GetPatchset()
3673 if patchset:
vapiera7fbd5a2016-06-16 09:17:49 -07003674 print(patchset)
phajdan.jr289d03e2016-08-16 08:21:06 -07003675 elif options.field == 'status':
3676 print(cl.GetStatus())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003677 elif options.field == 'url':
3678 url = cl.GetIssueURL()
3679 if url:
vapiera7fbd5a2016-06-16 09:17:49 -07003680 print(url)
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00003681 return 0
3682
3683 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
3684 if not branches:
3685 print('No local branch found.')
3686 return 0
3687
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003688 changes = [
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003689 Changelist(branchref=b, auth_config=auth_config)
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003690 for b in branches.splitlines()]
vapiera7fbd5a2016-06-16 09:17:49 -07003691 print('Branches associated with reviews:')
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003692 output = get_cl_statuses(changes,
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003693 fine_grained=not options.fast,
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003694 max_processes=options.maxjobs)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003695
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003696 branch_statuses = {}
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003697 alignment = max(5, max(len(ShortBranchName(c.GetBranch())) for c in changes))
3698 for cl in sorted(changes, key=lambda c: c.GetBranch()):
3699 branch = cl.GetBranch()
calamity@chromium.orgffde55c2015-03-12 00:44:17 +00003700 while branch not in branch_statuses:
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003701 c, status = output.next()
3702 branch_statuses[c.GetBranch()] = status
3703 status = branch_statuses.pop(branch)
3704 url = cl.GetIssueURL()
3705 if url and (not status or status == 'error'):
3706 # The issue probably doesn't exist anymore.
3707 url += ' (broken)'
3708
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003709 color = color_for_status(status)
maruel@chromium.org885f6512013-07-27 02:17:26 +00003710 reset = Fore.RESET
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00003711 if not setup_color.IS_TTY:
maruel@chromium.org885f6512013-07-27 02:17:26 +00003712 color = ''
3713 reset = ''
nodir@chromium.orga6de1f42015-06-10 04:23:17 +00003714 status_str = '(%s)' % status if status else ''
vapiera7fbd5a2016-06-16 09:17:49 -07003715 print(' %*s : %s%s %s%s' % (
clemensh@chromium.orgcbd7dc32016-05-31 10:33:50 +00003716 alignment, ShortBranchName(branch), color, url,
vapiera7fbd5a2016-06-16 09:17:49 -07003717 status_str, reset))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00003718
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003719 cl = Changelist(auth_config=auth_config)
vapiera7fbd5a2016-06-16 09:17:49 -07003720 print()
3721 print('Current branch:',)
3722 print(cl.GetBranch())
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003723 if not cl.GetIssue():
vapiera7fbd5a2016-06-16 09:17:49 -07003724 print('No issue assigned.')
dpranke@chromium.orgee87f582015-07-31 18:46:25 +00003725 return 0
vapiera7fbd5a2016-06-16 09:17:49 -07003726 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
maruel@chromium.org85616e02014-07-28 15:37:55 +00003727 if not options.fast:
vapiera7fbd5a2016-06-16 09:17:49 -07003728 print('Issue description:')
3729 print(cl.GetDescription(pretty=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003730 return 0
3731
3732
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003733def colorize_CMDstatus_doc():
3734 """To be called once in main() to add colors to git cl status help."""
3735 colors = [i for i in dir(Fore) if i[0].isupper()]
3736
3737 def colorize_line(line):
3738 for color in colors:
3739 if color in line.upper():
3740 # Extract whitespaces first and the leading '-'.
3741 indent = len(line) - len(line.lstrip(' ')) + 1
3742 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
3743 return line
3744
3745 lines = CMDstatus.__doc__.splitlines()
3746 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
3747
3748
phajdan.jre328cf92016-08-22 04:12:17 -07003749def write_json(path, contents):
3750 with open(path, 'w') as f:
3751 json.dump(contents, f)
3752
3753
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003754@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003755def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003756 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003757
3758 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003759 """
dnj@chromium.org406c4402015-03-03 17:22:28 +00003760 parser.add_option('-r', '--reverse', action='store_true',
3761 help='Lookup the branch(es) for the specified issues. If '
3762 'no issues are specified, all branches with mapped '
3763 'issues will be listed.')
phajdan.jre328cf92016-08-22 04:12:17 -07003764 parser.add_option('--json', help='Path to JSON output file.')
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003765 _add_codereview_select_options(parser)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003766 options, args = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003767 _process_codereview_select_options(parser, options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003768
dnj@chromium.org406c4402015-03-03 17:22:28 +00003769 if options.reverse:
3770 branches = RunGit(['for-each-ref', 'refs/heads',
3771 '--format=%(refname:short)']).splitlines()
3772
3773 # Reverse issue lookup.
3774 issue_branch_map = {}
3775 for branch in branches:
3776 cl = Changelist(branchref=branch)
3777 issue_branch_map.setdefault(cl.GetIssue(), []).append(branch)
3778 if not args:
3779 args = sorted(issue_branch_map.iterkeys())
phajdan.jre328cf92016-08-22 04:12:17 -07003780 result = {}
dnj@chromium.org406c4402015-03-03 17:22:28 +00003781 for issue in args:
3782 if not issue:
3783 continue
phajdan.jre328cf92016-08-22 04:12:17 -07003784 result[int(issue)] = issue_branch_map.get(int(issue))
vapiera7fbd5a2016-06-16 09:17:49 -07003785 print('Branch for issue number %s: %s' % (
3786 issue, ', '.join(issue_branch_map.get(int(issue)) or ('None',))))
phajdan.jre328cf92016-08-22 04:12:17 -07003787 if options.json:
3788 write_json(options.json, result)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003789 else:
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00003790 cl = Changelist(codereview=options.forced_codereview)
dnj@chromium.org406c4402015-03-03 17:22:28 +00003791 if len(args) > 0:
3792 try:
3793 issue = int(args[0])
3794 except ValueError:
3795 DieWithError('Pass a number to set the issue or none to list it.\n'
tandrii@chromium.org8930b3d2016-04-13 14:47:02 +00003796 'Maybe you want to run git cl status?')
dnj@chromium.org406c4402015-03-03 17:22:28 +00003797 cl.SetIssue(issue)
vapiera7fbd5a2016-06-16 09:17:49 -07003798 print('Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL()))
phajdan.jre328cf92016-08-22 04:12:17 -07003799 if options.json:
3800 write_json(options.json, {
3801 'issue': cl.GetIssue(),
3802 'issue_url': cl.GetIssueURL(),
3803 })
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003804 return 0
3805
3806
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003807def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003808 """Shows or posts review comments for any changelist."""
3809 parser.add_option('-a', '--add-comment', dest='comment',
3810 help='comment to add to an issue')
3811 parser.add_option('-i', dest='issue',
3812 help="review issue id (defaults to current issue)")
smut@google.comc85ac942015-09-15 16:34:43 +00003813 parser.add_option('-j', '--json-file',
3814 help='File to write JSON summary to')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003815 auth.add_auth_options(parser)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003816 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003817 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003818
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003819 issue = None
3820 if options.issue:
3821 try:
3822 issue = int(options.issue)
3823 except ValueError:
3824 DieWithError('A review issue id is expected to be a number')
3825
tandrii@chromium.orgaa5ced12016-03-29 09:41:14 +00003826 cl = Changelist(issue=issue, codereview='rietveld', auth_config=auth_config)
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003827
3828 if options.comment:
3829 cl.AddComment(options.comment)
3830 return 0
3831
3832 data = cl.GetIssueProperties()
smut@google.comc85ac942015-09-15 16:34:43 +00003833 summary = []
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00003834 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
smut@google.comc85ac942015-09-15 16:34:43 +00003835 summary.append({
3836 'date': message['date'],
3837 'lgtm': False,
3838 'message': message['text'],
3839 'not_lgtm': False,
3840 'sender': message['sender'],
3841 })
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003842 if message['disapproval']:
3843 color = Fore.RED
smut@google.comc85ac942015-09-15 16:34:43 +00003844 summary[-1]['not lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003845 elif message['approval']:
3846 color = Fore.GREEN
smut@google.comc85ac942015-09-15 16:34:43 +00003847 summary[-1]['lgtm'] = True
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003848 elif message['sender'] == data['owner_email']:
3849 color = Fore.MAGENTA
3850 else:
3851 color = Fore.BLUE
vapiera7fbd5a2016-06-16 09:17:49 -07003852 print('\n%s%s %s%s' % (
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003853 color, message['date'].split('.', 1)[0], message['sender'],
vapiera7fbd5a2016-06-16 09:17:49 -07003854 Fore.RESET))
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00003855 if message['text'].strip():
vapiera7fbd5a2016-06-16 09:17:49 -07003856 print('\n'.join(' ' + l for l in message['text'].splitlines()))
smut@google.comc85ac942015-09-15 16:34:43 +00003857 if options.json_file:
3858 with open(options.json_file, 'wb') as f:
3859 json.dump(summary, f)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00003860 return 0
3861
3862
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003863@subcommand.usage('[codereview url or issue id]')
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003864def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003865 """Brings up the editor for the current CL's description."""
smut@google.com34fb6b12015-07-13 20:03:26 +00003866 parser.add_option('-d', '--display', action='store_true',
3867 help='Display the description instead of opening an editor')
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003868 parser.add_option('-n', '--new-description',
dnjba1b0f32016-09-02 12:37:42 -07003869 help='New description to set for this issue (- for stdin, '
3870 '+ to load from local commit HEAD)')
dsansomee2d6fd92016-09-08 00:10:47 -07003871 parser.add_option('-f', '--force', action='store_true',
3872 help='Delete any unpublished Gerrit edits for this issue '
3873 'without prompting')
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003874
3875 _add_codereview_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003876 auth.add_auth_options(parser)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003877 options, args = parser.parse_args(args)
3878 _process_codereview_select_options(parser, options)
3879
3880 target_issue = None
3881 if len(args) > 0:
martiniss6eda05f2016-06-30 10:18:35 -07003882 target_issue = ParseIssueNumberArgument(args[0])
3883 if not target_issue.valid:
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003884 parser.print_help()
3885 return 1
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003886
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003887 auth_config = auth.extract_auth_config_from_options(options)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003888
martiniss6eda05f2016-06-30 10:18:35 -07003889 kwargs = {
3890 'auth_config': auth_config,
3891 'codereview': options.forced_codereview,
3892 }
3893 if target_issue:
3894 kwargs['issue'] = target_issue.issue
3895 if options.forced_codereview == 'rietveld':
3896 kwargs['rietveld_server'] = target_issue.hostname
3897
3898 cl = Changelist(**kwargs)
martiniss@chromium.org2b55fe32016-04-26 20:28:54 +00003899
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003900 if not cl.GetIssue():
3901 DieWithError('This branch has no associated changelist.')
3902 description = ChangeDescription(cl.GetDescription())
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003903
smut@google.com34fb6b12015-07-13 20:03:26 +00003904 if options.display:
vapiera7fbd5a2016-06-16 09:17:49 -07003905 print(description.description)
smut@google.com34fb6b12015-07-13 20:03:26 +00003906 return 0
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003907
3908 if options.new_description:
3909 text = options.new_description
3910 if text == '-':
3911 text = '\n'.join(l.rstrip() for l in sys.stdin)
dnjba1b0f32016-09-02 12:37:42 -07003912 elif text == '+':
3913 base_branch = cl.GetCommonAncestorWithUpstream()
3914 change = cl.GetChange(base_branch, None, local_description=True)
3915 text = change.FullDescriptionText()
martiniss@chromium.orgd6648e22016-04-29 19:22:16 +00003916
3917 description.set_description(text)
3918 else:
3919 description.prompt()
3920
wychen@chromium.org063e4e52015-04-03 06:51:44 +00003921 if cl.GetDescription() != description.description:
dsansomee2d6fd92016-09-08 00:10:47 -07003922 cl.UpdateDescription(description.description, force=options.force)
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00003923 return 0
3924
3925
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003926def CreateDescriptionFromLog(args):
3927 """Pulls out the commit log to use as a base for the CL description."""
3928 log_args = []
3929 if len(args) == 1 and not args[0].endswith('.'):
3930 log_args = [args[0] + '..']
3931 elif len(args) == 1 and args[0].endswith('...'):
3932 log_args = [args[0][:-1]]
3933 elif len(args) == 2:
3934 log_args = [args[0] + '..' + args[1]]
3935 else:
3936 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00003937 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003938
3939
thestig@chromium.org44202a22014-03-11 19:22:18 +00003940def CMDlint(parser, args):
3941 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003942 parser.add_option('--filter', action='append', metavar='-x,+y',
3943 help='Comma-separated list of cpplint\'s category-filters')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003944 auth.add_auth_options(parser)
3945 options, args = parser.parse_args(args)
3946 auth_config = auth.extract_auth_config_from_options(options)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003947
3948 # Access to a protected member _XX of a client class
3949 # pylint: disable=W0212
3950 try:
3951 import cpplint
3952 import cpplint_chromium
3953 except ImportError:
vapiera7fbd5a2016-06-16 09:17:49 -07003954 print('Your depot_tools is missing cpplint.py and/or cpplint_chromium.py.')
thestig@chromium.org44202a22014-03-11 19:22:18 +00003955 return 1
3956
3957 # Change the current working directory before calling lint so that it
3958 # shows the correct base.
3959 previous_cwd = os.getcwd()
3960 os.chdir(settings.GetRoot())
3961 try:
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00003962 cl = Changelist(auth_config=auth_config)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003963 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
3964 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003965 if not files:
vapiera7fbd5a2016-06-16 09:17:49 -07003966 print('Cannot lint an empty CL')
thestig@chromium.org5839eb52014-05-30 16:20:51 +00003967 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00003968
3969 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00003970 command = args + files
3971 if options.filter:
3972 command = ['--filter=' + ','.join(options.filter)] + command
3973 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003974
3975 white_regex = re.compile(settings.GetLintRegex())
3976 black_regex = re.compile(settings.GetLintIgnoreRegex())
3977 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
3978 for filename in filenames:
3979 if white_regex.match(filename):
3980 if black_regex.match(filename):
vapiera7fbd5a2016-06-16 09:17:49 -07003981 print('Ignoring file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003982 else:
3983 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
3984 extra_check_functions)
3985 else:
vapiera7fbd5a2016-06-16 09:17:49 -07003986 print('Skipping file %s' % filename)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003987 finally:
3988 os.chdir(previous_cwd)
vapiera7fbd5a2016-06-16 09:17:49 -07003989 print('Total errors found: %d\n' % cpplint._cpplint_state.error_count)
thestig@chromium.org44202a22014-03-11 19:22:18 +00003990 if cpplint._cpplint_state.error_count != 0:
3991 return 1
3992 return 0
3993
3994
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003995def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003996 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003997 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003998 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00003999 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00004000 help='Run checks even if tree is dirty')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004001 auth.add_auth_options(parser)
4002 options, args = parser.parse_args(args)
4003 auth_config = auth.extract_auth_config_from_options(options)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004004
sbc@chromium.org71437c02015-04-09 19:29:40 +00004005 if not options.force and git_common.is_dirty_git_tree('presubmit'):
vapiera7fbd5a2016-06-16 09:17:49 -07004006 print('use --force to check even if tree is dirty.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004007 return 1
4008
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004009 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004010 if args:
4011 base_branch = args[0]
4012 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004013 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004014 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004015
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00004016 cl.RunHook(
4017 committing=not options.upload,
4018 may_prompt=False,
4019 verbose=options.verbose,
4020 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00004021 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004022
4023
tandrii@chromium.org65874e12016-03-04 12:03:02 +00004024def GenerateGerritChangeId(message):
4025 """Returns Ixxxxxx...xxx change id.
4026
4027 Works the same way as
4028 https://gerrit-review.googlesource.com/tools/hooks/commit-msg
4029 but can be called on demand on all platforms.
4030
4031 The basic idea is to generate git hash of a state of the tree, original commit
4032 message, author/committer info and timestamps.
4033 """
4034 lines = []
4035 tree_hash = RunGitSilent(['write-tree'])
4036 lines.append('tree %s' % tree_hash.strip())
4037 code, parent = RunGitWithCode(['rev-parse', 'HEAD~0'], suppress_stderr=False)
4038 if code == 0:
4039 lines.append('parent %s' % parent.strip())
4040 author = RunGitSilent(['var', 'GIT_AUTHOR_IDENT'])
4041 lines.append('author %s' % author.strip())
4042 committer = RunGitSilent(['var', 'GIT_COMMITTER_IDENT'])
4043 lines.append('committer %s' % committer.strip())
4044 lines.append('')
4045 # Note: Gerrit's commit-hook actually cleans message of some lines and
4046 # whitespace. This code is not doing this, but it clearly won't decrease
4047 # entropy.
4048 lines.append(message)
4049 change_hash = RunCommand(['git', 'hash-object', '-t', 'commit', '--stdin'],
4050 stdin='\n'.join(lines))
4051 return 'I%s' % change_hash.strip()
4052
4053
wittman@chromium.org455dc922015-01-26 20:15:50 +00004054def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
4055 """Computes the remote branch ref to use for the CL.
4056
4057 Args:
4058 remote (str): The git remote for the CL.
4059 remote_branch (str): The git remote branch for the CL.
4060 target_branch (str): The target branch specified by the user.
4061 pending_prefix (str): The pending prefix from the settings.
4062 """
4063 if not (remote and remote_branch):
4064 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004065
wittman@chromium.org455dc922015-01-26 20:15:50 +00004066 if target_branch:
4067 # Cannonicalize branch references to the equivalent local full symbolic
4068 # refs, which are then translated into the remote full symbolic refs
4069 # below.
4070 if '/' not in target_branch:
4071 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
4072 else:
4073 prefix_replacements = (
4074 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
4075 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
4076 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
4077 )
4078 match = None
4079 for regex, replacement in prefix_replacements:
4080 match = re.search(regex, target_branch)
4081 if match:
4082 remote_branch = target_branch.replace(match.group(0), replacement)
4083 break
4084 if not match:
4085 # This is a branch path but not one we recognize; use as-is.
4086 remote_branch = target_branch
rmistry@google.comc68112d2015-03-03 12:48:06 +00004087 elif remote_branch in REFS_THAT_ALIAS_TO_OTHER_REFS:
4088 # Handle the refs that need to land in different refs.
4089 remote_branch = REFS_THAT_ALIAS_TO_OTHER_REFS[remote_branch]
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004090
wittman@chromium.org455dc922015-01-26 20:15:50 +00004091 # Create the true path to the remote branch.
4092 # Does the following translation:
4093 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
4094 # * refs/remotes/origin/master -> refs/heads/master
4095 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
4096 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
4097 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
4098 elif remote_branch.startswith('refs/remotes/%s/' % remote):
4099 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
4100 'refs/heads/')
4101 elif remote_branch.startswith('refs/remotes/branch-heads'):
4102 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
4103 # If a pending prefix exists then replace refs/ with it.
4104 if pending_prefix:
4105 remote_branch = remote_branch.replace('refs/', pending_prefix)
4106 return remote_branch
4107
4108
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004109def cleanup_list(l):
4110 """Fixes a list so that comma separated items are put as individual items.
4111
4112 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
4113 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
4114 """
4115 items = sum((i.split(',') for i in l), [])
4116 stripped_items = (i.strip() for i in items)
4117 return sorted(filter(None, stripped_items))
4118
4119
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004120@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004121def CMDupload(parser, args):
rmistry@google.com78948ed2015-07-08 23:09:57 +00004122 """Uploads the current changelist to codereview.
4123
4124 Can skip dependency patchset uploads for a branch by running:
4125 git config branch.branch_name.skip-deps-uploads True
4126 To unset run:
4127 git config --unset branch.branch_name.skip-deps-uploads
4128 Can also set the above globally by using the --global flag.
4129 """
ukai@chromium.orge8077812012-02-03 03:41:46 +00004130 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4131 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00004132 parser.add_option('--bypass-watchlists', action='store_true',
4133 dest='bypass_watchlists',
4134 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004135 parser.add_option('-f', action='store_true', dest='force',
4136 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00004137 parser.add_option('-m', dest='message', help='message for patchset')
tandriif9aefb72016-07-01 09:06:51 -07004138 parser.add_option('-b', '--bug',
4139 help='pre-populate the bug number(s) for this issue. '
4140 'If several, separate with commas')
tandriib80458a2016-06-23 12:20:07 -07004141 parser.add_option('--message-file', dest='message_file',
4142 help='file which contains message for patchset')
andybons@chromium.org962f9462016-02-03 20:00:42 +00004143 parser.add_option('-t', dest='title',
4144 help='title for patchset (Rietveld only)')
ukai@chromium.orge8077812012-02-03 03:41:46 +00004145 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004146 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004147 help='reviewer email addresses')
4148 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004149 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00004150 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00004151 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00004152 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004153 parser.add_option('--emulate_svn_auto_props',
4154 '--emulate-svn-auto-props',
4155 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00004156 dest="emulate_svn_auto_props",
4157 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00004158 parser.add_option('-c', '--use-commit-queue', action='store_true',
4159 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00004160 parser.add_option('--private', action='store_true',
4161 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00004162 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00004163 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00004164 metavar='TARGET',
4165 help='Apply CL to remote ref TARGET. ' +
4166 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00004167 parser.add_option('--squash', action='store_true',
4168 help='Squash multiple commits into one (Gerrit only)')
bauerb@chromium.org54b400c2016-01-14 10:08:25 +00004169 parser.add_option('--no-squash', action='store_true',
4170 help='Don\'t squash multiple commits into one ' +
4171 '(Gerrit only)')
rmistry9eadede2016-09-19 11:22:43 -07004172 parser.add_option('--topic', default=None,
4173 help='Topic to specify when uploading (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004174 parser.add_option('--email', default=None,
4175 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00004176 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
4177 help='add a set of OWNERS to TBR')
tandrii@chromium.orgd50452a2015-11-23 16:38:15 +00004178 parser.add_option('-d', '--cq-dry-run', dest='cq_dry_run',
4179 action='store_true',
rmistry@google.comef966222015-04-07 11:15:01 +00004180 help='Send the patchset to do a CQ dry run right after '
4181 'upload.')
rmistry@google.com2dd99862015-06-22 12:22:18 +00004182 parser.add_option('--dependencies', action='store_true',
4183 help='Uploads CLs of all the local branches that depend on '
4184 'the current branch')
pgervais@chromium.org91141372014-01-09 23:27:20 +00004185
rmistry@google.com2dd99862015-06-22 12:22:18 +00004186 orig_args = args
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004187 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004188 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004189 _add_codereview_select_options(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004190 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004191 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004192 auth_config = auth.extract_auth_config_from_options(options)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004193
sbc@chromium.org71437c02015-04-09 19:29:40 +00004194 if git_common.is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00004195 return 1
4196
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00004197 options.reviewers = cleanup_list(options.reviewers)
4198 options.cc = cleanup_list(options.cc)
4199
tandriib80458a2016-06-23 12:20:07 -07004200 if options.message_file:
4201 if options.message:
4202 parser.error('only one of --message and --message-file allowed.')
4203 options.message = gclient_utils.FileRead(options.message_file)
4204 options.message_file = None
4205
tandrii4d0545a2016-07-06 03:56:49 -07004206 if options.cq_dry_run and options.use_commit_queue:
4207 parser.error('only one of --use-commit-queue and --cq-dry-run allowed.')
4208
tandrii@chromium.org512d79c2016-03-31 12:55:28 +00004209 # For sanity of test expectations, do this otherwise lazy-loading *now*.
4210 settings.GetIsGerrit()
4211
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004212 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
tandrii@chromium.org9e6c3a52016-04-12 14:13:08 +00004213 return cl.CMDUpload(options, args, orig_args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00004214
4215
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004216def IsSubmoduleMergeCommit(ref):
4217 # When submodules are added to the repo, we expect there to be a single
4218 # non-git-svn merge commit at remote HEAD with a signature comment.
4219 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00004220 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004221 return RunGit(cmd) != ''
4222
4223
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004224def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004225 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004226
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004227 In case of Gerrit, uses Gerrit REST api to "submit" the issue, which pushes
4228 upstream and closes the issue automatically and atomically.
4229
4230 Otherwise (in case of Rietveld):
4231 Squashes branch into a single commit.
Andrii Shyshkalov06a25022016-11-24 16:47:00 +01004232 Updates commit message with metadata (e.g. pointer to review).
4233 Pushes the code upstream.
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004234 Updates review and closes.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004235 """
4236 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
4237 help='bypass upload presubmit hook')
4238 parser.add_option('-m', dest='message',
4239 help="override review description")
4240 parser.add_option('-f', action='store_true', dest='force',
4241 help="force yes to questions (don't prompt)")
4242 parser.add_option('-c', dest='contributor',
4243 help="external contributor for patch (appended to " +
4244 "description and used as author for git). Should be " +
4245 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00004246 add_git_similarity(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004247 auth.add_auth_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004248 (options, args) = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004249 auth_config = auth.extract_auth_config_from_options(options)
4250
4251 cl = Changelist(auth_config=auth_config)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004252
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004253 # TODO(tandrii): refactor this into _RietveldChangelistImpl method.
4254 if cl.IsGerrit():
4255 if options.message:
4256 # This could be implemented, but it requires sending a new patch to
4257 # Gerrit, as Gerrit unlike Rietveld versions messages with patchsets.
4258 # Besides, Gerrit has the ability to change the commit message on submit
4259 # automatically, thus there is no need to support this option (so far?).
4260 parser.error('-m MESSAGE option is not supported for Gerrit.')
4261 if options.contributor:
4262 parser.error(
4263 '-c CONTRIBUTOR option is not supported for Gerrit.\n'
4264 'Before uploading a commit to Gerrit, ensure it\'s author field is '
4265 'the contributor\'s "name <email>". If you can\'t upload such a '
4266 'commit for review, contact your repository admin and request'
4267 '"Forge-Author" permission.')
tandrii73449b02016-09-14 06:27:24 -07004268 if not cl.GetIssue():
4269 DieWithError('You must upload the issue first to Gerrit.\n'
4270 ' If you would rather have `git cl land` upload '
4271 'automatically for you, see http://crbug.com/642759')
tandrii@chromium.orgd68b62b2016-03-31 16:09:29 +00004272 return cl._codereview_impl.CMDLand(options.force, options.bypass_hooks,
4273 options.verbose)
4274
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004275 current = cl.GetBranch()
4276 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
4277 if not settings.GetIsGitSvn() and remote == '.':
vapiera7fbd5a2016-06-16 09:17:49 -07004278 print()
4279 print('Attempting to push branch %r into another local branch!' % current)
4280 print()
4281 print('Either reparent this branch on top of origin/master:')
4282 print(' git reparent-branch --root')
4283 print()
4284 print('OR run `git rebase-update` if you think the parent branch is ')
4285 print('already committed.')
4286 print()
4287 print(' Current parent: %r' % upstream_branch)
iannucci@chromium.org5724c962014-04-11 09:32:56 +00004288 return 1
4289
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004290 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004291 # Default to merging against our best guess of the upstream branch.
4292 args = [cl.GetUpstreamBranch()]
4293
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004294 if options.contributor:
4295 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
vapiera7fbd5a2016-06-16 09:17:49 -07004296 print("Please provide contibutor as 'First Last <email@example.com>'")
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004297 return 1
4298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004299 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004300 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004301
sbc@chromium.org71437c02015-04-09 19:29:40 +00004302 if git_common.is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004303 return 1
4304
4305 # This rev-list syntax means "show all commits not in my branch that
4306 # are in base_branch".
4307 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
4308 base_branch]).splitlines()
4309 if upstream_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004310 print('Base branch "%s" has %d commits '
4311 'not in this branch.' % (base_branch, len(upstream_commits)))
4312 print('Run "git merge %s" before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004313 return 1
4314
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004315 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004316 svn_head = None
4317 if cmd == 'dcommit' or base_has_submodules:
4318 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
4319 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004320
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004321 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004322 # If the base_head is a submodule merge commit, the first parent of the
4323 # base_head should be a git-svn commit, which is what we're interested in.
4324 base_svn_head = base_branch
4325 if base_has_submodules:
4326 base_svn_head += '^1'
4327
4328 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004329 if extra_commits:
vapiera7fbd5a2016-06-16 09:17:49 -07004330 print('This branch has %d additional commits not upstreamed yet.'
4331 % len(extra_commits.splitlines()))
4332 print('Upstream "%s" or rebase this branch on top of the upstream trunk '
4333 'before attempting to %s.' % (base_branch, cmd))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004334 return 1
4335
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004336 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004337 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00004338 author = None
4339 if options.contributor:
4340 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004341 hook_results = cl.RunHook(
4342 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004343 may_prompt=not options.force,
4344 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004345 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00004346 if not hook_results.should_continue():
4347 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004348
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004349 # Check the tree status if the tree status URL is set.
4350 status = GetTreeStatus()
4351 if 'closed' == status:
4352 print('The tree is closed. Please wait for it to reopen. Use '
4353 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4354 return 1
4355 elif 'unknown' == status:
4356 print('Unable to determine tree status. Please verify manually and '
4357 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
4358 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004359
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004360 change_desc = ChangeDescription(options.message)
4361 if not change_desc.description and cl.GetIssue():
4362 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004363
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004364 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00004365 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004366 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00004367 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004368 print('No description set.')
4369 print('Visit %s/edit to set it.' % (cl.GetIssueURL()))
erg@chromium.org1a173982012-08-29 20:43:05 +00004370 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004371
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004372 # Keep a separate copy for the commit message, because the commit message
4373 # contains the link to the Rietveld issue, while the Rietveld message contains
4374 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004375 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00004376 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00004377
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004378 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00004379 if cl.GetIssue():
smut@google.com4c61dcc2015-06-08 22:31:29 +00004380 # Xcode won't linkify this URL unless there is a non-whitespace character
sergiyb@chromium.org4b39c5f2015-07-07 10:33:12 +00004381 # after it. Add a period on a new line to circumvent this. Also add a space
4382 # before the period to make sure that Gitiles continues to correctly resolve
4383 # the URL.
4384 commit_desc.append_footer('Review URL: %s .' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004385 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004386 commit_desc.append_footer('Patch from %s.' % options.contributor)
4387
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00004388 print('Description:')
4389 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004390
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004391 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004392 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00004393 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004394
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004395 # We want to squash all this branch's commits into one commit with the proper
4396 # description. We do this by doing a "reset --soft" to the base branch (which
4397 # keeps the working copy the same), then dcommitting that. If origin/master
4398 # has a submodule merge commit, we'll also need to cherry-pick the squashed
4399 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004400 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004401 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
4402 # Delete the branches if they exist.
4403 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
4404 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
4405 result = RunGitWithCode(showref_cmd)
4406 if result[0] == 0:
4407 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004408
4409 # We might be in a directory that's present in this branch but not in the
4410 # trunk. Move up to the top of the tree so that git commands that expect a
4411 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004412 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004413 if rel_base_path:
4414 os.chdir(rel_base_path)
4415
4416 # Stuff our change into the merge branch.
4417 # We wrap in a try...finally block so if anything goes wrong,
4418 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004419 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004420 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004421 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004422 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004423 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00004424 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004425 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004426 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004427 RunGit(
4428 [
4429 'commit', '--author', options.contributor,
4430 '-m', commit_desc.description,
4431 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004432 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004433 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004434 if base_has_submodules:
4435 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
4436 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
4437 RunGit(['checkout', CHERRY_PICK_BRANCH])
4438 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004439 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00004440 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004441 mirror = settings.GetGitMirror(remote)
4442 pushurl = mirror.url if mirror else remote
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004443 pending_prefix = settings.GetPendingRefPrefix()
4444 if not pending_prefix or branch.startswith(pending_prefix):
4445 # If not using refs/pending/heads/* at all, or target ref is already set
4446 # to pending, then push to the target ref directly.
Andrii Shyshkalov813ec3c2016-11-24 17:06:01 +01004447 # NB(tandrii): I think branch.startswith(pending_prefix) never happens
4448 # in practise. I really tried to create a new branch tracking
4449 # refs/pending/heads/master directly and git cl land failed long before
4450 # reaching this. Disagree? Comment on http://crbug.com/642493.
4451 if pending_prefix:
4452 print('\n\nYOU GOT A CHANCE TO WIN A FREE GIFT!\n\n'
4453 'Grab your .git/config, add instructions how to reproduce '
4454 'this, and post it to http://crbug.com/642493.\n'
4455 'The first reporter gets a free "Black Swan" book from '
4456 'tandrii@\n\n')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004457 retcode, output = RunGitWithCode(
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004458 ['push', '--porcelain', pushurl, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004459 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004460 else:
4461 # Cherry-pick the change on top of pending ref and then push it.
4462 assert branch.startswith('refs/'), branch
4463 assert pending_prefix[-1] == '/', pending_prefix
4464 pending_ref = pending_prefix + branch[len('refs/'):]
tandriibf429402016-09-14 07:09:12 -07004465 retcode, output = PushToGitPending(pushurl, pending_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004466 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004467 if retcode == 0:
4468 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004469 else:
4470 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004471 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00004472 'svn', 'dcommit',
4473 '-C%s' % options.similarity,
4474 '--no-rebase', '--rmdir',
4475 ]
4476 if settings.GetForceHttpsCommitUrl():
4477 # Allow forcing https commit URLs for some projects that don't allow
4478 # committing to http URLs (like Google Code).
4479 remote_url = cl.GetGitSvnRemoteUrl()
4480 if urlparse.urlparse(remote_url).scheme == 'http':
4481 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00004482 cmd_args.append('--commit-url=%s' % remote_url)
4483 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004484 if 'Committed r' in output:
4485 revision = re.match(
4486 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
4487 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004488 finally:
4489 # And then swap back to the original branch and clean up.
4490 RunGit(['checkout', '-q', cl.GetBranch()])
4491 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00004492 if base_has_submodules:
4493 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004494
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004495 if not revision:
vapiera7fbd5a2016-06-16 09:17:49 -07004496 print('Failed to push. If this persists, please file a bug.')
iannucci@chromium.org34504a12014-08-29 23:51:37 +00004497 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004498
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004499 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004500 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004501 try:
4502 revision = WaitForRealCommit(remote, revision, base_branch, branch)
4503 # We set pushed_to_pending to False, since it made it all the way to the
4504 # real ref.
4505 pushed_to_pending = False
4506 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004507 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004508
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004509 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004510 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004511 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004512 if not to_pending:
4513 if viewvc_url and revision:
4514 change_desc.append_footer(
4515 'Committed: %s%s' % (viewvc_url, revision))
4516 elif revision:
4517 change_desc.append_footer('Committed: %s' % (revision,))
vapiera7fbd5a2016-06-16 09:17:49 -07004518 print('Closing issue '
4519 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00004520 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004521 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00004522 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00004523 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00004524 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00004525 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004526 if options.bypass_hooks:
4527 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
4528 else:
4529 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00004530 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004531
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004532 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004533 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vapiera7fbd5a2016-06-16 09:17:49 -07004534 print('The commit is in the pending queue (%s).' % pending_ref)
4535 print('It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
4536 'footer.' % branch)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004537
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00004538 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
4539 if os.path.isfile(hook):
4540 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00004541
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00004542 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004543
4544
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004545def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
vapiera7fbd5a2016-06-16 09:17:49 -07004546 print()
4547 print('Waiting for commit to be landed on %s...' % real_ref)
4548 print('(If you are impatient, you may Ctrl-C once without harm)')
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004549 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
4550 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004551 mirror = settings.GetGitMirror(remote)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004552
4553 loop = 0
4554 while True:
4555 sys.stdout.write('fetching (%d)... \r' % loop)
4556 sys.stdout.flush()
4557 loop += 1
4558
szager@chromium.org151ebcf2016-03-09 01:08:25 +00004559 if mirror:
4560 mirror.populate()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004561 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
4562 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
4563 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
4564 for commit in commits.splitlines():
4565 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
vapiera7fbd5a2016-06-16 09:17:49 -07004566 print('Found commit on %s' % real_ref)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00004567 return commit
4568
4569 current_rev = to_rev
4570
4571
tandriibf429402016-09-14 07:09:12 -07004572def PushToGitPending(remote, pending_ref):
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004573 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
4574
4575 Returns:
4576 (retcode of last operation, output log of last operation).
4577 """
4578 assert pending_ref.startswith('refs/'), pending_ref
4579 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
4580 cherry = RunGit(['rev-parse', 'HEAD']).strip()
4581 code = 0
4582 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004583 max_attempts = 3
4584 attempts_left = max_attempts
4585 while attempts_left:
4586 if attempts_left != max_attempts:
vapiera7fbd5a2016-06-16 09:17:49 -07004587 print('Retrying, %d attempts left...' % (attempts_left - 1,))
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004588 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004589
4590 # Fetch. Retry fetch errors.
vapiera7fbd5a2016-06-16 09:17:49 -07004591 print('Fetching pending ref %s...' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004592 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004593 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004594 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004595 print('Fetch failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004596 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004597 print(out.strip())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004598 continue
4599
4600 # Try to cherry pick. Abort on merge conflicts.
vapiera7fbd5a2016-06-16 09:17:49 -07004601 print('Cherry-picking commit on top of pending ref...')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004602 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004603 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004604 if code:
vapiera7fbd5a2016-06-16 09:17:49 -07004605 print('Your patch doesn\'t apply cleanly to ref \'%s\', '
4606 'the following files have merge conflicts:' % pending_ref)
4607 print(RunGit(['diff', '--name-status', '--diff-filter=U']).strip())
4608 print('Please rebase your patch and try again.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004609 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004610 return code, out
4611
4612 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vapiera7fbd5a2016-06-16 09:17:49 -07004613 print('Pushing commit to %s... It can take a while.' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004614 code, out = RunGitWithCode(
4615 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
4616 if code == 0:
4617 # Success.
vapiera7fbd5a2016-06-16 09:17:49 -07004618 print('Commit pushed to pending ref successfully!')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004619 return code, out
4620
vapiera7fbd5a2016-06-16 09:17:49 -07004621 print('Push failed with exit code %d.' % code)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004622 if out.strip():
vapiera7fbd5a2016-06-16 09:17:49 -07004623 print(out.strip())
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004624 if IsFatalPushFailure(out):
vapiera7fbd5a2016-06-16 09:17:49 -07004625 print('Fatal push error. Make sure your .netrc credentials and git '
4626 'user.email are correct and you have push access to the repo.')
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004627 return code, out
4628
vapiera7fbd5a2016-06-16 09:17:49 -07004629 print('All attempts to push to pending ref failed.')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004630 return code, out
4631
4632
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00004633def IsFatalPushFailure(push_stdout):
4634 """True if retrying push won't help."""
4635 return '(prohibited by Gerrit)' in push_stdout
4636
4637
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004638@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004639def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004640 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004641 if not settings.GetIsGitSvn():
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004642 if git_footers.get_footer_svn_id():
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004643 # If it looks like previous commits were mirrored with git-svn.
agable3b9a5bb2016-09-22 11:32:08 -07004644 message = """This repository appears to be a git-svn mirror, but we
4645don't support git-svn mirrors anymore."""
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004646 else:
4647 message = """This doesn't appear to be an SVN repository.
4648If your project has a true, writeable git repository, you probably want to run
4649'git cl land' instead.
4650If your project has a git mirror of an upstream SVN master, you probably need
4651to run 'git svn init'.
4652
4653Using the wrong command might cause your commit to appear to succeed, and the
4654review to be closed, without actually landing upstream. If you choose to
4655proceed, please verify that the commit lands upstream as expected."""
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00004656 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00004657 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
tandrii3bb82ff2016-06-17 07:36:36 -07004658 print('WARNING: chrome infrastructure is migrating SVN repos to Git.\n'
4659 'Please let us know of this project you are committing to:'
4660 ' http://crbug.com/600451')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004661 return SendUpstream(parser, args, 'dcommit')
4662
4663
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004664@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00004665def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004666 """Commits the current changelist via git."""
tandrii@chromium.org09d7a6a2016-03-04 15:44:48 +00004667 if settings.GetIsGitSvn() or git_footers.get_footer_svn_id():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004668 print('This appears to be an SVN repository.')
4669 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
mmoss@chromium.orgf0e41522015-06-10 19:52:01 +00004670 print('(Ignore if this is the first commit after migrating from svn->git)')
maruel@chromium.org90541732011-04-01 17:54:18 +00004671 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00004672 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004673
4674
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004675@subcommand.usage('<patch url or issue id or issue url>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004676def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00004677 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004678 parser.add_option('-b', dest='newbranch',
4679 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004680 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004681 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004682 parser.add_option('-d', '--directory', action='store', metavar='DIR',
4683 help='Change to the directory DIR immediately, '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004684 'before doing anything else. Rietveld only.')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00004685 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00004686 help='failed patches spew .rej files rather than '
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004687 'attempting a 3-way merge. Rietveld only.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004688 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004689 help='don\'t commit after patch applies. Rietveld only.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004690
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004691
4692 group = optparse.OptionGroup(
4693 parser,
4694 'Options for continuing work on the current issue uploaded from a '
4695 'different clone (e.g. different machine). Must be used independently '
4696 'from the other options. No issue number should be specified, and the '
4697 'branch must have an issue number associated with it')
4698 group.add_option('--reapply', action='store_true', dest='reapply',
4699 help='Reset the branch and reapply the issue.\n'
4700 'CAUTION: This will undo any local changes in this '
4701 'branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004702
4703 group.add_option('--pull', action='store_true', dest='pull',
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004704 help='Performs a pull before reapplying.')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004705 parser.add_option_group(group)
4706
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004707 auth.add_auth_options(parser)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004708 _add_codereview_select_options(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004709 (options, args) = parser.parse_args(args)
tandrii@chromium.orgdde64622016-04-13 17:11:21 +00004710 _process_codereview_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004711 auth_config = auth.extract_auth_config_from_options(options)
4712
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004713
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004714 if options.reapply :
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004715 if options.newbranch:
4716 parser.error('--reapply works on the current branch only')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004717 if len(args) > 0:
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004718 parser.error('--reapply implies no additional arguments')
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004719
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004720 cl = Changelist(auth_config=auth_config,
4721 codereview=options.forced_codereview)
4722 if not cl.GetIssue():
4723 parser.error('current branch must have an associated issue')
4724
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004725 upstream = cl.GetUpstreamBranch()
4726 if upstream == None:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004727 parser.error('No upstream branch specified. Cannot reset branch')
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004728
4729 RunGit(['reset', '--hard', upstream])
4730 if options.pull:
4731 RunGit(['pull'])
mtrofin@chromium.org1d88dd32016-02-04 16:25:12 +00004732
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004733 return cl.CMDPatchIssue(cl.GetIssue(), options.reject, options.nocommit,
4734 options.directory)
4735
4736 if len(args) != 1 or not args[0]:
4737 parser.error('Must specify issue number or url')
4738
4739 # We don't want uncommitted changes mixed up with the patch.
4740 if git_common.is_dirty_git_tree('patch'):
dsinclair@chromium.orgfbed6562015-09-25 21:22:36 +00004741 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004742
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004743 if options.newbranch:
4744 if options.force:
4745 RunGit(['branch', '-D', options.newbranch],
4746 stderr=subprocess2.PIPE, error_ok=True)
4747 RunGit(['new-branch', options.newbranch])
tandriidf09a462016-08-18 16:23:55 -07004748 elif not GetCurrentBranch():
4749 DieWithError('A branch is required to apply patch. Hint: use -b option.')
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004750
4751 cl = Changelist(auth_config=auth_config, codereview=options.forced_codereview)
4752
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004753 if cl.IsGerrit():
4754 if options.reject:
4755 parser.error('--reject is not supported with Gerrit codereview.')
4756 if options.nocommit:
4757 parser.error('--nocommit is not supported with Gerrit codereview.')
4758 if options.directory:
4759 parser.error('--directory is not supported with Gerrit codereview.')
4760
tandrii@chromium.orgc2786d92016-05-31 19:53:50 +00004761 return cl.CMDPatchIssue(args[0], options.reject, options.nocommit,
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00004762 options.directory)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004763
4764
4765def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004766 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004767 # Provide a wrapper for git svn rebase to help avoid accidental
4768 # git svn dcommit.
4769 # It's the only command that doesn't use parser at all since we just defer
4770 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00004771
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00004772 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004773
4774
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004775def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004776 """Fetches the tree status and returns either 'open', 'closed',
4777 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00004778 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004779 if url:
4780 status = urllib2.urlopen(url).read().lower()
4781 if status.find('closed') != -1 or status == '0':
4782 return 'closed'
4783 elif status.find('open') != -1 or status == '1':
4784 return 'open'
4785 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004786 return 'unset'
4787
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004788
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004789def GetTreeStatusReason():
4790 """Fetches the tree status from a json url and returns the message
4791 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00004792 url = settings.GetTreeStatusUrl()
4793 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004794 connection = urllib2.urlopen(json_url)
4795 status = json.loads(connection.read())
4796 connection.close()
4797 return status['message']
4798
dpranke@chromium.org970c5222011-03-12 00:32:24 +00004799
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004800def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004801 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004802 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004803 status = GetTreeStatus()
4804 if 'unset' == status:
vapiera7fbd5a2016-06-16 09:17:49 -07004805 print('You must configure your tree status URL by running "git cl config".')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004806 return 2
4807
vapiera7fbd5a2016-06-16 09:17:49 -07004808 print('The tree is %s' % status)
4809 print()
4810 print(GetTreeStatusReason())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004811 if status != 'open':
4812 return 1
4813 return 0
4814
4815
maruel@chromium.org15192402012-09-06 12:38:29 +00004816def CMDtry(parser, args):
qyearsley1fdfcb62016-10-24 13:22:03 -07004817 """Triggers try jobs using either BuildBucket or CQ dry run."""
tandrii1838bad2016-10-06 00:10:52 -07004818 group = optparse.OptionGroup(parser, 'Try job options')
maruel@chromium.org15192402012-09-06 12:38:29 +00004819 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004820 '-b', '--bot', action='append',
4821 help=('IMPORTANT: specify ONE builder per --bot flag. Use it multiple '
4822 'times to specify multiple builders. ex: '
4823 '"-b win_rel -b win_layout". See '
4824 'the try server waterfall for the builders name and the tests '
4825 'available.'))
maruel@chromium.org15192402012-09-06 12:38:29 +00004826 group.add_option(
borenet6c0efe62016-10-19 08:13:29 -07004827 '-B', '--bucket', default='',
4828 help=('Buildbucket bucket to send the try requests.'))
4829 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004830 '-m', '--master', default='',
4831 help=('Specify a try master where to run the tries.'))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004832 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004833 '-r', '--revision',
tandriif7b29d42016-10-07 08:45:41 -07004834 help='Revision to use for the try job; default: the revision will '
4835 'be determined by the try recipe that builder runs, which usually '
4836 'defaults to HEAD of origin/master')
maruel@chromium.org15192402012-09-06 12:38:29 +00004837 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004838 '-c', '--clobber', action='store_true', default=False,
tandriif7b29d42016-10-07 08:45:41 -07004839 help='Force a clobber before building; that is don\'t do an '
tandrii1838bad2016-10-06 00:10:52 -07004840 'incremental build')
maruel@chromium.org15192402012-09-06 12:38:29 +00004841 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004842 '--project',
4843 help='Override which project to use. Projects are defined '
tandriif7b29d42016-10-07 08:45:41 -07004844 'in recipe to determine to which repository or directory to '
4845 'apply the patch')
maruel@chromium.org15192402012-09-06 12:38:29 +00004846 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004847 '-p', '--property', dest='properties', action='append', default=[],
4848 help='Specify generic properties in the form -p key1=value1 -p '
tandriif7b29d42016-10-07 08:45:41 -07004849 'key2=value2 etc. The value will be treated as '
4850 'json if decodable, or as string otherwise. '
4851 'NOTE: using this may make your try job not usable for CQ, '
4852 'which will then schedule another try job with default properties')
sheyang@chromium.orgdb375572015-08-17 19:22:23 +00004853 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004854 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4855 help='Host of buildbucket. The default host is %default.')
maruel@chromium.org15192402012-09-06 12:38:29 +00004856 parser.add_option_group(group)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004857 auth.add_auth_options(parser)
maruel@chromium.org15192402012-09-06 12:38:29 +00004858 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004859 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org15192402012-09-06 12:38:29 +00004860
machenbach@chromium.org45453142015-09-15 08:45:22 +00004861 # Make sure that all properties are prop=value pairs.
4862 bad_params = [x for x in options.properties if '=' not in x]
4863 if bad_params:
4864 parser.error('Got properties with missing "=": %s' % bad_params)
4865
maruel@chromium.org15192402012-09-06 12:38:29 +00004866 if args:
4867 parser.error('Unknown arguments: %s' % args)
4868
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00004869 cl = Changelist(auth_config=auth_config)
maruel@chromium.org15192402012-09-06 12:38:29 +00004870 if not cl.GetIssue():
4871 parser.error('Need to upload first')
4872
tandriie113dfd2016-10-11 10:20:12 -07004873 error_message = cl.CannotTriggerTryJobReason()
4874 if error_message:
qyearsley99e2cdf2016-10-23 12:51:41 -07004875 parser.error('Can\'t trigger try jobs: %s' % error_message)
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00004876
borenet6c0efe62016-10-19 08:13:29 -07004877 if options.bucket and options.master:
4878 parser.error('Only one of --bucket and --master may be used.')
4879
qyearsley1fdfcb62016-10-24 13:22:03 -07004880 buckets = _get_bucket_map(cl, options, parser)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00004881
qyearsleydd49f942016-10-28 11:57:22 -07004882 # If no bots are listed and we couldn't get a list based on PRESUBMIT files,
4883 # then we default to triggering a CQ dry run (see http://crbug.com/625697).
qyearsley1fdfcb62016-10-24 13:22:03 -07004884 if not buckets:
qyearsley1fdfcb62016-10-24 13:22:03 -07004885 if options.verbose:
4886 print('git cl try with no bots now defaults to CQ Dry Run.')
4887 return cl.TriggerDryRun()
stip@chromium.org43064fd2013-12-18 20:07:44 +00004888
borenet6c0efe62016-10-19 08:13:29 -07004889 for builders in buckets.itervalues():
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004890 if any('triggered' in b for b in builders):
vapiera7fbd5a2016-06-16 09:17:49 -07004891 print('ERROR You are trying to send a job to a triggered bot. This type '
tandriide281ae2016-10-12 06:02:30 -07004892 'of bot requires an initial job from a parent (usually a builder). '
4893 'Instead send your job to the parent.\n'
vapiera7fbd5a2016-06-16 09:17:49 -07004894 'Bot list: %s' % builders, file=sys.stderr)
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00004895 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00004896
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00004897 patchset = cl.GetMostRecentPatchset()
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004898 # TODO(tandrii): Checking local patchset against remote patchset is only
4899 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4900 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandriide281ae2016-10-12 06:02:30 -07004901 print('Warning: Codereview server has newer patchsets (%s) than most '
4902 'recent upload from local checkout (%s). Did a previous upload '
4903 'fail?\n'
4904 'By default, git cl try uses the latest patchset from '
4905 'codereview, continuing to use patchset %s.\n' %
4906 (patchset, cl.GetPatchset(), patchset))
qyearsley1fdfcb62016-10-24 13:22:03 -07004907
tandrii568043b2016-10-11 07:49:18 -07004908 try:
borenet6c0efe62016-10-19 08:13:29 -07004909 _trigger_try_jobs(auth_config, cl, buckets, options, 'git_cl_try',
4910 patchset)
tandrii568043b2016-10-11 07:49:18 -07004911 except BuildbucketResponseException as ex:
4912 print('ERROR: %s' % ex)
4913 return 1
maruel@chromium.org15192402012-09-06 12:38:29 +00004914 return 0
4915
4916
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004917def CMDtry_results(parser, args):
tandrii1838bad2016-10-06 00:10:52 -07004918 """Prints info about try jobs associated with current CL."""
4919 group = optparse.OptionGroup(parser, 'Try job results options')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004920 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004921 '-p', '--patchset', type=int, help='patchset number if not current.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004922 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004923 '--print-master', action='store_true', help='print master name as well.')
tandrii@chromium.org6cf98c82016-03-15 11:56:00 +00004924 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004925 '--color', action='store_true', default=setup_color.IS_TTY,
4926 help='force color output, useful when piping output.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004927 group.add_option(
tandrii1838bad2016-10-06 00:10:52 -07004928 '--buildbucket-host', default='cr-buildbucket.appspot.com',
4929 help='Host of buildbucket. The default host is %default.')
qyearsley53f48a12016-09-01 10:45:13 -07004930 group.add_option(
4931 '--json', help='Path of JSON output file to write try job results to.')
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004932 parser.add_option_group(group)
4933 auth.add_auth_options(parser)
4934 options, args = parser.parse_args(args)
4935 if args:
4936 parser.error('Unrecognized args: %s' % ' '.join(args))
4937
4938 auth_config = auth.extract_auth_config_from_options(options)
4939 cl = Changelist(auth_config=auth_config)
4940 if not cl.GetIssue():
4941 parser.error('Need to upload first')
4942
tandrii221ab252016-10-06 08:12:04 -07004943 patchset = options.patchset
4944 if not patchset:
4945 patchset = cl.GetMostRecentPatchset()
4946 if not patchset:
4947 parser.error('Codereview doesn\'t know about issue %s. '
4948 'No access to issue or wrong issue number?\n'
4949 'Either upload first, or pass --patchset explicitely' %
4950 cl.GetIssue())
4951
Ravi Mistryfda50ca2016-11-14 10:19:18 -05004952 # TODO(tandrii): Checking local patchset against remote patchset is only
4953 # supported for Rietveld. Extend it to Gerrit or remove it completely.
4954 if not cl.IsGerrit() and patchset != cl.GetPatchset():
tandrii45b2a582016-10-11 03:14:16 -07004955 print('Warning: Codereview server has newer patchsets (%s) than most '
4956 'recent upload from local checkout (%s). Did a previous upload '
4957 'fail?\n'
tandriide281ae2016-10-12 06:02:30 -07004958 'By default, git cl try-results uses the latest patchset from '
4959 'codereview, continuing to use patchset %s.\n' %
tandrii45b2a582016-10-11 03:14:16 -07004960 (patchset, cl.GetPatchset(), patchset))
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004961 try:
tandrii221ab252016-10-06 08:12:04 -07004962 jobs = fetch_try_jobs(auth_config, cl, options.buildbucket_host, patchset)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004963 except BuildbucketResponseException as ex:
vapiera7fbd5a2016-06-16 09:17:49 -07004964 print('Buildbucket error: %s' % ex)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004965 return 1
qyearsley53f48a12016-09-01 10:45:13 -07004966 if options.json:
4967 write_try_results_json(options.json, jobs)
4968 else:
4969 print_try_jobs(options, jobs)
tandrii@chromium.orgb015fac2016-02-26 14:52:01 +00004970 return 0
4971
4972
maruel@chromium.org0633fb42013-08-16 20:06:14 +00004973@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004974def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00004975 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00004976 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004977 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00004978 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004979
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004980 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004981 if args:
4982 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004983 branch = cl.GetBranch()
stip7a3dd352016-09-22 17:32:28 -07004984 RunGit(['branch', '--set-upstream-to', args[0], branch])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004985 cl = Changelist()
vapiera7fbd5a2016-06-16 09:17:49 -07004986 print('Upstream branch set to %s' % (cl.GetUpstreamBranch(),))
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00004987
4988 # Clear configured merge-base, if there is one.
4989 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00004990 else:
vapiera7fbd5a2016-06-16 09:17:49 -07004991 print(cl.GetUpstreamBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00004992 return 0
4993
4994
thestig@chromium.org00858c82013-12-02 23:08:03 +00004995def CMDweb(parser, args):
4996 """Opens the current CL in the web browser."""
4997 _, args = parser.parse_args(args)
4998 if args:
4999 parser.error('Unrecognized args: %s' % ' '.join(args))
5000
5001 issue_url = Changelist().GetIssueURL()
5002 if not issue_url:
vapiera7fbd5a2016-06-16 09:17:49 -07005003 print('ERROR No issue to open', file=sys.stderr)
thestig@chromium.org00858c82013-12-02 23:08:03 +00005004 return 1
5005
5006 webbrowser.open(issue_url)
5007 return 0
5008
5009
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005010def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005011 """Sets the commit bit to trigger the Commit Queue."""
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005012 parser.add_option('-d', '--dry-run', action='store_true',
5013 help='trigger in dry run mode')
5014 parser.add_option('-c', '--clear', action='store_true',
5015 help='stop CQ run, if any')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005016 auth.add_auth_options(parser)
iannuccie53c9352016-08-17 14:40:40 -07005017 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005018 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005019 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005020 auth_config = auth.extract_auth_config_from_options(options)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005021 if args:
5022 parser.error('Unrecognized args: %s' % ' '.join(args))
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005023 if options.dry_run and options.clear:
5024 parser.error('Make up your mind: both --dry-run and --clear not allowed')
5025
iannuccie53c9352016-08-17 14:40:40 -07005026 cl = Changelist(auth_config=auth_config, issue=options.issue,
5027 codereview=options.forced_codereview)
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005028 if options.clear:
tandriid9e5ce52016-07-13 02:32:59 -07005029 state = _CQState.NONE
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005030 elif options.dry_run:
qyearsley1fdfcb62016-10-24 13:22:03 -07005031 # TODO(qyearsley): Use cl.TriggerDryRun.
tandrii@chromium.orgfa330e82016-04-13 17:09:52 +00005032 state = _CQState.DRY_RUN
5033 else:
5034 state = _CQState.COMMIT
5035 if not cl.GetIssue():
5036 parser.error('Must upload the issue first')
tandrii9de9ec62016-07-13 03:01:59 -07005037 cl.SetCQState(state)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00005038 return 0
5039
5040
groby@chromium.org411034a2013-02-26 15:12:01 +00005041def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005042 """Closes the issue."""
iannuccie53c9352016-08-17 14:40:40 -07005043 _add_codereview_issue_select_options(parser)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005044 auth.add_auth_options(parser)
5045 options, args = parser.parse_args(args)
iannuccie53c9352016-08-17 14:40:40 -07005046 _process_codereview_issue_select_options(parser, options)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005047 auth_config = auth.extract_auth_config_from_options(options)
groby@chromium.org411034a2013-02-26 15:12:01 +00005048 if args:
5049 parser.error('Unrecognized args: %s' % ' '.join(args))
iannuccie53c9352016-08-17 14:40:40 -07005050 cl = Changelist(auth_config=auth_config, issue=options.issue,
5051 codereview=options.forced_codereview)
groby@chromium.org411034a2013-02-26 15:12:01 +00005052 # Ensure there actually is an issue to close.
5053 cl.GetDescription()
5054 cl.CloseIssue()
5055 return 0
5056
5057
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005058def CMDdiff(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005059 """Shows differences between local tree and last upload."""
thomasanderson074beb22016-08-29 14:03:20 -07005060 parser.add_option(
5061 '--stat',
5062 action='store_true',
5063 dest='stat',
5064 help='Generate a diffstat')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005065 auth.add_auth_options(parser)
5066 options, args = parser.parse_args(args)
5067 auth_config = auth.extract_auth_config_from_options(options)
5068 if args:
5069 parser.error('Unrecognized args: %s' % ' '.join(args))
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005070
5071 # Uncommitted (staged and unstaged) changes will be destroyed by
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005072 # "git reset --hard" if there are merging conflicts in CMDPatchIssue().
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005073 # Staged changes would be committed along with the patch from last
5074 # upload, hence counted toward the "last upload" side in the final
5075 # diff output, and this is not what we want.
sbc@chromium.org71437c02015-04-09 19:29:40 +00005076 if git_common.is_dirty_git_tree('diff'):
wychen@chromium.org46309bf2015-04-03 21:04:49 +00005077 return 1
5078
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005079 cl = Changelist(auth_config=auth_config)
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005080 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005081 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00005082 if not issue:
5083 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005084 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005085 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005086
5087 # Create a new branch based on the merge-base
5088 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
tandrii@chromium.org534f67a2016-04-07 18:47:05 +00005089 # Clear cached branch in cl object, to avoid overwriting original CL branch
5090 # properties.
5091 cl.ClearBranch()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005092 try:
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005093 rtn = cl.CMDPatchIssue(issue, reject=False, nocommit=False, directory=None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005094 if rtn != 0:
wychen@chromium.orga872e752015-04-28 23:42:18 +00005095 RunGit(['reset', '--hard'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005096 return rtn
5097
wychen@chromium.org06928532015-02-03 02:11:29 +00005098 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005099 # branch containing the latest rietveld patch.
thomasanderson074beb22016-08-29 14:03:20 -07005100 cmd = ['git', 'diff']
5101 if options.stat:
5102 cmd.append('--stat')
5103 cmd.extend([TMP_BRANCH, branch, '--'])
5104 subprocess2.check_call(cmd)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00005105 finally:
5106 RunGit(['checkout', '-q', branch])
5107 RunGit(['branch', '-D', TMP_BRANCH])
5108
5109 return 0
5110
5111
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005112def CMDowners(parser, args):
wychen@chromium.org37b2ec02015-04-03 00:49:15 +00005113 """Interactively find the owners for reviewing."""
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005114 parser.add_option(
5115 '--no-color',
5116 action='store_true',
5117 help='Use this option to disable color output')
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005118 auth.add_auth_options(parser)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005119 options, args = parser.parse_args(args)
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005120 auth_config = auth.extract_auth_config_from_options(options)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005121
5122 author = RunGit(['config', 'user.email']).strip() or None
5123
vadimsh@chromium.orgcf6a5d22015-04-09 22:02:00 +00005124 cl = Changelist(auth_config=auth_config)
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005125
5126 if args:
5127 if len(args) > 1:
5128 parser.error('Unknown args')
5129 base_branch = args[0]
5130 else:
5131 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005132 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005133
5134 change = cl.GetChange(base_branch, None)
5135 return owners_finder.OwnersFinder(
5136 [f.LocalPath() for f in
5137 cl.GetChange(base_branch, None).AffectedFiles()],
5138 change.RepositoryRoot(), author,
dtu944b6052016-07-14 14:48:21 -07005139 fopen=file, os_path=os.path,
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00005140 disable_color=options.no_color).run()
5141
5142
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005143def BuildGitDiffCmd(diff_type, upstream_commit, args):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005144 """Generates a diff command."""
5145 # Generate diff for the current branch's changes.
5146 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix', diff_type,
5147 upstream_commit, '--' ]
5148
5149 if args:
5150 for arg in args:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005151 if os.path.isdir(arg) or os.path.isfile(arg):
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005152 diff_cmd.append(arg)
5153 else:
5154 DieWithError('Argument "%s" is not a file or a directory' % arg)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005155
5156 return diff_cmd
5157
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005158def MatchingFileType(file_name, extensions):
5159 """Returns true if the file name ends with one of the given extensions."""
5160 return bool([ext for ext in extensions if file_name.lower().endswith(ext)])
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005161
enne@chromium.org555cfe42014-01-29 18:21:39 +00005162@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005163def CMDformat(parser, args):
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005164 """Runs auto-formatting tools (clang-format etc.) on the diff."""
zengsterbf470142016-07-07 16:43:00 -07005165 CLANG_EXTS = ['.cc', '.cpp', '.h', '.m', '.mm', '.proto', '.java']
kylechar58edce22016-06-17 06:07:51 -07005166 GN_EXTS = ['.gn', '.gni', '.typemap']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00005167 parser.add_option('--full', action='store_true',
5168 help='Reformat the full content of all touched files')
5169 parser.add_option('--dry-run', action='store_true',
5170 help='Don\'t modify any file on disk.')
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005171 parser.add_option('--python', action='store_true',
5172 help='Format python code with yapf (experimental).')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00005173 parser.add_option('--diff', action='store_true',
5174 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005175 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005176
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005177 # git diff generates paths against the root of the repository. Change
5178 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00005179 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00005180 if rel_base_path:
5181 os.chdir(rel_base_path)
5182
digit@chromium.org29e47272013-05-17 17:01:46 +00005183 # Grab the merge-base commit, i.e. the upstream commit of the current
5184 # branch when it was created or the last time it was rebased. This is
5185 # to cover the case where the user may have called "git fetch origin",
5186 # moving the origin branch to a newer commit, but hasn't rebased yet.
5187 upstream_commit = None
5188 cl = Changelist()
5189 upstream_branch = cl.GetUpstreamBranch()
5190 if upstream_branch:
5191 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
5192 upstream_commit = upstream_commit.strip()
5193
5194 if not upstream_commit:
5195 DieWithError('Could not find base commit for this branch. '
5196 'Are you in detached state?')
5197
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005198 changed_files_cmd = BuildGitDiffCmd('--name-only', upstream_commit, args)
5199 diff_output = RunGit(changed_files_cmd)
5200 diff_files = diff_output.splitlines()
jkarlin@chromium.orgad21b922016-01-28 17:48:42 +00005201 # Filter out files deleted by this CL
5202 diff_files = [x for x in diff_files if os.path.isfile(x)]
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005203
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005204 clang_diff_files = [x for x in diff_files if MatchingFileType(x, CLANG_EXTS)]
5205 python_diff_files = [x for x in diff_files if MatchingFileType(x, ['.py'])]
5206 dart_diff_files = [x for x in diff_files if MatchingFileType(x, ['.dart'])]
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005207 gn_diff_files = [x for x in diff_files if MatchingFileType(x, GN_EXTS)]
digit@chromium.org29e47272013-05-17 17:01:46 +00005208
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00005209 top_dir = os.path.normpath(
5210 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
5211
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005212 # Set to 2 to signal to CheckPatchFormatted() that this patch isn't
5213 # formatted. This is used to block during the presubmit.
5214 return_value = 0
5215
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005216 if clang_diff_files:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005217 # Locate the clang-format binary in the checkout
5218 try:
5219 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
vapierfd77ac72016-06-16 08:33:57 -07005220 except clang_format.NotFoundError as e:
techtonik@gmail.com5573df12016-04-12 18:34:10 +00005221 DieWithError(e)
5222
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005223 if opts.full:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005224 cmd = [clang_format_tool]
5225 if not opts.dry_run and not opts.diff:
5226 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005227 stdout = RunCommand(cmd + clang_diff_files, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005228 if opts.diff:
5229 sys.stdout.write(stdout)
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005230 else:
5231 env = os.environ.copy()
5232 env['PATH'] = str(os.path.dirname(clang_format_tool))
5233 try:
5234 script = clang_format.FindClangFormatScriptInChromiumTree(
5235 'clang-format-diff.py')
vapierfd77ac72016-06-16 08:33:57 -07005236 except clang_format.NotFoundError as e:
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005237 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005238
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005239 cmd = [sys.executable, script, '-p0']
5240 if not opts.dry_run and not opts.diff:
5241 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00005242
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005243 diff_cmd = BuildGitDiffCmd('-U0', upstream_commit, clang_diff_files)
5244 diff_output = RunGit(diff_cmd)
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005245
sammc@chromium.org0b35f5d2016-02-25 22:39:23 +00005246 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
5247 if opts.diff:
5248 sys.stdout.write(stdout)
5249 if opts.dry_run and len(stdout) > 0:
5250 return_value = 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005251
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005252 # Similar code to above, but using yapf on .py files rather than clang-format
5253 # on C/C++ files
5254 if opts.python:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005255 yapf_tool = gclient_utils.FindExecutable('yapf')
5256 if yapf_tool is None:
5257 DieWithError('yapf not found in PATH')
5258
5259 if opts.full:
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005260 if python_diff_files:
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005261 cmd = [yapf_tool]
5262 if not opts.dry_run and not opts.diff:
5263 cmd.append('-i')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005264 stdout = RunCommand(cmd + python_diff_files, cwd=top_dir)
sbc@chromium.org9d0644d2015-06-05 23:16:54 +00005265 if opts.diff:
5266 sys.stdout.write(stdout)
5267 else:
5268 # TODO(sbc): yapf --lines mode still has some issues.
5269 # https://github.com/google/yapf/issues/154
5270 DieWithError('--python currently only works with --full')
5271
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005272 # Dart's formatter does not have the nice property of only operating on
5273 # modified chunks, so hard code full.
5274 if dart_diff_files:
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005275 try:
5276 command = [dart_format.FindDartFmtToolInChromiumTree()]
5277 if not opts.dry_run and not opts.diff:
5278 command.append('-w')
jkarlin@chromium.org6f7fa5e2016-01-20 19:32:21 +00005279 command.extend(dart_diff_files)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005280
ppi@chromium.org6593d932016-03-03 15:41:15 +00005281 stdout = RunCommand(command, cwd=top_dir)
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005282 if opts.dry_run and stdout:
5283 return_value = 2
5284 except dart_format.NotFoundError as e:
vapiera7fbd5a2016-06-16 09:17:49 -07005285 print('Warning: Unable to check Dart code formatting. Dart SDK not '
5286 'found in this checkout. Files in other languages are still '
5287 'formatted.')
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005288
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005289 # Format GN build files. Always run on full build files for canonical form.
5290 if gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005291 cmd = ['gn', 'format' ]
5292 if opts.dry_run or opts.diff:
5293 cmd.append('--dry-run')
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005294 for gn_diff_file in gn_diff_files:
brettw4b8ed592016-08-05 16:19:12 -07005295 gn_ret = subprocess2.call(cmd + [gn_diff_file],
5296 shell=sys.platform == 'win32',
5297 cwd=top_dir)
5298 if opts.dry_run and gn_ret == 2:
5299 return_value = 2 # Not formatted.
5300 elif opts.diff and gn_ret == 2:
5301 # TODO this should compute and print the actual diff.
5302 print("This change has GN build file diff for " + gn_diff_file)
5303 elif gn_ret != 0:
5304 # For non-dry run cases (and non-2 return values for dry-run), a
5305 # nonzero error code indicates a failure, probably because the file
5306 # doesn't parse.
5307 DieWithError("gn format failed on " + gn_diff_file +
5308 "\nTry running 'gn format' on this file manually.")
kylechar@chromium.org8b61f112016-02-05 13:28:58 +00005309
erg@chromium.orge0a7c5d2015-02-23 20:30:08 +00005310 return return_value
agable@chromium.orgfab8f822013-05-06 17:43:09 +00005311
5312
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005313@subcommand.usage('<codereview url or issue id>')
5314def CMDcheckout(parser, args):
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005315 """Checks out a branch associated with a given Rietveld or Gerrit issue."""
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005316 _, args = parser.parse_args(args)
5317
5318 if len(args) != 1:
5319 parser.print_help()
5320 return 1
5321
tandrii@chromium.orgf86c7d32016-04-01 19:27:30 +00005322 issue_arg = ParseIssueNumberArgument(args[0])
tandrii@chromium.orgde6c9a12016-04-11 15:33:53 +00005323 if not issue_arg.valid:
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005324 parser.print_help()
5325 return 1
tandrii@chromium.orgabd27e52016-04-11 15:43:32 +00005326 target_issue = str(issue_arg.issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005327
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005328 def find_issues(issueprefix):
tandrii@chromium.org26c8fd22016-04-11 21:33:21 +00005329 output = RunGit(['config', '--local', '--get-regexp',
5330 r'branch\..*\.%s' % issueprefix],
5331 error_ok=True)
5332 for key, issue in [x.split() for x in output.splitlines()]:
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005333 if issue == target_issue:
5334 yield re.sub(r'branch\.(.*)\.%s' % issueprefix, r'\1', key)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005335
tandrii@chromium.org5df290f2016-04-11 16:12:29 +00005336 branches = []
5337 for cls in _CODEREVIEW_IMPLEMENTATIONS.values():
tandrii5d48c322016-08-18 16:19:37 -07005338 branches.extend(find_issues(cls.IssueConfigKey()))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005339 if len(branches) == 0:
vapiera7fbd5a2016-06-16 09:17:49 -07005340 print('No branch found for issue %s.' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005341 return 1
5342 if len(branches) == 1:
5343 RunGit(['checkout', branches[0]])
5344 else:
vapiera7fbd5a2016-06-16 09:17:49 -07005345 print('Multiple branches match issue %s:' % target_issue)
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005346 for i in range(len(branches)):
vapiera7fbd5a2016-06-16 09:17:49 -07005347 print('%d: %s' % (i, branches[i]))
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005348 which = raw_input('Choose by index: ')
5349 try:
5350 RunGit(['checkout', branches[int(which)]])
5351 except (IndexError, ValueError):
vapiera7fbd5a2016-06-16 09:17:49 -07005352 print('Invalid selection, not checking out any branch.')
scottmg@chromium.org84a80c42015-09-22 20:40:37 +00005353 return 1
5354
5355 return 0
5356
5357
maruel@chromium.org29404b52014-09-08 22:58:00 +00005358def CMDlol(parser, args):
5359 # This command is intentionally undocumented.
vapiera7fbd5a2016-06-16 09:17:49 -07005360 print(zlib.decompress(base64.b64decode(
thakis@chromium.org3421c992014-11-02 02:20:32 +00005361 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
5362 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
5363 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
vapiera7fbd5a2016-06-16 09:17:49 -07005364 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00005365 return 0
5366
5367
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005368class OptionParser(optparse.OptionParser):
5369 """Creates the option parse and add --verbose support."""
5370 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005371 optparse.OptionParser.__init__(
5372 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005373 self.add_option(
5374 '-v', '--verbose', action='count', default=0,
5375 help='Use 2 times for more debugging info')
5376
5377 def parse_args(self, args=None, values=None):
5378 options, args = optparse.OptionParser.parse_args(self, args, values)
5379 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
5380 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
5381 return options, args
5382
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00005383
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005384def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005385 if sys.hexversion < 0x02060000:
vapiera7fbd5a2016-06-16 09:17:49 -07005386 print('\nYour python version %s is unsupported, please upgrade.\n' %
5387 (sys.version.split(' ', 1)[0],), file=sys.stderr)
maruel@chromium.org82798cb2012-02-23 18:16:12 +00005388 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005389
maruel@chromium.orgddd59412011-11-30 14:20:38 +00005390 # Reload settings.
5391 global settings
5392 settings = Settings()
5393
maruel@chromium.org39c0b222013-08-17 16:57:01 +00005394 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005395 dispatcher = subcommand.CommandDispatcher(__name__)
5396 try:
5397 return dispatcher.execute(OptionParser(), argv)
vadimsh@chromium.orgeed4df32015-04-10 21:30:20 +00005398 except auth.AuthenticationError as e:
5399 DieWithError(str(e))
vapierfd77ac72016-06-16 08:33:57 -07005400 except urllib2.HTTPError as e:
maruel@chromium.org0633fb42013-08-16 20:06:14 +00005401 if e.code != 500:
5402 raise
5403 DieWithError(
5404 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
5405 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
sbc@chromium.org013731e2015-02-26 18:28:43 +00005406 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00005407
5408
5409if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00005410 # These affect sys.stdout so do it outside of main() to simplify mocks in
5411 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00005412 fix_encoding.fix_encoding()
iannucci@chromium.org596cd5c2016-04-04 21:34:39 +00005413 setup_color.init()
sbc@chromium.org013731e2015-02-26 18:28:43 +00005414 try:
5415 sys.exit(main(sys.argv[1:]))
5416 except KeyboardInterrupt:
5417 sys.stderr.write('interrupted\n')
5418 sys.exit(1)