blob: 1b375e856ca190fa4c7217cdfb30316f5c37a088 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +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
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
thakis@chromium.org3421c992014-11-02 02:20:32 +000011import base64
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000012import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000013import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import logging
15import optparse
16import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000017import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000019import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import sys
bauerb@chromium.org27386dd2015-02-16 10:45:39 +000021import tempfile
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000023import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000025import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000026import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000027import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028
29try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000030 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000031except ImportError:
32 pass
33
maruel@chromium.org2a74d372011-03-29 19:05:50 +000034
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000035from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000036from third_party import upload
37import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000038import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000039import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000040import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000041import git_common
piman@chromium.org336f9122014-09-04 02:16:55 +000042import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000043import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000045import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000047import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000048import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000049import watchlists
50
maruel@chromium.org0633fb42013-08-16 20:06:14 +000051__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000052
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000053DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000054POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000056GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000057CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058
thestig@chromium.org44202a22014-03-11 19:22:18 +000059# Valid extensions for files we want to lint.
60DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
61DEFAULT_LINT_IGNORE_REGEX = r"$^"
62
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000063# Shortcut since it quickly becomes redundant.
64Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000065
maruel@chromium.orgddd59412011-11-30 14:20:38 +000066# Initialized in main()
67settings = None
68
69
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000071 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072 sys.exit(1)
73
74
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000075def GetNoGitPagerEnv():
76 env = os.environ.copy()
77 # 'cat' is a magical git string that disables pagers on all platforms.
78 env['GIT_PAGER'] = 'cat'
79 return env
80
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000081
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000082def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000083 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000084 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000085 except subprocess2.CalledProcessError as e:
86 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000087 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000089 'Command "%s" failed.\n%s' % (
90 ' '.join(args), error_message or e.stdout or ''))
91 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000092
93
94def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000095 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000096 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000097
98
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000099def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000100 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000101 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000102 if suppress_stderr:
103 stderr = subprocess2.VOID
104 else:
105 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000106 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000107 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000108 stdout=subprocess2.PIPE,
109 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000110 return code, out[0]
111 except ValueError:
112 # When the subprocess fails, it returns None. That triggers a ValueError
113 # when trying to unpack the return value into (out, code).
114 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000115
116
bauerb@chromium.org27386dd2015-02-16 10:45:39 +0000117def RunGitSilent(args):
118 """Returns stdout, suppresses stderr and ingores the return code."""
119 return RunGitWithCode(args, suppress_stderr=True)[1]
120
121
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000122def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000123 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000124 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000125 return (version.startswith(prefix) and
126 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000127
128
maruel@chromium.org90541732011-04-01 17:54:18 +0000129def ask_for_data(prompt):
130 try:
131 return raw_input(prompt)
132 except KeyboardInterrupt:
133 # Hide the exception.
134 sys.exit(1)
135
136
iannucci@chromium.org79540052012-10-19 23:15:26 +0000137def git_set_branch_value(key, value):
138 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000139 if not branch:
140 return
141
142 cmd = ['config']
143 if isinstance(value, int):
144 cmd.append('--int')
145 git_key = 'branch.%s.%s' % (branch, key)
146 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000147
148
149def git_get_branch_default(key, default):
150 branch = Changelist().GetBranch()
151 if branch:
152 git_key = 'branch.%s.%s' % (branch, key)
153 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
154 try:
155 return int(stdout.strip())
156 except ValueError:
157 pass
158 return default
159
160
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000161def add_git_similarity(parser):
162 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000163 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000164 help='Sets the percentage that a pair of files need to match in order to'
165 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000166 parser.add_option(
167 '--find-copies', action='store_true',
168 help='Allows git to look for copies.')
169 parser.add_option(
170 '--no-find-copies', action='store_false', dest='find_copies',
171 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000172
173 old_parser_args = parser.parse_args
174 def Parse(args):
175 options, args = old_parser_args(args)
176
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000177 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000178 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000179 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000180 print('Note: Saving similarity of %d%% in git config.'
181 % options.similarity)
182 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183
iannucci@chromium.org79540052012-10-19 23:15:26 +0000184 options.similarity = max(0, min(options.similarity, 100))
185
186 if options.find_copies is None:
187 options.find_copies = bool(
188 git_get_branch_default('git-find-copies', True))
189 else:
190 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000191
192 print('Using %d%% similarity for rename/copy detection. '
193 'Override with --similarity.' % options.similarity)
194
195 return options, args
196 parser.parse_args = Parse
197
198
ukai@chromium.org259e4682012-10-25 07:36:33 +0000199def is_dirty_git_tree(cmd):
200 # Make sure index is up-to-date before running diff-index.
201 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
202 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
203 if dirty:
204 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
205 print 'Uncommitted files: (git diff-index --name-status HEAD)'
206 print dirty[:4096]
207 if len(dirty) > 4096:
208 print '... (run "git diff-index --name-status HEAD" to see full output).'
209 return True
210 return False
211
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000212
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000213def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
214 """Return the corresponding git ref if |base_url| together with |glob_spec|
215 matches the full |url|.
216
217 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
218 """
219 fetch_suburl, as_ref = glob_spec.split(':')
220 if allow_wildcards:
221 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
222 if glob_match:
223 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
224 # "branches/{472,597,648}/src:refs/remotes/svn/*".
225 branch_re = re.escape(base_url)
226 if glob_match.group(1):
227 branch_re += '/' + re.escape(glob_match.group(1))
228 wildcard = glob_match.group(2)
229 if wildcard == '*':
230 branch_re += '([^/]*)'
231 else:
232 # Escape and replace surrounding braces with parentheses and commas
233 # with pipe symbols.
234 wildcard = re.escape(wildcard)
235 wildcard = re.sub('^\\\\{', '(', wildcard)
236 wildcard = re.sub('\\\\,', '|', wildcard)
237 wildcard = re.sub('\\\\}$', ')', wildcard)
238 branch_re += wildcard
239 if glob_match.group(3):
240 branch_re += re.escape(glob_match.group(3))
241 match = re.match(branch_re, url)
242 if match:
243 return re.sub('\*$', match.group(1), as_ref)
244
245 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
246 if fetch_suburl:
247 full_url = base_url + '/' + fetch_suburl
248 else:
249 full_url = base_url
250 if full_url == url:
251 return as_ref
252 return None
253
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000254
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000256 """Prints statistics about the change to the user."""
257 # --no-ext-diff is broken in some versions of Git, so try to work around
258 # this by overriding the environment (but there is still a problem if the
259 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000260 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000261 if 'GIT_EXTERNAL_DIFF' in env:
262 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000263
264 if find_copies:
265 similarity_options = ['--find-copies-harder', '-l100000',
266 '-C%s' % similarity]
267 else:
268 similarity_options = ['-M%s' % similarity]
269
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000270 try:
271 stdout = sys.stdout.fileno()
272 except AttributeError:
273 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000274 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000275 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000276 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000277 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000278
279
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000280class Settings(object):
281 def __init__(self):
282 self.default_server = None
283 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000284 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285 self.is_git_svn = None
286 self.svn_branch = None
287 self.tree_status_url = None
288 self.viewvc_url = None
289 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000290 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000291 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000292 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000293 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000294 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295
296 def LazyUpdateIfNeeded(self):
297 """Updates the settings from a codereview.settings file, if available."""
298 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000299 # The only value that actually changes the behavior is
300 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000301 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000302 error_ok=True
303 ).strip().lower()
304
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000305 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000306 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000307 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000308 # set updated to True to avoid infinite calling loop
309 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000310 self.updated = True
311 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000312 self.updated = True
313
314 def GetDefaultServerUrl(self, error_ok=False):
315 if not self.default_server:
316 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000317 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000318 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000319 if error_ok:
320 return self.default_server
321 if not self.default_server:
322 error_message = ('Could not find settings file. You must configure '
323 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000324 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000325 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326 return self.default_server
327
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000328 @staticmethod
329 def GetRelativeRoot():
330 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000331
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000332 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000333 if self.root is None:
334 self.root = os.path.abspath(self.GetRelativeRoot())
335 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000336
337 def GetIsGitSvn(self):
338 """Return true if this repo looks like it's using git-svn."""
339 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000340 if self.GetPendingRefPrefix():
341 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
342 self.is_git_svn = False
343 else:
344 # If you have any "svn-remote.*" config keys, we think you're using svn.
345 self.is_git_svn = RunGitWithCode(
346 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000347 return self.is_git_svn
348
349 def GetSVNBranch(self):
350 if self.svn_branch is None:
351 if not self.GetIsGitSvn():
352 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
353
354 # Try to figure out which remote branch we're based on.
355 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000356 # 1) iterate through our branch history and find the svn URL.
357 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000358
359 # regexp matching the git-svn line that contains the URL.
360 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
361
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 # We don't want to go through all of history, so read a line from the
363 # pipe at a time.
364 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000365 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000366 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
367 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000368 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000369 for line in proc.stdout:
370 match = git_svn_re.match(line)
371 if match:
372 url = match.group(1)
373 proc.stdout.close() # Cut pipe.
374 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000375
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000376 if url:
377 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
378 remotes = RunGit(['config', '--get-regexp',
379 r'^svn-remote\..*\.url']).splitlines()
380 for remote in remotes:
381 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000382 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000383 remote = match.group(1)
384 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000385 rewrite_root = RunGit(
386 ['config', 'svn-remote.%s.rewriteRoot' % remote],
387 error_ok=True).strip()
388 if rewrite_root:
389 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000390 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000391 ['config', 'svn-remote.%s.fetch' % remote],
392 error_ok=True).strip()
393 if fetch_spec:
394 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
395 if self.svn_branch:
396 break
397 branch_spec = RunGit(
398 ['config', 'svn-remote.%s.branches' % remote],
399 error_ok=True).strip()
400 if branch_spec:
401 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
402 if self.svn_branch:
403 break
404 tag_spec = RunGit(
405 ['config', 'svn-remote.%s.tags' % remote],
406 error_ok=True).strip()
407 if tag_spec:
408 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
409 if self.svn_branch:
410 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000411
412 if not self.svn_branch:
413 DieWithError('Can\'t guess svn branch -- try specifying it on the '
414 'command line')
415
416 return self.svn_branch
417
418 def GetTreeStatusUrl(self, error_ok=False):
419 if not self.tree_status_url:
420 error_message = ('You must configure your tree status URL by running '
421 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000422 self.tree_status_url = self._GetRietveldConfig(
423 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000424 return self.tree_status_url
425
426 def GetViewVCUrl(self):
427 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000428 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000429 return self.viewvc_url
430
rmistry@google.com90752582014-01-14 21:04:50 +0000431 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000432 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000433
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000434 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000435 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000436
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000437 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000438 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000439
ukai@chromium.orge8077812012-02-03 03:41:46 +0000440 def GetIsGerrit(self):
441 """Return true if this repo is assosiated with gerrit code review system."""
442 if self.is_gerrit is None:
443 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
444 return self.is_gerrit
445
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000446 def GetGitEditor(self):
447 """Return the editor specified in the git config, or None if none is."""
448 if self.git_editor is None:
449 self.git_editor = self._GetConfig('core.editor', error_ok=True)
450 return self.git_editor or None
451
thestig@chromium.org44202a22014-03-11 19:22:18 +0000452 def GetLintRegex(self):
453 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
454 DEFAULT_LINT_REGEX)
455
456 def GetLintIgnoreRegex(self):
457 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
458 DEFAULT_LINT_IGNORE_REGEX)
459
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000460 def GetProject(self):
461 if not self.project:
462 self.project = self._GetRietveldConfig('project', error_ok=True)
463 return self.project
464
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000465 def GetForceHttpsCommitUrl(self):
466 if not self.force_https_commit_url:
467 self.force_https_commit_url = self._GetRietveldConfig(
468 'force-https-commit-url', error_ok=True)
469 return self.force_https_commit_url
470
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000471 def GetPendingRefPrefix(self):
472 if not self.pending_ref_prefix:
473 self.pending_ref_prefix = self._GetRietveldConfig(
474 'pending-ref-prefix', error_ok=True)
475 return self.pending_ref_prefix
476
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000477 def _GetRietveldConfig(self, param, **kwargs):
478 return self._GetConfig('rietveld.' + param, **kwargs)
479
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000480 def _GetConfig(self, param, **kwargs):
481 self.LazyUpdateIfNeeded()
482 return RunGit(['config', param], **kwargs).strip()
483
484
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000485def ShortBranchName(branch):
486 """Convert a name like 'refs/heads/foo' to just 'foo'."""
487 return branch.replace('refs/heads/', '')
488
489
490class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000491 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000492 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000493 global settings
494 if not settings:
495 # Happens when git_cl.py is used as a utility library.
496 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000497 settings.GetDefaultServerUrl()
498 self.branchref = branchref
499 if self.branchref:
500 self.branch = ShortBranchName(self.branchref)
501 else:
502 self.branch = None
503 self.rietveld_server = None
504 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000505 self.lookedup_issue = False
506 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000507 self.has_description = False
508 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000509 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000510 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000511 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000512 self.cc = None
513 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000514 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000515 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000516
517 def GetCCList(self):
518 """Return the users cc'd on this CL.
519
520 Return is a string suitable for passing to gcl with the --cc flag.
521 """
522 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000523 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000524 more_cc = ','.join(self.watchers)
525 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
526 return self.cc
527
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000528 def GetCCListWithoutDefault(self):
529 """Return the users cc'd on this CL excluding default ones."""
530 if self.cc is None:
531 self.cc = ','.join(self.watchers)
532 return self.cc
533
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000534 def SetWatchers(self, watchers):
535 """Set the list of email addresses that should be cc'd based on the changed
536 files in this CL.
537 """
538 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000539
540 def GetBranch(self):
541 """Returns the short branch name, e.g. 'master'."""
542 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000543 branchref = RunGit(['symbolic-ref', 'HEAD'],
544 stderr=subprocess2.VOID, error_ok=True).strip()
545 if not branchref:
546 return None
547 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000548 self.branch = ShortBranchName(self.branchref)
549 return self.branch
550
551 def GetBranchRef(self):
552 """Returns the full branch name, e.g. 'refs/heads/master'."""
553 self.GetBranch() # Poke the lazy loader.
554 return self.branchref
555
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000556 @staticmethod
557 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000558 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000559 e.g. 'origin', 'refs/heads/master'
560 """
561 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000562 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
563 error_ok=True).strip()
564 if upstream_branch:
565 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
566 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000567 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
568 error_ok=True).strip()
569 if upstream_branch:
570 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000571 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000572 # Fall back on trying a git-svn upstream branch.
573 if settings.GetIsGitSvn():
574 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000576 # Else, try to guess the origin remote.
577 remote_branches = RunGit(['branch', '-r']).split()
578 if 'origin/master' in remote_branches:
579 # Fall back on origin/master if it exits.
580 remote = 'origin'
581 upstream_branch = 'refs/heads/master'
582 elif 'origin/trunk' in remote_branches:
583 # Fall back on origin/trunk if it exists. Generally a shared
584 # git-svn clone
585 remote = 'origin'
586 upstream_branch = 'refs/heads/trunk'
587 else:
588 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000589Either pass complete "git diff"-style arguments, like
590 git cl upload origin/master
591or verify this branch is set up to track another (via the --track argument to
592"git checkout -b ...").""")
593
594 return remote, upstream_branch
595
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000596 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000597 return git_common.get_or_create_merge_base(self.GetBranch(),
598 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000599
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600 def GetUpstreamBranch(self):
601 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000602 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000603 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000604 upstream_branch = upstream_branch.replace('refs/heads/',
605 'refs/remotes/%s/' % remote)
606 upstream_branch = upstream_branch.replace('refs/branch-heads/',
607 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000608 self.upstream_branch = upstream_branch
609 return self.upstream_branch
610
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000611 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000612 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000613 remote, branch = None, self.GetBranch()
614 seen_branches = set()
615 while branch not in seen_branches:
616 seen_branches.add(branch)
617 remote, branch = self.FetchUpstreamTuple(branch)
618 branch = ShortBranchName(branch)
619 if remote != '.' or branch.startswith('refs/remotes'):
620 break
621 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000622 remotes = RunGit(['remote'], error_ok=True).split()
623 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000624 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000625 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000626 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000627 logging.warning('Could not determine which remote this change is '
628 'associated with, so defaulting to "%s". This may '
629 'not be what you want. You may prevent this message '
630 'by running "git svn info" as documented here: %s',
631 self._remote,
632 GIT_INSTRUCTIONS_URL)
633 else:
634 logging.warn('Could not determine which remote this change is '
635 'associated with. You may prevent this message by '
636 'running "git svn info" as documented here: %s',
637 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000638 branch = 'HEAD'
639 if branch.startswith('refs/remotes'):
640 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000641 elif branch.startswith('refs/branch-heads/'):
642 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000643 else:
644 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000645 return self._remote
646
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000647 def GitSanityChecks(self, upstream_git_obj):
648 """Checks git repo status and ensures diff is from local commits."""
649
sbc@chromium.org79706062015-01-14 21:18:12 +0000650 if upstream_git_obj is None:
651 if self.GetBranch() is None:
652 print >> sys.stderr, (
653 'ERROR: unable to dertermine current branch (detached HEAD?)')
654 else:
655 print >> sys.stderr, (
656 'ERROR: no upstream branch')
657 return False
658
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000659 # Verify the commit we're diffing against is in our current branch.
660 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
661 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
662 if upstream_sha != common_ancestor:
663 print >> sys.stderr, (
664 'ERROR: %s is not in the current branch. You may need to rebase '
665 'your tracking branch' % upstream_sha)
666 return False
667
668 # List the commits inside the diff, and verify they are all local.
669 commits_in_diff = RunGit(
670 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
671 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
672 remote_branch = remote_branch.strip()
673 if code != 0:
674 _, remote_branch = self.GetRemoteBranch()
675
676 commits_in_remote = RunGit(
677 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
678
679 common_commits = set(commits_in_diff) & set(commits_in_remote)
680 if common_commits:
681 print >> sys.stderr, (
682 'ERROR: Your diff contains %d commits already in %s.\n'
683 'Run "git log --oneline %s..HEAD" to get a list of commits in '
684 'the diff. If you are using a custom git flow, you can override'
685 ' the reference used for this check with "git config '
686 'gitcl.remotebranch <git-ref>".' % (
687 len(common_commits), remote_branch, upstream_git_obj))
688 return False
689 return True
690
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000691 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000692 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000693
694 Returns None if it is not set.
695 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000696 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
697 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000698
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000699 def GetGitSvnRemoteUrl(self):
700 """Return the configured git-svn remote URL parsed from git svn info.
701
702 Returns None if it is not set.
703 """
704 # URL is dependent on the current directory.
705 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
706 if data:
707 keys = dict(line.split(': ', 1) for line in data.splitlines()
708 if ': ' in line)
709 return keys.get('URL', None)
710 return None
711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712 def GetRemoteUrl(self):
713 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
714
715 Returns None if there is no remote.
716 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000717 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000718 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
719
720 # If URL is pointing to a local directory, it is probably a git cache.
721 if os.path.isdir(url):
722 url = RunGit(['config', 'remote.%s.url' % remote],
723 error_ok=True,
724 cwd=url).strip()
725 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726
727 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000728 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000729 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000731 self.issue = int(issue) or None if issue else None
732 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733 return self.issue
734
735 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000736 if not self.rietveld_server:
737 # If we're on a branch then get the server potentially associated
738 # with that branch.
739 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000740 rietveld_server_config = self._RietveldServer()
741 if rietveld_server_config:
742 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
743 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000744 if not self.rietveld_server:
745 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 return self.rietveld_server
747
748 def GetIssueURL(self):
749 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000750 if not self.GetIssue():
751 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
753
754 def GetDescription(self, pretty=False):
755 if not self.has_description:
756 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000757 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000758 try:
759 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000760 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000761 if e.code == 404:
762 DieWithError(
763 ('\nWhile fetching the description for issue %d, received a '
764 '404 (not found)\n'
765 'error. It is likely that you deleted this '
766 'issue on the server. If this is the\n'
767 'case, please run\n\n'
768 ' git cl issue 0\n\n'
769 'to clear the association with the deleted issue. Then run '
770 'this command again.') % issue)
771 else:
772 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000773 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000774 except urllib2.URLError as e:
775 print >> sys.stderr, (
776 'Warning: Failed to retrieve CL description due to network '
777 'failure.')
778 self.description = ''
779
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000780 self.has_description = True
781 if pretty:
782 wrapper = textwrap.TextWrapper()
783 wrapper.initial_indent = wrapper.subsequent_indent = ' '
784 return wrapper.fill(self.description)
785 return self.description
786
787 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000788 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000789 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 patchset = RunGit(['config', self._PatchsetSetting()],
791 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000792 self.patchset = int(patchset) or None if patchset else None
793 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 return self.patchset
795
796 def SetPatchset(self, patchset):
797 """Set this branch's patchset. If patchset=0, clears the patchset."""
798 if patchset:
799 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000800 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 else:
802 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000803 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000804 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000806 def GetMostRecentPatchset(self):
807 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000808
809 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000810 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000811 '/download/issue%s_%s.diff' % (issue, patchset))
812
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000813 def GetIssueProperties(self):
814 if self._props is None:
815 issue = self.GetIssue()
816 if not issue:
817 self._props = {}
818 else:
819 self._props = self.RpcServer().get_issue_properties(issue, True)
820 return self._props
821
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000822 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000823 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000824
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000825 def AddComment(self, message):
826 return self.RpcServer().add_comment(self.GetIssue(), message)
827
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828 def SetIssue(self, issue):
829 """Set this branch's issue. If issue=0, clears the issue."""
830 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000831 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832 RunGit(['config', self._IssueSetting(), str(issue)])
833 if self.rietveld_server:
834 RunGit(['config', self._RietveldServer(), self.rietveld_server])
835 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000836 current_issue = self.GetIssue()
837 if current_issue:
838 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000839 self.issue = None
840 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000842 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000843 if not self.GitSanityChecks(upstream_branch):
844 DieWithError('\nGit sanity check failure')
845
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000846 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000847 if not root:
848 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000849 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000850
851 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000852 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000853 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000854 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000855 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000856 except subprocess2.CalledProcessError:
857 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000858 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000859 'This branch probably doesn\'t exist anymore. To reset the\n'
860 'tracking branch, please run\n'
861 ' git branch --set-upstream %s trunk\n'
862 'replacing trunk with origin/master or the relevant branch') %
863 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000864
maruel@chromium.org52424302012-08-29 15:14:30 +0000865 issue = self.GetIssue()
866 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000867 if issue:
868 description = self.GetDescription()
869 else:
870 # If the change was never uploaded, use the log messages of all commits
871 # up to the branch point, as git cl upload will prefill the description
872 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000873 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
874 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000875
876 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000877 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000878 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000879 name,
880 description,
881 absroot,
882 files,
883 issue,
884 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000885 author,
886 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000887
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +0000888 def GetStatus(self):
889 """Apply a rough heuristic to give a simple summary of an issue's review
890 or CQ status, assuming adherence to a common workflow.
891
892 Returns None if no issue for this branch, or one of the following keywords:
893 * 'error' - error from review tool (including deleted issues)
894 * 'unsent' - not sent for review
895 * 'waiting' - waiting for review
896 * 'reply' - waiting for owner to reply to review
897 * 'lgtm' - LGTM from at least one approved reviewer
898 * 'commit' - in the commit queue
899 * 'closed' - closed
900 """
901 if not self.GetIssue():
902 return None
903
904 try:
905 props = self.GetIssueProperties()
906 except urllib2.HTTPError:
907 return 'error'
908
909 if props.get('closed'):
910 # Issue is closed.
911 return 'closed'
912 if props.get('commit'):
913 # Issue is in the commit queue.
914 return 'commit'
915
916 try:
917 reviewers = self.GetApprovingReviewers()
918 except urllib2.HTTPError:
919 return 'error'
920
921 if reviewers:
922 # Was LGTM'ed.
923 return 'lgtm'
924
925 messages = props.get('messages') or []
926
927 if not messages:
928 # No message was sent.
929 return 'unsent'
930 if messages[-1]['sender'] != props.get('owner_email'):
931 # Non-LGTM reply from non-owner
932 return 'reply'
933 return 'waiting'
934
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000935 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000936 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000937
938 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000939 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000940 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000941 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000942 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000943 except presubmit_support.PresubmitFailure, e:
944 DieWithError(
945 ('%s\nMaybe your depot_tools is out of date?\n'
946 'If all fails, contact maruel@') % e)
947
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000948 def UpdateDescription(self, description):
949 self.description = description
950 return self.RpcServer().update_description(
951 self.GetIssue(), self.description)
952
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000953 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000954 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000955 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000957 def SetFlag(self, flag, value):
958 """Patchset must match."""
959 if not self.GetPatchset():
960 DieWithError('The patchset needs to match. Send another patchset.')
961 try:
962 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000963 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000964 except urllib2.HTTPError, e:
965 if e.code == 404:
966 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
967 if e.code == 403:
968 DieWithError(
969 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
970 'match?') % (self.GetIssue(), self.GetPatchset()))
971 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000973 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974 """Returns an upload.RpcServer() to access this review's rietveld instance.
975 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000976 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000977 self._rpc_server = rietveld.CachingRietveld(
978 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000979 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980
981 def _IssueSetting(self):
982 """Return the git setting that stores this change's issue."""
983 return 'branch.%s.rietveldissue' % self.GetBranch()
984
985 def _PatchsetSetting(self):
986 """Return the git setting that stores this change's most recent patchset."""
987 return 'branch.%s.rietveldpatchset' % self.GetBranch()
988
989 def _RietveldServer(self):
990 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000991 branch = self.GetBranch()
992 if branch:
993 return 'branch.%s.rietveldserver' % branch
994 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995
996
997def GetCodereviewSettingsInteractively():
998 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000999 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001000 server = settings.GetDefaultServerUrl(error_ok=True)
1001 prompt = 'Rietveld server (host[:port])'
1002 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +00001003 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004 if not server and not newserver:
1005 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001006 if newserver:
1007 newserver = gclient_utils.UpgradeToHttps(newserver)
1008 if newserver != server:
1009 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001011 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001012 prompt = caption
1013 if initial:
1014 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001015 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016 if new_val == 'x':
1017 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001018 elif new_val:
1019 if is_url:
1020 new_val = gclient_utils.UpgradeToHttps(new_val)
1021 if new_val != initial:
1022 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001024 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001025 SetProperty(settings.GetDefaultPrivateFlag(),
1026 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001028 'tree-status-url', False)
1029 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001030 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031
1032 # TODO: configure a default branch to diff against, rather than this
1033 # svn-based hackery.
1034
1035
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001036class ChangeDescription(object):
1037 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001038 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001039 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001040
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001041 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001042 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001043
agable@chromium.org42c20792013-09-12 17:34:49 +00001044 @property # www.logilab.org/ticket/89786
1045 def description(self): # pylint: disable=E0202
1046 return '\n'.join(self._description_lines)
1047
1048 def set_description(self, desc):
1049 if isinstance(desc, basestring):
1050 lines = desc.splitlines()
1051 else:
1052 lines = [line.rstrip() for line in desc]
1053 while lines and not lines[0]:
1054 lines.pop(0)
1055 while lines and not lines[-1]:
1056 lines.pop(-1)
1057 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001058
piman@chromium.org336f9122014-09-04 02:16:55 +00001059 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001060 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001061 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001062 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001063 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001064 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001065
agable@chromium.org42c20792013-09-12 17:34:49 +00001066 # Get the set of R= and TBR= lines and remove them from the desciption.
1067 regexp = re.compile(self.R_LINE)
1068 matches = [regexp.match(line) for line in self._description_lines]
1069 new_desc = [l for i, l in enumerate(self._description_lines)
1070 if not matches[i]]
1071 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001072
agable@chromium.org42c20792013-09-12 17:34:49 +00001073 # Construct new unified R= and TBR= lines.
1074 r_names = []
1075 tbr_names = []
1076 for match in matches:
1077 if not match:
1078 continue
1079 people = cleanup_list([match.group(2).strip()])
1080 if match.group(1) == 'TBR':
1081 tbr_names.extend(people)
1082 else:
1083 r_names.extend(people)
1084 for name in r_names:
1085 if name not in reviewers:
1086 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001087 if add_owners_tbr:
1088 owners_db = owners.Database(change.RepositoryRoot(),
1089 fopen=file, os_path=os.path, glob=glob.glob)
1090 all_reviewers = set(tbr_names + reviewers)
1091 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1092 all_reviewers)
1093 tbr_names.extend(owners_db.reviewers_for(missing_files,
1094 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001095 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1096 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1097
1098 # Put the new lines in the description where the old first R= line was.
1099 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1100 if 0 <= line_loc < len(self._description_lines):
1101 if new_tbr_line:
1102 self._description_lines.insert(line_loc, new_tbr_line)
1103 if new_r_line:
1104 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001105 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001106 if new_r_line:
1107 self.append_footer(new_r_line)
1108 if new_tbr_line:
1109 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001110
1111 def prompt(self):
1112 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001113 self.set_description([
1114 '# Enter a description of the change.',
1115 '# This will be displayed on the codereview site.',
1116 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001117 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001118 '--------------------',
1119 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001120
agable@chromium.org42c20792013-09-12 17:34:49 +00001121 regexp = re.compile(self.BUG_LINE)
1122 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001123 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001124 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001125 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001126 if not content:
1127 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001128 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001129
1130 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001131 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1132 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001133 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001134 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001135
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001136 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001137 if self._description_lines:
1138 # Add an empty line if either the last line or the new line isn't a tag.
1139 last_line = self._description_lines[-1]
1140 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1141 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1142 self._description_lines.append('')
1143 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001144
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001145 def get_reviewers(self):
1146 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001147 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1148 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001149 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001150
1151
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001152def get_approving_reviewers(props):
1153 """Retrieves the reviewers that approved a CL from the issue properties with
1154 messages.
1155
1156 Note that the list may contain reviewers that are not committer, thus are not
1157 considered by the CQ.
1158 """
1159 return sorted(
1160 set(
1161 message['sender']
1162 for message in props['messages']
1163 if message['approval'] and message['sender'] in props['reviewers']
1164 )
1165 )
1166
1167
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168def FindCodereviewSettingsFile(filename='codereview.settings'):
1169 """Finds the given file starting in the cwd and going up.
1170
1171 Only looks up to the top of the repository unless an
1172 'inherit-review-settings-ok' file exists in the root of the repository.
1173 """
1174 inherit_ok_file = 'inherit-review-settings-ok'
1175 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001176 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1178 root = '/'
1179 while True:
1180 if filename in os.listdir(cwd):
1181 if os.path.isfile(os.path.join(cwd, filename)):
1182 return open(os.path.join(cwd, filename))
1183 if cwd == root:
1184 break
1185 cwd = os.path.dirname(cwd)
1186
1187
1188def LoadCodereviewSettingsFromFile(fileobj):
1189 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001190 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001191
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 def SetProperty(name, setting, unset_error_ok=False):
1193 fullname = 'rietveld.' + name
1194 if setting in keyvals:
1195 RunGit(['config', fullname, keyvals[setting]])
1196 else:
1197 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1198
1199 SetProperty('server', 'CODE_REVIEW_SERVER')
1200 # Only server setting is required. Other settings can be absent.
1201 # In that case, we ignore errors raised during option deletion attempt.
1202 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001203 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001204 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1205 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001206 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001207 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001208 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1209 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001210 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001211 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001212 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001213
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001214 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001215 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001216
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001217 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1218 #should be of the form
1219 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1220 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1221 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1222 keyvals['ORIGIN_URL_CONFIG']])
1223
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001225def urlretrieve(source, destination):
1226 """urllib is broken for SSL connections via a proxy therefore we
1227 can't use urllib.urlretrieve()."""
1228 with open(destination, 'w') as f:
1229 f.write(urllib2.urlopen(source).read())
1230
1231
ukai@chromium.org712d6102013-11-27 00:52:58 +00001232def hasSheBang(fname):
1233 """Checks fname is a #! script."""
1234 with open(fname) as f:
1235 return f.read(2).startswith('#!')
1236
1237
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001238def DownloadHooks(force):
1239 """downloads hooks
1240
1241 Args:
1242 force: True to update hooks. False to install hooks if not present.
1243 """
1244 if not settings.GetIsGerrit():
1245 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001246 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001247 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1248 if not os.access(dst, os.X_OK):
1249 if os.path.exists(dst):
1250 if not force:
1251 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001252 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001253 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001254 if not hasSheBang(dst):
1255 DieWithError('Not a script: %s\n'
1256 'You need to download from\n%s\n'
1257 'into .git/hooks/commit-msg and '
1258 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001259 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1260 except Exception:
1261 if os.path.exists(dst):
1262 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001263 DieWithError('\nFailed to download hooks.\n'
1264 'You need to download from\n%s\n'
1265 'into .git/hooks/commit-msg and '
1266 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001267
1268
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001269@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001271 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001273 parser.add_option('--activate-update', action='store_true',
1274 help='activate auto-updating [rietveld] section in '
1275 '.git/config')
1276 parser.add_option('--deactivate-update', action='store_true',
1277 help='deactivate auto-updating [rietveld] section in '
1278 '.git/config')
1279 options, args = parser.parse_args(args)
1280
1281 if options.deactivate_update:
1282 RunGit(['config', 'rietveld.autoupdate', 'false'])
1283 return
1284
1285 if options.activate_update:
1286 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1287 return
1288
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001289 if len(args) == 0:
1290 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001291 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292 return 0
1293
1294 url = args[0]
1295 if not url.endswith('codereview.settings'):
1296 url = os.path.join(url, 'codereview.settings')
1297
1298 # Load code review settings and download hooks (if available).
1299 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001300 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301 return 0
1302
1303
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001304def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001305 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001306 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1307 branch = ShortBranchName(branchref)
1308 _, args = parser.parse_args(args)
1309 if not args:
1310 print("Current base-url:")
1311 return RunGit(['config', 'branch.%s.base-url' % branch],
1312 error_ok=False).strip()
1313 else:
1314 print("Setting base-url to %s" % args[0])
1315 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1316 error_ok=False).strip()
1317
1318
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001319def color_for_status(status):
1320 """Maps a Changelist status to color, for CMDstatus and other tools."""
1321 return {
1322 'unsent': Fore.RED,
1323 'waiting': Fore.BLUE,
1324 'reply': Fore.YELLOW,
1325 'lgtm': Fore.GREEN,
1326 'commit': Fore.MAGENTA,
1327 'closed': Fore.CYAN,
1328 'error': Fore.WHITE,
1329 }.get(status, Fore.WHITE)
1330
1331
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001333 """Show status of changelists.
1334
1335 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001336 - Red not sent for review or broken
1337 - Blue waiting for review
1338 - Yellow waiting for you to reply to review
1339 - Green LGTM'ed
1340 - Magenta in the commit queue
1341 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001342
1343 Also see 'git cl comments'.
1344 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001345 parser.add_option('--field',
1346 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001347 parser.add_option('-f', '--fast', action='store_true',
1348 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001350 if args:
1351 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001354 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001355 if options.field.startswith('desc'):
1356 print cl.GetDescription()
1357 elif options.field == 'id':
1358 issueid = cl.GetIssue()
1359 if issueid:
1360 print issueid
1361 elif options.field == 'patch':
1362 patchset = cl.GetPatchset()
1363 if patchset:
1364 print patchset
1365 elif options.field == 'url':
1366 url = cl.GetIssueURL()
1367 if url:
1368 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001369 return 0
1370
1371 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1372 if not branches:
1373 print('No local branch found.')
1374 return 0
1375
1376 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001377 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001378 alignment = max(5, max(len(b) for b in branches))
1379 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001380 # Adhoc thread pool to request data concurrently.
1381 output = Queue.Queue()
1382
1383 # Silence upload.py otherwise it becomes unweldly.
1384 upload.verbosity = 0
1385
1386 if not options.fast:
1387 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001388 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001389 c = Changelist(branchref=b)
1390 i = c.GetIssueURL()
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001391 status = c.GetStatus()
1392 color = color_for_status(status)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001393
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001394 if i and (not status or status == 'error'):
1395 # The issue probably doesn't exist anymore.
1396 i += ' (broken)'
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001397
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001398 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001399
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001400 # Process one branch synchronously to work through authentication, then
1401 # spawn threads to process all the other branches in parallel.
1402 if branches:
1403 fetch(branches[0])
1404 threads = [
1405 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001406 for t in threads:
1407 t.daemon = True
1408 t.start()
1409 else:
1410 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1411 for b in branches:
1412 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001413 url = c.GetIssueURL()
1414 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001415
1416 tmp = {}
1417 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001418 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001419 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001420 b, i, color = output.get()
1421 tmp[b] = (i, color)
1422 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001423 reset = Fore.RESET
1424 if not sys.stdout.isatty():
1425 color = ''
1426 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001427 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001428 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001429
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001430 cl = Changelist()
1431 print
1432 print 'Current branch:',
1433 if not cl.GetIssue():
1434 print 'no issue assigned.'
1435 return 0
1436 print cl.GetBranch()
1437 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001438 if not options.fast:
1439 print 'Issue description:'
1440 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 return 0
1442
1443
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001444def colorize_CMDstatus_doc():
1445 """To be called once in main() to add colors to git cl status help."""
1446 colors = [i for i in dir(Fore) if i[0].isupper()]
1447
1448 def colorize_line(line):
1449 for color in colors:
1450 if color in line.upper():
1451 # Extract whitespaces first and the leading '-'.
1452 indent = len(line) - len(line.lstrip(' ')) + 1
1453 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1454 return line
1455
1456 lines = CMDstatus.__doc__.splitlines()
1457 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1458
1459
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001460@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001462 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463
1464 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001465 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001466 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467
1468 cl = Changelist()
1469 if len(args) > 0:
1470 try:
1471 issue = int(args[0])
1472 except ValueError:
1473 DieWithError('Pass a number to set the issue or none to list it.\n'
1474 'Maybe you want to run git cl status?')
1475 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001476 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477 return 0
1478
1479
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001480def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001481 """Shows or posts review comments for any changelist."""
1482 parser.add_option('-a', '--add-comment', dest='comment',
1483 help='comment to add to an issue')
1484 parser.add_option('-i', dest='issue',
1485 help="review issue id (defaults to current issue)")
1486 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001487
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001488 issue = None
1489 if options.issue:
1490 try:
1491 issue = int(options.issue)
1492 except ValueError:
1493 DieWithError('A review issue id is expected to be a number')
1494
1495 cl = Changelist(issue=issue)
1496
1497 if options.comment:
1498 cl.AddComment(options.comment)
1499 return 0
1500
1501 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001502 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001503 if message['disapproval']:
1504 color = Fore.RED
1505 elif message['approval']:
1506 color = Fore.GREEN
1507 elif message['sender'] == data['owner_email']:
1508 color = Fore.MAGENTA
1509 else:
1510 color = Fore.BLUE
1511 print '\n%s%s %s%s' % (
1512 color, message['date'].split('.', 1)[0], message['sender'],
1513 Fore.RESET)
1514 if message['text'].strip():
1515 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001516 return 0
1517
1518
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001519def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001520 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001521 cl = Changelist()
1522 if not cl.GetIssue():
1523 DieWithError('This branch has no associated changelist.')
1524 description = ChangeDescription(cl.GetDescription())
1525 description.prompt()
1526 cl.UpdateDescription(description.description)
1527 return 0
1528
1529
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001530def CreateDescriptionFromLog(args):
1531 """Pulls out the commit log to use as a base for the CL description."""
1532 log_args = []
1533 if len(args) == 1 and not args[0].endswith('.'):
1534 log_args = [args[0] + '..']
1535 elif len(args) == 1 and args[0].endswith('...'):
1536 log_args = [args[0][:-1]]
1537 elif len(args) == 2:
1538 log_args = [args[0] + '..' + args[1]]
1539 else:
1540 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001541 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542
1543
thestig@chromium.org44202a22014-03-11 19:22:18 +00001544def CMDlint(parser, args):
1545 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001546 parser.add_option('--filter', action='append', metavar='-x,+y',
1547 help='Comma-separated list of cpplint\'s category-filters')
1548 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001549
1550 # Access to a protected member _XX of a client class
1551 # pylint: disable=W0212
1552 try:
1553 import cpplint
1554 import cpplint_chromium
1555 except ImportError:
1556 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1557 return 1
1558
1559 # Change the current working directory before calling lint so that it
1560 # shows the correct base.
1561 previous_cwd = os.getcwd()
1562 os.chdir(settings.GetRoot())
1563 try:
1564 cl = Changelist()
1565 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1566 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001567 if not files:
1568 print "Cannot lint an empty CL"
1569 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001570
1571 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001572 command = args + files
1573 if options.filter:
1574 command = ['--filter=' + ','.join(options.filter)] + command
1575 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001576
1577 white_regex = re.compile(settings.GetLintRegex())
1578 black_regex = re.compile(settings.GetLintIgnoreRegex())
1579 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1580 for filename in filenames:
1581 if white_regex.match(filename):
1582 if black_regex.match(filename):
1583 print "Ignoring file %s" % filename
1584 else:
1585 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1586 extra_check_functions)
1587 else:
1588 print "Skipping file %s" % filename
1589 finally:
1590 os.chdir(previous_cwd)
1591 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1592 if cpplint._cpplint_state.error_count != 0:
1593 return 1
1594 return 0
1595
1596
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001598 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001599 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001600 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001601 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001602 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001603 (options, args) = parser.parse_args(args)
1604
ukai@chromium.org259e4682012-10-25 07:36:33 +00001605 if not options.force and is_dirty_git_tree('presubmit'):
1606 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607 return 1
1608
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001609 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001610 if args:
1611 base_branch = args[0]
1612 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001613 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001614 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001615
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001616 cl.RunHook(
1617 committing=not options.upload,
1618 may_prompt=False,
1619 verbose=options.verbose,
1620 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001621 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001622
1623
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001624def AddChangeIdToCommitMessage(options, args):
1625 """Re-commits using the current message, assumes the commit hook is in
1626 place.
1627 """
1628 log_desc = options.message or CreateDescriptionFromLog(args)
1629 git_command = ['commit', '--amend', '-m', log_desc]
1630 RunGit(git_command)
1631 new_log_desc = CreateDescriptionFromLog(args)
1632 if CHANGE_ID in new_log_desc:
1633 print 'git-cl: Added Change-Id to commit message.'
1634 else:
1635 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1636
1637
piman@chromium.org336f9122014-09-04 02:16:55 +00001638def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001639 """upload the current branch to gerrit."""
1640 # We assume the remote called "origin" is the one we want.
1641 # It is probably not worthwhile to support different workflows.
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001642 gerrit_remote = 'origin'
ukai@chromium.orge8077812012-02-03 03:41:46 +00001643 branch = 'master'
1644 if options.target_branch:
1645 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001646
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001647 change_desc = ChangeDescription(
1648 options.message or CreateDescriptionFromLog(args))
1649 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001650 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651 return 1
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001652
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001653 if options.squash:
1654 # Try to get the message from a previous upload.
1655 shadow_branch = 'refs/heads/git_cl_uploads/' + cl.GetBranch()
1656 message = RunGitSilent(['show', '--format=%s\n\n%b', '-s', shadow_branch])
1657 if not message:
1658 if not options.force:
1659 change_desc.prompt()
1660
1661 if CHANGE_ID not in change_desc.description:
1662 # Run the commit-msg hook without modifying the head commit by writing
1663 # the commit message to a temporary file and running the hook over it,
1664 # then reading the file back in.
1665 commit_msg_hook = os.path.join(settings.GetRoot(), '.git', 'hooks',
1666 'commit-msg')
1667 file_handle, msg_file = tempfile.mkstemp(text=True,
1668 prefix='commit_msg')
1669 try:
1670 try:
1671 with os.fdopen(file_handle, 'w') as fileobj:
1672 fileobj.write(change_desc.description)
1673 finally:
1674 os.close(file_handle)
1675 RunCommand([commit_msg_hook, msg_file])
1676 change_desc.set_description(gclient_utils.FileRead(msg_file))
1677 finally:
1678 os.remove(msg_file)
1679
1680 if not change_desc.description:
1681 print "Description is empty; aborting."
1682 return 1
1683
1684 message = change_desc.description
1685
1686 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1687 if remote is '.':
1688 # If our upstream branch is local, we base our squashed commit on its
1689 # squashed version.
1690 parent = ('refs/heads/git_cl_uploads/' +
1691 scm.GIT.ShortBranchName(upstream_branch))
1692
1693 # Verify that the upstream branch has been uploaded too, otherwise Gerrit
1694 # will create additional CLs when uploading.
1695 if (RunGitSilent(['rev-parse', upstream_branch + ':']) !=
1696 RunGitSilent(['rev-parse', parent + ':'])):
1697 print 'Upload upstream branch ' + upstream_branch + ' first.'
1698 return 1
1699 else:
1700 parent = cl.GetCommonAncestorWithUpstream()
1701
1702 tree = RunGit(['rev-parse', 'HEAD:']).strip()
1703 ref_to_push = RunGit(['commit-tree', tree, '-p', parent,
1704 '-m', message]).strip()
1705 else:
1706 if CHANGE_ID not in change_desc.description:
1707 AddChangeIdToCommitMessage(options, args)
1708 ref_to_push = 'HEAD'
1709 parent = '%s/%s' % (gerrit_remote, branch)
1710
1711 commits = RunGitSilent(['rev-list', '%s..%s' % (parent,
1712 ref_to_push)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001713 if len(commits) > 1:
1714 print('WARNING: This will upload %d commits. Run the following command '
1715 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001716 print('git log %s..%s' % (parent, ref_to_push))
1717 print('You can also use `git squash-branch` to squash these into a single '
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001718 'commit.')
1719 ask_for_data('About to upload; enter to confirm.')
1720
piman@chromium.org336f9122014-09-04 02:16:55 +00001721 if options.reviewers or options.tbr_owners:
1722 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723
ukai@chromium.orge8077812012-02-03 03:41:46 +00001724 receive_options = []
1725 cc = cl.GetCCList().split(',')
1726 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001727 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001728 cc = filter(None, cc)
1729 if cc:
1730 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001731 if change_desc.get_reviewers():
1732 receive_options.extend(
1733 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001734
ukai@chromium.orge8077812012-02-03 03:41:46 +00001735 git_command = ['push']
1736 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001737 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001738 ' '.join(receive_options))
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001739 git_command += [gerrit_remote, ref_to_push + ':refs/for/' + branch]
ukai@chromium.orge8077812012-02-03 03:41:46 +00001740 RunGit(git_command)
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001741
1742 if options.squash:
1743 head = RunGit(['rev-parse', 'HEAD']).strip()
1744 RunGit(['update-ref', '-m', 'Uploaded ' + head, shadow_branch, ref_to_push])
1745
ukai@chromium.orge8077812012-02-03 03:41:46 +00001746 # TODO(ukai): parse Change-Id: and set issue number?
1747 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001748
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001749
wittman@chromium.org455dc922015-01-26 20:15:50 +00001750def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1751 """Computes the remote branch ref to use for the CL.
1752
1753 Args:
1754 remote (str): The git remote for the CL.
1755 remote_branch (str): The git remote branch for the CL.
1756 target_branch (str): The target branch specified by the user.
1757 pending_prefix (str): The pending prefix from the settings.
1758 """
1759 if not (remote and remote_branch):
1760 return None
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001761
wittman@chromium.org455dc922015-01-26 20:15:50 +00001762 if target_branch:
1763 # Cannonicalize branch references to the equivalent local full symbolic
1764 # refs, which are then translated into the remote full symbolic refs
1765 # below.
1766 if '/' not in target_branch:
1767 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1768 else:
1769 prefix_replacements = (
1770 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1771 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1772 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1773 )
1774 match = None
1775 for regex, replacement in prefix_replacements:
1776 match = re.search(regex, target_branch)
1777 if match:
1778 remote_branch = target_branch.replace(match.group(0), replacement)
1779 break
1780 if not match:
1781 # This is a branch path but not one we recognize; use as-is.
1782 remote_branch = target_branch
1783 elif (not remote_branch.startswith('refs/remotes/branch-heads') and
1784 not remote_branch.startswith('refs/remotes/%s/refs' % remote)):
1785 # Default to master for refs that are not branches.
1786 remote_branch = 'refs/remotes/%s/master' % remote
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001787
wittman@chromium.org455dc922015-01-26 20:15:50 +00001788 # Create the true path to the remote branch.
1789 # Does the following translation:
1790 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1791 # * refs/remotes/origin/master -> refs/heads/master
1792 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1793 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1794 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1795 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1796 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1797 'refs/heads/')
1798 elif remote_branch.startswith('refs/remotes/branch-heads'):
1799 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1800 # If a pending prefix exists then replace refs/ with it.
1801 if pending_prefix:
1802 remote_branch = remote_branch.replace('refs/', pending_prefix)
1803 return remote_branch
1804
1805
piman@chromium.org336f9122014-09-04 02:16:55 +00001806def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001807 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1809 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810 if options.emulate_svn_auto_props:
1811 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812
1813 change_desc = None
1814
pgervais@chromium.org91141372014-01-09 23:27:20 +00001815 if options.email is not None:
1816 upload_args.extend(['--email', options.email])
1817
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001819 if options.title:
1820 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001821 if options.message:
1822 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001823 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824 print ("This branch is associated with issue %s. "
1825 "Adding patch to that issue." % cl.GetIssue())
1826 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001827 if options.title:
1828 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001829 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001830 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00001831 if options.reviewers or options.tbr_owners:
1832 change_desc.update_reviewers(options.reviewers,
1833 options.tbr_owners,
1834 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001835 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001836 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001837
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001838 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001839 print "Description is empty; aborting."
1840 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001841
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001842 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001843 if change_desc.get_reviewers():
1844 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001845 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001846 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001847 DieWithError("Must specify reviewers to send email.")
1848 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001849
1850 # We check this before applying rietveld.private assuming that in
1851 # rietveld.cc only addresses which we can send private CLs to are listed
1852 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1853 # --private is specified explicitly on the command line.
1854 if options.private:
1855 logging.warn('rietveld.cc is ignored since private flag is specified. '
1856 'You need to review and add them manually if necessary.')
1857 cc = cl.GetCCListWithoutDefault()
1858 else:
1859 cc = cl.GetCCList()
1860 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001861 if cc:
1862 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001864 if options.private or settings.GetDefaultPrivateFlag() == "True":
1865 upload_args.append('--private')
1866
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001867 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001868 if not options.find_copies:
1869 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001870
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871 # Include the upstream repo's URL in the change -- this is useful for
1872 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001873 remote_url = cl.GetGitBaseUrlFromConfig()
1874 if not remote_url:
1875 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001876 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001877 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00001878 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1879 remote_url = (cl.GetRemoteUrl() + '@'
1880 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881 if remote_url:
1882 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00001883 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00001884 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1885 settings.GetPendingRefPrefix())
1886 if target_ref:
1887 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001888
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001889 project = settings.GetProject()
1890 if project:
1891 upload_args.extend(['--project', project])
1892
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001893 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001894 upload_args = ['upload'] + upload_args + args
1895 logging.info('upload.RealMain(%s)', upload_args)
1896 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001897 issue = int(issue)
1898 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001899 except KeyboardInterrupt:
1900 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001901 except:
1902 # If we got an exception after the user typed a description for their
1903 # change, back up the description before re-raising.
1904 if change_desc:
1905 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1906 print '\nGot exception while uploading -- saving description to %s\n' \
1907 % backup_path
1908 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001909 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001910 backup_file.close()
1911 raise
1912
1913 if not cl.GetIssue():
1914 cl.SetIssue(issue)
1915 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001916
1917 if options.use_commit_queue:
1918 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919 return 0
1920
1921
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001922def cleanup_list(l):
1923 """Fixes a list so that comma separated items are put as individual items.
1924
1925 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1926 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1927 """
1928 items = sum((i.split(',') for i in l), [])
1929 stripped_items = (i.strip() for i in items)
1930 return sorted(filter(None, stripped_items))
1931
1932
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001933@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001934def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001935 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001936 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1937 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001938 parser.add_option('--bypass-watchlists', action='store_true',
1939 dest='bypass_watchlists',
1940 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001941 parser.add_option('-f', action='store_true', dest='force',
1942 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001943 parser.add_option('-m', dest='message', help='message for patchset')
1944 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001945 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001946 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001947 help='reviewer email addresses')
1948 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001949 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001950 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001951 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001952 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001953 parser.add_option('--emulate_svn_auto_props',
1954 '--emulate-svn-auto-props',
1955 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001956 dest="emulate_svn_auto_props",
1957 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001958 parser.add_option('-c', '--use-commit-queue', action='store_true',
1959 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001960 parser.add_option('--private', action='store_true',
1961 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001962 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001963 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00001964 metavar='TARGET',
1965 help='Apply CL to remote ref TARGET. ' +
1966 'Default: remote branch head, or master')
bauerb@chromium.org27386dd2015-02-16 10:45:39 +00001967 parser.add_option('--squash', action='store_true',
1968 help='Squash multiple commits into one (Gerrit only)')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001969 parser.add_option('--email', default=None,
1970 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00001971 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
1972 help='add a set of OWNERS to TBR')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001973
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001974 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001975 (options, args) = parser.parse_args(args)
1976
ukai@chromium.org259e4682012-10-25 07:36:33 +00001977 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001978 return 1
1979
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001980 options.reviewers = cleanup_list(options.reviewers)
1981 options.cc = cleanup_list(options.cc)
1982
ukai@chromium.orge8077812012-02-03 03:41:46 +00001983 cl = Changelist()
1984 if args:
1985 # TODO(ukai): is it ok for gerrit case?
1986 base_branch = args[0]
1987 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00001988 if cl.GetBranch() is None:
1989 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1990
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001991 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001992 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001993 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001994
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001995 # Apply watchlists on upload.
1996 change = cl.GetChange(base_branch, None)
1997 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1998 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001999 if not options.bypass_watchlists:
2000 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002001
ukai@chromium.orge8077812012-02-03 03:41:46 +00002002 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00002003 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002004 # Set the reviewer list now so that presubmit checks can access it.
2005 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00002006 change_description.update_reviewers(options.reviewers,
2007 options.tbr_owners,
2008 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00002009 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002010 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00002011 may_prompt=not options.force,
2012 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00002013 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002014 if not hook_results.should_continue():
2015 return 1
2016 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00002017 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00002018
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002019 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002020 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002021 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00002022 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002023 print ('The last upload made from this repository was patchset #%d but '
2024 'the most recent patchset on the server is #%d.'
2025 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00002026 print ('Uploading will still work, but if you\'ve uploaded to this issue '
2027 'from another machine or branch the patch you\'re uploading now '
2028 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00002029 ask_for_data('About to upload; enter to confirm.')
2030
iannucci@chromium.org79540052012-10-19 23:15:26 +00002031 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00002032 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00002033 return GerritUpload(options, args, cl, change)
2034 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002035 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00002036 git_set_branch_value('last-upload-hash',
2037 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00002038
2039 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00002040
2041
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002042def IsSubmoduleMergeCommit(ref):
2043 # When submodules are added to the repo, we expect there to be a single
2044 # non-git-svn merge commit at remote HEAD with a signature comment.
2045 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00002046 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002047 return RunGit(cmd) != ''
2048
2049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002050def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002051 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002052
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002053 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054 Updates changelog with metadata (e.g. pointer to review).
2055 Pushes/dcommits the code upstream.
2056 Updates review and closes.
2057 """
2058 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
2059 help='bypass upload presubmit hook')
2060 parser.add_option('-m', dest='message',
2061 help="override review description")
2062 parser.add_option('-f', action='store_true', dest='force',
2063 help="force yes to questions (don't prompt)")
2064 parser.add_option('-c', dest='contributor',
2065 help="external contributor for patch (appended to " +
2066 "description and used as author for git). Should be " +
2067 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002068 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002069 (options, args) = parser.parse_args(args)
2070 cl = Changelist()
2071
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002072 current = cl.GetBranch()
2073 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2074 if not settings.GetIsGitSvn() and remote == '.':
2075 print
2076 print 'Attempting to push branch %r into another local branch!' % current
2077 print
2078 print 'Either reparent this branch on top of origin/master:'
2079 print ' git reparent-branch --root'
2080 print
2081 print 'OR run `git rebase-update` if you think the parent branch is already'
2082 print 'committed.'
2083 print
2084 print ' Current parent: %r' % upstream_branch
2085 return 1
2086
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002087 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002088 # Default to merging against our best guess of the upstream branch.
2089 args = [cl.GetUpstreamBranch()]
2090
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002091 if options.contributor:
2092 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2093 print "Please provide contibutor as 'First Last <email@example.com>'"
2094 return 1
2095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002096 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002097 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098
ukai@chromium.org259e4682012-10-25 07:36:33 +00002099 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002100 return 1
2101
2102 # This rev-list syntax means "show all commits not in my branch that
2103 # are in base_branch".
2104 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2105 base_branch]).splitlines()
2106 if upstream_commits:
2107 print ('Base branch "%s" has %d commits '
2108 'not in this branch.' % (base_branch, len(upstream_commits)))
2109 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2110 return 1
2111
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002112 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002113 svn_head = None
2114 if cmd == 'dcommit' or base_has_submodules:
2115 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2116 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002117
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002119 # If the base_head is a submodule merge commit, the first parent of the
2120 # base_head should be a git-svn commit, which is what we're interested in.
2121 base_svn_head = base_branch
2122 if base_has_submodules:
2123 base_svn_head += '^1'
2124
2125 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126 if extra_commits:
2127 print ('This branch has %d additional commits not upstreamed yet.'
2128 % len(extra_commits.splitlines()))
2129 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2130 'before attempting to %s.' % (base_branch, cmd))
2131 return 1
2132
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002133 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002134 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002135 author = None
2136 if options.contributor:
2137 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002138 hook_results = cl.RunHook(
2139 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002140 may_prompt=not options.force,
2141 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002142 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002143 if not hook_results.should_continue():
2144 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002145
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002146 # Check the tree status if the tree status URL is set.
2147 status = GetTreeStatus()
2148 if 'closed' == status:
2149 print('The tree is closed. Please wait for it to reopen. Use '
2150 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2151 return 1
2152 elif 'unknown' == status:
2153 print('Unable to determine tree status. Please verify manually and '
2154 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2155 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002156 else:
2157 breakpad.SendStack(
2158 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002159 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2160 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002161 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002162
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002163 change_desc = ChangeDescription(options.message)
2164 if not change_desc.description and cl.GetIssue():
2165 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002166
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002167 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002168 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002169 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002170 else:
2171 print 'No description set.'
2172 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2173 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002174
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002175 # Keep a separate copy for the commit message, because the commit message
2176 # contains the link to the Rietveld issue, while the Rietveld message contains
2177 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002178 # Keep a separate copy for the commit message.
2179 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002180 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002181
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002182 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002183 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002184 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002185 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002186 commit_desc.append_footer('Patch from %s.' % options.contributor)
2187
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002188 print('Description:')
2189 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002191 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002193 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002194
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002195 # We want to squash all this branch's commits into one commit with the proper
2196 # description. We do this by doing a "reset --soft" to the base branch (which
2197 # keeps the working copy the same), then dcommitting that. If origin/master
2198 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2199 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002200 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002201 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2202 # Delete the branches if they exist.
2203 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2204 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2205 result = RunGitWithCode(showref_cmd)
2206 if result[0] == 0:
2207 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002208
2209 # We might be in a directory that's present in this branch but not in the
2210 # trunk. Move up to the top of the tree so that git commands that expect a
2211 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002212 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002213 if rel_base_path:
2214 os.chdir(rel_base_path)
2215
2216 # Stuff our change into the merge branch.
2217 # We wrap in a try...finally block so if anything goes wrong,
2218 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002219 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002220 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002221 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002222 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002223 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002224 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002225 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002226 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002227 RunGit(
2228 [
2229 'commit', '--author', options.contributor,
2230 '-m', commit_desc.description,
2231 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002232 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002233 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002234 if base_has_submodules:
2235 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2236 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2237 RunGit(['checkout', CHERRY_PICK_BRANCH])
2238 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002239 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002240 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002241 pending_prefix = settings.GetPendingRefPrefix()
2242 if not pending_prefix or branch.startswith(pending_prefix):
2243 # If not using refs/pending/heads/* at all, or target ref is already set
2244 # to pending, then push to the target ref directly.
2245 retcode, output = RunGitWithCode(
2246 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002247 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002248 else:
2249 # Cherry-pick the change on top of pending ref and then push it.
2250 assert branch.startswith('refs/'), branch
2251 assert pending_prefix[-1] == '/', pending_prefix
2252 pending_ref = pending_prefix + branch[len('refs/'):]
2253 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002254 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002255 if retcode == 0:
2256 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002257 else:
2258 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002259 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002260 'svn', 'dcommit',
2261 '-C%s' % options.similarity,
2262 '--no-rebase', '--rmdir',
2263 ]
2264 if settings.GetForceHttpsCommitUrl():
2265 # Allow forcing https commit URLs for some projects that don't allow
2266 # committing to http URLs (like Google Code).
2267 remote_url = cl.GetGitSvnRemoteUrl()
2268 if urlparse.urlparse(remote_url).scheme == 'http':
2269 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002270 cmd_args.append('--commit-url=%s' % remote_url)
2271 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002272 if 'Committed r' in output:
2273 revision = re.match(
2274 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2275 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002276 finally:
2277 # And then swap back to the original branch and clean up.
2278 RunGit(['checkout', '-q', cl.GetBranch()])
2279 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002280 if base_has_submodules:
2281 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002282
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002283 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002284 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002285 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002286
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002287 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002288 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002289 try:
2290 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2291 # We set pushed_to_pending to False, since it made it all the way to the
2292 # real ref.
2293 pushed_to_pending = False
2294 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002295 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002296
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002297 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002298 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002299 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002300 if not to_pending:
2301 if viewvc_url and revision:
2302 change_desc.append_footer(
2303 'Committed: %s%s' % (viewvc_url, revision))
2304 elif revision:
2305 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002306 print ('Closing issue '
2307 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002308 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002309 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002310 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002311 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002312 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002313 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002314 if options.bypass_hooks:
2315 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2316 else:
2317 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002318 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002319 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002320
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002321 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002322 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2323 print 'The commit is in the pending queue (%s).' % pending_ref
2324 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002325 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002326 'footer.' % branch)
2327
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002328 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2329 if os.path.isfile(hook):
2330 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002331
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002332 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002333
2334
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002335def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2336 print
2337 print 'Waiting for commit to be landed on %s...' % real_ref
2338 print '(If you are impatient, you may Ctrl-C once without harm)'
2339 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2340 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2341
2342 loop = 0
2343 while True:
2344 sys.stdout.write('fetching (%d)... \r' % loop)
2345 sys.stdout.flush()
2346 loop += 1
2347
2348 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2349 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2350 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2351 for commit in commits.splitlines():
2352 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2353 print 'Found commit on %s' % real_ref
2354 return commit
2355
2356 current_rev = to_rev
2357
2358
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002359def PushToGitPending(remote, pending_ref, upstream_ref):
2360 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2361
2362 Returns:
2363 (retcode of last operation, output log of last operation).
2364 """
2365 assert pending_ref.startswith('refs/'), pending_ref
2366 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2367 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2368 code = 0
2369 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002370 max_attempts = 3
2371 attempts_left = max_attempts
2372 while attempts_left:
2373 if attempts_left != max_attempts:
2374 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2375 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002376
2377 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002378 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002379 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002380 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002381 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002382 print 'Fetch failed with exit code %d.' % code
2383 if out.strip():
2384 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002385 continue
2386
2387 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002388 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002389 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002390 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002391 if code:
2392 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002393 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2394 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002395 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2396 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002397 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002398 return code, out
2399
2400 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002401 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002402 code, out = RunGitWithCode(
2403 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2404 if code == 0:
2405 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002406 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002407 return code, out
2408
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002409 print 'Push failed with exit code %d.' % code
2410 if out.strip():
2411 print out.strip()
2412 if IsFatalPushFailure(out):
2413 print (
2414 'Fatal push error. Make sure your .netrc credentials and git '
2415 'user.email are correct and you have push access to the repo.')
2416 return code, out
2417
2418 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002419 return code, out
2420
2421
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002422def IsFatalPushFailure(push_stdout):
2423 """True if retrying push won't help."""
2424 return '(prohibited by Gerrit)' in push_stdout
2425
2426
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002427@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002428def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002429 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002430 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002431 message = """This doesn't appear to be an SVN repository.
2432If your project has a git mirror with an upstream SVN master, you probably need
2433to run 'git svn init', see your project's git mirror documentation.
2434If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002435to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002436Choose wisely, if you get this wrong, your commit might appear to succeed but
2437will instead be silently ignored."""
2438 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002439 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002440 return SendUpstream(parser, args, 'dcommit')
2441
2442
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002443@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002444def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002445 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002446 if settings.GetIsGitSvn():
2447 print('This appears to be an SVN repository.')
2448 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002449 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002450 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002451
2452
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002453@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002454def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002455 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002456 parser.add_option('-b', dest='newbranch',
2457 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002458 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002459 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002460 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2461 help='Change to the directory DIR immediately, '
2462 'before doing anything else.')
2463 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002464 help='failed patches spew .rej files rather than '
2465 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002466 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2467 help="don't commit after patch applies")
2468 (options, args) = parser.parse_args(args)
2469 if len(args) != 1:
2470 parser.print_help()
2471 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002472 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002473
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002474 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002475 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002476
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002477 if options.newbranch:
2478 if options.force:
2479 RunGit(['branch', '-D', options.newbranch],
2480 stderr=subprocess2.PIPE, error_ok=True)
2481 RunGit(['checkout', '-b', options.newbranch,
2482 Changelist().GetUpstreamBranch()])
2483
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002484 return PatchIssue(issue_arg, options.reject, options.nocommit,
2485 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002486
2487
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002488def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002489 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002490 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002491 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002492 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002493 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002494 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002495 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002496 # Assume it's a URL to the patch. Default to https.
2497 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002498 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002499 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002500 DieWithError('Must pass an issue ID or full URL for '
2501 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002502 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002503 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002504 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002505
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002506 # Switch up to the top-level directory, if necessary, in preparation for
2507 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002508 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002509 if top:
2510 os.chdir(top)
2511
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002512 # Git patches have a/ at the beginning of source paths. We strip that out
2513 # with a sed script rather than the -p flag to patch so we can feed either
2514 # Git or svn-style patches into the same apply command.
2515 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002516 try:
2517 patch_data = subprocess2.check_output(
2518 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2519 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002520 DieWithError('Git patch mungling failed.')
2521 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002522
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002523 # We use "git apply" to apply the patch instead of "patch" so that we can
2524 # pick up file adds.
2525 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002526 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002527 if directory:
2528 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002529 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002530 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002531 elif IsGitVersionAtLeast('1.7.12'):
2532 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002533 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002534 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002535 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002536 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002537 DieWithError('Failed to apply the patch')
2538
2539 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002540 if not nocommit:
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002541 RunGit(['commit', '-m', ('patch from issue %(i)s at patchset '
2542 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2543 % {'i': issue, 'p': patchset})])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002544 cl = Changelist()
2545 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002546 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002547 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002548 else:
2549 print "Patch applied to index."
2550 return 0
2551
2552
2553def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002554 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002555 # Provide a wrapper for git svn rebase to help avoid accidental
2556 # git svn dcommit.
2557 # It's the only command that doesn't use parser at all since we just defer
2558 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002559
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002560 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002561
2562
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002563def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002564 """Fetches the tree status and returns either 'open', 'closed',
2565 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002566 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002567 if url:
2568 status = urllib2.urlopen(url).read().lower()
2569 if status.find('closed') != -1 or status == '0':
2570 return 'closed'
2571 elif status.find('open') != -1 or status == '1':
2572 return 'open'
2573 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002574 return 'unset'
2575
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002576
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002577def GetTreeStatusReason():
2578 """Fetches the tree status from a json url and returns the message
2579 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002580 url = settings.GetTreeStatusUrl()
2581 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002582 connection = urllib2.urlopen(json_url)
2583 status = json.loads(connection.read())
2584 connection.close()
2585 return status['message']
2586
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002587
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002588def GetBuilderMaster(bot_list):
2589 """For a given builder, fetch the master from AE if available."""
2590 map_url = 'https://builders-map.appspot.com/'
2591 try:
2592 master_map = json.load(urllib2.urlopen(map_url))
2593 except urllib2.URLError as e:
2594 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2595 (map_url, e))
2596 except ValueError as e:
2597 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2598 if not master_map:
2599 return None, 'Failed to build master map.'
2600
2601 result_master = ''
2602 for bot in bot_list:
2603 builder = bot.split(':', 1)[0]
2604 master_list = master_map.get(builder, [])
2605 if not master_list:
2606 return None, ('No matching master for builder %s.' % builder)
2607 elif len(master_list) > 1:
2608 return None, ('The builder name %s exists in multiple masters %s.' %
2609 (builder, master_list))
2610 else:
2611 cur_master = master_list[0]
2612 if not result_master:
2613 result_master = cur_master
2614 elif result_master != cur_master:
2615 return None, 'The builders do not belong to the same master.'
2616 return result_master, None
2617
2618
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002619def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002620 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002621 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622 status = GetTreeStatus()
2623 if 'unset' == status:
2624 print 'You must configure your tree status URL by running "git cl config".'
2625 return 2
2626
2627 print "The tree is %s" % status
2628 print
2629 print GetTreeStatusReason()
2630 if status != 'open':
2631 return 1
2632 return 0
2633
2634
maruel@chromium.org15192402012-09-06 12:38:29 +00002635def CMDtry(parser, args):
2636 """Triggers a try job through Rietveld."""
2637 group = optparse.OptionGroup(parser, "Try job options")
2638 group.add_option(
2639 "-b", "--bot", action="append",
2640 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2641 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002642 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002643 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002644 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002645 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002646 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002647 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002648 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002649 "-r", "--revision",
2650 help="Revision to use for the try job; default: the "
2651 "revision will be determined by the try server; see "
2652 "its waterfall for more info")
2653 group.add_option(
2654 "-c", "--clobber", action="store_true", default=False,
2655 help="Force a clobber before building; e.g. don't do an "
2656 "incremental build")
2657 group.add_option(
2658 "--project",
2659 help="Override which project to use. Projects are defined "
2660 "server-side to define what default bot set to use")
2661 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002662 "-n", "--name", help="Try job name; default to current branch name")
2663 parser.add_option_group(group)
2664 options, args = parser.parse_args(args)
2665
2666 if args:
2667 parser.error('Unknown arguments: %s' % args)
2668
2669 cl = Changelist()
2670 if not cl.GetIssue():
2671 parser.error('Need to upload first')
2672
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002673 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002674 if props.get('closed'):
2675 parser.error('Cannot send tryjobs for a closed CL')
2676
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002677 if props.get('private'):
2678 parser.error('Cannot use trybots with private issue')
2679
maruel@chromium.org15192402012-09-06 12:38:29 +00002680 if not options.name:
2681 options.name = cl.GetBranch()
2682
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002683 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002684 options.master, err_msg = GetBuilderMaster(options.bot)
2685 if err_msg:
2686 parser.error('Tryserver master cannot be found because: %s\n'
2687 'Please manually specify the tryserver master'
2688 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002689
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002690 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002691 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002692 if not options.bot:
2693 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002694
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002695 # Get try masters from PRESUBMIT.py files.
2696 masters = presubmit_support.DoGetTryMasters(
2697 change,
2698 change.LocalPaths(),
2699 settings.GetRoot(),
2700 None,
2701 None,
2702 options.verbose,
2703 sys.stdout)
2704 if masters:
2705 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002706
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002707 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2708 options.bot = presubmit_support.DoGetTrySlaves(
2709 change,
2710 change.LocalPaths(),
2711 settings.GetRoot(),
2712 None,
2713 None,
2714 options.verbose,
2715 sys.stdout)
2716 if not options.bot:
2717 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002718
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002719 builders_and_tests = {}
2720 # TODO(machenbach): The old style command-line options don't support
2721 # multiple try masters yet.
2722 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2723 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2724
2725 for bot in old_style:
2726 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002727 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002728 elif ',' in bot:
2729 parser.error('Specify one bot per --bot flag')
2730 else:
2731 builders_and_tests.setdefault(bot, []).append('defaulttests')
2732
2733 for bot, tests in new_style:
2734 builders_and_tests.setdefault(bot, []).extend(tests)
2735
2736 # Return a master map with one master to be backwards compatible. The
2737 # master name defaults to an empty string, which will cause the master
2738 # not to be set on rietveld (deprecated).
2739 return {options.master: builders_and_tests}
2740
2741 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002742
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002743 for builders in masters.itervalues():
2744 if any('triggered' in b for b in builders):
2745 print >> sys.stderr, (
2746 'ERROR You are trying to send a job to a triggered bot. This type of'
2747 ' bot requires an\ninitial job from a parent (usually a builder). '
2748 'Instead send your job to the parent.\n'
2749 'Bot list: %s' % builders)
2750 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002751
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002752 patchset = cl.GetMostRecentPatchset()
2753 if patchset and patchset != cl.GetPatchset():
2754 print(
2755 '\nWARNING Mismatch between local config and server. Did a previous '
2756 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2757 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002758 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002759 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002760 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002761 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002762 except urllib2.HTTPError, e:
2763 if e.code == 404:
2764 print('404 from rietveld; '
2765 'did you mean to use "git try" instead of "git cl try"?')
2766 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002767 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002768
2769 for (master, builders) in masters.iteritems():
2770 if master:
2771 print 'Master: %s' % master
2772 length = max(len(builder) for builder in builders)
2773 for builder in sorted(builders):
2774 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002775 return 0
2776
2777
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002778@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002779def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002780 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002781 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002782 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002783 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002784
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002785 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002786 if args:
2787 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002788 branch = cl.GetBranch()
2789 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002790 cl = Changelist()
2791 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002792
2793 # Clear configured merge-base, if there is one.
2794 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002795 else:
2796 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002797 return 0
2798
2799
thestig@chromium.org00858c82013-12-02 23:08:03 +00002800def CMDweb(parser, args):
2801 """Opens the current CL in the web browser."""
2802 _, args = parser.parse_args(args)
2803 if args:
2804 parser.error('Unrecognized args: %s' % ' '.join(args))
2805
2806 issue_url = Changelist().GetIssueURL()
2807 if not issue_url:
2808 print >> sys.stderr, 'ERROR No issue to open'
2809 return 1
2810
2811 webbrowser.open(issue_url)
2812 return 0
2813
2814
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002815def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002816 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002817 _, args = parser.parse_args(args)
2818 if args:
2819 parser.error('Unrecognized args: %s' % ' '.join(args))
2820 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002821 props = cl.GetIssueProperties()
2822 if props.get('private'):
2823 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002824 cl.SetFlag('commit', '1')
2825 return 0
2826
2827
groby@chromium.org411034a2013-02-26 15:12:01 +00002828def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002829 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002830 _, args = parser.parse_args(args)
2831 if args:
2832 parser.error('Unrecognized args: %s' % ' '.join(args))
2833 cl = Changelist()
2834 # Ensure there actually is an issue to close.
2835 cl.GetDescription()
2836 cl.CloseIssue()
2837 return 0
2838
2839
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002840def CMDdiff(parser, args):
2841 """shows differences between local tree and last upload."""
2842 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002843 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002844 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002845 if not issue:
2846 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002847 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002848 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002849
2850 # Create a new branch based on the merge-base
2851 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2852 try:
2853 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002854 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002855 if rtn != 0:
2856 return rtn
2857
wychen@chromium.org06928532015-02-03 02:11:29 +00002858 # Switch back to starting branch and diff against the temporary
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002859 # branch containing the latest rietveld patch.
wychen@chromium.org06928532015-02-03 02:11:29 +00002860 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch, '--'])
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002861 finally:
2862 RunGit(['checkout', '-q', branch])
2863 RunGit(['branch', '-D', TMP_BRANCH])
2864
2865 return 0
2866
2867
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002868def CMDowners(parser, args):
2869 """interactively find the owners for reviewing"""
2870 parser.add_option(
2871 '--no-color',
2872 action='store_true',
2873 help='Use this option to disable color output')
2874 options, args = parser.parse_args(args)
2875
2876 author = RunGit(['config', 'user.email']).strip() or None
2877
2878 cl = Changelist()
2879
2880 if args:
2881 if len(args) > 1:
2882 parser.error('Unknown args')
2883 base_branch = args[0]
2884 else:
2885 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002886 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002887
2888 change = cl.GetChange(base_branch, None)
2889 return owners_finder.OwnersFinder(
2890 [f.LocalPath() for f in
2891 cl.GetChange(base_branch, None).AffectedFiles()],
2892 change.RepositoryRoot(), author,
2893 fopen=file, os_path=os.path, glob=glob.glob,
2894 disable_color=options.no_color).run()
2895
2896
enne@chromium.org555cfe42014-01-29 18:21:39 +00002897@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002898def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002899 """Runs clang-format on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00002900 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002901 parser.add_option('--full', action='store_true',
2902 help='Reformat the full content of all touched files')
2903 parser.add_option('--dry-run', action='store_true',
2904 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002905 parser.add_option('--diff', action='store_true',
2906 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002907 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002908
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002909 # git diff generates paths against the root of the repository. Change
2910 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002911 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002912 if rel_base_path:
2913 os.chdir(rel_base_path)
2914
digit@chromium.org29e47272013-05-17 17:01:46 +00002915 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002916 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002917 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002918 # Only list the names of modified files.
2919 diff_cmd.append('--name-only')
2920 else:
2921 # Only generate context-less patches.
2922 diff_cmd.append('-U0')
2923
2924 # Grab the merge-base commit, i.e. the upstream commit of the current
2925 # branch when it was created or the last time it was rebased. This is
2926 # to cover the case where the user may have called "git fetch origin",
2927 # moving the origin branch to a newer commit, but hasn't rebased yet.
2928 upstream_commit = None
2929 cl = Changelist()
2930 upstream_branch = cl.GetUpstreamBranch()
2931 if upstream_branch:
2932 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2933 upstream_commit = upstream_commit.strip()
2934
2935 if not upstream_commit:
2936 DieWithError('Could not find base commit for this branch. '
2937 'Are you in detached state?')
2938
2939 diff_cmd.append(upstream_commit)
2940
2941 # Handle source file filtering.
2942 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002943 if args:
2944 for arg in args:
2945 if os.path.isdir(arg):
2946 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2947 elif os.path.isfile(arg):
2948 diff_cmd.append(arg)
2949 else:
2950 DieWithError('Argument "%s" is not a file or a directory' % arg)
2951 else:
2952 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002953 diff_output = RunGit(diff_cmd)
2954
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002955 top_dir = os.path.normpath(
2956 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2957
2958 # Locate the clang-format binary in the checkout
2959 try:
2960 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2961 except clang_format.NotFoundError, e:
2962 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002963
digit@chromium.org29e47272013-05-17 17:01:46 +00002964 if opts.full:
2965 # diff_output is a list of files to send to clang-format.
2966 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002967 if not files:
2968 print "Nothing to format."
2969 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002970 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002971 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002972 cmd.append('-i')
2973 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002974 if opts.diff:
2975 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002976 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002977 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00002978 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00002979 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002980 try:
2981 script = clang_format.FindClangFormatScriptInChromiumTree(
2982 'clang-format-diff.py')
2983 except clang_format.NotFoundError, e:
2984 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002985
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002986 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002987 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002988 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002989
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002990 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002991 if opts.diff:
2992 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002993 if opts.dry_run and len(stdout) > 0:
2994 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002995
2996 return 0
2997
2998
maruel@chromium.org29404b52014-09-08 22:58:00 +00002999def CMDlol(parser, args):
3000 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00003001 print zlib.decompress(base64.b64decode(
3002 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
3003 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
3004 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
3005 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00003006 return 0
3007
3008
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003009class OptionParser(optparse.OptionParser):
3010 """Creates the option parse and add --verbose support."""
3011 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003012 optparse.OptionParser.__init__(
3013 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003014 self.add_option(
3015 '-v', '--verbose', action='count', default=0,
3016 help='Use 2 times for more debugging info')
3017
3018 def parse_args(self, args=None, values=None):
3019 options, args = optparse.OptionParser.parse_args(self, args, values)
3020 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
3021 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
3022 return options, args
3023
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00003024
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003025def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00003026 if sys.hexversion < 0x02060000:
3027 print >> sys.stderr, (
3028 '\nYour python version %s is unsupported, please upgrade.\n' %
3029 sys.version.split(' ', 1)[0])
3030 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003031
maruel@chromium.orgddd59412011-11-30 14:20:38 +00003032 # Reload settings.
3033 global settings
3034 settings = Settings()
3035
maruel@chromium.org39c0b222013-08-17 16:57:01 +00003036 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00003037 dispatcher = subcommand.CommandDispatcher(__name__)
3038 try:
3039 return dispatcher.execute(OptionParser(), argv)
3040 except urllib2.HTTPError, e:
3041 if e.code != 500:
3042 raise
3043 DieWithError(
3044 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
3045 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003046
3047
3048if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003049 # These affect sys.stdout so do it outside of main() to simplify mocks in
3050 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00003051 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00003052 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00003053 sys.exit(main(sys.argv[1:]))