blob: 4c91b6c762147b7ce5c886df94d8fdd906995725 [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
644 # Verify the commit we're diffing against is in our current branch.
645 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
646 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
647 if upstream_sha != common_ancestor:
648 print >> sys.stderr, (
649 'ERROR: %s is not in the current branch. You may need to rebase '
650 'your tracking branch' % upstream_sha)
651 return False
652
653 # List the commits inside the diff, and verify they are all local.
654 commits_in_diff = RunGit(
655 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
656 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
657 remote_branch = remote_branch.strip()
658 if code != 0:
659 _, remote_branch = self.GetRemoteBranch()
660
661 commits_in_remote = RunGit(
662 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
663
664 common_commits = set(commits_in_diff) & set(commits_in_remote)
665 if common_commits:
666 print >> sys.stderr, (
667 'ERROR: Your diff contains %d commits already in %s.\n'
668 'Run "git log --oneline %s..HEAD" to get a list of commits in '
669 'the diff. If you are using a custom git flow, you can override'
670 ' the reference used for this check with "git config '
671 'gitcl.remotebranch <git-ref>".' % (
672 len(common_commits), remote_branch, upstream_git_obj))
673 return False
674 return True
675
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000676 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000677 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000678
679 Returns None if it is not set.
680 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000681 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
682 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000683
kjellander@chromium.org6abc6522014-12-02 07:34:49 +0000684 def GetGitSvnRemoteUrl(self):
685 """Return the configured git-svn remote URL parsed from git svn info.
686
687 Returns None if it is not set.
688 """
689 # URL is dependent on the current directory.
690 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
691 if data:
692 keys = dict(line.split(': ', 1) for line in data.splitlines()
693 if ': ' in line)
694 return keys.get('URL', None)
695 return None
696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000697 def GetRemoteUrl(self):
698 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
699
700 Returns None if there is no remote.
701 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000702 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000703 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
704
705 # If URL is pointing to a local directory, it is probably a git cache.
706 if os.path.isdir(url):
707 url = RunGit(['config', 'remote.%s.url' % remote],
708 error_ok=True,
709 cwd=url).strip()
710 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711
712 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000713 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000714 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000716 self.issue = int(issue) or None if issue else None
717 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 return self.issue
719
720 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000721 if not self.rietveld_server:
722 # If we're on a branch then get the server potentially associated
723 # with that branch.
724 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000725 rietveld_server_config = self._RietveldServer()
726 if rietveld_server_config:
727 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
728 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000729 if not self.rietveld_server:
730 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 return self.rietveld_server
732
733 def GetIssueURL(self):
734 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000735 if not self.GetIssue():
736 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
738
739 def GetDescription(self, pretty=False):
740 if not self.has_description:
741 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000742 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000743 try:
744 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000745 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000746 if e.code == 404:
747 DieWithError(
748 ('\nWhile fetching the description for issue %d, received a '
749 '404 (not found)\n'
750 'error. It is likely that you deleted this '
751 'issue on the server. If this is the\n'
752 'case, please run\n\n'
753 ' git cl issue 0\n\n'
754 'to clear the association with the deleted issue. Then run '
755 'this command again.') % issue)
756 else:
757 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000758 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000759 except urllib2.URLError as e:
760 print >> sys.stderr, (
761 'Warning: Failed to retrieve CL description due to network '
762 'failure.')
763 self.description = ''
764
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765 self.has_description = True
766 if pretty:
767 wrapper = textwrap.TextWrapper()
768 wrapper.initial_indent = wrapper.subsequent_indent = ' '
769 return wrapper.fill(self.description)
770 return self.description
771
772 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000773 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000774 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000775 patchset = RunGit(['config', self._PatchsetSetting()],
776 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000777 self.patchset = int(patchset) or None if patchset else None
778 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000779 return self.patchset
780
781 def SetPatchset(self, patchset):
782 """Set this branch's patchset. If patchset=0, clears the patchset."""
783 if patchset:
784 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000785 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 else:
787 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000788 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000789 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000791 def GetMostRecentPatchset(self):
792 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000793
794 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000795 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000796 '/download/issue%s_%s.diff' % (issue, patchset))
797
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000798 def GetIssueProperties(self):
799 if self._props is None:
800 issue = self.GetIssue()
801 if not issue:
802 self._props = {}
803 else:
804 self._props = self.RpcServer().get_issue_properties(issue, True)
805 return self._props
806
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000807 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000808 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000809
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000810 def AddComment(self, message):
811 return self.RpcServer().add_comment(self.GetIssue(), message)
812
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813 def SetIssue(self, issue):
814 """Set this branch's issue. If issue=0, clears the issue."""
815 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000816 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000817 RunGit(['config', self._IssueSetting(), str(issue)])
818 if self.rietveld_server:
819 RunGit(['config', self._RietveldServer(), self.rietveld_server])
820 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000821 current_issue = self.GetIssue()
822 if current_issue:
823 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000824 self.issue = None
825 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000827 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000828 if not self.GitSanityChecks(upstream_branch):
829 DieWithError('\nGit sanity check failure')
830
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000831 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000832 if not root:
833 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000834 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000835
836 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000837 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000838 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000839 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000840 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000841 except subprocess2.CalledProcessError:
842 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000843 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000844 'This branch probably doesn\'t exist anymore. To reset the\n'
845 'tracking branch, please run\n'
846 ' git branch --set-upstream %s trunk\n'
847 'replacing trunk with origin/master or the relevant branch') %
848 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000849
maruel@chromium.org52424302012-08-29 15:14:30 +0000850 issue = self.GetIssue()
851 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000852 if issue:
853 description = self.GetDescription()
854 else:
855 # If the change was never uploaded, use the log messages of all commits
856 # up to the branch point, as git cl upload will prefill the description
857 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000858 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
859 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000860
861 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000862 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000863 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000864 name,
865 description,
866 absroot,
867 files,
868 issue,
869 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000870 author,
871 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000872
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +0000873 def GetStatus(self):
874 """Apply a rough heuristic to give a simple summary of an issue's review
875 or CQ status, assuming adherence to a common workflow.
876
877 Returns None if no issue for this branch, or one of the following keywords:
878 * 'error' - error from review tool (including deleted issues)
879 * 'unsent' - not sent for review
880 * 'waiting' - waiting for review
881 * 'reply' - waiting for owner to reply to review
882 * 'lgtm' - LGTM from at least one approved reviewer
883 * 'commit' - in the commit queue
884 * 'closed' - closed
885 """
886 if not self.GetIssue():
887 return None
888
889 try:
890 props = self.GetIssueProperties()
891 except urllib2.HTTPError:
892 return 'error'
893
894 if props.get('closed'):
895 # Issue is closed.
896 return 'closed'
897 if props.get('commit'):
898 # Issue is in the commit queue.
899 return 'commit'
900
901 try:
902 reviewers = self.GetApprovingReviewers()
903 except urllib2.HTTPError:
904 return 'error'
905
906 if reviewers:
907 # Was LGTM'ed.
908 return 'lgtm'
909
910 messages = props.get('messages') or []
911
912 if not messages:
913 # No message was sent.
914 return 'unsent'
915 if messages[-1]['sender'] != props.get('owner_email'):
916 # Non-LGTM reply from non-owner
917 return 'reply'
918 return 'waiting'
919
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000920 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000921 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000922
923 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000924 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000925 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000926 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000927 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000928 except presubmit_support.PresubmitFailure, e:
929 DieWithError(
930 ('%s\nMaybe your depot_tools is out of date?\n'
931 'If all fails, contact maruel@') % e)
932
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000933 def UpdateDescription(self, description):
934 self.description = description
935 return self.RpcServer().update_description(
936 self.GetIssue(), self.description)
937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000938 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000939 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000940 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000941
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000942 def SetFlag(self, flag, value):
943 """Patchset must match."""
944 if not self.GetPatchset():
945 DieWithError('The patchset needs to match. Send another patchset.')
946 try:
947 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000948 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000949 except urllib2.HTTPError, e:
950 if e.code == 404:
951 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
952 if e.code == 403:
953 DieWithError(
954 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
955 'match?') % (self.GetIssue(), self.GetPatchset()))
956 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000958 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959 """Returns an upload.RpcServer() to access this review's rietveld instance.
960 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000961 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000962 self._rpc_server = rietveld.CachingRietveld(
963 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000964 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000965
966 def _IssueSetting(self):
967 """Return the git setting that stores this change's issue."""
968 return 'branch.%s.rietveldissue' % self.GetBranch()
969
970 def _PatchsetSetting(self):
971 """Return the git setting that stores this change's most recent patchset."""
972 return 'branch.%s.rietveldpatchset' % self.GetBranch()
973
974 def _RietveldServer(self):
975 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000976 branch = self.GetBranch()
977 if branch:
978 return 'branch.%s.rietveldserver' % branch
979 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980
981
982def GetCodereviewSettingsInteractively():
983 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000984 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000985 server = settings.GetDefaultServerUrl(error_ok=True)
986 prompt = 'Rietveld server (host[:port])'
987 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000988 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989 if not server and not newserver:
990 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000991 if newserver:
992 newserver = gclient_utils.UpgradeToHttps(newserver)
993 if newserver != server:
994 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000995
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000996 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997 prompt = caption
998 if initial:
999 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +00001000 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001001 if new_val == 'x':
1002 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001003 elif new_val:
1004 if is_url:
1005 new_val = gclient_utils.UpgradeToHttps(new_val)
1006 if new_val != initial:
1007 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001009 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001010 SetProperty(settings.GetDefaultPrivateFlag(),
1011 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001012 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001013 'tree-status-url', False)
1014 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +00001015 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001016
1017 # TODO: configure a default branch to diff against, rather than this
1018 # svn-based hackery.
1019
1020
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001021class ChangeDescription(object):
1022 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001023 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001024 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001025
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001026 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001027 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001028
agable@chromium.org42c20792013-09-12 17:34:49 +00001029 @property # www.logilab.org/ticket/89786
1030 def description(self): # pylint: disable=E0202
1031 return '\n'.join(self._description_lines)
1032
1033 def set_description(self, desc):
1034 if isinstance(desc, basestring):
1035 lines = desc.splitlines()
1036 else:
1037 lines = [line.rstrip() for line in desc]
1038 while lines and not lines[0]:
1039 lines.pop(0)
1040 while lines and not lines[-1]:
1041 lines.pop(-1)
1042 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001043
piman@chromium.org336f9122014-09-04 02:16:55 +00001044 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001045 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001046 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001047 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001048 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001049 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001050
agable@chromium.org42c20792013-09-12 17:34:49 +00001051 # Get the set of R= and TBR= lines and remove them from the desciption.
1052 regexp = re.compile(self.R_LINE)
1053 matches = [regexp.match(line) for line in self._description_lines]
1054 new_desc = [l for i, l in enumerate(self._description_lines)
1055 if not matches[i]]
1056 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001057
agable@chromium.org42c20792013-09-12 17:34:49 +00001058 # Construct new unified R= and TBR= lines.
1059 r_names = []
1060 tbr_names = []
1061 for match in matches:
1062 if not match:
1063 continue
1064 people = cleanup_list([match.group(2).strip()])
1065 if match.group(1) == 'TBR':
1066 tbr_names.extend(people)
1067 else:
1068 r_names.extend(people)
1069 for name in r_names:
1070 if name not in reviewers:
1071 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001072 if add_owners_tbr:
1073 owners_db = owners.Database(change.RepositoryRoot(),
1074 fopen=file, os_path=os.path, glob=glob.glob)
1075 all_reviewers = set(tbr_names + reviewers)
1076 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1077 all_reviewers)
1078 tbr_names.extend(owners_db.reviewers_for(missing_files,
1079 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001080 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1081 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1082
1083 # Put the new lines in the description where the old first R= line was.
1084 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1085 if 0 <= line_loc < len(self._description_lines):
1086 if new_tbr_line:
1087 self._description_lines.insert(line_loc, new_tbr_line)
1088 if new_r_line:
1089 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001090 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001091 if new_r_line:
1092 self.append_footer(new_r_line)
1093 if new_tbr_line:
1094 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001095
1096 def prompt(self):
1097 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001098 self.set_description([
1099 '# Enter a description of the change.',
1100 '# This will be displayed on the codereview site.',
1101 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001102 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001103 '--------------------',
1104 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001105
agable@chromium.org42c20792013-09-12 17:34:49 +00001106 regexp = re.compile(self.BUG_LINE)
1107 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001108 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001109 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001110 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001111 if not content:
1112 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001113 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001114
1115 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001116 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1117 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001118 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001119 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001120
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001121 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001122 if self._description_lines:
1123 # Add an empty line if either the last line or the new line isn't a tag.
1124 last_line = self._description_lines[-1]
1125 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1126 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1127 self._description_lines.append('')
1128 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001129
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001130 def get_reviewers(self):
1131 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001132 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1133 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001134 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001135
1136
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001137def get_approving_reviewers(props):
1138 """Retrieves the reviewers that approved a CL from the issue properties with
1139 messages.
1140
1141 Note that the list may contain reviewers that are not committer, thus are not
1142 considered by the CQ.
1143 """
1144 return sorted(
1145 set(
1146 message['sender']
1147 for message in props['messages']
1148 if message['approval'] and message['sender'] in props['reviewers']
1149 )
1150 )
1151
1152
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153def FindCodereviewSettingsFile(filename='codereview.settings'):
1154 """Finds the given file starting in the cwd and going up.
1155
1156 Only looks up to the top of the repository unless an
1157 'inherit-review-settings-ok' file exists in the root of the repository.
1158 """
1159 inherit_ok_file = 'inherit-review-settings-ok'
1160 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001161 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1163 root = '/'
1164 while True:
1165 if filename in os.listdir(cwd):
1166 if os.path.isfile(os.path.join(cwd, filename)):
1167 return open(os.path.join(cwd, filename))
1168 if cwd == root:
1169 break
1170 cwd = os.path.dirname(cwd)
1171
1172
1173def LoadCodereviewSettingsFromFile(fileobj):
1174 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001175 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 def SetProperty(name, setting, unset_error_ok=False):
1178 fullname = 'rietveld.' + name
1179 if setting in keyvals:
1180 RunGit(['config', fullname, keyvals[setting]])
1181 else:
1182 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1183
1184 SetProperty('server', 'CODE_REVIEW_SERVER')
1185 # Only server setting is required. Other settings can be absent.
1186 # In that case, we ignore errors raised during option deletion attempt.
1187 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001188 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1190 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001191 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001192 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001193 SetProperty('force-https-commit-url', 'FORCE_HTTPS_COMMIT_URL',
1194 unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001195 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001196 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001197 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001199 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001200 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001201
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1203 #should be of the form
1204 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1205 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1206 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1207 keyvals['ORIGIN_URL_CONFIG']])
1208
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001209
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001210def urlretrieve(source, destination):
1211 """urllib is broken for SSL connections via a proxy therefore we
1212 can't use urllib.urlretrieve()."""
1213 with open(destination, 'w') as f:
1214 f.write(urllib2.urlopen(source).read())
1215
1216
ukai@chromium.org712d6102013-11-27 00:52:58 +00001217def hasSheBang(fname):
1218 """Checks fname is a #! script."""
1219 with open(fname) as f:
1220 return f.read(2).startswith('#!')
1221
1222
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001223def DownloadHooks(force):
1224 """downloads hooks
1225
1226 Args:
1227 force: True to update hooks. False to install hooks if not present.
1228 """
1229 if not settings.GetIsGerrit():
1230 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001231 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001232 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1233 if not os.access(dst, os.X_OK):
1234 if os.path.exists(dst):
1235 if not force:
1236 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001237 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001238 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001239 if not hasSheBang(dst):
1240 DieWithError('Not a script: %s\n'
1241 'You need to download from\n%s\n'
1242 'into .git/hooks/commit-msg and '
1243 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001244 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1245 except Exception:
1246 if os.path.exists(dst):
1247 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001248 DieWithError('\nFailed to download hooks.\n'
1249 'You need to download from\n%s\n'
1250 'into .git/hooks/commit-msg and '
1251 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001252
1253
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001254@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001256 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001258 parser.add_option('--activate-update', action='store_true',
1259 help='activate auto-updating [rietveld] section in '
1260 '.git/config')
1261 parser.add_option('--deactivate-update', action='store_true',
1262 help='deactivate auto-updating [rietveld] section in '
1263 '.git/config')
1264 options, args = parser.parse_args(args)
1265
1266 if options.deactivate_update:
1267 RunGit(['config', 'rietveld.autoupdate', 'false'])
1268 return
1269
1270 if options.activate_update:
1271 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1272 return
1273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274 if len(args) == 0:
1275 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001276 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277 return 0
1278
1279 url = args[0]
1280 if not url.endswith('codereview.settings'):
1281 url = os.path.join(url, 'codereview.settings')
1282
1283 # Load code review settings and download hooks (if available).
1284 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001285 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 return 0
1287
1288
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001289def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001290 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001291 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1292 branch = ShortBranchName(branchref)
1293 _, args = parser.parse_args(args)
1294 if not args:
1295 print("Current base-url:")
1296 return RunGit(['config', 'branch.%s.base-url' % branch],
1297 error_ok=False).strip()
1298 else:
1299 print("Setting base-url to %s" % args[0])
1300 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1301 error_ok=False).strip()
1302
1303
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001304def color_for_status(status):
1305 """Maps a Changelist status to color, for CMDstatus and other tools."""
1306 return {
1307 'unsent': Fore.RED,
1308 'waiting': Fore.BLUE,
1309 'reply': Fore.YELLOW,
1310 'lgtm': Fore.GREEN,
1311 'commit': Fore.MAGENTA,
1312 'closed': Fore.CYAN,
1313 'error': Fore.WHITE,
1314 }.get(status, Fore.WHITE)
1315
1316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001318 """Show status of changelists.
1319
1320 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001321 - Red not sent for review or broken
1322 - Blue waiting for review
1323 - Yellow waiting for you to reply to review
1324 - Green LGTM'ed
1325 - Magenta in the commit queue
1326 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001327
1328 Also see 'git cl comments'.
1329 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 parser.add_option('--field',
1331 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001332 parser.add_option('-f', '--fast', action='store_true',
1333 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001335 if args:
1336 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001339 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001340 if options.field.startswith('desc'):
1341 print cl.GetDescription()
1342 elif options.field == 'id':
1343 issueid = cl.GetIssue()
1344 if issueid:
1345 print issueid
1346 elif options.field == 'patch':
1347 patchset = cl.GetPatchset()
1348 if patchset:
1349 print patchset
1350 elif options.field == 'url':
1351 url = cl.GetIssueURL()
1352 if url:
1353 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001354 return 0
1355
1356 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1357 if not branches:
1358 print('No local branch found.')
1359 return 0
1360
1361 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001362 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001363 alignment = max(5, max(len(b) for b in branches))
1364 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001365 # Adhoc thread pool to request data concurrently.
1366 output = Queue.Queue()
1367
1368 # Silence upload.py otherwise it becomes unweldly.
1369 upload.verbosity = 0
1370
1371 if not options.fast:
1372 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001373 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001374 c = Changelist(branchref=b)
1375 i = c.GetIssueURL()
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001376 status = c.GetStatus()
1377 color = color_for_status(status)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001378
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001379 if i and (not status or status == 'error'):
1380 # The issue probably doesn't exist anymore.
1381 i += ' (broken)'
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001382
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001383 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001384
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001385 # Process one branch synchronously to work through authentication, then
1386 # spawn threads to process all the other branches in parallel.
1387 if branches:
1388 fetch(branches[0])
1389 threads = [
1390 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001391 for t in threads:
1392 t.daemon = True
1393 t.start()
1394 else:
1395 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1396 for b in branches:
1397 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001398 url = c.GetIssueURL()
1399 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001400
1401 tmp = {}
1402 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001403 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001404 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001405 b, i, color = output.get()
1406 tmp[b] = (i, color)
1407 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001408 reset = Fore.RESET
1409 if not sys.stdout.isatty():
1410 color = ''
1411 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001412 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001413 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001414
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001415 cl = Changelist()
1416 print
1417 print 'Current branch:',
1418 if not cl.GetIssue():
1419 print 'no issue assigned.'
1420 return 0
1421 print cl.GetBranch()
1422 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001423 if not options.fast:
1424 print 'Issue description:'
1425 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426 return 0
1427
1428
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001429def colorize_CMDstatus_doc():
1430 """To be called once in main() to add colors to git cl status help."""
1431 colors = [i for i in dir(Fore) if i[0].isupper()]
1432
1433 def colorize_line(line):
1434 for color in colors:
1435 if color in line.upper():
1436 # Extract whitespaces first and the leading '-'.
1437 indent = len(line) - len(line.lstrip(' ')) + 1
1438 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1439 return line
1440
1441 lines = CMDstatus.__doc__.splitlines()
1442 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1443
1444
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001445@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001447 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448
1449 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001450 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001451 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001452
1453 cl = Changelist()
1454 if len(args) > 0:
1455 try:
1456 issue = int(args[0])
1457 except ValueError:
1458 DieWithError('Pass a number to set the issue or none to list it.\n'
1459 'Maybe you want to run git cl status?')
1460 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001461 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001462 return 0
1463
1464
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001465def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001466 """Shows or posts review comments for any changelist."""
1467 parser.add_option('-a', '--add-comment', dest='comment',
1468 help='comment to add to an issue')
1469 parser.add_option('-i', dest='issue',
1470 help="review issue id (defaults to current issue)")
1471 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001472
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001473 issue = None
1474 if options.issue:
1475 try:
1476 issue = int(options.issue)
1477 except ValueError:
1478 DieWithError('A review issue id is expected to be a number')
1479
1480 cl = Changelist(issue=issue)
1481
1482 if options.comment:
1483 cl.AddComment(options.comment)
1484 return 0
1485
1486 data = cl.GetIssueProperties()
maruel@chromium.org5cab2d32014-11-11 18:32:41 +00001487 for message in sorted(data.get('messages', []), key=lambda x: x['date']):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001488 if message['disapproval']:
1489 color = Fore.RED
1490 elif message['approval']:
1491 color = Fore.GREEN
1492 elif message['sender'] == data['owner_email']:
1493 color = Fore.MAGENTA
1494 else:
1495 color = Fore.BLUE
1496 print '\n%s%s %s%s' % (
1497 color, message['date'].split('.', 1)[0], message['sender'],
1498 Fore.RESET)
1499 if message['text'].strip():
1500 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001501 return 0
1502
1503
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001504def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001505 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001506 cl = Changelist()
1507 if not cl.GetIssue():
1508 DieWithError('This branch has no associated changelist.')
1509 description = ChangeDescription(cl.GetDescription())
1510 description.prompt()
1511 cl.UpdateDescription(description.description)
1512 return 0
1513
1514
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515def CreateDescriptionFromLog(args):
1516 """Pulls out the commit log to use as a base for the CL description."""
1517 log_args = []
1518 if len(args) == 1 and not args[0].endswith('.'):
1519 log_args = [args[0] + '..']
1520 elif len(args) == 1 and args[0].endswith('...'):
1521 log_args = [args[0][:-1]]
1522 elif len(args) == 2:
1523 log_args = [args[0] + '..' + args[1]]
1524 else:
1525 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001526 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527
1528
thestig@chromium.org44202a22014-03-11 19:22:18 +00001529def CMDlint(parser, args):
1530 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001531 parser.add_option('--filter', action='append', metavar='-x,+y',
1532 help='Comma-separated list of cpplint\'s category-filters')
1533 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001534
1535 # Access to a protected member _XX of a client class
1536 # pylint: disable=W0212
1537 try:
1538 import cpplint
1539 import cpplint_chromium
1540 except ImportError:
1541 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1542 return 1
1543
1544 # Change the current working directory before calling lint so that it
1545 # shows the correct base.
1546 previous_cwd = os.getcwd()
1547 os.chdir(settings.GetRoot())
1548 try:
1549 cl = Changelist()
1550 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1551 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001552 if not files:
1553 print "Cannot lint an empty CL"
1554 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001555
1556 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001557 command = args + files
1558 if options.filter:
1559 command = ['--filter=' + ','.join(options.filter)] + command
1560 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001561
1562 white_regex = re.compile(settings.GetLintRegex())
1563 black_regex = re.compile(settings.GetLintIgnoreRegex())
1564 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1565 for filename in filenames:
1566 if white_regex.match(filename):
1567 if black_regex.match(filename):
1568 print "Ignoring file %s" % filename
1569 else:
1570 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1571 extra_check_functions)
1572 else:
1573 print "Skipping file %s" % filename
1574 finally:
1575 os.chdir(previous_cwd)
1576 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1577 if cpplint._cpplint_state.error_count != 0:
1578 return 1
1579 return 0
1580
1581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001583 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001584 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001585 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001586 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001587 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001588 (options, args) = parser.parse_args(args)
1589
ukai@chromium.org259e4682012-10-25 07:36:33 +00001590 if not options.force and is_dirty_git_tree('presubmit'):
1591 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001592 return 1
1593
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001594 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001595 if args:
1596 base_branch = args[0]
1597 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001598 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001599 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001600
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001601 cl.RunHook(
1602 committing=not options.upload,
1603 may_prompt=False,
1604 verbose=options.verbose,
1605 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001606 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607
1608
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001609def AddChangeIdToCommitMessage(options, args):
1610 """Re-commits using the current message, assumes the commit hook is in
1611 place.
1612 """
1613 log_desc = options.message or CreateDescriptionFromLog(args)
1614 git_command = ['commit', '--amend', '-m', log_desc]
1615 RunGit(git_command)
1616 new_log_desc = CreateDescriptionFromLog(args)
1617 if CHANGE_ID in new_log_desc:
1618 print 'git-cl: Added Change-Id to commit message.'
1619 else:
1620 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1621
1622
piman@chromium.org336f9122014-09-04 02:16:55 +00001623def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001624 """upload the current branch to gerrit."""
1625 # We assume the remote called "origin" is the one we want.
1626 # It is probably not worthwhile to support different workflows.
1627 remote = 'origin'
1628 branch = 'master'
1629 if options.target_branch:
1630 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001631
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001632 change_desc = ChangeDescription(
1633 options.message or CreateDescriptionFromLog(args))
1634 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001635 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001636 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001637 if CHANGE_ID not in change_desc.description:
1638 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001639
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001640 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001641 if len(commits) > 1:
1642 print('WARNING: This will upload %d commits. Run the following command '
1643 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001644 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001645 print('You can also use `git squash-branch` to squash these into a single'
1646 'commit.')
1647 ask_for_data('About to upload; enter to confirm.')
1648
piman@chromium.org336f9122014-09-04 02:16:55 +00001649 if options.reviewers or options.tbr_owners:
1650 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651
ukai@chromium.orge8077812012-02-03 03:41:46 +00001652 receive_options = []
1653 cc = cl.GetCCList().split(',')
1654 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001655 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001656 cc = filter(None, cc)
1657 if cc:
1658 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001659 if change_desc.get_reviewers():
1660 receive_options.extend(
1661 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001662
ukai@chromium.orge8077812012-02-03 03:41:46 +00001663 git_command = ['push']
1664 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001665 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001666 ' '.join(receive_options))
1667 git_command += [remote, 'HEAD:refs/for/' + branch]
1668 RunGit(git_command)
1669 # TODO(ukai): parse Change-Id: and set issue number?
1670 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001671
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001672
piman@chromium.org336f9122014-09-04 02:16:55 +00001673def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001674 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001675 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1676 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001677 if options.emulate_svn_auto_props:
1678 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001679
1680 change_desc = None
1681
pgervais@chromium.org91141372014-01-09 23:27:20 +00001682 if options.email is not None:
1683 upload_args.extend(['--email', options.email])
1684
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001685 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001686 if options.title:
1687 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001688 if options.message:
1689 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001690 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691 print ("This branch is associated with issue %s. "
1692 "Adding patch to that issue." % cl.GetIssue())
1693 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001694 if options.title:
1695 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001696 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001697 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00001698 if options.reviewers or options.tbr_owners:
1699 change_desc.update_reviewers(options.reviewers,
1700 options.tbr_owners,
1701 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001702 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001703 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001704
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001705 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001706 print "Description is empty; aborting."
1707 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001708
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001709 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001710 if change_desc.get_reviewers():
1711 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001712 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001713 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001714 DieWithError("Must specify reviewers to send email.")
1715 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001716
1717 # We check this before applying rietveld.private assuming that in
1718 # rietveld.cc only addresses which we can send private CLs to are listed
1719 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1720 # --private is specified explicitly on the command line.
1721 if options.private:
1722 logging.warn('rietveld.cc is ignored since private flag is specified. '
1723 'You need to review and add them manually if necessary.')
1724 cc = cl.GetCCListWithoutDefault()
1725 else:
1726 cc = cl.GetCCList()
1727 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001728 if cc:
1729 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001731 if options.private or settings.GetDefaultPrivateFlag() == "True":
1732 upload_args.append('--private')
1733
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001734 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001735 if not options.find_copies:
1736 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001737
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738 # Include the upstream repo's URL in the change -- this is useful for
1739 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001740 remote_url = cl.GetGitBaseUrlFromConfig()
1741 if not remote_url:
1742 if settings.GetIsGitSvn():
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00001743 remote_url = cl.GetGitSvnRemoteUrl()
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001744 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00001745 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1746 remote_url = (cl.GetRemoteUrl() + '@'
1747 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001748 if remote_url:
1749 upload_args.extend(['--base_url', remote_url])
rmistry@google.comd1e37582014-12-10 20:58:24 +00001750 remote, remote_branch = cl.GetRemoteBranch()
1751 if remote and remote_branch:
1752 # Create the true path to the remote branch.
1753 # Does the following translation:
1754 # * refs/remotes/origin/refs/diff/test -> refs/diff/test
1755 # * refs/remotes/origin/master -> refs/heads/master
1756 # * refs/remotes/branch-heads/test -> refs/branch-heads/test
1757 if remote_branch.startswith('refs/remotes/%s/refs/' % remote):
1758 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote, '')
1759 elif remote_branch.startswith('refs/remotes/%s/' % remote):
1760 remote_branch = remote_branch.replace('refs/remotes/%s/' % remote,
1761 'refs/heads/')
1762 elif remote_branch.startswith('refs/remotes/branch-heads'):
1763 remote_branch = remote_branch.replace('refs/remotes/', 'refs/')
rmistry@google.com2138f502014-12-11 23:41:21 +00001764 pending_prefix = settings.GetPendingRefPrefix()
1765 # If a pending prefix exists then replace refs/ with it.
1766 if pending_prefix:
1767 remote_branch = remote_branch.replace('refs/', pending_prefix)
rmistry@google.comd1e37582014-12-10 20:58:24 +00001768 upload_args.extend(['--target_ref', remote_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001770 project = settings.GetProject()
1771 if project:
1772 upload_args.extend(['--project', project])
1773
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001774 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001775 upload_args = ['upload'] + upload_args + args
1776 logging.info('upload.RealMain(%s)', upload_args)
1777 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001778 issue = int(issue)
1779 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001780 except KeyboardInterrupt:
1781 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782 except:
1783 # If we got an exception after the user typed a description for their
1784 # change, back up the description before re-raising.
1785 if change_desc:
1786 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1787 print '\nGot exception while uploading -- saving description to %s\n' \
1788 % backup_path
1789 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001790 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791 backup_file.close()
1792 raise
1793
1794 if not cl.GetIssue():
1795 cl.SetIssue(issue)
1796 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001797
1798 if options.use_commit_queue:
1799 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001800 return 0
1801
1802
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001803def cleanup_list(l):
1804 """Fixes a list so that comma separated items are put as individual items.
1805
1806 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1807 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1808 """
1809 items = sum((i.split(',') for i in l), [])
1810 stripped_items = (i.strip() for i in items)
1811 return sorted(filter(None, stripped_items))
1812
1813
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001814@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001815def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001816 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001817 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1818 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001819 parser.add_option('--bypass-watchlists', action='store_true',
1820 dest='bypass_watchlists',
1821 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001822 parser.add_option('-f', action='store_true', dest='force',
1823 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001824 parser.add_option('-m', dest='message', help='message for patchset')
1825 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001826 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001827 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001828 help='reviewer email addresses')
1829 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001830 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001831 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001832 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001833 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001834 parser.add_option('--emulate_svn_auto_props',
1835 '--emulate-svn-auto-props',
1836 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001837 dest="emulate_svn_auto_props",
1838 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001839 parser.add_option('-c', '--use-commit-queue', action='store_true',
1840 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001841 parser.add_option('--private', action='store_true',
1842 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001843 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001844 '--target-branch',
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001845 help='When uploading to gerrit, remote branch to '
1846 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001847 parser.add_option('--email', default=None,
1848 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00001849 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
1850 help='add a set of OWNERS to TBR')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001851
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001852 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001853 (options, args) = parser.parse_args(args)
1854
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001855 if options.target_branch and not settings.GetIsGerrit():
1856 parser.error('Use --target_branch for non gerrit repository.')
1857
ukai@chromium.org259e4682012-10-25 07:36:33 +00001858 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001859 return 1
1860
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001861 options.reviewers = cleanup_list(options.reviewers)
1862 options.cc = cleanup_list(options.cc)
1863
ukai@chromium.orge8077812012-02-03 03:41:46 +00001864 cl = Changelist()
1865 if args:
1866 # TODO(ukai): is it ok for gerrit case?
1867 base_branch = args[0]
1868 else:
luqui@chromium.org64e14362015-01-07 00:29:29 +00001869 if cl.GetBranch() is None:
1870 DieWithError('Can\'t upload from detached HEAD state. Get on a branch!')
1871
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001872 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001873 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001874 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001875
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001876 # Apply watchlists on upload.
1877 change = cl.GetChange(base_branch, None)
1878 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1879 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001880 if not options.bypass_watchlists:
1881 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001882
ukai@chromium.orge8077812012-02-03 03:41:46 +00001883 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00001884 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001885 # Set the reviewer list now so that presubmit checks can access it.
1886 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00001887 change_description.update_reviewers(options.reviewers,
1888 options.tbr_owners,
1889 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001890 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001891 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001892 may_prompt=not options.force,
1893 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001894 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001895 if not hook_results.should_continue():
1896 return 1
1897 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001898 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001899
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001900 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001901 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001902 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001903 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001904 print ('The last upload made from this repository was patchset #%d but '
1905 'the most recent patchset on the server is #%d.'
1906 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001907 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1908 'from another machine or branch the patch you\'re uploading now '
1909 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001910 ask_for_data('About to upload; enter to confirm.')
1911
iannucci@chromium.org79540052012-10-19 23:15:26 +00001912 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001913 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00001914 return GerritUpload(options, args, cl, change)
1915 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001916 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001917 git_set_branch_value('last-upload-hash',
1918 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001919
1920 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001921
1922
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001923def IsSubmoduleMergeCommit(ref):
1924 # When submodules are added to the repo, we expect there to be a single
1925 # non-git-svn merge commit at remote HEAD with a signature comment.
1926 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001927 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001928 return RunGit(cmd) != ''
1929
1930
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001932 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001934 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001935 Updates changelog with metadata (e.g. pointer to review).
1936 Pushes/dcommits the code upstream.
1937 Updates review and closes.
1938 """
1939 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1940 help='bypass upload presubmit hook')
1941 parser.add_option('-m', dest='message',
1942 help="override review description")
1943 parser.add_option('-f', action='store_true', dest='force',
1944 help="force yes to questions (don't prompt)")
1945 parser.add_option('-c', dest='contributor',
1946 help="external contributor for patch (appended to " +
1947 "description and used as author for git). Should be " +
1948 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001949 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001950 (options, args) = parser.parse_args(args)
1951 cl = Changelist()
1952
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001953 current = cl.GetBranch()
1954 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1955 if not settings.GetIsGitSvn() and remote == '.':
1956 print
1957 print 'Attempting to push branch %r into another local branch!' % current
1958 print
1959 print 'Either reparent this branch on top of origin/master:'
1960 print ' git reparent-branch --root'
1961 print
1962 print 'OR run `git rebase-update` if you think the parent branch is already'
1963 print 'committed.'
1964 print
1965 print ' Current parent: %r' % upstream_branch
1966 return 1
1967
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001968 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001969 # Default to merging against our best guess of the upstream branch.
1970 args = [cl.GetUpstreamBranch()]
1971
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001972 if options.contributor:
1973 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1974 print "Please provide contibutor as 'First Last <email@example.com>'"
1975 return 1
1976
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001978 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979
ukai@chromium.org259e4682012-10-25 07:36:33 +00001980 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981 return 1
1982
1983 # This rev-list syntax means "show all commits not in my branch that
1984 # are in base_branch".
1985 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1986 base_branch]).splitlines()
1987 if upstream_commits:
1988 print ('Base branch "%s" has %d commits '
1989 'not in this branch.' % (base_branch, len(upstream_commits)))
1990 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1991 return 1
1992
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001993 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001994 svn_head = None
1995 if cmd == 'dcommit' or base_has_submodules:
1996 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1997 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001998
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002000 # If the base_head is a submodule merge commit, the first parent of the
2001 # base_head should be a git-svn commit, which is what we're interested in.
2002 base_svn_head = base_branch
2003 if base_has_submodules:
2004 base_svn_head += '^1'
2005
2006 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007 if extra_commits:
2008 print ('This branch has %d additional commits not upstreamed yet.'
2009 % len(extra_commits.splitlines()))
2010 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
2011 'before attempting to %s.' % (base_branch, cmd))
2012 return 1
2013
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002014 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002015 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00002016 author = None
2017 if options.contributor:
2018 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002019 hook_results = cl.RunHook(
2020 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002021 may_prompt=not options.force,
2022 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002023 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00002024 if not hook_results.should_continue():
2025 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002027 # Check the tree status if the tree status URL is set.
2028 status = GetTreeStatus()
2029 if 'closed' == status:
2030 print('The tree is closed. Please wait for it to reopen. Use '
2031 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2032 return 1
2033 elif 'unknown' == status:
2034 print('Unable to determine tree status. Please verify manually and '
2035 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
2036 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00002037 else:
2038 breakpad.SendStack(
2039 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002040 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2041 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002042 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002044 change_desc = ChangeDescription(options.message)
2045 if not change_desc.description and cl.GetIssue():
2046 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002047
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002048 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002049 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002050 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002051 else:
2052 print 'No description set.'
2053 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2054 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002056 # Keep a separate copy for the commit message, because the commit message
2057 # contains the link to the Rietveld issue, while the Rietveld message contains
2058 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002059 # Keep a separate copy for the commit message.
2060 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002061 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002062
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002063 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002064 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002065 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002066 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002067 commit_desc.append_footer('Patch from %s.' % options.contributor)
2068
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002069 print('Description:')
2070 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002071
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002072 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002073 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002074 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002075
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002076 # We want to squash all this branch's commits into one commit with the proper
2077 # description. We do this by doing a "reset --soft" to the base branch (which
2078 # keeps the working copy the same), then dcommitting that. If origin/master
2079 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2080 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002081 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002082 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2083 # Delete the branches if they exist.
2084 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2085 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2086 result = RunGitWithCode(showref_cmd)
2087 if result[0] == 0:
2088 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002089
2090 # We might be in a directory that's present in this branch but not in the
2091 # trunk. Move up to the top of the tree so that git commands that expect a
2092 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002093 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002094 if rel_base_path:
2095 os.chdir(rel_base_path)
2096
2097 # Stuff our change into the merge branch.
2098 # We wrap in a try...finally block so if anything goes wrong,
2099 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002100 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002101 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002102 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002103 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002104 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002105 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002106 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002107 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002108 RunGit(
2109 [
2110 'commit', '--author', options.contributor,
2111 '-m', commit_desc.description,
2112 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002114 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002115 if base_has_submodules:
2116 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2117 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2118 RunGit(['checkout', CHERRY_PICK_BRANCH])
2119 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002120 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002121 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002122 pending_prefix = settings.GetPendingRefPrefix()
2123 if not pending_prefix or branch.startswith(pending_prefix):
2124 # If not using refs/pending/heads/* at all, or target ref is already set
2125 # to pending, then push to the target ref directly.
2126 retcode, output = RunGitWithCode(
2127 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002128 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002129 else:
2130 # Cherry-pick the change on top of pending ref and then push it.
2131 assert branch.startswith('refs/'), branch
2132 assert pending_prefix[-1] == '/', pending_prefix
2133 pending_ref = pending_prefix + branch[len('refs/'):]
2134 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002135 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002136 if retcode == 0:
2137 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138 else:
2139 # dcommit the merge branch.
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002140 cmd_args = [
kjellander@chromium.org6abc6522014-12-02 07:34:49 +00002141 'svn', 'dcommit',
2142 '-C%s' % options.similarity,
2143 '--no-rebase', '--rmdir',
2144 ]
2145 if settings.GetForceHttpsCommitUrl():
2146 # Allow forcing https commit URLs for some projects that don't allow
2147 # committing to http URLs (like Google Code).
2148 remote_url = cl.GetGitSvnRemoteUrl()
2149 if urlparse.urlparse(remote_url).scheme == 'http':
2150 remote_url = remote_url.replace('http://', 'https://')
iannucci@chromium.orga1950c42014-12-05 22:15:56 +00002151 cmd_args.append('--commit-url=%s' % remote_url)
2152 _, output = RunGitWithCode(cmd_args)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002153 if 'Committed r' in output:
2154 revision = re.match(
2155 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2156 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002157 finally:
2158 # And then swap back to the original branch and clean up.
2159 RunGit(['checkout', '-q', cl.GetBranch()])
2160 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002161 if base_has_submodules:
2162 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002163
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002164 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002165 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002166 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002167
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002168 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002169 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002170 try:
2171 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2172 # We set pushed_to_pending to False, since it made it all the way to the
2173 # real ref.
2174 pushed_to_pending = False
2175 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002176 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002177
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002178 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002179 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002180 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002181 if not to_pending:
2182 if viewvc_url and revision:
2183 change_desc.append_footer(
2184 'Committed: %s%s' % (viewvc_url, revision))
2185 elif revision:
2186 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002187 print ('Closing issue '
2188 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002189 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002191 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002192 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002193 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002194 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002195 if options.bypass_hooks:
2196 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2197 else:
2198 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002199 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002200 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002201
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002202 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002203 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2204 print 'The commit is in the pending queue (%s).' % pending_ref
2205 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002206 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002207 'footer.' % branch)
2208
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002209 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2210 if os.path.isfile(hook):
2211 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002212
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002213 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002214
2215
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002216def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2217 print
2218 print 'Waiting for commit to be landed on %s...' % real_ref
2219 print '(If you are impatient, you may Ctrl-C once without harm)'
2220 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2221 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2222
2223 loop = 0
2224 while True:
2225 sys.stdout.write('fetching (%d)... \r' % loop)
2226 sys.stdout.flush()
2227 loop += 1
2228
2229 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2230 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2231 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2232 for commit in commits.splitlines():
2233 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2234 print 'Found commit on %s' % real_ref
2235 return commit
2236
2237 current_rev = to_rev
2238
2239
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002240def PushToGitPending(remote, pending_ref, upstream_ref):
2241 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2242
2243 Returns:
2244 (retcode of last operation, output log of last operation).
2245 """
2246 assert pending_ref.startswith('refs/'), pending_ref
2247 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2248 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2249 code = 0
2250 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002251 max_attempts = 3
2252 attempts_left = max_attempts
2253 while attempts_left:
2254 if attempts_left != max_attempts:
2255 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2256 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002257
2258 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002259 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002260 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002261 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002262 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002263 print 'Fetch failed with exit code %d.' % code
2264 if out.strip():
2265 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002266 continue
2267
2268 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002269 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002270 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002271 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002272 if code:
2273 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002274 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2275 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002276 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2277 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002278 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002279 return code, out
2280
2281 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002282 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002283 code, out = RunGitWithCode(
2284 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2285 if code == 0:
2286 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002287 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002288 return code, out
2289
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002290 print 'Push failed with exit code %d.' % code
2291 if out.strip():
2292 print out.strip()
2293 if IsFatalPushFailure(out):
2294 print (
2295 'Fatal push error. Make sure your .netrc credentials and git '
2296 'user.email are correct and you have push access to the repo.')
2297 return code, out
2298
2299 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002300 return code, out
2301
2302
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002303def IsFatalPushFailure(push_stdout):
2304 """True if retrying push won't help."""
2305 return '(prohibited by Gerrit)' in push_stdout
2306
2307
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002308@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002309def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002310 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002311 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002312 message = """This doesn't appear to be an SVN repository.
2313If your project has a git mirror with an upstream SVN master, you probably need
2314to run 'git svn init', see your project's git mirror documentation.
2315If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002316to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002317Choose wisely, if you get this wrong, your commit might appear to succeed but
2318will instead be silently ignored."""
2319 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002320 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002321 return SendUpstream(parser, args, 'dcommit')
2322
2323
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002324@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002325def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002326 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002327 if settings.GetIsGitSvn():
2328 print('This appears to be an SVN repository.')
2329 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002330 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002331 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002332
2333
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002334@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002335def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002336 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002337 parser.add_option('-b', dest='newbranch',
2338 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002339 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002340 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002341 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2342 help='Change to the directory DIR immediately, '
2343 'before doing anything else.')
2344 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002345 help='failed patches spew .rej files rather than '
2346 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002347 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2348 help="don't commit after patch applies")
2349 (options, args) = parser.parse_args(args)
2350 if len(args) != 1:
2351 parser.print_help()
2352 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002353 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002354
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002355 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002356 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002357
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002358 if options.newbranch:
2359 if options.force:
2360 RunGit(['branch', '-D', options.newbranch],
2361 stderr=subprocess2.PIPE, error_ok=True)
2362 RunGit(['checkout', '-b', options.newbranch,
2363 Changelist().GetUpstreamBranch()])
2364
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002365 return PatchIssue(issue_arg, options.reject, options.nocommit,
2366 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002367
2368
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002369def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002370 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002371 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002372 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002373 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002374 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002375 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002376 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002377 # Assume it's a URL to the patch. Default to https.
2378 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002379 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002380 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002381 DieWithError('Must pass an issue ID or full URL for '
2382 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002383 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002384 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002385 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002386
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002387 # Switch up to the top-level directory, if necessary, in preparation for
2388 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002389 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002390 if top:
2391 os.chdir(top)
2392
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002393 # Git patches have a/ at the beginning of source paths. We strip that out
2394 # with a sed script rather than the -p flag to patch so we can feed either
2395 # Git or svn-style patches into the same apply command.
2396 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002397 try:
2398 patch_data = subprocess2.check_output(
2399 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2400 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002401 DieWithError('Git patch mungling failed.')
2402 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002404 # We use "git apply" to apply the patch instead of "patch" so that we can
2405 # pick up file adds.
2406 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002407 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002408 if directory:
2409 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002410 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002411 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002412 elif IsGitVersionAtLeast('1.7.12'):
2413 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002414 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002415 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002416 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002417 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002418 DieWithError('Failed to apply the patch')
2419
2420 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002421 if not nocommit:
carlosk@chromium.org71284d92014-11-14 18:12:50 +00002422 RunGit(['commit', '-m', ('patch from issue %(i)s at patchset '
2423 '%(p)s (http://crrev.com/%(i)s#ps%(p)s)'
2424 % {'i': issue, 'p': patchset})])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002425 cl = Changelist()
2426 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002427 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002428 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002429 else:
2430 print "Patch applied to index."
2431 return 0
2432
2433
2434def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002435 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002436 # Provide a wrapper for git svn rebase to help avoid accidental
2437 # git svn dcommit.
2438 # It's the only command that doesn't use parser at all since we just defer
2439 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002440
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002441 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002442
2443
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002444def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002445 """Fetches the tree status and returns either 'open', 'closed',
2446 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002447 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002448 if url:
2449 status = urllib2.urlopen(url).read().lower()
2450 if status.find('closed') != -1 or status == '0':
2451 return 'closed'
2452 elif status.find('open') != -1 or status == '1':
2453 return 'open'
2454 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002455 return 'unset'
2456
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002457
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002458def GetTreeStatusReason():
2459 """Fetches the tree status from a json url and returns the message
2460 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002461 url = settings.GetTreeStatusUrl()
2462 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002463 connection = urllib2.urlopen(json_url)
2464 status = json.loads(connection.read())
2465 connection.close()
2466 return status['message']
2467
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002468
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002469def GetBuilderMaster(bot_list):
2470 """For a given builder, fetch the master from AE if available."""
2471 map_url = 'https://builders-map.appspot.com/'
2472 try:
2473 master_map = json.load(urllib2.urlopen(map_url))
2474 except urllib2.URLError as e:
2475 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2476 (map_url, e))
2477 except ValueError as e:
2478 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2479 if not master_map:
2480 return None, 'Failed to build master map.'
2481
2482 result_master = ''
2483 for bot in bot_list:
2484 builder = bot.split(':', 1)[0]
2485 master_list = master_map.get(builder, [])
2486 if not master_list:
2487 return None, ('No matching master for builder %s.' % builder)
2488 elif len(master_list) > 1:
2489 return None, ('The builder name %s exists in multiple masters %s.' %
2490 (builder, master_list))
2491 else:
2492 cur_master = master_list[0]
2493 if not result_master:
2494 result_master = cur_master
2495 elif result_master != cur_master:
2496 return None, 'The builders do not belong to the same master.'
2497 return result_master, None
2498
2499
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002500def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002501 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002502 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002503 status = GetTreeStatus()
2504 if 'unset' == status:
2505 print 'You must configure your tree status URL by running "git cl config".'
2506 return 2
2507
2508 print "The tree is %s" % status
2509 print
2510 print GetTreeStatusReason()
2511 if status != 'open':
2512 return 1
2513 return 0
2514
2515
maruel@chromium.org15192402012-09-06 12:38:29 +00002516def CMDtry(parser, args):
2517 """Triggers a try job through Rietveld."""
2518 group = optparse.OptionGroup(parser, "Try job options")
2519 group.add_option(
2520 "-b", "--bot", action="append",
2521 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2522 "times to specify multiple builders. ex: "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002523 "'-b win_rel:ui_tests,webkit_unit_tests -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002524 "the try server waterfall for the builders name and the tests "
2525 "available. Can also be used to specify gtest_filter, e.g. "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002526 "-b win_rel:base_unittests:ValuesTest.*Value"))
maruel@chromium.org15192402012-09-06 12:38:29 +00002527 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002528 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002529 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002530 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002531 "-r", "--revision",
2532 help="Revision to use for the try job; default: the "
2533 "revision will be determined by the try server; see "
2534 "its waterfall for more info")
2535 group.add_option(
2536 "-c", "--clobber", action="store_true", default=False,
2537 help="Force a clobber before building; e.g. don't do an "
2538 "incremental build")
2539 group.add_option(
2540 "--project",
2541 help="Override which project to use. Projects are defined "
2542 "server-side to define what default bot set to use")
2543 group.add_option(
2544 "-t", "--testfilter", action="append", default=[],
2545 help=("Apply a testfilter to all the selected builders. Unless the "
2546 "builders configurations are similar, use multiple "
2547 "--bot <builder>:<test> arguments."))
2548 group.add_option(
2549 "-n", "--name", help="Try job name; default to current branch name")
2550 parser.add_option_group(group)
2551 options, args = parser.parse_args(args)
2552
2553 if args:
2554 parser.error('Unknown arguments: %s' % args)
2555
2556 cl = Changelist()
2557 if not cl.GetIssue():
2558 parser.error('Need to upload first')
2559
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002560 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002561 if props.get('closed'):
2562 parser.error('Cannot send tryjobs for a closed CL')
2563
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002564 if props.get('private'):
2565 parser.error('Cannot use trybots with private issue')
2566
maruel@chromium.org15192402012-09-06 12:38:29 +00002567 if not options.name:
2568 options.name = cl.GetBranch()
2569
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002570 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002571 options.master, err_msg = GetBuilderMaster(options.bot)
2572 if err_msg:
2573 parser.error('Tryserver master cannot be found because: %s\n'
2574 'Please manually specify the tryserver master'
2575 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002576
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002577 def GetMasterMap():
2578 # Process --bot and --testfilter.
2579 if not options.bot:
2580 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002581
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002582 # Get try masters from PRESUBMIT.py files.
2583 masters = presubmit_support.DoGetTryMasters(
2584 change,
2585 change.LocalPaths(),
2586 settings.GetRoot(),
2587 None,
2588 None,
2589 options.verbose,
2590 sys.stdout)
2591 if masters:
2592 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002593
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002594 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2595 options.bot = presubmit_support.DoGetTrySlaves(
2596 change,
2597 change.LocalPaths(),
2598 settings.GetRoot(),
2599 None,
2600 None,
2601 options.verbose,
2602 sys.stdout)
2603 if not options.bot:
2604 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002605
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002606 builders_and_tests = {}
2607 # TODO(machenbach): The old style command-line options don't support
2608 # multiple try masters yet.
2609 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2610 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2611
2612 for bot in old_style:
2613 if ':' in bot:
2614 builder, tests = bot.split(':', 1)
2615 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2616 elif ',' in bot:
2617 parser.error('Specify one bot per --bot flag')
2618 else:
2619 builders_and_tests.setdefault(bot, []).append('defaulttests')
2620
2621 for bot, tests in new_style:
2622 builders_and_tests.setdefault(bot, []).extend(tests)
2623
2624 # Return a master map with one master to be backwards compatible. The
2625 # master name defaults to an empty string, which will cause the master
2626 # not to be set on rietveld (deprecated).
2627 return {options.master: builders_and_tests}
2628
2629 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002630
maruel@chromium.org15192402012-09-06 12:38:29 +00002631 if options.testfilter:
2632 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002633 masters = dict((master, dict(
2634 (b, forced_tests) for b, t in slaves.iteritems()
2635 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002636
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002637 for builders in masters.itervalues():
2638 if any('triggered' in b for b in builders):
2639 print >> sys.stderr, (
2640 'ERROR You are trying to send a job to a triggered bot. This type of'
2641 ' bot requires an\ninitial job from a parent (usually a builder). '
2642 'Instead send your job to the parent.\n'
2643 'Bot list: %s' % builders)
2644 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002645
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002646 patchset = cl.GetMostRecentPatchset()
2647 if patchset and patchset != cl.GetPatchset():
2648 print(
2649 '\nWARNING Mismatch between local config and server. Did a previous '
2650 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2651 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002652 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002653 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002654 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002655 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002656 except urllib2.HTTPError, e:
2657 if e.code == 404:
2658 print('404 from rietveld; '
2659 'did you mean to use "git try" instead of "git cl try"?')
2660 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002661 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002662
2663 for (master, builders) in masters.iteritems():
2664 if master:
2665 print 'Master: %s' % master
2666 length = max(len(builder) for builder in builders)
2667 for builder in sorted(builders):
2668 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002669 return 0
2670
2671
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002672@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002673def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002674 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002675 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002676 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002677 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002678
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002679 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002680 if args:
2681 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002682 branch = cl.GetBranch()
2683 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002684 cl = Changelist()
2685 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002686
2687 # Clear configured merge-base, if there is one.
2688 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002689 else:
2690 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002691 return 0
2692
2693
thestig@chromium.org00858c82013-12-02 23:08:03 +00002694def CMDweb(parser, args):
2695 """Opens the current CL in the web browser."""
2696 _, args = parser.parse_args(args)
2697 if args:
2698 parser.error('Unrecognized args: %s' % ' '.join(args))
2699
2700 issue_url = Changelist().GetIssueURL()
2701 if not issue_url:
2702 print >> sys.stderr, 'ERROR No issue to open'
2703 return 1
2704
2705 webbrowser.open(issue_url)
2706 return 0
2707
2708
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002709def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002710 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002711 _, args = parser.parse_args(args)
2712 if args:
2713 parser.error('Unrecognized args: %s' % ' '.join(args))
2714 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002715 props = cl.GetIssueProperties()
2716 if props.get('private'):
2717 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002718 cl.SetFlag('commit', '1')
2719 return 0
2720
2721
groby@chromium.org411034a2013-02-26 15:12:01 +00002722def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002723 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002724 _, args = parser.parse_args(args)
2725 if args:
2726 parser.error('Unrecognized args: %s' % ' '.join(args))
2727 cl = Changelist()
2728 # Ensure there actually is an issue to close.
2729 cl.GetDescription()
2730 cl.CloseIssue()
2731 return 0
2732
2733
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002734def CMDdiff(parser, args):
2735 """shows differences between local tree and last upload."""
2736 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002737 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002738 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002739 if not issue:
2740 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002741 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002742 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002743
2744 # Create a new branch based on the merge-base
2745 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2746 try:
2747 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002748 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002749 if rtn != 0:
2750 return rtn
2751
2752 # Switch back to starting brand and diff against the temporary
2753 # branch containing the latest rietveld patch.
2754 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2755 finally:
2756 RunGit(['checkout', '-q', branch])
2757 RunGit(['branch', '-D', TMP_BRANCH])
2758
2759 return 0
2760
2761
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002762def CMDowners(parser, args):
2763 """interactively find the owners for reviewing"""
2764 parser.add_option(
2765 '--no-color',
2766 action='store_true',
2767 help='Use this option to disable color output')
2768 options, args = parser.parse_args(args)
2769
2770 author = RunGit(['config', 'user.email']).strip() or None
2771
2772 cl = Changelist()
2773
2774 if args:
2775 if len(args) > 1:
2776 parser.error('Unknown args')
2777 base_branch = args[0]
2778 else:
2779 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002780 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002781
2782 change = cl.GetChange(base_branch, None)
2783 return owners_finder.OwnersFinder(
2784 [f.LocalPath() for f in
2785 cl.GetChange(base_branch, None).AffectedFiles()],
2786 change.RepositoryRoot(), author,
2787 fopen=file, os_path=os.path, glob=glob.glob,
2788 disable_color=options.no_color).run()
2789
2790
enne@chromium.org555cfe42014-01-29 18:21:39 +00002791@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002792def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002793 """Runs clang-format on the diff."""
thakis@chromium.org9819b1b2014-12-09 21:21:53 +00002794 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto', '.java']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002795 parser.add_option('--full', action='store_true',
2796 help='Reformat the full content of all touched files')
2797 parser.add_option('--dry-run', action='store_true',
2798 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002799 parser.add_option('--diff', action='store_true',
2800 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002801 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002802
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002803 # git diff generates paths against the root of the repository. Change
2804 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002805 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002806 if rel_base_path:
2807 os.chdir(rel_base_path)
2808
digit@chromium.org29e47272013-05-17 17:01:46 +00002809 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002810 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002811 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002812 # Only list the names of modified files.
2813 diff_cmd.append('--name-only')
2814 else:
2815 # Only generate context-less patches.
2816 diff_cmd.append('-U0')
2817
2818 # Grab the merge-base commit, i.e. the upstream commit of the current
2819 # branch when it was created or the last time it was rebased. This is
2820 # to cover the case where the user may have called "git fetch origin",
2821 # moving the origin branch to a newer commit, but hasn't rebased yet.
2822 upstream_commit = None
2823 cl = Changelist()
2824 upstream_branch = cl.GetUpstreamBranch()
2825 if upstream_branch:
2826 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2827 upstream_commit = upstream_commit.strip()
2828
2829 if not upstream_commit:
2830 DieWithError('Could not find base commit for this branch. '
2831 'Are you in detached state?')
2832
2833 diff_cmd.append(upstream_commit)
2834
2835 # Handle source file filtering.
2836 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002837 if args:
2838 for arg in args:
2839 if os.path.isdir(arg):
2840 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2841 elif os.path.isfile(arg):
2842 diff_cmd.append(arg)
2843 else:
2844 DieWithError('Argument "%s" is not a file or a directory' % arg)
2845 else:
2846 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002847 diff_output = RunGit(diff_cmd)
2848
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002849 top_dir = os.path.normpath(
2850 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2851
2852 # Locate the clang-format binary in the checkout
2853 try:
2854 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2855 except clang_format.NotFoundError, e:
2856 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002857
digit@chromium.org29e47272013-05-17 17:01:46 +00002858 if opts.full:
2859 # diff_output is a list of files to send to clang-format.
2860 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002861 if not files:
2862 print "Nothing to format."
2863 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002864 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002865 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002866 cmd.append('-i')
2867 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002868 if opts.diff:
2869 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002870 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002871 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00002872 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00002873 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002874 try:
2875 script = clang_format.FindClangFormatScriptInChromiumTree(
2876 'clang-format-diff.py')
2877 except clang_format.NotFoundError, e:
2878 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002879
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002880 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002881 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002882 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002883
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002884 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002885 if opts.diff:
2886 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002887 if opts.dry_run and len(stdout) > 0:
2888 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002889
2890 return 0
2891
2892
maruel@chromium.org29404b52014-09-08 22:58:00 +00002893def CMDlol(parser, args):
2894 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00002895 print zlib.decompress(base64.b64decode(
2896 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
2897 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
2898 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
2899 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00002900 return 0
2901
2902
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002903class OptionParser(optparse.OptionParser):
2904 """Creates the option parse and add --verbose support."""
2905 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002906 optparse.OptionParser.__init__(
2907 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002908 self.add_option(
2909 '-v', '--verbose', action='count', default=0,
2910 help='Use 2 times for more debugging info')
2911
2912 def parse_args(self, args=None, values=None):
2913 options, args = optparse.OptionParser.parse_args(self, args, values)
2914 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2915 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2916 return options, args
2917
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002918
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002919def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002920 if sys.hexversion < 0x02060000:
2921 print >> sys.stderr, (
2922 '\nYour python version %s is unsupported, please upgrade.\n' %
2923 sys.version.split(' ', 1)[0])
2924 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002925
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002926 # Reload settings.
2927 global settings
2928 settings = Settings()
2929
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002930 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002931 dispatcher = subcommand.CommandDispatcher(__name__)
2932 try:
2933 return dispatcher.execute(OptionParser(), argv)
2934 except urllib2.HTTPError, e:
2935 if e.code != 500:
2936 raise
2937 DieWithError(
2938 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2939 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002940
2941
2942if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002943 # These affect sys.stdout so do it outside of main() to simplify mocks in
2944 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002945 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002946 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002947 sys.exit(main(sys.argv[1:]))