blob: 4edb6a0af8e1ee834f7f8305e92e435a3c1f9cee [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000022import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000024import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000025import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000026import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027
28try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000029 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030except ImportError:
31 pass
32
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035from third_party import upload
36import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000037import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000038import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000039import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
piman@chromium.org336f9122014-09-04 02:16:55 +000041import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000042import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000044import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000046import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000047import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048import watchlists
49
maruel@chromium.org0633fb42013-08-16 20:06:14 +000050__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000051
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000052DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000053POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000055GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000056CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057
thestig@chromium.org44202a22014-03-11 19:22:18 +000058# Valid extensions for files we want to lint.
59DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
60DEFAULT_LINT_IGNORE_REGEX = r"$^"
61
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000062# Shortcut since it quickly becomes redundant.
63Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000064
maruel@chromium.orgddd59412011-11-30 14:20:38 +000065# Initialized in main()
66settings = None
67
68
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000070 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071 sys.exit(1)
72
73
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000074def GetNoGitPagerEnv():
75 env = os.environ.copy()
76 # 'cat' is a magical git string that disables pagers on all platforms.
77 env['GIT_PAGER'] = 'cat'
78 return env
79
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000080
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000081def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000082 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000083 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000084 except subprocess2.CalledProcessError as e:
85 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000086 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000088 'Command "%s" failed.\n%s' % (
89 ' '.join(args), error_message or e.stdout or ''))
90 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091
92
93def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000094 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000095 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096
97
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000098def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000099 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000100 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000101 if suppress_stderr:
102 stderr = subprocess2.VOID
103 else:
104 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000105 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000106 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000107 stdout=subprocess2.PIPE,
108 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000109 return code, out[0]
110 except ValueError:
111 # When the subprocess fails, it returns None. That triggers a ValueError
112 # when trying to unpack the return value into (out, code).
113 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
115
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000116def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000117 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000118 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000119 return (version.startswith(prefix) and
120 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000121
122
maruel@chromium.org90541732011-04-01 17:54:18 +0000123def ask_for_data(prompt):
124 try:
125 return raw_input(prompt)
126 except KeyboardInterrupt:
127 # Hide the exception.
128 sys.exit(1)
129
130
iannucci@chromium.org79540052012-10-19 23:15:26 +0000131def git_set_branch_value(key, value):
132 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000133 if not branch:
134 return
135
136 cmd = ['config']
137 if isinstance(value, int):
138 cmd.append('--int')
139 git_key = 'branch.%s.%s' % (branch, key)
140 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000141
142
143def git_get_branch_default(key, default):
144 branch = Changelist().GetBranch()
145 if branch:
146 git_key = 'branch.%s.%s' % (branch, key)
147 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
148 try:
149 return int(stdout.strip())
150 except ValueError:
151 pass
152 return default
153
154
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155def add_git_similarity(parser):
156 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000158 help='Sets the percentage that a pair of files need to match in order to'
159 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160 parser.add_option(
161 '--find-copies', action='store_true',
162 help='Allows git to look for copies.')
163 parser.add_option(
164 '--no-find-copies', action='store_false', dest='find_copies',
165 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000166
167 old_parser_args = parser.parse_args
168 def Parse(args):
169 options, args = old_parser_args(args)
170
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000171 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000173 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 print('Note: Saving similarity of %d%% in git config.'
175 % options.similarity)
176 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000177
iannucci@chromium.org79540052012-10-19 23:15:26 +0000178 options.similarity = max(0, min(options.similarity, 100))
179
180 if options.find_copies is None:
181 options.find_copies = bool(
182 git_get_branch_default('git-find-copies', True))
183 else:
184 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185
186 print('Using %d%% similarity for rename/copy detection. '
187 'Override with --similarity.' % options.similarity)
188
189 return options, args
190 parser.parse_args = Parse
191
192
ukai@chromium.org259e4682012-10-25 07:36:33 +0000193def is_dirty_git_tree(cmd):
194 # Make sure index is up-to-date before running diff-index.
195 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
196 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
197 if dirty:
198 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
199 print 'Uncommitted files: (git diff-index --name-status HEAD)'
200 print dirty[:4096]
201 if len(dirty) > 4096:
202 print '... (run "git diff-index --name-status HEAD" to see full output).'
203 return True
204 return False
205
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000206
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000207def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
208 """Return the corresponding git ref if |base_url| together with |glob_spec|
209 matches the full |url|.
210
211 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
212 """
213 fetch_suburl, as_ref = glob_spec.split(':')
214 if allow_wildcards:
215 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
216 if glob_match:
217 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
218 # "branches/{472,597,648}/src:refs/remotes/svn/*".
219 branch_re = re.escape(base_url)
220 if glob_match.group(1):
221 branch_re += '/' + re.escape(glob_match.group(1))
222 wildcard = glob_match.group(2)
223 if wildcard == '*':
224 branch_re += '([^/]*)'
225 else:
226 # Escape and replace surrounding braces with parentheses and commas
227 # with pipe symbols.
228 wildcard = re.escape(wildcard)
229 wildcard = re.sub('^\\\\{', '(', wildcard)
230 wildcard = re.sub('\\\\,', '|', wildcard)
231 wildcard = re.sub('\\\\}$', ')', wildcard)
232 branch_re += wildcard
233 if glob_match.group(3):
234 branch_re += re.escape(glob_match.group(3))
235 match = re.match(branch_re, url)
236 if match:
237 return re.sub('\*$', match.group(1), as_ref)
238
239 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
240 if fetch_suburl:
241 full_url = base_url + '/' + fetch_suburl
242 else:
243 full_url = base_url
244 if full_url == url:
245 return as_ref
246 return None
247
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000248
iannucci@chromium.org79540052012-10-19 23:15:26 +0000249def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000250 """Prints statistics about the change to the user."""
251 # --no-ext-diff is broken in some versions of Git, so try to work around
252 # this by overriding the environment (but there is still a problem if the
253 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000254 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000255 if 'GIT_EXTERNAL_DIFF' in env:
256 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000257
258 if find_copies:
259 similarity_options = ['--find-copies-harder', '-l100000',
260 '-C%s' % similarity]
261 else:
262 similarity_options = ['-M%s' % similarity]
263
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000264 try:
265 stdout = sys.stdout.fileno()
266 except AttributeError:
267 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000268 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000269 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000270 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000271 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000272
273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000274class Settings(object):
275 def __init__(self):
276 self.default_server = None
277 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000278 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000279 self.is_git_svn = None
280 self.svn_branch = None
281 self.tree_status_url = None
282 self.viewvc_url = None
283 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000284 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000285 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000286 self.project = None
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000287 self.force_https_commit_url = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000288 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000289
290 def LazyUpdateIfNeeded(self):
291 """Updates the settings from a codereview.settings file, if available."""
292 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000293 # The only value that actually changes the behavior is
294 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000295 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000296 error_ok=True
297 ).strip().lower()
298
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000299 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000300 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000301 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000302 # set updated to True to avoid infinite calling loop
303 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000304 self.updated = True
305 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000306 self.updated = True
307
308 def GetDefaultServerUrl(self, error_ok=False):
309 if not self.default_server:
310 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000311 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000312 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000313 if error_ok:
314 return self.default_server
315 if not self.default_server:
316 error_message = ('Could not find settings file. You must configure '
317 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000318 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000319 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000320 return self.default_server
321
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000322 @staticmethod
323 def GetRelativeRoot():
324 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000325
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000327 if self.root is None:
328 self.root = os.path.abspath(self.GetRelativeRoot())
329 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000330
331 def GetIsGitSvn(self):
332 """Return true if this repo looks like it's using git-svn."""
333 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000334 if self.GetPendingRefPrefix():
335 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
336 self.is_git_svn = False
337 else:
338 # If you have any "svn-remote.*" config keys, we think you're using svn.
339 self.is_git_svn = RunGitWithCode(
340 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000341 return self.is_git_svn
342
343 def GetSVNBranch(self):
344 if self.svn_branch is None:
345 if not self.GetIsGitSvn():
346 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
347
348 # Try to figure out which remote branch we're based on.
349 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000350 # 1) iterate through our branch history and find the svn URL.
351 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000352
353 # regexp matching the git-svn line that contains the URL.
354 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
355
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000356 # We don't want to go through all of history, so read a line from the
357 # pipe at a time.
358 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000359 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000360 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
361 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000362 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000363 for line in proc.stdout:
364 match = git_svn_re.match(line)
365 if match:
366 url = match.group(1)
367 proc.stdout.close() # Cut pipe.
368 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000369
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000370 if url:
371 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
372 remotes = RunGit(['config', '--get-regexp',
373 r'^svn-remote\..*\.url']).splitlines()
374 for remote in remotes:
375 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000376 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000377 remote = match.group(1)
378 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000379 rewrite_root = RunGit(
380 ['config', 'svn-remote.%s.rewriteRoot' % remote],
381 error_ok=True).strip()
382 if rewrite_root:
383 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000384 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000385 ['config', 'svn-remote.%s.fetch' % remote],
386 error_ok=True).strip()
387 if fetch_spec:
388 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
389 if self.svn_branch:
390 break
391 branch_spec = RunGit(
392 ['config', 'svn-remote.%s.branches' % remote],
393 error_ok=True).strip()
394 if branch_spec:
395 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
396 if self.svn_branch:
397 break
398 tag_spec = RunGit(
399 ['config', 'svn-remote.%s.tags' % remote],
400 error_ok=True).strip()
401 if tag_spec:
402 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
403 if self.svn_branch:
404 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000405
406 if not self.svn_branch:
407 DieWithError('Can\'t guess svn branch -- try specifying it on the '
408 'command line')
409
410 return self.svn_branch
411
412 def GetTreeStatusUrl(self, error_ok=False):
413 if not self.tree_status_url:
414 error_message = ('You must configure your tree status URL by running '
415 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000416 self.tree_status_url = self._GetRietveldConfig(
417 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000418 return self.tree_status_url
419
420 def GetViewVCUrl(self):
421 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000422 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000423 return self.viewvc_url
424
rmistry@google.com90752582014-01-14 21:04:50 +0000425 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000426 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000427
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000428 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000429 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000430
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000431 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000432 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000433
ukai@chromium.orge8077812012-02-03 03:41:46 +0000434 def GetIsGerrit(self):
435 """Return true if this repo is assosiated with gerrit code review system."""
436 if self.is_gerrit is None:
437 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
438 return self.is_gerrit
439
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000440 def GetGitEditor(self):
441 """Return the editor specified in the git config, or None if none is."""
442 if self.git_editor is None:
443 self.git_editor = self._GetConfig('core.editor', error_ok=True)
444 return self.git_editor or None
445
thestig@chromium.org44202a22014-03-11 19:22:18 +0000446 def GetLintRegex(self):
447 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
448 DEFAULT_LINT_REGEX)
449
450 def GetLintIgnoreRegex(self):
451 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
452 DEFAULT_LINT_IGNORE_REGEX)
453
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000454 def GetProject(self):
455 if not self.project:
456 self.project = self._GetRietveldConfig('project', error_ok=True)
457 return self.project
458
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000459 def GetForceHttpsCommitUrl(self):
460 if not self.force_https_commit_url:
461 self.force_https_commit_url = self._GetRietveldConfig(
462 'force-https-commit-url', error_ok=True)
463 return self.force_https_commit_url
464
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000465 def GetPendingRefPrefix(self):
466 if not self.pending_ref_prefix:
467 self.pending_ref_prefix = self._GetRietveldConfig(
468 'pending-ref-prefix', error_ok=True)
469 return self.pending_ref_prefix
470
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000471 def _GetRietveldConfig(self, param, **kwargs):
472 return self._GetConfig('rietveld.' + param, **kwargs)
473
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000474 def _GetConfig(self, param, **kwargs):
475 self.LazyUpdateIfNeeded()
476 return RunGit(['config', param], **kwargs).strip()
477
478
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000479def ShortBranchName(branch):
480 """Convert a name like 'refs/heads/foo' to just 'foo'."""
481 return branch.replace('refs/heads/', '')
482
483
484class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000485 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000486 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000487 global settings
488 if not settings:
489 # Happens when git_cl.py is used as a utility library.
490 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 settings.GetDefaultServerUrl()
492 self.branchref = branchref
493 if self.branchref:
494 self.branch = ShortBranchName(self.branchref)
495 else:
496 self.branch = None
497 self.rietveld_server = None
498 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000499 self.lookedup_issue = False
500 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000501 self.has_description = False
502 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000503 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000504 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000505 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000506 self.cc = None
507 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000508 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000509 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000510
511 def GetCCList(self):
512 """Return the users cc'd on this CL.
513
514 Return is a string suitable for passing to gcl with the --cc flag.
515 """
516 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000517 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000518 more_cc = ','.join(self.watchers)
519 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
520 return self.cc
521
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000522 def GetCCListWithoutDefault(self):
523 """Return the users cc'd on this CL excluding default ones."""
524 if self.cc is None:
525 self.cc = ','.join(self.watchers)
526 return self.cc
527
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000528 def SetWatchers(self, watchers):
529 """Set the list of email addresses that should be cc'd based on the changed
530 files in this CL.
531 """
532 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000533
534 def GetBranch(self):
535 """Returns the short branch name, e.g. 'master'."""
536 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000537 branchref = RunGit(['symbolic-ref', 'HEAD'],
538 stderr=subprocess2.VOID, error_ok=True).strip()
539 if not branchref:
540 return None
541 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000542 self.branch = ShortBranchName(self.branchref)
543 return self.branch
544
545 def GetBranchRef(self):
546 """Returns the full branch name, e.g. 'refs/heads/master'."""
547 self.GetBranch() # Poke the lazy loader.
548 return self.branchref
549
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000550 @staticmethod
551 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000552 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000553 e.g. 'origin', 'refs/heads/master'
554 """
555 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000556 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
557 error_ok=True).strip()
558 if upstream_branch:
559 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
560 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000561 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
562 error_ok=True).strip()
563 if upstream_branch:
564 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000565 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000566 # Fall back on trying a git-svn upstream branch.
567 if settings.GetIsGitSvn():
568 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000570 # Else, try to guess the origin remote.
571 remote_branches = RunGit(['branch', '-r']).split()
572 if 'origin/master' in remote_branches:
573 # Fall back on origin/master if it exits.
574 remote = 'origin'
575 upstream_branch = 'refs/heads/master'
576 elif 'origin/trunk' in remote_branches:
577 # Fall back on origin/trunk if it exists. Generally a shared
578 # git-svn clone
579 remote = 'origin'
580 upstream_branch = 'refs/heads/trunk'
581 else:
582 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583Either pass complete "git diff"-style arguments, like
584 git cl upload origin/master
585or verify this branch is set up to track another (via the --track argument to
586"git checkout -b ...").""")
587
588 return remote, upstream_branch
589
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000590 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000591 return git_common.get_or_create_merge_base(self.GetBranch(),
592 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000593
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000594 def GetUpstreamBranch(self):
595 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000596 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000597 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000598 upstream_branch = upstream_branch.replace('refs/heads/',
599 'refs/remotes/%s/' % remote)
600 upstream_branch = upstream_branch.replace('refs/branch-heads/',
601 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000602 self.upstream_branch = upstream_branch
603 return self.upstream_branch
604
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000605 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000606 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000607 remote, branch = None, self.GetBranch()
608 seen_branches = set()
609 while branch not in seen_branches:
610 seen_branches.add(branch)
611 remote, branch = self.FetchUpstreamTuple(branch)
612 branch = ShortBranchName(branch)
613 if remote != '.' or branch.startswith('refs/remotes'):
614 break
615 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000616 remotes = RunGit(['remote'], error_ok=True).split()
617 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000618 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000619 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000620 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000621 logging.warning('Could not determine which remote this change is '
622 'associated with, so defaulting to "%s". This may '
623 'not be what you want. You may prevent this message '
624 'by running "git svn info" as documented here: %s',
625 self._remote,
626 GIT_INSTRUCTIONS_URL)
627 else:
628 logging.warn('Could not determine which remote this change is '
629 'associated with. You may prevent this message by '
630 'running "git svn info" as documented here: %s',
631 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000632 branch = 'HEAD'
633 if branch.startswith('refs/remotes'):
634 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000635 elif branch.startswith('refs/branch-heads/'):
636 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000637 else:
638 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000639 return self._remote
640
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000641 def GitSanityChecks(self, upstream_git_obj):
642 """Checks git repo status and ensures diff is from local commits."""
643
sbc@chromium.org79706062015-01-14 21:18:12 +0000644 if upstream_git_obj is None:
645 if self.GetBranch() is None:
646 print >> sys.stderr, (
647 'ERROR: unable to dertermine current branch (detached HEAD?)')
648 else:
649 print >> sys.stderr, (
650 'ERROR: no upstream branch')
651 return False
652
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000653 # Verify the commit we're diffing against is in our current branch.
654 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
655 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
656 if upstream_sha != common_ancestor:
657 print >> sys.stderr, (
658 'ERROR: %s is not in the current branch. You may need to rebase '
659 'your tracking branch' % upstream_sha)
660 return False
661
662 # List the commits inside the diff, and verify they are all local.
663 commits_in_diff = RunGit(
664 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
665 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
666 remote_branch = remote_branch.strip()
667 if code != 0:
668 _, remote_branch = self.GetRemoteBranch()
669
670 commits_in_remote = RunGit(
671 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
672
673 common_commits = set(commits_in_diff) & set(commits_in_remote)
674 if common_commits:
675 print >> sys.stderr, (
676 'ERROR: Your diff contains %d commits already in %s.\n'
677 'Run "git log --oneline %s..HEAD" to get a list of commits in '
678 'the diff. If you are using a custom git flow, you can override'
679 ' the reference used for this check with "git config '
680 'gitcl.remotebranch <git-ref>".' % (
681 len(common_commits), remote_branch, upstream_git_obj))
682 return False
683 return True
684
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000685 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000686 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000687
688 Returns None if it is not set.
689 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000690 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
691 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000692
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000693 def GetGitSvnRemoteUrl(self):
694 """Return the configured git-svn remote URL parsed from git svn info.
695
696 Returns None if it is not set.
697 """
698 # URL is dependent on the current directory.
699 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
700 if data:
701 keys = dict(line.split(': ', 1) for line in data.splitlines()
702 if ': ' in line)
703 return keys.get('URL', None)
704 return None
705
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706 def GetRemoteUrl(self):
707 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
708
709 Returns None if there is no remote.
710 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000711 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000712 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
713
714 # If URL is pointing to a local directory, it is probably a git cache.
715 if os.path.isdir(url):
716 url = RunGit(['config', 'remote.%s.url' % remote],
717 error_ok=True,
718 cwd=url).strip()
719 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720
721 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000722 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000723 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000725 self.issue = int(issue) or None if issue else None
726 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727 return self.issue
728
729 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000730 if not self.rietveld_server:
731 # If we're on a branch then get the server potentially associated
732 # with that branch.
733 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000734 rietveld_server_config = self._RietveldServer()
735 if rietveld_server_config:
736 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
737 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000738 if not self.rietveld_server:
739 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740 return self.rietveld_server
741
742 def GetIssueURL(self):
743 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000744 if not self.GetIssue():
745 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
747
748 def GetDescription(self, pretty=False):
749 if not self.has_description:
750 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000751 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000752 try:
753 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000754 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000755 if e.code == 404:
756 DieWithError(
757 ('\nWhile fetching the description for issue %d, received a '
758 '404 (not found)\n'
759 'error. It is likely that you deleted this '
760 'issue on the server. If this is the\n'
761 'case, please run\n\n'
762 ' git cl issue 0\n\n'
763 'to clear the association with the deleted issue. Then run '
764 'this command again.') % issue)
765 else:
766 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000767 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000768 except urllib2.URLError as e:
769 print >> sys.stderr, (
770 'Warning: Failed to retrieve CL description due to network '
771 'failure.')
772 self.description = ''
773
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000774 self.has_description = True
775 if pretty:
776 wrapper = textwrap.TextWrapper()
777 wrapper.initial_indent = wrapper.subsequent_indent = ' '
778 return wrapper.fill(self.description)
779 return self.description
780
781 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000782 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000783 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000784 patchset = RunGit(['config', self._PatchsetSetting()],
785 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000786 self.patchset = int(patchset) or None if patchset else None
787 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 return self.patchset
789
790 def SetPatchset(self, patchset):
791 """Set this branch's patchset. If patchset=0, clears the patchset."""
792 if patchset:
793 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000794 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795 else:
796 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000797 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000798 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000800 def GetMostRecentPatchset(self):
801 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000802
803 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000804 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000805 '/download/issue%s_%s.diff' % (issue, patchset))
806
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000807 def GetIssueProperties(self):
808 if self._props is None:
809 issue = self.GetIssue()
810 if not issue:
811 self._props = {}
812 else:
813 self._props = self.RpcServer().get_issue_properties(issue, True)
814 return self._props
815
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000816 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000817 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000818
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000819 def AddComment(self, message):
820 return self.RpcServer().add_comment(self.GetIssue(), message)
821
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822 def SetIssue(self, issue):
823 """Set this branch's issue. If issue=0, clears the issue."""
824 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000825 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826 RunGit(['config', self._IssueSetting(), str(issue)])
827 if self.rietveld_server:
828 RunGit(['config', self._RietveldServer(), self.rietveld_server])
829 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000830 current_issue = self.GetIssue()
831 if current_issue:
832 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000833 self.issue = None
834 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000836 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000837 if not self.GitSanityChecks(upstream_branch):
838 DieWithError('\nGit sanity check failure')
839
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000840 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000841 if not root:
842 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000843 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000844
845 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000846 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000847 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000848 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000849 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000850 except subprocess2.CalledProcessError:
851 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000852 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000853 'This branch probably doesn\'t exist anymore. To reset the\n'
854 'tracking branch, please run\n'
855 ' git branch --set-upstream %s trunk\n'
856 'replacing trunk with origin/master or the relevant branch') %
857 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000858
maruel@chromium.org52424302012-08-29 15:14:30 +0000859 issue = self.GetIssue()
860 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000861 if issue:
862 description = self.GetDescription()
863 else:
864 # If the change was never uploaded, use the log messages of all commits
865 # up to the branch point, as git cl upload will prefill the description
866 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000867 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
868 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000869
870 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000871 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000872 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000873 name,
874 description,
875 absroot,
876 files,
877 issue,
878 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000879 author,
880 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000881
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +0000882 def GetStatus(self):
883 """Apply a rough heuristic to give a simple summary of an issue's review
884 or CQ status, assuming adherence to a common workflow.
885
886 Returns None if no issue for this branch, or one of the following keywords:
887 * 'error' - error from review tool (including deleted issues)
888 * 'unsent' - not sent for review
889 * 'waiting' - waiting for review
890 * 'reply' - waiting for owner to reply to review
891 * 'lgtm' - LGTM from at least one approved reviewer
892 * 'commit' - in the commit queue
893 * 'closed' - closed
894 """
895 if not self.GetIssue():
896 return None
897
898 try:
899 props = self.GetIssueProperties()
900 except urllib2.HTTPError:
901 return 'error'
902
903 if props.get('closed'):
904 # Issue is closed.
905 return 'closed'
906 if props.get('commit'):
907 # Issue is in the commit queue.
908 return 'commit'
909
910 try:
911 reviewers = self.GetApprovingReviewers()
912 except urllib2.HTTPError:
913 return 'error'
914
915 if reviewers:
916 # Was LGTM'ed.
917 return 'lgtm'
918
919 messages = props.get('messages') or []
920
921 if not messages:
922 # No message was sent.
923 return 'unsent'
924 if messages[-1]['sender'] != props.get('owner_email'):
925 # Non-LGTM reply from non-owner
926 return 'reply'
927 return 'waiting'
928
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000929 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000930 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000931
932 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000933 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000934 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000935 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000936 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000937 except presubmit_support.PresubmitFailure, e:
938 DieWithError(
939 ('%s\nMaybe your depot_tools is out of date?\n'
940 'If all fails, contact maruel@') % e)
941
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000942 def UpdateDescription(self, description):
943 self.description = description
944 return self.RpcServer().update_description(
945 self.GetIssue(), self.description)
946
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000947 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000948 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000949 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000951 def SetFlag(self, flag, value):
952 """Patchset must match."""
953 if not self.GetPatchset():
954 DieWithError('The patchset needs to match. Send another patchset.')
955 try:
956 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000957 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000958 except urllib2.HTTPError, e:
959 if e.code == 404:
960 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
961 if e.code == 403:
962 DieWithError(
963 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
964 'match?') % (self.GetIssue(), self.GetPatchset()))
965 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000966
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000967 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968 """Returns an upload.RpcServer() to access this review's rietveld instance.
969 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000970 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000971 self._rpc_server = rietveld.CachingRietveld(
972 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000973 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974
975 def _IssueSetting(self):
976 """Return the git setting that stores this change's issue."""
977 return 'branch.%s.rietveldissue' % self.GetBranch()
978
979 def _PatchsetSetting(self):
980 """Return the git setting that stores this change's most recent patchset."""
981 return 'branch.%s.rietveldpatchset' % self.GetBranch()
982
983 def _RietveldServer(self):
984 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000985 branch = self.GetBranch()
986 if branch:
987 return 'branch.%s.rietveldserver' % branch
988 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989
990
991def GetCodereviewSettingsInteractively():
992 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000993 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000994 server = settings.GetDefaultServerUrl(error_ok=True)
995 prompt = 'Rietveld server (host[:port])'
996 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000997 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 if not server and not newserver:
999 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001000 if newserver:
1001 newserver = gclient_utils.UpgradeToHttps(newserver)
1002 if newserver != server:
1003 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001004
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001005 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006 prompt = caption
1007 if initial:
1008 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001009 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001010 if new_val == 'x':
1011 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001012 elif new_val:
1013 if is_url:
1014 new_val = gclient_utils.UpgradeToHttps(new_val)
1015 if new_val != initial:
1016 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001017
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001018 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001019 SetProperty(settings.GetDefaultPrivateFlag(),
1020 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001021 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001022 'tree-status-url', False)
1023 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001024 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025
1026 # TODO: configure a default branch to diff against, rather than this
1027 # svn-based hackery.
1028
1029
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001030class ChangeDescription(object):
1031 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001032 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001033 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001034
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001035 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001036 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001037
agable@chromium.org42c20792013-09-12 17:34:49 +00001038 @property # www.logilab.org/ticket/89786
1039 def description(self): # pylint: disable=E0202
1040 return '\n'.join(self._description_lines)
1041
1042 def set_description(self, desc):
1043 if isinstance(desc, basestring):
1044 lines = desc.splitlines()
1045 else:
1046 lines = [line.rstrip() for line in desc]
1047 while lines and not lines[0]:
1048 lines.pop(0)
1049 while lines and not lines[-1]:
1050 lines.pop(-1)
1051 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001052
piman@chromium.org336f9122014-09-04 02:16:55 +00001053 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001054 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001055 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001056 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001057 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001058 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001059
agable@chromium.org42c20792013-09-12 17:34:49 +00001060 # Get the set of R= and TBR= lines and remove them from the desciption.
1061 regexp = re.compile(self.R_LINE)
1062 matches = [regexp.match(line) for line in self._description_lines]
1063 new_desc = [l for i, l in enumerate(self._description_lines)
1064 if not matches[i]]
1065 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001066
agable@chromium.org42c20792013-09-12 17:34:49 +00001067 # Construct new unified R= and TBR= lines.
1068 r_names = []
1069 tbr_names = []
1070 for match in matches:
1071 if not match:
1072 continue
1073 people = cleanup_list([match.group(2).strip()])
1074 if match.group(1) == 'TBR':
1075 tbr_names.extend(people)
1076 else:
1077 r_names.extend(people)
1078 for name in r_names:
1079 if name not in reviewers:
1080 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001081 if add_owners_tbr:
1082 owners_db = owners.Database(change.RepositoryRoot(),
1083 fopen=file, os_path=os.path, glob=glob.glob)
1084 all_reviewers = set(tbr_names + reviewers)
1085 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1086 all_reviewers)
1087 tbr_names.extend(owners_db.reviewers_for(missing_files,
1088 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001089 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1090 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1091
1092 # Put the new lines in the description where the old first R= line was.
1093 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1094 if 0 <= line_loc < len(self._description_lines):
1095 if new_tbr_line:
1096 self._description_lines.insert(line_loc, new_tbr_line)
1097 if new_r_line:
1098 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001099 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001100 if new_r_line:
1101 self.append_footer(new_r_line)
1102 if new_tbr_line:
1103 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001104
1105 def prompt(self):
1106 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001107 self.set_description([
1108 '# Enter a description of the change.',
1109 '# This will be displayed on the codereview site.',
1110 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001111 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001112 '--------------------',
1113 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001114
agable@chromium.org42c20792013-09-12 17:34:49 +00001115 regexp = re.compile(self.BUG_LINE)
1116 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001117 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001118 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001119 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001120 if not content:
1121 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001122 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001123
1124 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001125 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1126 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001127 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001128 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001129
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001130 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001131 if self._description_lines:
1132 # Add an empty line if either the last line or the new line isn't a tag.
1133 last_line = self._description_lines[-1]
1134 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1135 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1136 self._description_lines.append('')
1137 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001138
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001139 def get_reviewers(self):
1140 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001141 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1142 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001143 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001144
1145
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001146def get_approving_reviewers(props):
1147 """Retrieves the reviewers that approved a CL from the issue properties with
1148 messages.
1149
1150 Note that the list may contain reviewers that are not committer, thus are not
1151 considered by the CQ.
1152 """
1153 return sorted(
1154 set(
1155 message['sender']
1156 for message in props['messages']
1157 if message['approval'] and message['sender'] in props['reviewers']
1158 )
1159 )
1160
1161
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162def FindCodereviewSettingsFile(filename='codereview.settings'):
1163 """Finds the given file starting in the cwd and going up.
1164
1165 Only looks up to the top of the repository unless an
1166 'inherit-review-settings-ok' file exists in the root of the repository.
1167 """
1168 inherit_ok_file = 'inherit-review-settings-ok'
1169 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001170 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1172 root = '/'
1173 while True:
1174 if filename in os.listdir(cwd):
1175 if os.path.isfile(os.path.join(cwd, filename)):
1176 return open(os.path.join(cwd, filename))
1177 if cwd == root:
1178 break
1179 cwd = os.path.dirname(cwd)
1180
1181
1182def LoadCodereviewSettingsFromFile(fileobj):
1183 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001184 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186 def SetProperty(name, setting, unset_error_ok=False):
1187 fullname = 'rietveld.' + name
1188 if setting in keyvals:
1189 RunGit(['config', fullname, keyvals[setting]])
1190 else:
1191 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1192
1193 SetProperty('server', 'CODE_REVIEW_SERVER')
1194 # Only server setting is required. Other settings can be absent.
1195 # In that case, we ignore errors raised during option deletion attempt.
1196 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001197 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1199 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001200 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001201 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001202 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1203 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001204 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001205 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001206 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001208 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001209 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001210
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1212 #should be of the form
1213 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1214 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1215 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1216 keyvals['ORIGIN_URL_CONFIG']])
1217
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001218
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001219def urlretrieve(source, destination):
1220 """urllib is broken for SSL connections via a proxy therefore we
1221 can't use urllib.urlretrieve()."""
1222 with open(destination, 'w') as f:
1223 f.write(urllib2.urlopen(source).read())
1224
1225
ukai@chromium.org712d6102013-11-27 00:52:58 +00001226def hasSheBang(fname):
1227 """Checks fname is a #! script."""
1228 with open(fname) as f:
1229 return f.read(2).startswith('#!')
1230
1231
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001232def DownloadHooks(force):
1233 """downloads hooks
1234
1235 Args:
1236 force: True to update hooks. False to install hooks if not present.
1237 """
1238 if not settings.GetIsGerrit():
1239 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001240 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001241 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1242 if not os.access(dst, os.X_OK):
1243 if os.path.exists(dst):
1244 if not force:
1245 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001246 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001247 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001248 if not hasSheBang(dst):
1249 DieWithError('Not a script: %s\n'
1250 'You need to download from\n%s\n'
1251 'into .git/hooks/commit-msg and '
1252 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001253 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1254 except Exception:
1255 if os.path.exists(dst):
1256 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001257 DieWithError('\nFailed to download hooks.\n'
1258 'You need to download from\n%s\n'
1259 'into .git/hooks/commit-msg and '
1260 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001261
1262
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001263@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001265 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001267 parser.add_option('--activate-update', action='store_true',
1268 help='activate auto-updating [rietveld] section in '
1269 '.git/config')
1270 parser.add_option('--deactivate-update', action='store_true',
1271 help='deactivate auto-updating [rietveld] section in '
1272 '.git/config')
1273 options, args = parser.parse_args(args)
1274
1275 if options.deactivate_update:
1276 RunGit(['config', 'rietveld.autoupdate', 'false'])
1277 return
1278
1279 if options.activate_update:
1280 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1281 return
1282
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001283 if len(args) == 0:
1284 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001285 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 return 0
1287
1288 url = args[0]
1289 if not url.endswith('codereview.settings'):
1290 url = os.path.join(url, 'codereview.settings')
1291
1292 # Load code review settings and download hooks (if available).
1293 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001294 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295 return 0
1296
1297
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001298def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001299 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001300 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1301 branch = ShortBranchName(branchref)
1302 _, args = parser.parse_args(args)
1303 if not args:
1304 print("Current base-url:")
1305 return RunGit(['config', 'branch.%s.base-url' % branch],
1306 error_ok=False).strip()
1307 else:
1308 print("Setting base-url to %s" % args[0])
1309 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1310 error_ok=False).strip()
1311
1312
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001313def color_for_status(status):
1314 """Maps a Changelist status to color, for CMDstatus and other tools."""
1315 return {
1316 'unsent': Fore.RED,
1317 'waiting': Fore.BLUE,
1318 'reply': Fore.YELLOW,
1319 'lgtm': Fore.GREEN,
1320 'commit': Fore.MAGENTA,
1321 'closed': Fore.CYAN,
1322 'error': Fore.WHITE,
1323 }.get(status, Fore.WHITE)
1324
1325
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001327 """Show status of changelists.
1328
1329 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001330 - Red not sent for review or broken
1331 - Blue waiting for review
1332 - Yellow waiting for you to reply to review
1333 - Green LGTM'ed
1334 - Magenta in the commit queue
1335 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001336
1337 Also see 'git cl comments'.
1338 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 parser.add_option('--field',
1340 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001341 parser.add_option('-f', '--fast', action='store_true',
1342 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001344 if args:
1345 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001348 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001349 if options.field.startswith('desc'):
1350 print cl.GetDescription()
1351 elif options.field == 'id':
1352 issueid = cl.GetIssue()
1353 if issueid:
1354 print issueid
1355 elif options.field == 'patch':
1356 patchset = cl.GetPatchset()
1357 if patchset:
1358 print patchset
1359 elif options.field == 'url':
1360 url = cl.GetIssueURL()
1361 if url:
1362 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001363 return 0
1364
1365 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1366 if not branches:
1367 print('No local branch found.')
1368 return 0
1369
1370 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001371 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001372 alignment = max(5, max(len(b) for b in branches))
1373 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001374 # Adhoc thread pool to request data concurrently.
1375 output = Queue.Queue()
1376
1377 # Silence upload.py otherwise it becomes unweldly.
1378 upload.verbosity = 0
1379
1380 if not options.fast:
1381 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001382 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001383 c = Changelist(branchref=b)
1384 i = c.GetIssueURL()
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001385 status = c.GetStatus()
1386 color = color_for_status(status)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001387
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001388 if i and (not status or status == 'error'):
1389 # The issue probably doesn't exist anymore.
1390 i += ' (broken)'
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001391
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001392 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001393
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001394 # Process one branch synchronously to work through authentication, then
1395 # spawn threads to process all the other branches in parallel.
1396 if branches:
1397 fetch(branches[0])
1398 threads = [
1399 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001400 for t in threads:
1401 t.daemon = True
1402 t.start()
1403 else:
1404 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1405 for b in branches:
1406 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001407 url = c.GetIssueURL()
1408 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001409
1410 tmp = {}
1411 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001412 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001413 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001414 b, i, color = output.get()
1415 tmp[b] = (i, color)
1416 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001417 reset = Fore.RESET
1418 if not sys.stdout.isatty():
1419 color = ''
1420 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001421 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001422 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001423
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001424 cl = Changelist()
1425 print
1426 print 'Current branch:',
1427 if not cl.GetIssue():
1428 print 'no issue assigned.'
1429 return 0
1430 print cl.GetBranch()
1431 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001432 if not options.fast:
1433 print 'Issue description:'
1434 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 return 0
1436
1437
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001438def colorize_CMDstatus_doc():
1439 """To be called once in main() to add colors to git cl status help."""
1440 colors = [i for i in dir(Fore) if i[0].isupper()]
1441
1442 def colorize_line(line):
1443 for color in colors:
1444 if color in line.upper():
1445 # Extract whitespaces first and the leading '-'.
1446 indent = len(line) - len(line.lstrip(' ')) + 1
1447 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1448 return line
1449
1450 lines = CMDstatus.__doc__.splitlines()
1451 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1452
1453
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001454@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001456 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457
1458 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001459 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001460 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461
1462 cl = Changelist()
1463 if len(args) > 0:
1464 try:
1465 issue = int(args[0])
1466 except ValueError:
1467 DieWithError('Pass a number to set the issue or none to list it.\n'
1468 'Maybe you want to run git cl status?')
1469 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001470 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001471 return 0
1472
1473
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001474def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001475 """Shows or posts review comments for any changelist."""
1476 parser.add_option('-a', '--add-comment', dest='comment',
1477 help='comment to add to an issue')
1478 parser.add_option('-i', dest='issue',
1479 help="review issue id (defaults to current issue)")
1480 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001481
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001482 issue = None
1483 if options.issue:
1484 try:
1485 issue = int(options.issue)
1486 except ValueError:
1487 DieWithError('A review issue id is expected to be a number')
1488
1489 cl = Changelist(issue=issue)
1490
1491 if options.comment:
1492 cl.AddComment(options.comment)
1493 return 0
1494
1495 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001496 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001497 if message['disapproval']:
1498 color = Fore.RED
1499 elif message['approval']:
1500 color = Fore.GREEN
1501 elif message['sender'] == data['owner_email']:
1502 color = Fore.MAGENTA
1503 else:
1504 color = Fore.BLUE
1505 print '\n%s%s %s%s' % (
1506 color, message['date'].split('.', 1)[0], message['sender'],
1507 Fore.RESET)
1508 if message['text'].strip():
1509 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001510 return 0
1511
1512
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001513def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001514 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001515 cl = Changelist()
1516 if not cl.GetIssue():
1517 DieWithError('This branch has no associated changelist.')
1518 description = ChangeDescription(cl.GetDescription())
1519 description.prompt()
1520 cl.UpdateDescription(description.description)
1521 return 0
1522
1523
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001524def CreateDescriptionFromLog(args):
1525 """Pulls out the commit log to use as a base for the CL description."""
1526 log_args = []
1527 if len(args) == 1 and not args[0].endswith('.'):
1528 log_args = [args[0] + '..']
1529 elif len(args) == 1 and args[0].endswith('...'):
1530 log_args = [args[0][:-1]]
1531 elif len(args) == 2:
1532 log_args = [args[0] + '..' + args[1]]
1533 else:
1534 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001535 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536
1537
thestig@chromium.org44202a22014-03-11 19:22:18 +00001538def CMDlint(parser, args):
1539 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001540 parser.add_option('--filter', action='append', metavar='-x,+y',
1541 help='Comma-separated list of cpplint\'s category-filters')
1542 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001543
1544 # Access to a protected member _XX of a client class
1545 # pylint: disable=W0212
1546 try:
1547 import cpplint
1548 import cpplint_chromium
1549 except ImportError:
1550 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1551 return 1
1552
1553 # Change the current working directory before calling lint so that it
1554 # shows the correct base.
1555 previous_cwd = os.getcwd()
1556 os.chdir(settings.GetRoot())
1557 try:
1558 cl = Changelist()
1559 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1560 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001561 if not files:
1562 print "Cannot lint an empty CL"
1563 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001564
1565 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001566 command = args + files
1567 if options.filter:
1568 command = ['--filter=' + ','.join(options.filter)] + command
1569 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001570
1571 white_regex = re.compile(settings.GetLintRegex())
1572 black_regex = re.compile(settings.GetLintIgnoreRegex())
1573 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1574 for filename in filenames:
1575 if white_regex.match(filename):
1576 if black_regex.match(filename):
1577 print "Ignoring file %s" % filename
1578 else:
1579 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1580 extra_check_functions)
1581 else:
1582 print "Skipping file %s" % filename
1583 finally:
1584 os.chdir(previous_cwd)
1585 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1586 if cpplint._cpplint_state.error_count != 0:
1587 return 1
1588 return 0
1589
1590
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001591def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001592 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001593 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001594 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001595 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001596 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001597 (options, args) = parser.parse_args(args)
1598
ukai@chromium.org259e4682012-10-25 07:36:33 +00001599 if not options.force and is_dirty_git_tree('presubmit'):
1600 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001601 return 1
1602
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001603 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001604 if args:
1605 base_branch = args[0]
1606 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001607 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001608 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001610 cl.RunHook(
1611 committing=not options.upload,
1612 may_prompt=False,
1613 verbose=options.verbose,
1614 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001615 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616
1617
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001618def AddChangeIdToCommitMessage(options, args):
1619 """Re-commits using the current message, assumes the commit hook is in
1620 place.
1621 """
1622 log_desc = options.message or CreateDescriptionFromLog(args)
1623 git_command = ['commit', '--amend', '-m', log_desc]
1624 RunGit(git_command)
1625 new_log_desc = CreateDescriptionFromLog(args)
1626 if CHANGE_ID in new_log_desc:
1627 print 'git-cl: Added Change-Id to commit message.'
1628 else:
1629 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1630
1631
piman@chromium.org336f9122014-09-04 02:16:55 +00001632def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001633 """upload the current branch to gerrit."""
1634 # We assume the remote called "origin" is the one we want.
1635 # It is probably not worthwhile to support different workflows.
1636 remote = 'origin'
1637 branch = 'master'
1638 if options.target_branch:
1639 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001641 change_desc = ChangeDescription(
1642 options.message or CreateDescriptionFromLog(args))
1643 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001644 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001646 if CHANGE_ID not in change_desc.description:
1647 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001648
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001649 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001650 if len(commits) > 1:
1651 print('WARNING: This will upload %d commits. Run the following command '
1652 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001653 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001654 print('You can also use `git squash-branch` to squash these into a single'
1655 'commit.')
1656 ask_for_data('About to upload; enter to confirm.')
1657
piman@chromium.org336f9122014-09-04 02:16:55 +00001658 if options.reviewers or options.tbr_owners:
1659 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001660
ukai@chromium.orge8077812012-02-03 03:41:46 +00001661 receive_options = []
1662 cc = cl.GetCCList().split(',')
1663 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001664 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001665 cc = filter(None, cc)
1666 if cc:
1667 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001668 if change_desc.get_reviewers():
1669 receive_options.extend(
1670 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001671
ukai@chromium.orge8077812012-02-03 03:41:46 +00001672 git_command = ['push']
1673 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001674 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001675 ' '.join(receive_options))
1676 git_command += [remote, 'HEAD:refs/for/' + branch]
1677 RunGit(git_command)
1678 # TODO(ukai): parse Change-Id: and set issue number?
1679 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001680
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001681
wittman@chromium.org455dc922015-01-26 20:15:50 +00001682def GetTargetRef(remote, remote_branch, target_branch, pending_prefix):
1683 """Computes the remote branch ref to use for the CL.
1684
1685 Args:
1686 remote (str): The git remote for the CL.
1687 remote_branch (str): The git remote branch for the CL.
1688 target_branch (str): The target branch specified by the user.
1689 pending_prefix (str): The pending prefix from the settings.
1690 """
1691 if not (remote and remote_branch):
1692 return None
1693
1694 if target_branch:
1695 # Cannonicalize branch references to the equivalent local full symbolic
1696 # refs, which are then translated into the remote full symbolic refs
1697 # below.
1698 if '/' not in target_branch:
1699 remote_branch = 'refs/remotes/%s/%s' % (remote, target_branch)
1700 else:
1701 prefix_replacements = (
1702 ('^((refs/)?remotes/)?branch-heads/', 'refs/remotes/branch-heads/'),
1703 ('^((refs/)?remotes/)?%s/' % remote, 'refs/remotes/%s/' % remote),
1704 ('^(refs/)?heads/', 'refs/remotes/%s/' % remote),
1705 )
1706 match = None
1707 for regex, replacement in prefix_replacements:
1708 match = re.search(regex, target_branch)
1709 if match:
1710 remote_branch = target_branch.replace(match.group(0), replacement)
1711 break
1712 if not match:
1713 # This is a branch path but not one we recognize; use as-is.
1714 remote_branch = target_branch
1715 elif (not remote_branch.startswith('refs/remotes/branch-heads') and
1716 not remote_branch.startswith('refs/remotes/%s/refs' % remote)):
1717 # Default to master for refs that are not branches.
1718 remote_branch = 'refs/remotes/%s/master' % remote
1719
1720 # Create the true path to the remote branch.
1721 # Does the following translation:
1722 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1723 # * refs/remotes/origin/master -> refs/heads/master
1724 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1725 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1726 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1727 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1728 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1729 'refs/heads/')
1730 elif remote_branch.startswith('refs/remotes/branch-heads'):
1731 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
1732 # If a pending prefix exists then replace refs/ with it.
1733 if pending_prefix:
1734 remote_branch = remote_branch.replace('refs/', pending_prefix)
1735 return remote_branch
1736
1737
piman@chromium.org336f9122014-09-04 02:16:55 +00001738def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001739 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1741 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001742 if options.emulate_svn_auto_props:
1743 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001744
1745 change_desc = None
1746
pgervais@chromium.org91141372014-01-09 23:27:20 +00001747 if options.email is not None:
1748 upload_args.extend(['--email', options.email])
1749
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001751 if options.title:
1752 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001753 if options.message:
1754 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001755 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 print ("This branch is associated with issue %s. "
1757 "Adding patch to that issue." % cl.GetIssue())
1758 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001759 if options.title:
1760 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001761 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001762 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00001763 if options.reviewers or options.tbr_owners:
1764 change_desc.update_reviewers(options.reviewers,
1765 options.tbr_owners,
1766 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001767 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001768 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001769
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001770 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771 print "Description is empty; aborting."
1772 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001773
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001774 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001775 if change_desc.get_reviewers():
1776 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001777 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001778 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001779 DieWithError("Must specify reviewers to send email.")
1780 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001781
1782 # We check this before applying rietveld.private assuming that in
1783 # rietveld.cc only addresses which we can send private CLs to are listed
1784 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1785 # --private is specified explicitly on the command line.
1786 if options.private:
1787 logging.warn('rietveld.cc is ignored since private flag is specified. '
1788 'You need to review and add them manually if necessary.')
1789 cc = cl.GetCCListWithoutDefault()
1790 else:
1791 cc = cl.GetCCList()
1792 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001793 if cc:
1794 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001796 if options.private or settings.GetDefaultPrivateFlag() == "True":
1797 upload_args.append('--private')
1798
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001799 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001800 if not options.find_copies:
1801 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803 # Include the upstream repo's URL in the change -- this is useful for
1804 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001805 remote_url = cl.GetGitBaseUrlFromConfig()
1806 if not remote_url:
1807 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001808 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001809 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00001810 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1811 remote_url = (cl.GetRemoteUrl() + '@'
1812 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001813 if remote_url:
1814 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00001815 remote, remote_branch = cl.GetRemoteBranch()
wittman@chromium.org455dc922015-01-26 20:15:50 +00001816 target_ref = GetTargetRef(remote, remote_branch, options.target_branch,
1817 settings.GetPendingRefPrefix())
1818 if target_ref:
1819 upload_args.extend(['--target_ref', target_ref])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001820
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001821 project = settings.GetProject()
1822 if project:
1823 upload_args.extend(['--project', project])
1824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001825 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001826 upload_args = ['upload'] + upload_args + args
1827 logging.info('upload.RealMain(%s)', upload_args)
1828 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001829 issue = int(issue)
1830 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001831 except KeyboardInterrupt:
1832 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001833 except:
1834 # If we got an exception after the user typed a description for their
1835 # change, back up the description before re-raising.
1836 if change_desc:
1837 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1838 print '\nGot exception while uploading -- saving description to %s\n' \
1839 % backup_path
1840 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001841 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001842 backup_file.close()
1843 raise
1844
1845 if not cl.GetIssue():
1846 cl.SetIssue(issue)
1847 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001848
1849 if options.use_commit_queue:
1850 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851 return 0
1852
1853
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001854def cleanup_list(l):
1855 """Fixes a list so that comma separated items are put as individual items.
1856
1857 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1858 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1859 """
1860 items = sum((i.split(',') for i in l), [])
1861 stripped_items = (i.strip() for i in items)
1862 return sorted(filter(None, stripped_items))
1863
1864
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001865@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001866def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001867 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001868 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1869 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001870 parser.add_option('--bypass-watchlists', action='store_true',
1871 dest='bypass_watchlists',
1872 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001873 parser.add_option('-f', action='store_true', dest='force',
1874 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001875 parser.add_option('-m', dest='message', help='message for patchset')
1876 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001877 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001878 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001879 help='reviewer email addresses')
1880 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001881 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001882 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001883 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001884 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001885 parser.add_option('--emulate_svn_auto_props',
1886 '--emulate-svn-auto-props',
1887 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001888 dest="emulate_svn_auto_props",
1889 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001890 parser.add_option('-c', '--use-commit-queue', action='store_true',
1891 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001892 parser.add_option('--private', action='store_true',
1893 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001894 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001895 '--target-branch',
wittman@chromium.org455dc922015-01-26 20:15:50 +00001896 metavar='TARGET',
1897 help='Apply CL to remote ref TARGET. ' +
1898 'Default: remote branch head, or master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001899 parser.add_option('--email', default=None,
1900 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00001901 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
1902 help='add a set of OWNERS to TBR')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001903
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001904 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001905 (options, args) = parser.parse_args(args)
1906
ukai@chromium.org259e4682012-10-25 07:36:33 +00001907 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001908 return 1
1909
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001910 options.reviewers = cleanup_list(options.reviewers)
1911 options.cc = cleanup_list(options.cc)
1912
ukai@chromium.orge8077812012-02-03 03:41:46 +00001913 cl = Changelist()
1914 if args:
1915 # TODO(ukai): is it ok for gerrit case?
1916 base_branch = args[0]
1917 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00001918 if cl.GetBranch() is None:
1919 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1920
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001921 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001922 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001923 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001924
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001925 # Apply watchlists on upload.
1926 change = cl.GetChange(base_branch, None)
1927 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1928 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001929 if not options.bypass_watchlists:
1930 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001931
ukai@chromium.orge8077812012-02-03 03:41:46 +00001932 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00001933 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001934 # Set the reviewer list now so that presubmit checks can access it.
1935 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00001936 change_description.update_reviewers(options.reviewers,
1937 options.tbr_owners,
1938 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001939 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001940 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001941 may_prompt=not options.force,
1942 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001943 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001944 if not hook_results.should_continue():
1945 return 1
1946 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001947 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001948
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001949 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001950 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001951 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001952 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001953 print ('The last upload made from this repository was patchset #%d but '
1954 'the most recent patchset on the server is #%d.'
1955 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001956 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1957 'from another machine or branch the patch you\'re uploading now '
1958 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001959 ask_for_data('About to upload; enter to confirm.')
1960
iannucci@chromium.org79540052012-10-19 23:15:26 +00001961 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001962 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00001963 return GerritUpload(options, args, cl, change)
1964 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001965 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001966 git_set_branch_value('last-upload-hash',
1967 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001968
1969 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001970
1971
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001972def IsSubmoduleMergeCommit(ref):
1973 # When submodules are added to the repo, we expect there to be a single
1974 # non-git-svn merge commit at remote HEAD with a signature comment.
1975 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001976 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001977 return RunGit(cmd) != ''
1978
1979
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001980def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001981 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001982
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001983 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001984 Updates changelog with metadata (e.g. pointer to review).
1985 Pushes/dcommits the code upstream.
1986 Updates review and closes.
1987 """
1988 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1989 help='bypass upload presubmit hook')
1990 parser.add_option('-m', dest='message',
1991 help="override review description")
1992 parser.add_option('-f', action='store_true', dest='force',
1993 help="force yes to questions (don't prompt)")
1994 parser.add_option('-c', dest='contributor',
1995 help="external contributor for patch (appended to " +
1996 "description and used as author for git). Should be " +
1997 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001998 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999 (options, args) = parser.parse_args(args)
2000 cl = Changelist()
2001
iannucci@chromium.org5724c962014-04-11 09:32:56 +00002002 current = cl.GetBranch()
2003 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
2004 if not settings.GetIsGitSvn() and remote == '.':
2005 print
2006 print 'Attempting to push branch %r into another local branch!' % current
2007 print
2008 print 'Either reparent this branch on top of origin/master:'
2009 print ' git reparent-branch --root'
2010 print
2011 print 'OR run `git rebase-update` if you think the parent branch is already'
2012 print 'committed.'
2013 print
2014 print ' Current parent: %r' % upstream_branch
2015 return 1
2016
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002017 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018 # Default to merging against our best guess of the upstream branch.
2019 args = [cl.GetUpstreamBranch()]
2020
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002021 if options.contributor:
2022 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
2023 print "Please provide contibutor as 'First Last <email@example.com>'"
2024 return 1
2025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002027 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028
ukai@chromium.org259e4682012-10-25 07:36:33 +00002029 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002030 return 1
2031
2032 # This rev-list syntax means "show all commits not in my branch that
2033 # are in base_branch".
2034 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
2035 base_branch]).splitlines()
2036 if upstream_commits:
2037 print ('Base branch "%s" has %d commits '
2038 'not in this branch.' % (base_branch, len(upstream_commits)))
2039 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
2040 return 1
2041
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002042 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002043 svn_head = None
2044 if cmd == 'dcommit' or base_has_submodules:
2045 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
2046 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002047
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002048 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002049 # If the base_head is a submodule merge commit, the first parent of the
2050 # base_head should be a git-svn commit, which is what we're interested in.
2051 base_svn_head = base_branch
2052 if base_has_submodules:
2053 base_svn_head += '^1'
2054
2055 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056 if extra_commits:
2057 print ('This branch has %d additional commits not upstreamed yet.'
2058 % len(extra_commits.splitlines()))
2059 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2060 'before attempting to %s.' % (base_branch, cmd))
2061 return 1
2062
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002063 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002064 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002065 author = None
2066 if options.contributor:
2067 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002068 hook_results = cl.RunHook(
2069 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002070 may_prompt=not options.force,
2071 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002072 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002073 if not hook_results.should_continue():
2074 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002075
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002076 # Check the tree status if the tree status URL is set.
2077 status = GetTreeStatus()
2078 if 'closed' == status:
2079 print('The tree is closed. Please wait for it to reopen. Use '
2080 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2081 return 1
2082 elif 'unknown' == status:
2083 print('Unable to determine tree status. Please verify manually and '
2084 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2085 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002086 else:
2087 breakpad.SendStack(
2088 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002089 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2090 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002091 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002092
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002093 change_desc = ChangeDescription(options.message)
2094 if not change_desc.description and cl.GetIssue():
2095 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002096
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002097 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002098 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002099 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002100 else:
2101 print 'No description set.'
2102 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2103 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002104
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002105 # Keep a separate copy for the commit message, because the commit message
2106 # contains the link to the Rietveld issue, while the Rietveld message contains
2107 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002108 # Keep a separate copy for the commit message.
2109 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002110 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002111
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002112 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002113 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002114 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002116 commit_desc.append_footer('Patch from %s.' % options.contributor)
2117
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002118 print('Description:')
2119 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002121 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002123 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002125 # We want to squash all this branch's commits into one commit with the proper
2126 # description. We do this by doing a "reset --soft" to the base branch (which
2127 # keeps the working copy the same), then dcommitting that. If origin/master
2128 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2129 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002130 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002131 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2132 # Delete the branches if they exist.
2133 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2134 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2135 result = RunGitWithCode(showref_cmd)
2136 if result[0] == 0:
2137 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138
2139 # We might be in a directory that's present in this branch but not in the
2140 # trunk. Move up to the top of the tree so that git commands that expect a
2141 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002142 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002143 if rel_base_path:
2144 os.chdir(rel_base_path)
2145
2146 # Stuff our change into the merge branch.
2147 # We wrap in a try...finally block so if anything goes wrong,
2148 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002149 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002150 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002151 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002152 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002153 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002154 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002155 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002156 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002157 RunGit(
2158 [
2159 'commit', '--author', options.contributor,
2160 '-m', commit_desc.description,
2161 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002162 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002163 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002164 if base_has_submodules:
2165 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2166 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2167 RunGit(['checkout', CHERRY_PICK_BRANCH])
2168 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002169 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002170 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002171 pending_prefix = settings.GetPendingRefPrefix()
2172 if not pending_prefix or branch.startswith(pending_prefix):
2173 # If not using refs/pending/heads/* at all, or target ref is already set
2174 # to pending, then push to the target ref directly.
2175 retcode, output = RunGitWithCode(
2176 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002177 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002178 else:
2179 # Cherry-pick the change on top of pending ref and then push it.
2180 assert branch.startswith('refs/'), branch
2181 assert pending_prefix[-1] == '/', pending_prefix
2182 pending_ref = pending_prefix + branch[len('refs/'):]
2183 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002184 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002185 if retcode == 0:
2186 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187 else:
2188 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002189 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002190 'svn', 'dcommit',
2191 '-C%s' % options.similarity,
2192 '--no-rebase', '--rmdir',
2193 ]
2194 if settings.GetForceHttpsCommitUrl():
2195 # Allow forcing https commit URLs for some projects that don't allow
2196 # committing to http URLs (like Google Code).
2197 remote_url = cl.GetGitSvnRemoteUrl()
2198 if urlparse.urlparse(remote_url).scheme == 'http':
2199 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002200 cmd_args.append('--commit-url=%s' % remote_url)
2201 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002202 if 'Committed r' in output:
2203 revision = re.match(
2204 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2205 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002206 finally:
2207 # And then swap back to the original branch and clean up.
2208 RunGit(['checkout', '-q', cl.GetBranch()])
2209 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002210 if base_has_submodules:
2211 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002212
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002213 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002214 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002215 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002216
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002217 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002218 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002219 try:
2220 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2221 # We set pushed_to_pending to False, since it made it all the way to the
2222 # real ref.
2223 pushed_to_pending = False
2224 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002225 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002226
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002227 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002228 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002229 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002230 if not to_pending:
2231 if viewvc_url and revision:
2232 change_desc.append_footer(
2233 'Committed: %s%s' % (viewvc_url, revision))
2234 elif revision:
2235 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002236 print ('Closing issue '
2237 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002238 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002239 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002240 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002241 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002242 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002243 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002244 if options.bypass_hooks:
2245 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2246 else:
2247 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002248 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002249 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002250
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002251 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002252 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2253 print 'The commit is in the pending queue (%s).' % pending_ref
2254 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002255 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002256 'footer.' % branch)
2257
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002258 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2259 if os.path.isfile(hook):
2260 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002261
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002262 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002263
2264
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002265def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2266 print
2267 print 'Waiting for commit to be landed on %s...' % real_ref
2268 print '(If you are impatient, you may Ctrl-C once without harm)'
2269 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2270 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2271
2272 loop = 0
2273 while True:
2274 sys.stdout.write('fetching (%d)... \r' % loop)
2275 sys.stdout.flush()
2276 loop += 1
2277
2278 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2279 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2280 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2281 for commit in commits.splitlines():
2282 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2283 print 'Found commit on %s' % real_ref
2284 return commit
2285
2286 current_rev = to_rev
2287
2288
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002289def PushToGitPending(remote, pending_ref, upstream_ref):
2290 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2291
2292 Returns:
2293 (retcode of last operation, output log of last operation).
2294 """
2295 assert pending_ref.startswith('refs/'), pending_ref
2296 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2297 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2298 code = 0
2299 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002300 max_attempts = 3
2301 attempts_left = max_attempts
2302 while attempts_left:
2303 if attempts_left != max_attempts:
2304 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2305 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002306
2307 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002308 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002309 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002310 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002311 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002312 print 'Fetch failed with exit code %d.' % code
2313 if out.strip():
2314 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002315 continue
2316
2317 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002318 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002319 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002320 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002321 if code:
2322 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002323 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2324 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002325 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2326 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002327 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002328 return code, out
2329
2330 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002331 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002332 code, out = RunGitWithCode(
2333 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2334 if code == 0:
2335 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002336 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002337 return code, out
2338
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002339 print 'Push failed with exit code %d.' % code
2340 if out.strip():
2341 print out.strip()
2342 if IsFatalPushFailure(out):
2343 print (
2344 'Fatal push error. Make sure your .netrc credentials and git '
2345 'user.email are correct and you have push access to the repo.')
2346 return code, out
2347
2348 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002349 return code, out
2350
2351
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002352def IsFatalPushFailure(push_stdout):
2353 """True if retrying push won't help."""
2354 return '(prohibited by Gerrit)' in push_stdout
2355
2356
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002357@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002358def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002359 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002360 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002361 message = """This doesn't appear to be an SVN repository.
2362If your project has a git mirror with an upstream SVN master, you probably need
2363to run 'git svn init', see your project's git mirror documentation.
2364If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002365to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002366Choose wisely, if you get this wrong, your commit might appear to succeed but
2367will instead be silently ignored."""
2368 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002369 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002370 return SendUpstream(parser, args, 'dcommit')
2371
2372
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002373@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002374def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002375 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002376 if settings.GetIsGitSvn():
2377 print('This appears to be an SVN repository.')
2378 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002379 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002380 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002381
2382
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002383@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002384def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002385 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002386 parser.add_option('-b', dest='newbranch',
2387 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002388 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002389 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002390 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2391 help='Change to the directory DIR immediately, '
2392 'before doing anything else.')
2393 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002394 help='failed patches spew .rej files rather than '
2395 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002396 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2397 help="don't commit after patch applies")
2398 (options, args) = parser.parse_args(args)
2399 if len(args) != 1:
2400 parser.print_help()
2401 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002402 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002403
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002404 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002405 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002406
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002407 if options.newbranch:
2408 if options.force:
2409 RunGit(['branch', '-D', options.newbranch],
2410 stderr=subprocess2.PIPE, error_ok=True)
2411 RunGit(['checkout', '-b', options.newbranch,
2412 Changelist().GetUpstreamBranch()])
2413
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002414 return PatchIssue(issue_arg, options.reject, options.nocommit,
2415 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002416
2417
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002418def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002419 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002420 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002421 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002422 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002423 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002424 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002425 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002426 # Assume it's a URL to the patch. Default to https.
2427 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002428 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002429 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002430 DieWithError('Must pass an issue ID or full URL for '
2431 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002432 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002433 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002434 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002435
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002436 # Switch up to the top-level directory, if necessary, in preparation for
2437 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002438 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002439 if top:
2440 os.chdir(top)
2441
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002442 # Git patches have a/ at the beginning of source paths. We strip that out
2443 # with a sed script rather than the -p flag to patch so we can feed either
2444 # Git or svn-style patches into the same apply command.
2445 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002446 try:
2447 patch_data = subprocess2.check_output(
2448 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2449 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002450 DieWithError('Git patch mungling failed.')
2451 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002452
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002453 # We use "git apply" to apply the patch instead of "patch" so that we can
2454 # pick up file adds.
2455 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002456 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002457 if directory:
2458 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002459 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002460 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002461 elif IsGitVersionAtLeast('1.7.12'):
2462 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002463 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002464 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002465 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002466 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002467 DieWithError('Failed to apply the patch')
2468
2469 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002470 if not nocommit:
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002471 RunGit(['commit', '-m', ('patch from issue %(i)s at patchset '
2472 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2473 % {'i': issue, 'p': patchset})])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002474 cl = Changelist()
2475 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002476 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002477 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002478 else:
2479 print "Patch applied to index."
2480 return 0
2481
2482
2483def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002484 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002485 # Provide a wrapper for git svn rebase to help avoid accidental
2486 # git svn dcommit.
2487 # It's the only command that doesn't use parser at all since we just defer
2488 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002489
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002490 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002491
2492
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002493def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002494 """Fetches the tree status and returns either 'open', 'closed',
2495 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002496 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002497 if url:
2498 status = urllib2.urlopen(url).read().lower()
2499 if status.find('closed') != -1 or status == '0':
2500 return 'closed'
2501 elif status.find('open') != -1 or status == '1':
2502 return 'open'
2503 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002504 return 'unset'
2505
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002506
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002507def GetTreeStatusReason():
2508 """Fetches the tree status from a json url and returns the message
2509 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002510 url = settings.GetTreeStatusUrl()
2511 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002512 connection = urllib2.urlopen(json_url)
2513 status = json.loads(connection.read())
2514 connection.close()
2515 return status['message']
2516
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002517
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002518def GetBuilderMaster(bot_list):
2519 """For a given builder, fetch the master from AE if available."""
2520 map_url = 'https://builders-map.appspot.com/'
2521 try:
2522 master_map = json.load(urllib2.urlopen(map_url))
2523 except urllib2.URLError as e:
2524 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2525 (map_url, e))
2526 except ValueError as e:
2527 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2528 if not master_map:
2529 return None, 'Failed to build master map.'
2530
2531 result_master = ''
2532 for bot in bot_list:
2533 builder = bot.split(':', 1)[0]
2534 master_list = master_map.get(builder, [])
2535 if not master_list:
2536 return None, ('No matching master for builder %s.' % builder)
2537 elif len(master_list) > 1:
2538 return None, ('The builder name %s exists in multiple masters %s.' %
2539 (builder, master_list))
2540 else:
2541 cur_master = master_list[0]
2542 if not result_master:
2543 result_master = cur_master
2544 elif result_master != cur_master:
2545 return None, 'The builders do not belong to the same master.'
2546 return result_master, None
2547
2548
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002549def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002550 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002551 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002552 status = GetTreeStatus()
2553 if 'unset' == status:
2554 print 'You must configure your tree status URL by running "git cl config".'
2555 return 2
2556
2557 print "The tree is %s" % status
2558 print
2559 print GetTreeStatusReason()
2560 if status != 'open':
2561 return 1
2562 return 0
2563
2564
maruel@chromium.org15192402012-09-06 12:38:29 +00002565def CMDtry(parser, args):
2566 """Triggers a try job through Rietveld."""
2567 group = optparse.OptionGroup(parser, "Try job options")
2568 group.add_option(
2569 "-b", "--bot", action="append",
2570 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2571 "times to specify multiple builders. ex: "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002572 "'-b win_rel -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002573 "the try server waterfall for the builders name and the tests "
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002574 "available."))
maruel@chromium.org15192402012-09-06 12:38:29 +00002575 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002576 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002577 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002578 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002579 "-r", "--revision",
2580 help="Revision to use for the try job; default: the "
2581 "revision will be determined by the try server; see "
2582 "its waterfall for more info")
2583 group.add_option(
2584 "-c", "--clobber", action="store_true", default=False,
2585 help="Force a clobber before building; e.g. don't do an "
2586 "incremental build")
2587 group.add_option(
2588 "--project",
2589 help="Override which project to use. Projects are defined "
2590 "server-side to define what default bot set to use")
2591 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002592 "-n", "--name", help="Try job name; default to current branch name")
2593 parser.add_option_group(group)
2594 options, args = parser.parse_args(args)
2595
2596 if args:
2597 parser.error('Unknown arguments: %s' % args)
2598
2599 cl = Changelist()
2600 if not cl.GetIssue():
2601 parser.error('Need to upload first')
2602
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002603 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002604 if props.get('closed'):
2605 parser.error('Cannot send tryjobs for a closed CL')
2606
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002607 if props.get('private'):
2608 parser.error('Cannot use trybots with private issue')
2609
maruel@chromium.org15192402012-09-06 12:38:29 +00002610 if not options.name:
2611 options.name = cl.GetBranch()
2612
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002613 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002614 options.master, err_msg = GetBuilderMaster(options.bot)
2615 if err_msg:
2616 parser.error('Tryserver master cannot be found because: %s\n'
2617 'Please manually specify the tryserver master'
2618 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002619
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002620 def GetMasterMap():
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002621 # Process --bot.
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002622 if not options.bot:
2623 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002624
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002625 # Get try masters from PRESUBMIT.py files.
2626 masters = presubmit_support.DoGetTryMasters(
2627 change,
2628 change.LocalPaths(),
2629 settings.GetRoot(),
2630 None,
2631 None,
2632 options.verbose,
2633 sys.stdout)
2634 if masters:
2635 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002636
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002637 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2638 options.bot = presubmit_support.DoGetTrySlaves(
2639 change,
2640 change.LocalPaths(),
2641 settings.GetRoot(),
2642 None,
2643 None,
2644 options.verbose,
2645 sys.stdout)
2646 if not options.bot:
2647 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002648
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002649 builders_and_tests = {}
2650 # TODO(machenbach): The old style command-line options don't support
2651 # multiple try masters yet.
2652 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2653 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2654
2655 for bot in old_style:
2656 if ':' in bot:
phajdan.jr@chromium.org52914132015-01-22 10:37:09 +00002657 parser.error('Specifying testfilter is no longer supported')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002658 elif ',' in bot:
2659 parser.error('Specify one bot per --bot flag')
2660 else:
2661 builders_and_tests.setdefault(bot, []).append('defaulttests')
2662
2663 for bot, tests in new_style:
2664 builders_and_tests.setdefault(bot, []).extend(tests)
2665
2666 # Return a master map with one master to be backwards compatible. The
2667 # master name defaults to an empty string, which will cause the master
2668 # not to be set on rietveld (deprecated).
2669 return {options.master: builders_and_tests}
2670
2671 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002672
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002673 for builders in masters.itervalues():
2674 if any('triggered' in b for b in builders):
2675 print >> sys.stderr, (
2676 'ERROR You are trying to send a job to a triggered bot. This type of'
2677 ' bot requires an\ninitial job from a parent (usually a builder). '
2678 'Instead send your job to the parent.\n'
2679 'Bot list: %s' % builders)
2680 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002681
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002682 patchset = cl.GetMostRecentPatchset()
2683 if patchset and patchset != cl.GetPatchset():
2684 print(
2685 '\nWARNING Mismatch between local config and server. Did a previous '
2686 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2687 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002688 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002689 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002690 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002691 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002692 except urllib2.HTTPError, e:
2693 if e.code == 404:
2694 print('404 from rietveld; '
2695 'did you mean to use "git try" instead of "git cl try"?')
2696 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002697 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002698
2699 for (master, builders) in masters.iteritems():
2700 if master:
2701 print 'Master: %s' % master
2702 length = max(len(builder) for builder in builders)
2703 for builder in sorted(builders):
2704 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002705 return 0
2706
2707
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002708@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002709def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002710 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002711 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002712 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002713 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002714
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002715 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002716 if args:
2717 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002718 branch = cl.GetBranch()
2719 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002720 cl = Changelist()
2721 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002722
2723 # Clear configured merge-base, if there is one.
2724 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002725 else:
2726 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002727 return 0
2728
2729
thestig@chromium.org00858c82013-12-02 23:08:03 +00002730def CMDweb(parser, args):
2731 """Opens the current CL in the web browser."""
2732 _, args = parser.parse_args(args)
2733 if args:
2734 parser.error('Unrecognized args: %s' % ' '.join(args))
2735
2736 issue_url = Changelist().GetIssueURL()
2737 if not issue_url:
2738 print >> sys.stderr, 'ERROR No issue to open'
2739 return 1
2740
2741 webbrowser.open(issue_url)
2742 return 0
2743
2744
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002745def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002746 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002747 _, args = parser.parse_args(args)
2748 if args:
2749 parser.error('Unrecognized args: %s' % ' '.join(args))
2750 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002751 props = cl.GetIssueProperties()
2752 if props.get('private'):
2753 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002754 cl.SetFlag('commit', '1')
2755 return 0
2756
2757
groby@chromium.org411034a2013-02-26 15:12:01 +00002758def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002759 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002760 _, args = parser.parse_args(args)
2761 if args:
2762 parser.error('Unrecognized args: %s' % ' '.join(args))
2763 cl = Changelist()
2764 # Ensure there actually is an issue to close.
2765 cl.GetDescription()
2766 cl.CloseIssue()
2767 return 0
2768
2769
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002770def CMDdiff(parser, args):
2771 """shows differences between local tree and last upload."""
2772 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002773 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002774 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002775 if not issue:
2776 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002777 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002778 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002779
2780 # Create a new branch based on the merge-base
2781 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2782 try:
2783 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002784 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002785 if rtn != 0:
2786 return rtn
2787
2788 # Switch back to starting brand and diff against the temporary
2789 # branch containing the latest rietveld patch.
2790 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2791 finally:
2792 RunGit(['checkout', '-q', branch])
2793 RunGit(['branch', '-D', TMP_BRANCH])
2794
2795 return 0
2796
2797
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002798def CMDowners(parser, args):
2799 """interactively find the owners for reviewing"""
2800 parser.add_option(
2801 '--no-color',
2802 action='store_true',
2803 help='Use this option to disable color output')
2804 options, args = parser.parse_args(args)
2805
2806 author = RunGit(['config', 'user.email']).strip() or None
2807
2808 cl = Changelist()
2809
2810 if args:
2811 if len(args) > 1:
2812 parser.error('Unknown args')
2813 base_branch = args[0]
2814 else:
2815 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002816 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002817
2818 change = cl.GetChange(base_branch, None)
2819 return owners_finder.OwnersFinder(
2820 [f.LocalPath() for f in
2821 cl.GetChange(base_branch, None).AffectedFiles()],
2822 change.RepositoryRoot(), author,
2823 fopen=file, os_path=os.path, glob=glob.glob,
2824 disable_color=options.no_color).run()
2825
2826
enne@chromium.org555cfe42014-01-29 18:21:39 +00002827@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002828def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002829 """Runs clang-format on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00002830 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002831 parser.add_option('--full', action='store_true',
2832 help='Reformat the full content of all touched files')
2833 parser.add_option('--dry-run', action='store_true',
2834 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002835 parser.add_option('--diff', action='store_true',
2836 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002837 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002838
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002839 # git diff generates paths against the root of the repository. Change
2840 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002841 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002842 if rel_base_path:
2843 os.chdir(rel_base_path)
2844
digit@chromium.org29e47272013-05-17 17:01:46 +00002845 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002846 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002847 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002848 # Only list the names of modified files.
2849 diff_cmd.append('--name-only')
2850 else:
2851 # Only generate context-less patches.
2852 diff_cmd.append('-U0')
2853
2854 # Grab the merge-base commit, i.e. the upstream commit of the current
2855 # branch when it was created or the last time it was rebased. This is
2856 # to cover the case where the user may have called "git fetch origin",
2857 # moving the origin branch to a newer commit, but hasn't rebased yet.
2858 upstream_commit = None
2859 cl = Changelist()
2860 upstream_branch = cl.GetUpstreamBranch()
2861 if upstream_branch:
2862 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2863 upstream_commit = upstream_commit.strip()
2864
2865 if not upstream_commit:
2866 DieWithError('Could not find base commit for this branch. '
2867 'Are you in detached state?')
2868
2869 diff_cmd.append(upstream_commit)
2870
2871 # Handle source file filtering.
2872 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002873 if args:
2874 for arg in args:
2875 if os.path.isdir(arg):
2876 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2877 elif os.path.isfile(arg):
2878 diff_cmd.append(arg)
2879 else:
2880 DieWithError('Argument "%s" is not a file or a directory' % arg)
2881 else:
2882 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002883 diff_output = RunGit(diff_cmd)
2884
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002885 top_dir = os.path.normpath(
2886 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2887
2888 # Locate the clang-format binary in the checkout
2889 try:
2890 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2891 except clang_format.NotFoundError, e:
2892 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002893
digit@chromium.org29e47272013-05-17 17:01:46 +00002894 if opts.full:
2895 # diff_output is a list of files to send to clang-format.
2896 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002897 if not files:
2898 print "Nothing to format."
2899 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002900 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002901 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002902 cmd.append('-i')
2903 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002904 if opts.diff:
2905 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002906 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002907 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00002908 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00002909 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002910 try:
2911 script = clang_format.FindClangFormatScriptInChromiumTree(
2912 'clang-format-diff.py')
2913 except clang_format.NotFoundError, e:
2914 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002915
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002916 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002917 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002918 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002919
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002920 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002921 if opts.diff:
2922 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002923 if opts.dry_run and len(stdout) > 0:
2924 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002925
2926 return 0
2927
2928
maruel@chromium.org29404b52014-09-08 22:58:00 +00002929def CMDlol(parser, args):
2930 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00002931 print zlib.decompress(base64.b64decode(
2932 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
2933 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
2934 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
2935 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00002936 return 0
2937
2938
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002939class OptionParser(optparse.OptionParser):
2940 """Creates the option parse and add --verbose support."""
2941 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002942 optparse.OptionParser.__init__(
2943 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002944 self.add_option(
2945 '-v', '--verbose', action='count', default=0,
2946 help='Use 2 times for more debugging info')
2947
2948 def parse_args(self, args=None, values=None):
2949 options, args = optparse.OptionParser.parse_args(self, args, values)
2950 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2951 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2952 return options, args
2953
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002954
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002955def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002956 if sys.hexversion < 0x02060000:
2957 print >> sys.stderr, (
2958 '\nYour python version %s is unsupported, please upgrade.\n' %
2959 sys.version.split(' ', 1)[0])
2960 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002961
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002962 # Reload settings.
2963 global settings
2964 settings = Settings()
2965
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002966 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002967 dispatcher = subcommand.CommandDispatcher(__name__)
2968 try:
2969 return dispatcher.execute(OptionParser(), argv)
2970 except urllib2.HTTPError, e:
2971 if e.code != 500:
2972 raise
2973 DieWithError(
2974 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2975 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002976
2977
2978if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002979 # These affect sys.stdout so do it outside of main() to simplify mocks in
2980 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002981 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002982 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002983 sys.exit(main(sys.argv[1:]))