blob: 82d5a78abb6f5780fe0dafcca334cf03353e8a1a [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
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000035import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000036import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000037import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000038import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000039import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000041import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000044import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045
maruel@chromium.org0633fb42013-08-16 20:06:14 +000046__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000048DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000049POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000051GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000052CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053
thestig@chromium.org44202a22014-03-11 19:22:18 +000054# Valid extensions for files we want to lint.
55DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
56DEFAULT_LINT_IGNORE_REGEX = r"$^"
57
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000058# Shortcut since it quickly becomes redundant.
59Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000060
maruel@chromium.orgddd59412011-11-30 14:20:38 +000061# Initialized in main()
62settings = None
63
64
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000066 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067 sys.exit(1)
68
69
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000070def GetNoGitPagerEnv():
71 env = os.environ.copy()
72 # 'cat' is a magical git string that disables pagers on all platforms.
73 env['GIT_PAGER'] = 'cat'
74 return env
75
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000076def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000077 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000078 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000079 except subprocess2.CalledProcessError as e:
80 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000081 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000082 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 'Command "%s" failed.\n%s' % (
84 ' '.join(args), error_message or e.stdout or ''))
85 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000086
87
88def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000089 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000090 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091
92
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000093def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000094 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000095 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000096 if suppress_stderr:
97 stderr = subprocess2.VOID
98 else:
99 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000100 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000101 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000102 stdout=subprocess2.PIPE,
103 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000104 return code, out[0]
105 except ValueError:
106 # When the subprocess fails, it returns None. That triggers a ValueError
107 # when trying to unpack the return value into (out, code).
108 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000109
110
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000111def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000112 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000113 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000114 return (version.startswith(prefix) and
115 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000116
117
maruel@chromium.org90541732011-04-01 17:54:18 +0000118def ask_for_data(prompt):
119 try:
120 return raw_input(prompt)
121 except KeyboardInterrupt:
122 # Hide the exception.
123 sys.exit(1)
124
125
iannucci@chromium.org79540052012-10-19 23:15:26 +0000126def git_set_branch_value(key, value):
127 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000128 if not branch:
129 return
130
131 cmd = ['config']
132 if isinstance(value, int):
133 cmd.append('--int')
134 git_key = 'branch.%s.%s' % (branch, key)
135 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000136
137
138def git_get_branch_default(key, default):
139 branch = Changelist().GetBranch()
140 if branch:
141 git_key = 'branch.%s.%s' % (branch, key)
142 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
143 try:
144 return int(stdout.strip())
145 except ValueError:
146 pass
147 return default
148
149
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000150def add_git_similarity(parser):
151 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000152 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153 help='Sets the percentage that a pair of files need to match in order to'
154 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 parser.add_option(
156 '--find-copies', action='store_true',
157 help='Allows git to look for copies.')
158 parser.add_option(
159 '--no-find-copies', action='store_false', dest='find_copies',
160 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000161
162 old_parser_args = parser.parse_args
163 def Parse(args):
164 options, args = old_parser_args(args)
165
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000166 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000167 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000168 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169 print('Note: Saving similarity of %d%% in git config.'
170 % options.similarity)
171 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000172
iannucci@chromium.org79540052012-10-19 23:15:26 +0000173 options.similarity = max(0, min(options.similarity, 100))
174
175 if options.find_copies is None:
176 options.find_copies = bool(
177 git_get_branch_default('git-find-copies', True))
178 else:
179 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000180
181 print('Using %d%% similarity for rename/copy detection. '
182 'Override with --similarity.' % options.similarity)
183
184 return options, args
185 parser.parse_args = Parse
186
187
ukai@chromium.org259e4682012-10-25 07:36:33 +0000188def is_dirty_git_tree(cmd):
189 # Make sure index is up-to-date before running diff-index.
190 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
191 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
192 if dirty:
193 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
194 print 'Uncommitted files: (git diff-index --name-status HEAD)'
195 print dirty[:4096]
196 if len(dirty) > 4096:
197 print '... (run "git diff-index --name-status HEAD" to see full output).'
198 return True
199 return False
200
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000201
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000202def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
203 """Return the corresponding git ref if |base_url| together with |glob_spec|
204 matches the full |url|.
205
206 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
207 """
208 fetch_suburl, as_ref = glob_spec.split(':')
209 if allow_wildcards:
210 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
211 if glob_match:
212 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
213 # "branches/{472,597,648}/src:refs/remotes/svn/*".
214 branch_re = re.escape(base_url)
215 if glob_match.group(1):
216 branch_re += '/' + re.escape(glob_match.group(1))
217 wildcard = glob_match.group(2)
218 if wildcard == '*':
219 branch_re += '([^/]*)'
220 else:
221 # Escape and replace surrounding braces with parentheses and commas
222 # with pipe symbols.
223 wildcard = re.escape(wildcard)
224 wildcard = re.sub('^\\\\{', '(', wildcard)
225 wildcard = re.sub('\\\\,', '|', wildcard)
226 wildcard = re.sub('\\\\}$', ')', wildcard)
227 branch_re += wildcard
228 if glob_match.group(3):
229 branch_re += re.escape(glob_match.group(3))
230 match = re.match(branch_re, url)
231 if match:
232 return re.sub('\*$', match.group(1), as_ref)
233
234 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
235 if fetch_suburl:
236 full_url = base_url + '/' + fetch_suburl
237 else:
238 full_url = base_url
239 if full_url == url:
240 return as_ref
241 return None
242
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000243
iannucci@chromium.org79540052012-10-19 23:15:26 +0000244def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000245 """Prints statistics about the change to the user."""
246 # --no-ext-diff is broken in some versions of Git, so try to work around
247 # this by overriding the environment (but there is still a problem if the
248 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000249 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000250 if 'GIT_EXTERNAL_DIFF' in env:
251 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000252
253 if find_copies:
254 similarity_options = ['--find-copies-harder', '-l100000',
255 '-C%s' % similarity]
256 else:
257 similarity_options = ['-M%s' % similarity]
258
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000259 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000260 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000261 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000262 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000263
264
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000265class Settings(object):
266 def __init__(self):
267 self.default_server = None
268 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000269 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000270 self.is_git_svn = None
271 self.svn_branch = None
272 self.tree_status_url = None
273 self.viewvc_url = None
274 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000275 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000276 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000277
278 def LazyUpdateIfNeeded(self):
279 """Updates the settings from a codereview.settings file, if available."""
280 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000281 # The only value that actually changes the behavior is
282 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000283 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000284 error_ok=True
285 ).strip().lower()
286
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000287 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000288 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000289 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000290 # set updated to True to avoid infinite calling loop
291 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000292 self.updated = True
293 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000294 self.updated = True
295
296 def GetDefaultServerUrl(self, error_ok=False):
297 if not self.default_server:
298 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000299 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000300 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000301 if error_ok:
302 return self.default_server
303 if not self.default_server:
304 error_message = ('Could not find settings file. You must configure '
305 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000306 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000307 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000308 return self.default_server
309
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000310 @staticmethod
311 def GetRelativeRoot():
312 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000313
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000314 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000315 if self.root is None:
316 self.root = os.path.abspath(self.GetRelativeRoot())
317 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000318
319 def GetIsGitSvn(self):
320 """Return true if this repo looks like it's using git-svn."""
321 if self.is_git_svn is None:
322 # If you have any "svn-remote.*" config keys, we think you're using svn.
323 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000324 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000325 return self.is_git_svn
326
327 def GetSVNBranch(self):
328 if self.svn_branch is None:
329 if not self.GetIsGitSvn():
330 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
331
332 # Try to figure out which remote branch we're based on.
333 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000334 # 1) iterate through our branch history and find the svn URL.
335 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000336
337 # regexp matching the git-svn line that contains the URL.
338 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
339
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000340 # We don't want to go through all of history, so read a line from the
341 # pipe at a time.
342 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000343 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000344 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
345 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000346 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000347 for line in proc.stdout:
348 match = git_svn_re.match(line)
349 if match:
350 url = match.group(1)
351 proc.stdout.close() # Cut pipe.
352 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000353
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000354 if url:
355 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
356 remotes = RunGit(['config', '--get-regexp',
357 r'^svn-remote\..*\.url']).splitlines()
358 for remote in remotes:
359 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000360 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000361 remote = match.group(1)
362 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000363 rewrite_root = RunGit(
364 ['config', 'svn-remote.%s.rewriteRoot' % remote],
365 error_ok=True).strip()
366 if rewrite_root:
367 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000368 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000369 ['config', 'svn-remote.%s.fetch' % remote],
370 error_ok=True).strip()
371 if fetch_spec:
372 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
373 if self.svn_branch:
374 break
375 branch_spec = RunGit(
376 ['config', 'svn-remote.%s.branches' % remote],
377 error_ok=True).strip()
378 if branch_spec:
379 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
380 if self.svn_branch:
381 break
382 tag_spec = RunGit(
383 ['config', 'svn-remote.%s.tags' % remote],
384 error_ok=True).strip()
385 if tag_spec:
386 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
387 if self.svn_branch:
388 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000389
390 if not self.svn_branch:
391 DieWithError('Can\'t guess svn branch -- try specifying it on the '
392 'command line')
393
394 return self.svn_branch
395
396 def GetTreeStatusUrl(self, error_ok=False):
397 if not self.tree_status_url:
398 error_message = ('You must configure your tree status URL by running '
399 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000400 self.tree_status_url = self._GetRietveldConfig(
401 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402 return self.tree_status_url
403
404 def GetViewVCUrl(self):
405 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000406 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000407 return self.viewvc_url
408
rmistry@google.com90752582014-01-14 21:04:50 +0000409 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000410 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000411
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000412 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000413 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000414
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000415 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000416 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000417
ukai@chromium.orge8077812012-02-03 03:41:46 +0000418 def GetIsGerrit(self):
419 """Return true if this repo is assosiated with gerrit code review system."""
420 if self.is_gerrit is None:
421 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
422 return self.is_gerrit
423
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000424 def GetGitEditor(self):
425 """Return the editor specified in the git config, or None if none is."""
426 if self.git_editor is None:
427 self.git_editor = self._GetConfig('core.editor', error_ok=True)
428 return self.git_editor or None
429
thestig@chromium.org44202a22014-03-11 19:22:18 +0000430 def GetLintRegex(self):
431 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
432 DEFAULT_LINT_REGEX)
433
434 def GetLintIgnoreRegex(self):
435 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
436 DEFAULT_LINT_IGNORE_REGEX)
437
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000438 def _GetRietveldConfig(self, param, **kwargs):
439 return self._GetConfig('rietveld.' + param, **kwargs)
440
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000441 def _GetConfig(self, param, **kwargs):
442 self.LazyUpdateIfNeeded()
443 return RunGit(['config', param], **kwargs).strip()
444
445
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000446def ShortBranchName(branch):
447 """Convert a name like 'refs/heads/foo' to just 'foo'."""
448 return branch.replace('refs/heads/', '')
449
450
451class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000452 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000454 global settings
455 if not settings:
456 # Happens when git_cl.py is used as a utility library.
457 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 settings.GetDefaultServerUrl()
459 self.branchref = branchref
460 if self.branchref:
461 self.branch = ShortBranchName(self.branchref)
462 else:
463 self.branch = None
464 self.rietveld_server = None
465 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000466 self.lookedup_issue = False
467 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000468 self.has_description = False
469 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000470 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000471 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000472 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000473 self.cc = None
474 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000475 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000476 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000477
478 def GetCCList(self):
479 """Return the users cc'd on this CL.
480
481 Return is a string suitable for passing to gcl with the --cc flag.
482 """
483 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000484 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000485 more_cc = ','.join(self.watchers)
486 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
487 return self.cc
488
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000489 def GetCCListWithoutDefault(self):
490 """Return the users cc'd on this CL excluding default ones."""
491 if self.cc is None:
492 self.cc = ','.join(self.watchers)
493 return self.cc
494
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000495 def SetWatchers(self, watchers):
496 """Set the list of email addresses that should be cc'd based on the changed
497 files in this CL.
498 """
499 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000500
501 def GetBranch(self):
502 """Returns the short branch name, e.g. 'master'."""
503 if not self.branch:
504 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
505 self.branch = ShortBranchName(self.branchref)
506 return self.branch
507
508 def GetBranchRef(self):
509 """Returns the full branch name, e.g. 'refs/heads/master'."""
510 self.GetBranch() # Poke the lazy loader.
511 return self.branchref
512
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000513 @staticmethod
514 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000515 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000516 e.g. 'origin', 'refs/heads/master'
517 """
518 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000519 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
520 error_ok=True).strip()
521 if upstream_branch:
522 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
523 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000524 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
525 error_ok=True).strip()
526 if upstream_branch:
527 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000528 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000529 # Fall back on trying a git-svn upstream branch.
530 if settings.GetIsGitSvn():
531 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000532 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000533 # Else, try to guess the origin remote.
534 remote_branches = RunGit(['branch', '-r']).split()
535 if 'origin/master' in remote_branches:
536 # Fall back on origin/master if it exits.
537 remote = 'origin'
538 upstream_branch = 'refs/heads/master'
539 elif 'origin/trunk' in remote_branches:
540 # Fall back on origin/trunk if it exists. Generally a shared
541 # git-svn clone
542 remote = 'origin'
543 upstream_branch = 'refs/heads/trunk'
544 else:
545 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000546Either pass complete "git diff"-style arguments, like
547 git cl upload origin/master
548or verify this branch is set up to track another (via the --track argument to
549"git checkout -b ...").""")
550
551 return remote, upstream_branch
552
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000553 def GetCommonAncestorWithUpstream(self):
554 return RunGit(['merge-base', self.GetUpstreamBranch(), 'HEAD']).strip()
555
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000556 def GetUpstreamBranch(self):
557 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000558 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000559 if remote is not '.':
560 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
561 self.upstream_branch = upstream_branch
562 return self.upstream_branch
563
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000564 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000565 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000566 remote, branch = None, self.GetBranch()
567 seen_branches = set()
568 while branch not in seen_branches:
569 seen_branches.add(branch)
570 remote, branch = self.FetchUpstreamTuple(branch)
571 branch = ShortBranchName(branch)
572 if remote != '.' or branch.startswith('refs/remotes'):
573 break
574 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000575 remotes = RunGit(['remote'], error_ok=True).split()
576 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000577 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000578 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000579 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000580 logging.warning('Could not determine which remote this change is '
581 'associated with, so defaulting to "%s". This may '
582 'not be what you want. You may prevent this message '
583 'by running "git svn info" as documented here: %s',
584 self._remote,
585 GIT_INSTRUCTIONS_URL)
586 else:
587 logging.warn('Could not determine which remote this change is '
588 'associated with. You may prevent this message by '
589 'running "git svn info" as documented here: %s',
590 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000591 branch = 'HEAD'
592 if branch.startswith('refs/remotes'):
593 self._remote = (remote, branch)
594 else:
595 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000596 return self._remote
597
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000598 def GitSanityChecks(self, upstream_git_obj):
599 """Checks git repo status and ensures diff is from local commits."""
600
601 # Verify the commit we're diffing against is in our current branch.
602 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
603 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
604 if upstream_sha != common_ancestor:
605 print >> sys.stderr, (
606 'ERROR: %s is not in the current branch. You may need to rebase '
607 'your tracking branch' % upstream_sha)
608 return False
609
610 # List the commits inside the diff, and verify they are all local.
611 commits_in_diff = RunGit(
612 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
613 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
614 remote_branch = remote_branch.strip()
615 if code != 0:
616 _, remote_branch = self.GetRemoteBranch()
617
618 commits_in_remote = RunGit(
619 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
620
621 common_commits = set(commits_in_diff) & set(commits_in_remote)
622 if common_commits:
623 print >> sys.stderr, (
624 'ERROR: Your diff contains %d commits already in %s.\n'
625 'Run "git log --oneline %s..HEAD" to get a list of commits in '
626 'the diff. If you are using a custom git flow, you can override'
627 ' the reference used for this check with "git config '
628 'gitcl.remotebranch <git-ref>".' % (
629 len(common_commits), remote_branch, upstream_git_obj))
630 return False
631 return True
632
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000633 def GetGitBaseUrlFromConfig(self):
634 """Return the configured base URL from branch.<branchname>.baseurl.
635
636 Returns None if it is not set.
637 """
638 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
639 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000640
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000641 def GetRemoteUrl(self):
642 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
643
644 Returns None if there is no remote.
645 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000646 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
648
649 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000650 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000651 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000652 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000653 self.issue = int(issue) or None if issue else None
654 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 return self.issue
656
657 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000658 if not self.rietveld_server:
659 # If we're on a branch then get the server potentially associated
660 # with that branch.
661 if self.GetIssue():
662 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
663 ['config', self._RietveldServer()], error_ok=True).strip())
664 if not self.rietveld_server:
665 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000666 return self.rietveld_server
667
668 def GetIssueURL(self):
669 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000670 if not self.GetIssue():
671 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000672 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
673
674 def GetDescription(self, pretty=False):
675 if not self.has_description:
676 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000677 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000678 try:
679 self.description = self.RpcServer().get_description(issue).strip()
680 except urllib2.HTTPError, e:
681 if e.code == 404:
682 DieWithError(
683 ('\nWhile fetching the description for issue %d, received a '
684 '404 (not found)\n'
685 'error. It is likely that you deleted this '
686 'issue on the server. If this is the\n'
687 'case, please run\n\n'
688 ' git cl issue 0\n\n'
689 'to clear the association with the deleted issue. Then run '
690 'this command again.') % issue)
691 else:
692 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000693 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000694 self.has_description = True
695 if pretty:
696 wrapper = textwrap.TextWrapper()
697 wrapper.initial_indent = wrapper.subsequent_indent = ' '
698 return wrapper.fill(self.description)
699 return self.description
700
701 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000702 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000703 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000704 patchset = RunGit(['config', self._PatchsetSetting()],
705 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000706 self.patchset = int(patchset) or None if patchset else None
707 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 return self.patchset
709
710 def SetPatchset(self, patchset):
711 """Set this branch's patchset. If patchset=0, clears the patchset."""
712 if patchset:
713 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000714 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 else:
716 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000717 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000718 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000720 def GetMostRecentPatchset(self):
721 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000722
723 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000724 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000725 '/download/issue%s_%s.diff' % (issue, patchset))
726
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000727 def GetIssueProperties(self):
728 if self._props is None:
729 issue = self.GetIssue()
730 if not issue:
731 self._props = {}
732 else:
733 self._props = self.RpcServer().get_issue_properties(issue, True)
734 return self._props
735
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000736 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000737 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000738
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 def SetIssue(self, issue):
740 """Set this branch's issue. If issue=0, clears the issue."""
741 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000742 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 RunGit(['config', self._IssueSetting(), str(issue)])
744 if self.rietveld_server:
745 RunGit(['config', self._RietveldServer(), self.rietveld_server])
746 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000747 current_issue = self.GetIssue()
748 if current_issue:
749 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000750 self.issue = None
751 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000752
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000753 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000754 if not self.GitSanityChecks(upstream_branch):
755 DieWithError('\nGit sanity check failure')
756
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000757 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000758 if not root:
759 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000760 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000761
762 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000763 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000764 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000765 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000766 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000767 except subprocess2.CalledProcessError:
768 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000769 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000770 'This branch probably doesn\'t exist anymore. To reset the\n'
771 'tracking branch, please run\n'
772 ' git branch --set-upstream %s trunk\n'
773 'replacing trunk with origin/master or the relevant branch') %
774 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000775
maruel@chromium.org52424302012-08-29 15:14:30 +0000776 issue = self.GetIssue()
777 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000778 if issue:
779 description = self.GetDescription()
780 else:
781 # If the change was never uploaded, use the log messages of all commits
782 # up to the branch point, as git cl upload will prefill the description
783 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000784 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
785 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000786
787 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000788 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000789 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000790 name,
791 description,
792 absroot,
793 files,
794 issue,
795 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000796 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000797
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000798 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000799 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000800
801 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000802 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000803 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000804 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000805 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000806 except presubmit_support.PresubmitFailure, e:
807 DieWithError(
808 ('%s\nMaybe your depot_tools is out of date?\n'
809 'If all fails, contact maruel@') % e)
810
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000811 def UpdateDescription(self, description):
812 self.description = description
813 return self.RpcServer().update_description(
814 self.GetIssue(), self.description)
815
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000817 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000818 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000820 def SetFlag(self, flag, value):
821 """Patchset must match."""
822 if not self.GetPatchset():
823 DieWithError('The patchset needs to match. Send another patchset.')
824 try:
825 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000826 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000827 except urllib2.HTTPError, e:
828 if e.code == 404:
829 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
830 if e.code == 403:
831 DieWithError(
832 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
833 'match?') % (self.GetIssue(), self.GetPatchset()))
834 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000836 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000837 """Returns an upload.RpcServer() to access this review's rietveld instance.
838 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000839 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000840 self._rpc_server = rietveld.CachingRietveld(
841 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000842 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843
844 def _IssueSetting(self):
845 """Return the git setting that stores this change's issue."""
846 return 'branch.%s.rietveldissue' % self.GetBranch()
847
848 def _PatchsetSetting(self):
849 """Return the git setting that stores this change's most recent patchset."""
850 return 'branch.%s.rietveldpatchset' % self.GetBranch()
851
852 def _RietveldServer(self):
853 """Returns the git setting that stores this change's rietveld server."""
854 return 'branch.%s.rietveldserver' % self.GetBranch()
855
856
857def GetCodereviewSettingsInteractively():
858 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000859 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000860 server = settings.GetDefaultServerUrl(error_ok=True)
861 prompt = 'Rietveld server (host[:port])'
862 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000863 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864 if not server and not newserver:
865 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000866 if newserver:
867 newserver = gclient_utils.UpgradeToHttps(newserver)
868 if newserver != server:
869 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000871 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 prompt = caption
873 if initial:
874 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000875 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876 if new_val == 'x':
877 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000878 elif new_val:
879 if is_url:
880 new_val = gclient_utils.UpgradeToHttps(new_val)
881 if new_val != initial:
882 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000883
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000884 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000885 SetProperty(settings.GetDefaultPrivateFlag(),
886 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000887 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000888 'tree-status-url', False)
889 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000890 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000891
892 # TODO: configure a default branch to diff against, rather than this
893 # svn-based hackery.
894
895
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000896class ChangeDescription(object):
897 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000898 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000899 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000900
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000901 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000902 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000903
agable@chromium.org42c20792013-09-12 17:34:49 +0000904 @property # www.logilab.org/ticket/89786
905 def description(self): # pylint: disable=E0202
906 return '\n'.join(self._description_lines)
907
908 def set_description(self, desc):
909 if isinstance(desc, basestring):
910 lines = desc.splitlines()
911 else:
912 lines = [line.rstrip() for line in desc]
913 while lines and not lines[0]:
914 lines.pop(0)
915 while lines and not lines[-1]:
916 lines.pop(-1)
917 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000918
919 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000920 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000921 assert isinstance(reviewers, list), reviewers
922 if not reviewers:
923 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000924 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000925
agable@chromium.org42c20792013-09-12 17:34:49 +0000926 # Get the set of R= and TBR= lines and remove them from the desciption.
927 regexp = re.compile(self.R_LINE)
928 matches = [regexp.match(line) for line in self._description_lines]
929 new_desc = [l for i, l in enumerate(self._description_lines)
930 if not matches[i]]
931 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000932
agable@chromium.org42c20792013-09-12 17:34:49 +0000933 # Construct new unified R= and TBR= lines.
934 r_names = []
935 tbr_names = []
936 for match in matches:
937 if not match:
938 continue
939 people = cleanup_list([match.group(2).strip()])
940 if match.group(1) == 'TBR':
941 tbr_names.extend(people)
942 else:
943 r_names.extend(people)
944 for name in r_names:
945 if name not in reviewers:
946 reviewers.append(name)
947 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
948 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
949
950 # Put the new lines in the description where the old first R= line was.
951 line_loc = next((i for i, match in enumerate(matches) if match), -1)
952 if 0 <= line_loc < len(self._description_lines):
953 if new_tbr_line:
954 self._description_lines.insert(line_loc, new_tbr_line)
955 if new_r_line:
956 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000957 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000958 if new_r_line:
959 self.append_footer(new_r_line)
960 if new_tbr_line:
961 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000962
963 def prompt(self):
964 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000965 self.set_description([
966 '# Enter a description of the change.',
967 '# This will be displayed on the codereview site.',
968 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000969 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000970 '--------------------',
971 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000972
agable@chromium.org42c20792013-09-12 17:34:49 +0000973 regexp = re.compile(self.BUG_LINE)
974 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000975 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000976 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000977 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000978 if not content:
979 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000980 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000981
982 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000983 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
984 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000985 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000986 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000987
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000988 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000989 if self._description_lines:
990 # Add an empty line if either the last line or the new line isn't a tag.
991 last_line = self._description_lines[-1]
992 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
993 not presubmit_support.Change.TAG_LINE_RE.match(line)):
994 self._description_lines.append('')
995 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000996
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000997 def get_reviewers(self):
998 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000999 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1000 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001001 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001002
1003
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001004def get_approving_reviewers(props):
1005 """Retrieves the reviewers that approved a CL from the issue properties with
1006 messages.
1007
1008 Note that the list may contain reviewers that are not committer, thus are not
1009 considered by the CQ.
1010 """
1011 return sorted(
1012 set(
1013 message['sender']
1014 for message in props['messages']
1015 if message['approval'] and message['sender'] in props['reviewers']
1016 )
1017 )
1018
1019
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020def FindCodereviewSettingsFile(filename='codereview.settings'):
1021 """Finds the given file starting in the cwd and going up.
1022
1023 Only looks up to the top of the repository unless an
1024 'inherit-review-settings-ok' file exists in the root of the repository.
1025 """
1026 inherit_ok_file = 'inherit-review-settings-ok'
1027 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001028 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1030 root = '/'
1031 while True:
1032 if filename in os.listdir(cwd):
1033 if os.path.isfile(os.path.join(cwd, filename)):
1034 return open(os.path.join(cwd, filename))
1035 if cwd == root:
1036 break
1037 cwd = os.path.dirname(cwd)
1038
1039
1040def LoadCodereviewSettingsFromFile(fileobj):
1041 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001042 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 def SetProperty(name, setting, unset_error_ok=False):
1045 fullname = 'rietveld.' + name
1046 if setting in keyvals:
1047 RunGit(['config', fullname, keyvals[setting]])
1048 else:
1049 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1050
1051 SetProperty('server', 'CODE_REVIEW_SERVER')
1052 # Only server setting is required. Other settings can be absent.
1053 # In that case, we ignore errors raised during option deletion attempt.
1054 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001055 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1057 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001058 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001059 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1060 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001061
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001062 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001063 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001064
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1066 #should be of the form
1067 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1068 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1069 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1070 keyvals['ORIGIN_URL_CONFIG']])
1071
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001073def urlretrieve(source, destination):
1074 """urllib is broken for SSL connections via a proxy therefore we
1075 can't use urllib.urlretrieve()."""
1076 with open(destination, 'w') as f:
1077 f.write(urllib2.urlopen(source).read())
1078
1079
ukai@chromium.org712d6102013-11-27 00:52:58 +00001080def hasSheBang(fname):
1081 """Checks fname is a #! script."""
1082 with open(fname) as f:
1083 return f.read(2).startswith('#!')
1084
1085
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001086def DownloadHooks(force):
1087 """downloads hooks
1088
1089 Args:
1090 force: True to update hooks. False to install hooks if not present.
1091 """
1092 if not settings.GetIsGerrit():
1093 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001094 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001095 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1096 if not os.access(dst, os.X_OK):
1097 if os.path.exists(dst):
1098 if not force:
1099 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001100 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001101 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001102 if not hasSheBang(dst):
1103 DieWithError('Not a script: %s\n'
1104 'You need to download from\n%s\n'
1105 'into .git/hooks/commit-msg and '
1106 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001107 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1108 except Exception:
1109 if os.path.exists(dst):
1110 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001111 DieWithError('\nFailed to download hooks.\n'
1112 'You need to download from\n%s\n'
1113 'into .git/hooks/commit-msg and '
1114 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001115
1116
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001117@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001119 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001121 parser.add_option('--activate-update', action='store_true',
1122 help='activate auto-updating [rietveld] section in '
1123 '.git/config')
1124 parser.add_option('--deactivate-update', action='store_true',
1125 help='deactivate auto-updating [rietveld] section in '
1126 '.git/config')
1127 options, args = parser.parse_args(args)
1128
1129 if options.deactivate_update:
1130 RunGit(['config', 'rietveld.autoupdate', 'false'])
1131 return
1132
1133 if options.activate_update:
1134 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1135 return
1136
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 if len(args) == 0:
1138 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001139 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 return 0
1141
1142 url = args[0]
1143 if not url.endswith('codereview.settings'):
1144 url = os.path.join(url, 'codereview.settings')
1145
1146 # Load code review settings and download hooks (if available).
1147 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001148 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 return 0
1150
1151
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001152def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001153 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001154 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1155 branch = ShortBranchName(branchref)
1156 _, args = parser.parse_args(args)
1157 if not args:
1158 print("Current base-url:")
1159 return RunGit(['config', 'branch.%s.base-url' % branch],
1160 error_ok=False).strip()
1161 else:
1162 print("Setting base-url to %s" % args[0])
1163 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1164 error_ok=False).strip()
1165
1166
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001168 """Show status of changelists.
1169
1170 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001171 - Red not sent for review or broken
1172 - Blue waiting for review
1173 - Yellow waiting for you to reply to review
1174 - Green LGTM'ed
1175 - Magenta in the commit queue
1176 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001177
1178 Also see 'git cl comments'.
1179 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 parser.add_option('--field',
1181 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001182 parser.add_option('-f', '--fast', action='store_true',
1183 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001185 if args:
1186 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001188 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001189 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 if options.field.startswith('desc'):
1191 print cl.GetDescription()
1192 elif options.field == 'id':
1193 issueid = cl.GetIssue()
1194 if issueid:
1195 print issueid
1196 elif options.field == 'patch':
1197 patchset = cl.GetPatchset()
1198 if patchset:
1199 print patchset
1200 elif options.field == 'url':
1201 url = cl.GetIssueURL()
1202 if url:
1203 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001204 return 0
1205
1206 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1207 if not branches:
1208 print('No local branch found.')
1209 return 0
1210
1211 changes = (Changelist(branchref=b) for b in branches.splitlines())
1212 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1213 alignment = max(5, max(len(b) for b in branches))
1214 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001215 # Adhoc thread pool to request data concurrently.
1216 output = Queue.Queue()
1217
1218 # Silence upload.py otherwise it becomes unweldly.
1219 upload.verbosity = 0
1220
1221 if not options.fast:
1222 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001223 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001224 c = Changelist(branchref=b)
1225 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001226 props = {}
1227 r = None
1228 if i:
1229 try:
1230 props = c.GetIssueProperties()
1231 r = c.GetApprovingReviewers() if i else None
1232 except urllib2.HTTPError:
1233 # The issue probably doesn't exist anymore.
1234 i += ' (broken)'
1235
1236 msgs = props.get('messages') or []
1237
1238 if not i:
1239 color = Fore.WHITE
1240 elif props.get('closed'):
1241 # Issue is closed.
1242 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001243 elif props.get('commit'):
1244 # Issue is in the commit queue.
1245 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001246 elif r:
1247 # Was LGTM'ed.
1248 color = Fore.GREEN
1249 elif not msgs:
1250 # No message was sent.
1251 color = Fore.RED
1252 elif msgs[-1]['sender'] != props.get('owner_email'):
1253 color = Fore.YELLOW
1254 else:
1255 color = Fore.BLUE
1256 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001257
1258 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1259 for t in threads:
1260 t.daemon = True
1261 t.start()
1262 else:
1263 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1264 for b in branches:
1265 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001266 url = c.GetIssueURL()
1267 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001268
1269 tmp = {}
1270 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001271 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001272 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001273 b, i, color = output.get()
1274 tmp[b] = (i, color)
1275 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001276 reset = Fore.RESET
1277 if not sys.stdout.isatty():
1278 color = ''
1279 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001280 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001281 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001282
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001283 cl = Changelist()
1284 print
1285 print 'Current branch:',
1286 if not cl.GetIssue():
1287 print 'no issue assigned.'
1288 return 0
1289 print cl.GetBranch()
1290 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1291 print 'Issue description:'
1292 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 return 0
1294
1295
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001296def colorize_CMDstatus_doc():
1297 """To be called once in main() to add colors to git cl status help."""
1298 colors = [i for i in dir(Fore) if i[0].isupper()]
1299
1300 def colorize_line(line):
1301 for color in colors:
1302 if color in line.upper():
1303 # Extract whitespaces first and the leading '-'.
1304 indent = len(line) - len(line.lstrip(' ')) + 1
1305 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1306 return line
1307
1308 lines = CMDstatus.__doc__.splitlines()
1309 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1310
1311
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001312@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001314 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315
1316 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001317 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001318 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319
1320 cl = Changelist()
1321 if len(args) > 0:
1322 try:
1323 issue = int(args[0])
1324 except ValueError:
1325 DieWithError('Pass a number to set the issue or none to list it.\n'
1326 'Maybe you want to run git cl status?')
1327 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001328 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 return 0
1330
1331
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001332def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001333 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001334 (_, args) = parser.parse_args(args)
1335 if args:
1336 parser.error('Unsupported argument: %s' % args)
1337
1338 cl = Changelist()
1339 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001340 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001341 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001342 if message['disapproval']:
1343 color = Fore.RED
1344 elif message['approval']:
1345 color = Fore.GREEN
1346 elif message['sender'] == data['owner_email']:
1347 color = Fore.MAGENTA
1348 else:
1349 color = Fore.BLUE
1350 print '\n%s%s %s%s' % (
1351 color, message['date'].split('.', 1)[0], message['sender'],
1352 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001353 if message['text'].strip():
1354 print '\n'.join(' ' + l for l in message['text'].splitlines())
1355 return 0
1356
1357
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001358def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001359 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001360 cl = Changelist()
1361 if not cl.GetIssue():
1362 DieWithError('This branch has no associated changelist.')
1363 description = ChangeDescription(cl.GetDescription())
1364 description.prompt()
1365 cl.UpdateDescription(description.description)
1366 return 0
1367
1368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369def CreateDescriptionFromLog(args):
1370 """Pulls out the commit log to use as a base for the CL description."""
1371 log_args = []
1372 if len(args) == 1 and not args[0].endswith('.'):
1373 log_args = [args[0] + '..']
1374 elif len(args) == 1 and args[0].endswith('...'):
1375 log_args = [args[0][:-1]]
1376 elif len(args) == 2:
1377 log_args = [args[0] + '..' + args[1]]
1378 else:
1379 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001380 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381
1382
thestig@chromium.org44202a22014-03-11 19:22:18 +00001383def CMDlint(parser, args):
1384 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001385 parser.add_option('--filter', action='append', metavar='-x,+y',
1386 help='Comma-separated list of cpplint\'s category-filters')
1387 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001388
1389 # Access to a protected member _XX of a client class
1390 # pylint: disable=W0212
1391 try:
1392 import cpplint
1393 import cpplint_chromium
1394 except ImportError:
1395 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1396 return 1
1397
1398 # Change the current working directory before calling lint so that it
1399 # shows the correct base.
1400 previous_cwd = os.getcwd()
1401 os.chdir(settings.GetRoot())
1402 try:
1403 cl = Changelist()
1404 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1405 files = [f.LocalPath() for f in change.AffectedFiles()]
1406
1407 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001408 command = args + files
1409 if options.filter:
1410 command = ['--filter=' + ','.join(options.filter)] + command
1411 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001412
1413 white_regex = re.compile(settings.GetLintRegex())
1414 black_regex = re.compile(settings.GetLintIgnoreRegex())
1415 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1416 for filename in filenames:
1417 if white_regex.match(filename):
1418 if black_regex.match(filename):
1419 print "Ignoring file %s" % filename
1420 else:
1421 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1422 extra_check_functions)
1423 else:
1424 print "Skipping file %s" % filename
1425 finally:
1426 os.chdir(previous_cwd)
1427 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1428 if cpplint._cpplint_state.error_count != 0:
1429 return 1
1430 return 0
1431
1432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001434 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001435 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001437 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001438 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 (options, args) = parser.parse_args(args)
1440
ukai@chromium.org259e4682012-10-25 07:36:33 +00001441 if not options.force and is_dirty_git_tree('presubmit'):
1442 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001443 return 1
1444
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001445 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 if args:
1447 base_branch = args[0]
1448 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001449 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001450 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001451
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001452 cl.RunHook(
1453 committing=not options.upload,
1454 may_prompt=False,
1455 verbose=options.verbose,
1456 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001457 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001458
1459
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001460def AddChangeIdToCommitMessage(options, args):
1461 """Re-commits using the current message, assumes the commit hook is in
1462 place.
1463 """
1464 log_desc = options.message or CreateDescriptionFromLog(args)
1465 git_command = ['commit', '--amend', '-m', log_desc]
1466 RunGit(git_command)
1467 new_log_desc = CreateDescriptionFromLog(args)
1468 if CHANGE_ID in new_log_desc:
1469 print 'git-cl: Added Change-Id to commit message.'
1470 else:
1471 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1472
1473
ukai@chromium.orge8077812012-02-03 03:41:46 +00001474def GerritUpload(options, args, cl):
1475 """upload the current branch to gerrit."""
1476 # We assume the remote called "origin" is the one we want.
1477 # It is probably not worthwhile to support different workflows.
1478 remote = 'origin'
1479 branch = 'master'
1480 if options.target_branch:
1481 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001483 change_desc = ChangeDescription(
1484 options.message or CreateDescriptionFromLog(args))
1485 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001486 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001488 if CHANGE_ID not in change_desc.description:
1489 AddChangeIdToCommitMessage(options, args)
1490 if options.reviewers:
1491 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492
ukai@chromium.orge8077812012-02-03 03:41:46 +00001493 receive_options = []
1494 cc = cl.GetCCList().split(',')
1495 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001496 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001497 cc = filter(None, cc)
1498 if cc:
1499 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001500 if change_desc.get_reviewers():
1501 receive_options.extend(
1502 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001503
ukai@chromium.orge8077812012-02-03 03:41:46 +00001504 git_command = ['push']
1505 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001506 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001507 ' '.join(receive_options))
1508 git_command += [remote, 'HEAD:refs/for/' + branch]
1509 RunGit(git_command)
1510 # TODO(ukai): parse Change-Id: and set issue number?
1511 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001512
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513
ukai@chromium.orge8077812012-02-03 03:41:46 +00001514def RietveldUpload(options, args, cl):
1515 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1517 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518 if options.emulate_svn_auto_props:
1519 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520
1521 change_desc = None
1522
pgervais@chromium.org91141372014-01-09 23:27:20 +00001523 if options.email is not None:
1524 upload_args.extend(['--email', options.email])
1525
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001526 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001527 if options.title:
1528 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001529 if options.message:
1530 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001531 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001532 print ("This branch is associated with issue %s. "
1533 "Adding patch to that issue." % cl.GetIssue())
1534 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001535 if options.title:
1536 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001537 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001538 change_desc = ChangeDescription(message)
1539 if options.reviewers:
1540 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001541 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001542 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001543
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001544 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545 print "Description is empty; aborting."
1546 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001547
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001548 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001549 if change_desc.get_reviewers():
1550 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001551 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001552 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001553 DieWithError("Must specify reviewers to send email.")
1554 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001555
1556 # We check this before applying rietveld.private assuming that in
1557 # rietveld.cc only addresses which we can send private CLs to are listed
1558 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1559 # --private is specified explicitly on the command line.
1560 if options.private:
1561 logging.warn('rietveld.cc is ignored since private flag is specified. '
1562 'You need to review and add them manually if necessary.')
1563 cc = cl.GetCCListWithoutDefault()
1564 else:
1565 cc = cl.GetCCList()
1566 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001567 if cc:
1568 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001569
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001570 if options.private or settings.GetDefaultPrivateFlag() == "True":
1571 upload_args.append('--private')
1572
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001573 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001574 if not options.find_copies:
1575 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001576
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577 # Include the upstream repo's URL in the change -- this is useful for
1578 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001579 remote_url = cl.GetGitBaseUrlFromConfig()
1580 if not remote_url:
1581 if settings.GetIsGitSvn():
1582 # URL is dependent on the current directory.
1583 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1584 if data:
1585 keys = dict(line.split(': ', 1) for line in data.splitlines()
1586 if ': ' in line)
1587 remote_url = keys.get('URL', None)
1588 else:
1589 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1590 remote_url = (cl.GetRemoteUrl() + '@'
1591 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001592 if remote_url:
1593 upload_args.extend(['--base_url', remote_url])
1594
1595 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001596 upload_args = ['upload'] + upload_args + args
1597 logging.info('upload.RealMain(%s)', upload_args)
1598 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001599 issue = int(issue)
1600 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001601 except KeyboardInterrupt:
1602 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001603 except:
1604 # If we got an exception after the user typed a description for their
1605 # change, back up the description before re-raising.
1606 if change_desc:
1607 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1608 print '\nGot exception while uploading -- saving description to %s\n' \
1609 % backup_path
1610 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001611 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001612 backup_file.close()
1613 raise
1614
1615 if not cl.GetIssue():
1616 cl.SetIssue(issue)
1617 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001618
1619 if options.use_commit_queue:
1620 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621 return 0
1622
1623
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001624def cleanup_list(l):
1625 """Fixes a list so that comma separated items are put as individual items.
1626
1627 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1628 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1629 """
1630 items = sum((i.split(',') for i in l), [])
1631 stripped_items = (i.strip() for i in items)
1632 return sorted(filter(None, stripped_items))
1633
1634
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001635@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001636def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001637 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001638 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1639 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001640 parser.add_option('--bypass-watchlists', action='store_true',
1641 dest='bypass_watchlists',
1642 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001643 parser.add_option('-f', action='store_true', dest='force',
1644 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001645 parser.add_option('-m', dest='message', help='message for patchset')
1646 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001647 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001648 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001649 help='reviewer email addresses')
1650 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001651 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001652 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001653 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001654 help='send email to reviewer immediately')
1655 parser.add_option("--emulate_svn_auto_props", action="store_true",
1656 dest="emulate_svn_auto_props",
1657 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001658 parser.add_option('-c', '--use-commit-queue', action='store_true',
1659 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001660 parser.add_option('--private', action='store_true',
1661 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001662 parser.add_option('--target_branch',
1663 help='When uploading to gerrit, remote branch to '
1664 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001665 parser.add_option('--email', default=None,
1666 help='email address to use to connect to Rietveld')
1667
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001668 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001669 (options, args) = parser.parse_args(args)
1670
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001671 if options.target_branch and not settings.GetIsGerrit():
1672 parser.error('Use --target_branch for non gerrit repository.')
1673
ukai@chromium.org259e4682012-10-25 07:36:33 +00001674 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001675 return 1
1676
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001677 options.reviewers = cleanup_list(options.reviewers)
1678 options.cc = cleanup_list(options.cc)
1679
ukai@chromium.orge8077812012-02-03 03:41:46 +00001680 cl = Changelist()
1681 if args:
1682 # TODO(ukai): is it ok for gerrit case?
1683 base_branch = args[0]
1684 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001685 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001686 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001687 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001688
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001689 # Apply watchlists on upload.
1690 change = cl.GetChange(base_branch, None)
1691 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1692 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001693 if not options.bypass_watchlists:
1694 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001695
ukai@chromium.orge8077812012-02-03 03:41:46 +00001696 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001697 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001698 may_prompt=not options.force,
1699 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001700 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001701 if not hook_results.should_continue():
1702 return 1
1703 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001704 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001705
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001706 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001707 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001708 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001709 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001710 print ('The last upload made from this repository was patchset #%d but '
1711 'the most recent patchset on the server is #%d.'
1712 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001713 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1714 'from another machine or branch the patch you\'re uploading now '
1715 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001716 ask_for_data('About to upload; enter to confirm.')
1717
iannucci@chromium.org79540052012-10-19 23:15:26 +00001718 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001719 if settings.GetIsGerrit():
1720 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001721 ret = RietveldUpload(options, args, cl)
1722 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001723 git_set_branch_value('last-upload-hash',
1724 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001725
1726 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001727
1728
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001729def IsSubmoduleMergeCommit(ref):
1730 # When submodules are added to the repo, we expect there to be a single
1731 # non-git-svn merge commit at remote HEAD with a signature comment.
1732 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001733 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001734 return RunGit(cmd) != ''
1735
1736
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001737def SendUpstream(parser, args, cmd):
1738 """Common code for CmdPush and CmdDCommit
1739
1740 Squashed commit into a single.
1741 Updates changelog with metadata (e.g. pointer to review).
1742 Pushes/dcommits the code upstream.
1743 Updates review and closes.
1744 """
1745 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1746 help='bypass upload presubmit hook')
1747 parser.add_option('-m', dest='message',
1748 help="override review description")
1749 parser.add_option('-f', action='store_true', dest='force',
1750 help="force yes to questions (don't prompt)")
1751 parser.add_option('-c', dest='contributor',
1752 help="external contributor for patch (appended to " +
1753 "description and used as author for git). Should be " +
1754 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001755 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001756 (options, args) = parser.parse_args(args)
1757 cl = Changelist()
1758
1759 if not args or cmd == 'push':
1760 # Default to merging against our best guess of the upstream branch.
1761 args = [cl.GetUpstreamBranch()]
1762
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001763 if options.contributor:
1764 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1765 print "Please provide contibutor as 'First Last <email@example.com>'"
1766 return 1
1767
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001769 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001770
ukai@chromium.org259e4682012-10-25 07:36:33 +00001771 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 return 1
1773
1774 # This rev-list syntax means "show all commits not in my branch that
1775 # are in base_branch".
1776 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1777 base_branch]).splitlines()
1778 if upstream_commits:
1779 print ('Base branch "%s" has %d commits '
1780 'not in this branch.' % (base_branch, len(upstream_commits)))
1781 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1782 return 1
1783
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001784 # This is the revision `svn dcommit` will commit on top of.
1785 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1786 '--pretty=format:%H'])
1787
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001788 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001789 # If the base_head is a submodule merge commit, the first parent of the
1790 # base_head should be a git-svn commit, which is what we're interested in.
1791 base_svn_head = base_branch
1792 if base_has_submodules:
1793 base_svn_head += '^1'
1794
1795 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001796 if extra_commits:
1797 print ('This branch has %d additional commits not upstreamed yet.'
1798 % len(extra_commits.splitlines()))
1799 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1800 'before attempting to %s.' % (base_branch, cmd))
1801 return 1
1802
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001803 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001804 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001805 author = None
1806 if options.contributor:
1807 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001808 hook_results = cl.RunHook(
1809 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001810 may_prompt=not options.force,
1811 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001812 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001813 if not hook_results.should_continue():
1814 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815
1816 if cmd == 'dcommit':
1817 # Check the tree status if the tree status URL is set.
1818 status = GetTreeStatus()
1819 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001820 print('The tree is closed. Please wait for it to reopen. Use '
1821 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001822 return 1
1823 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001824 print('Unable to determine tree status. Please verify manually and '
1825 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001826 else:
1827 breakpad.SendStack(
1828 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001829 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1830 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001831 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001833 change_desc = ChangeDescription(options.message)
1834 if not change_desc.description and cl.GetIssue():
1835 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001836
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001837 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001838 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001839 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001840 else:
1841 print 'No description set.'
1842 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1843 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001844
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001845 # Keep a separate copy for the commit message, because the commit message
1846 # contains the link to the Rietveld issue, while the Rietveld message contains
1847 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001848 # Keep a separate copy for the commit message.
1849 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001850 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001851
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001852 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001853 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001854 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001855 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001856 commit_desc.append_footer('Patch from %s.' % options.contributor)
1857
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001858 print('Description:')
1859 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860
1861 branches = [base_branch, cl.GetBranchRef()]
1862 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001863 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001864 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001865
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001866 # We want to squash all this branch's commits into one commit with the proper
1867 # description. We do this by doing a "reset --soft" to the base branch (which
1868 # keeps the working copy the same), then dcommitting that. If origin/master
1869 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1870 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001872 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1873 # Delete the branches if they exist.
1874 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1875 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1876 result = RunGitWithCode(showref_cmd)
1877 if result[0] == 0:
1878 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001879
1880 # We might be in a directory that's present in this branch but not in the
1881 # trunk. Move up to the top of the tree so that git commands that expect a
1882 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001883 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884 if rel_base_path:
1885 os.chdir(rel_base_path)
1886
1887 # Stuff our change into the merge branch.
1888 # We wrap in a try...finally block so if anything goes wrong,
1889 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001890 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001891 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001892 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1893 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001894 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001895 RunGit(
1896 [
1897 'commit', '--author', options.contributor,
1898 '-m', commit_desc.description,
1899 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001900 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001901 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001902 if base_has_submodules:
1903 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1904 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1905 RunGit(['checkout', CHERRY_PICK_BRANCH])
1906 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001907 if cmd == 'push':
1908 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001909 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001910 retcode, output = RunGitWithCode(
1911 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1912 logging.debug(output)
1913 else:
1914 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001915 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001916 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001917 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001918 finally:
1919 # And then swap back to the original branch and clean up.
1920 RunGit(['checkout', '-q', cl.GetBranch()])
1921 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001922 if base_has_submodules:
1923 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924
1925 if cl.GetIssue():
1926 if cmd == 'dcommit' and 'Committed r' in output:
1927 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1928 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001929 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1930 for l in output.splitlines(False))
1931 match = filter(None, match)
1932 if len(match) != 1:
1933 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1934 output)
1935 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001936 else:
1937 return 1
1938 viewvc_url = settings.GetViewVCUrl()
1939 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001940 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001941 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001942 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001943 print ('Closing issue '
1944 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001945 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001946 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001947 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001948 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001949 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001950 if options.bypass_hooks:
1951 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1952 else:
1953 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001954 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001955 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001956
1957 if retcode == 0:
1958 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1959 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001960 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001961
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962 return 0
1963
1964
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001965@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001966def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001967 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001969 message = """This doesn't appear to be an SVN repository.
1970If your project has a git mirror with an upstream SVN master, you probably need
1971to run 'git svn init', see your project's git mirror documentation.
1972If your project has a true writeable upstream repository, you probably want
1973to run 'git cl push' instead.
1974Choose wisely, if you get this wrong, your commit might appear to succeed but
1975will instead be silently ignored."""
1976 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001977 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001978 return SendUpstream(parser, args, 'dcommit')
1979
1980
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001981@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001982def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001983 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001984 if settings.GetIsGitSvn():
1985 print('This appears to be an SVN repository.')
1986 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001987 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001988 return SendUpstream(parser, args, 'push')
1989
1990
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001991@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001993 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001994 parser.add_option('-b', dest='newbranch',
1995 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001996 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001997 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001998 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1999 help='Change to the directory DIR immediately, '
2000 'before doing anything else.')
2001 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002002 help='failed patches spew .rej files rather than '
2003 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2005 help="don't commit after patch applies")
2006 (options, args) = parser.parse_args(args)
2007 if len(args) != 1:
2008 parser.print_help()
2009 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002010 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002012 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002013 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002014
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002015 if options.newbranch:
2016 if options.force:
2017 RunGit(['branch', '-D', options.newbranch],
2018 stderr=subprocess2.PIPE, error_ok=True)
2019 RunGit(['checkout', '-b', options.newbranch,
2020 Changelist().GetUpstreamBranch()])
2021
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002022 return PatchIssue(issue_arg, options.reject, options.nocommit,
2023 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002024
2025
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002026def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002027 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002029 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002030 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002031 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002032 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002034 # Assume it's a URL to the patch. Default to https.
2035 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002036 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002037 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038 DieWithError('Must pass an issue ID or full URL for '
2039 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002040 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002041 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002042 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002043
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002044 # Switch up to the top-level directory, if necessary, in preparation for
2045 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002046 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002047 if top:
2048 os.chdir(top)
2049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002050 # Git patches have a/ at the beginning of source paths. We strip that out
2051 # with a sed script rather than the -p flag to patch so we can feed either
2052 # Git or svn-style patches into the same apply command.
2053 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002054 try:
2055 patch_data = subprocess2.check_output(
2056 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2057 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058 DieWithError('Git patch mungling failed.')
2059 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002060
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002061 # We use "git apply" to apply the patch instead of "patch" so that we can
2062 # pick up file adds.
2063 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002064 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002065 if directory:
2066 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002067 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002069 elif IsGitVersionAtLeast('1.7.12'):
2070 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002071 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002072 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002073 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002074 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002075 DieWithError('Failed to apply the patch')
2076
2077 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002078 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2080 cl = Changelist()
2081 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002082 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002083 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002084 else:
2085 print "Patch applied to index."
2086 return 0
2087
2088
2089def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002090 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091 # Provide a wrapper for git svn rebase to help avoid accidental
2092 # git svn dcommit.
2093 # It's the only command that doesn't use parser at all since we just defer
2094 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002095
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002096 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002097
2098
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002099def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002100 """Fetches the tree status and returns either 'open', 'closed',
2101 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002102 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002103 if url:
2104 status = urllib2.urlopen(url).read().lower()
2105 if status.find('closed') != -1 or status == '0':
2106 return 'closed'
2107 elif status.find('open') != -1 or status == '1':
2108 return 'open'
2109 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002110 return 'unset'
2111
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002112
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113def GetTreeStatusReason():
2114 """Fetches the tree status from a json url and returns the message
2115 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002116 url = settings.GetTreeStatusUrl()
2117 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002118 connection = urllib2.urlopen(json_url)
2119 status = json.loads(connection.read())
2120 connection.close()
2121 return status['message']
2122
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002123
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002124def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002125 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002126 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002127 status = GetTreeStatus()
2128 if 'unset' == status:
2129 print 'You must configure your tree status URL by running "git cl config".'
2130 return 2
2131
2132 print "The tree is %s" % status
2133 print
2134 print GetTreeStatusReason()
2135 if status != 'open':
2136 return 1
2137 return 0
2138
2139
maruel@chromium.org15192402012-09-06 12:38:29 +00002140def CMDtry(parser, args):
2141 """Triggers a try job through Rietveld."""
2142 group = optparse.OptionGroup(parser, "Try job options")
2143 group.add_option(
2144 "-b", "--bot", action="append",
2145 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2146 "times to specify multiple builders. ex: "
2147 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2148 "the try server waterfall for the builders name and the tests "
2149 "available. Can also be used to specify gtest_filter, e.g. "
2150 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2151 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002152 "-m", "--master", default='',
2153 help=("Specify a try master where to run the tries."))
2154 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002155 "-r", "--revision",
2156 help="Revision to use for the try job; default: the "
2157 "revision will be determined by the try server; see "
2158 "its waterfall for more info")
2159 group.add_option(
2160 "-c", "--clobber", action="store_true", default=False,
2161 help="Force a clobber before building; e.g. don't do an "
2162 "incremental build")
2163 group.add_option(
2164 "--project",
2165 help="Override which project to use. Projects are defined "
2166 "server-side to define what default bot set to use")
2167 group.add_option(
2168 "-t", "--testfilter", action="append", default=[],
2169 help=("Apply a testfilter to all the selected builders. Unless the "
2170 "builders configurations are similar, use multiple "
2171 "--bot <builder>:<test> arguments."))
2172 group.add_option(
2173 "-n", "--name", help="Try job name; default to current branch name")
2174 parser.add_option_group(group)
2175 options, args = parser.parse_args(args)
2176
2177 if args:
2178 parser.error('Unknown arguments: %s' % args)
2179
2180 cl = Changelist()
2181 if not cl.GetIssue():
2182 parser.error('Need to upload first')
2183
2184 if not options.name:
2185 options.name = cl.GetBranch()
2186
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002187 if options.bot and not options.master:
2188 parser.error('For manually specified bots please also specify the '
2189 'tryserver master, e.g. "-m tryserver.chromium".')
2190
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002191 def GetMasterMap():
2192 # Process --bot and --testfilter.
2193 if not options.bot:
2194 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002195
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002196 # Get try masters from PRESUBMIT.py files.
2197 masters = presubmit_support.DoGetTryMasters(
2198 change,
2199 change.LocalPaths(),
2200 settings.GetRoot(),
2201 None,
2202 None,
2203 options.verbose,
2204 sys.stdout)
2205 if masters:
2206 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002207
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002208 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2209 options.bot = presubmit_support.DoGetTrySlaves(
2210 change,
2211 change.LocalPaths(),
2212 settings.GetRoot(),
2213 None,
2214 None,
2215 options.verbose,
2216 sys.stdout)
2217 if not options.bot:
2218 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002219
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002220 builders_and_tests = {}
2221 # TODO(machenbach): The old style command-line options don't support
2222 # multiple try masters yet.
2223 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2224 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2225
2226 for bot in old_style:
2227 if ':' in bot:
2228 builder, tests = bot.split(':', 1)
2229 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2230 elif ',' in bot:
2231 parser.error('Specify one bot per --bot flag')
2232 else:
2233 builders_and_tests.setdefault(bot, []).append('defaulttests')
2234
2235 for bot, tests in new_style:
2236 builders_and_tests.setdefault(bot, []).extend(tests)
2237
2238 # Return a master map with one master to be backwards compatible. The
2239 # master name defaults to an empty string, which will cause the master
2240 # not to be set on rietveld (deprecated).
2241 return {options.master: builders_and_tests}
2242
2243 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002244
maruel@chromium.org15192402012-09-06 12:38:29 +00002245 if options.testfilter:
2246 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002247 masters = dict((master, dict(
2248 (b, forced_tests) for b, t in slaves.iteritems()
2249 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002250
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002251 for builders in masters.itervalues():
2252 if any('triggered' in b for b in builders):
2253 print >> sys.stderr, (
2254 'ERROR You are trying to send a job to a triggered bot. This type of'
2255 ' bot requires an\ninitial job from a parent (usually a builder). '
2256 'Instead send your job to the parent.\n'
2257 'Bot list: %s' % builders)
2258 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002259
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002260 patchset = cl.GetMostRecentPatchset()
2261 if patchset and patchset != cl.GetPatchset():
2262 print(
2263 '\nWARNING Mismatch between local config and server. Did a previous '
2264 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2265 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002266 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002267 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002268 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002269 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002270 except urllib2.HTTPError, e:
2271 if e.code == 404:
2272 print('404 from rietveld; '
2273 'did you mean to use "git try" instead of "git cl try"?')
2274 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002275 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002276
2277 for (master, builders) in masters.iteritems():
2278 if master:
2279 print 'Master: %s' % master
2280 length = max(len(builder) for builder in builders)
2281 for builder in sorted(builders):
2282 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002283 return 0
2284
2285
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002286@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002287def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002288 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002289 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002290 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002291 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002292
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002293 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002294 if args:
2295 # One arg means set upstream branch.
2296 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2297 cl = Changelist()
2298 print "Upstream branch set to " + cl.GetUpstreamBranch()
2299 else:
2300 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002301 return 0
2302
2303
thestig@chromium.org00858c82013-12-02 23:08:03 +00002304def CMDweb(parser, args):
2305 """Opens the current CL in the web browser."""
2306 _, args = parser.parse_args(args)
2307 if args:
2308 parser.error('Unrecognized args: %s' % ' '.join(args))
2309
2310 issue_url = Changelist().GetIssueURL()
2311 if not issue_url:
2312 print >> sys.stderr, 'ERROR No issue to open'
2313 return 1
2314
2315 webbrowser.open(issue_url)
2316 return 0
2317
2318
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002319def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002320 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002321 _, args = parser.parse_args(args)
2322 if args:
2323 parser.error('Unrecognized args: %s' % ' '.join(args))
2324 cl = Changelist()
2325 cl.SetFlag('commit', '1')
2326 return 0
2327
2328
groby@chromium.org411034a2013-02-26 15:12:01 +00002329def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002330 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002331 _, args = parser.parse_args(args)
2332 if args:
2333 parser.error('Unrecognized args: %s' % ' '.join(args))
2334 cl = Changelist()
2335 # Ensure there actually is an issue to close.
2336 cl.GetDescription()
2337 cl.CloseIssue()
2338 return 0
2339
2340
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002341def CMDdiff(parser, args):
2342 """shows differences between local tree and last upload."""
2343 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002344 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002345 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002346 if not issue:
2347 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002348 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002349 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002350
2351 # Create a new branch based on the merge-base
2352 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2353 try:
2354 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002355 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002356 if rtn != 0:
2357 return rtn
2358
2359 # Switch back to starting brand and diff against the temporary
2360 # branch containing the latest rietveld patch.
2361 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2362 finally:
2363 RunGit(['checkout', '-q', branch])
2364 RunGit(['branch', '-D', TMP_BRANCH])
2365
2366 return 0
2367
2368
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002369def CMDowners(parser, args):
2370 """interactively find the owners for reviewing"""
2371 parser.add_option(
2372 '--no-color',
2373 action='store_true',
2374 help='Use this option to disable color output')
2375 options, args = parser.parse_args(args)
2376
2377 author = RunGit(['config', 'user.email']).strip() or None
2378
2379 cl = Changelist()
2380
2381 if args:
2382 if len(args) > 1:
2383 parser.error('Unknown args')
2384 base_branch = args[0]
2385 else:
2386 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002387 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002388
2389 change = cl.GetChange(base_branch, None)
2390 return owners_finder.OwnersFinder(
2391 [f.LocalPath() for f in
2392 cl.GetChange(base_branch, None).AffectedFiles()],
2393 change.RepositoryRoot(), author,
2394 fopen=file, os_path=os.path, glob=glob.glob,
2395 disable_color=options.no_color).run()
2396
2397
enne@chromium.org555cfe42014-01-29 18:21:39 +00002398@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002399def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002400 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002401 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002402 parser.add_option('--full', action='store_true',
2403 help='Reformat the full content of all touched files')
2404 parser.add_option('--dry-run', action='store_true',
2405 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002406 parser.add_option('--diff', action='store_true',
2407 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002408 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002409
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002410 # git diff generates paths against the root of the repository. Change
2411 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002412 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002413 if rel_base_path:
2414 os.chdir(rel_base_path)
2415
digit@chromium.org29e47272013-05-17 17:01:46 +00002416 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002417 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002418 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002419 # Only list the names of modified files.
2420 diff_cmd.append('--name-only')
2421 else:
2422 # Only generate context-less patches.
2423 diff_cmd.append('-U0')
2424
2425 # Grab the merge-base commit, i.e. the upstream commit of the current
2426 # branch when it was created or the last time it was rebased. This is
2427 # to cover the case where the user may have called "git fetch origin",
2428 # moving the origin branch to a newer commit, but hasn't rebased yet.
2429 upstream_commit = None
2430 cl = Changelist()
2431 upstream_branch = cl.GetUpstreamBranch()
2432 if upstream_branch:
2433 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2434 upstream_commit = upstream_commit.strip()
2435
2436 if not upstream_commit:
2437 DieWithError('Could not find base commit for this branch. '
2438 'Are you in detached state?')
2439
2440 diff_cmd.append(upstream_commit)
2441
2442 # Handle source file filtering.
2443 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002444 if args:
2445 for arg in args:
2446 if os.path.isdir(arg):
2447 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2448 elif os.path.isfile(arg):
2449 diff_cmd.append(arg)
2450 else:
2451 DieWithError('Argument "%s" is not a file or a directory' % arg)
2452 else:
2453 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002454 diff_output = RunGit(diff_cmd)
2455
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002456 top_dir = os.path.normpath(
2457 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2458
2459 # Locate the clang-format binary in the checkout
2460 try:
2461 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2462 except clang_format.NotFoundError, e:
2463 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002464
digit@chromium.org29e47272013-05-17 17:01:46 +00002465 if opts.full:
2466 # diff_output is a list of files to send to clang-format.
2467 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002468 if not files:
2469 print "Nothing to format."
2470 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002471 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002472 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002473 cmd.append('-i')
2474 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002475 if opts.diff:
2476 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002477 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002478 env = os.environ.copy()
2479 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002480 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002481 try:
2482 script = clang_format.FindClangFormatScriptInChromiumTree(
2483 'clang-format-diff.py')
2484 except clang_format.NotFoundError, e:
2485 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002486
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002487 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002488 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002489 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002490
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002491 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002492 if opts.diff:
2493 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002494 if opts.dry_run and len(stdout) > 0:
2495 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002496
2497 return 0
2498
2499
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002500class OptionParser(optparse.OptionParser):
2501 """Creates the option parse and add --verbose support."""
2502 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002503 optparse.OptionParser.__init__(
2504 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002505 self.add_option(
2506 '-v', '--verbose', action='count', default=0,
2507 help='Use 2 times for more debugging info')
2508
2509 def parse_args(self, args=None, values=None):
2510 options, args = optparse.OptionParser.parse_args(self, args, values)
2511 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2512 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2513 return options, args
2514
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002515
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002516def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002517 if sys.hexversion < 0x02060000:
2518 print >> sys.stderr, (
2519 '\nYour python version %s is unsupported, please upgrade.\n' %
2520 sys.version.split(' ', 1)[0])
2521 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002522
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002523 # Reload settings.
2524 global settings
2525 settings = Settings()
2526
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002527 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002528 dispatcher = subcommand.CommandDispatcher(__name__)
2529 try:
2530 return dispatcher.execute(OptionParser(), argv)
2531 except urllib2.HTTPError, e:
2532 if e.code != 500:
2533 raise
2534 DieWithError(
2535 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2536 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002537
2538
2539if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002540 # These affect sys.stdout so do it outside of main() to simplify mocks in
2541 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002542 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002543 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002544 sys.exit(main(sys.argv[1:]))