blob: 12f2c87c998b2e6d88d638d6c4b6508f37d31b53 [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."""
1385 _, args = parser.parse_args(args)
1386
1387 # Access to a protected member _XX of a client class
1388 # pylint: disable=W0212
1389 try:
1390 import cpplint
1391 import cpplint_chromium
1392 except ImportError:
1393 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1394 return 1
1395
1396 # Change the current working directory before calling lint so that it
1397 # shows the correct base.
1398 previous_cwd = os.getcwd()
1399 os.chdir(settings.GetRoot())
1400 try:
1401 cl = Changelist()
1402 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1403 files = [f.LocalPath() for f in change.AffectedFiles()]
1404
1405 # Process cpplints arguments if any.
1406 filenames = cpplint.ParseArguments(args + files)
1407
1408 white_regex = re.compile(settings.GetLintRegex())
1409 black_regex = re.compile(settings.GetLintIgnoreRegex())
1410 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1411 for filename in filenames:
1412 if white_regex.match(filename):
1413 if black_regex.match(filename):
1414 print "Ignoring file %s" % filename
1415 else:
1416 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1417 extra_check_functions)
1418 else:
1419 print "Skipping file %s" % filename
1420 finally:
1421 os.chdir(previous_cwd)
1422 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1423 if cpplint._cpplint_state.error_count != 0:
1424 return 1
1425 return 0
1426
1427
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001428def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001429 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001430 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001432 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001433 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001434 (options, args) = parser.parse_args(args)
1435
ukai@chromium.org259e4682012-10-25 07:36:33 +00001436 if not options.force and is_dirty_git_tree('presubmit'):
1437 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438 return 1
1439
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001440 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001441 if args:
1442 base_branch = args[0]
1443 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001444 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001445 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001447 cl.RunHook(
1448 committing=not options.upload,
1449 may_prompt=False,
1450 verbose=options.verbose,
1451 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001452 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453
1454
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001455def AddChangeIdToCommitMessage(options, args):
1456 """Re-commits using the current message, assumes the commit hook is in
1457 place.
1458 """
1459 log_desc = options.message or CreateDescriptionFromLog(args)
1460 git_command = ['commit', '--amend', '-m', log_desc]
1461 RunGit(git_command)
1462 new_log_desc = CreateDescriptionFromLog(args)
1463 if CHANGE_ID in new_log_desc:
1464 print 'git-cl: Added Change-Id to commit message.'
1465 else:
1466 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1467
1468
ukai@chromium.orge8077812012-02-03 03:41:46 +00001469def GerritUpload(options, args, cl):
1470 """upload the current branch to gerrit."""
1471 # We assume the remote called "origin" is the one we want.
1472 # It is probably not worthwhile to support different workflows.
1473 remote = 'origin'
1474 branch = 'master'
1475 if options.target_branch:
1476 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001478 change_desc = ChangeDescription(
1479 options.message or CreateDescriptionFromLog(args))
1480 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001481 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001483 if CHANGE_ID not in change_desc.description:
1484 AddChangeIdToCommitMessage(options, args)
1485 if options.reviewers:
1486 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487
ukai@chromium.orge8077812012-02-03 03:41:46 +00001488 receive_options = []
1489 cc = cl.GetCCList().split(',')
1490 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001491 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001492 cc = filter(None, cc)
1493 if cc:
1494 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001495 if change_desc.get_reviewers():
1496 receive_options.extend(
1497 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001498
ukai@chromium.orge8077812012-02-03 03:41:46 +00001499 git_command = ['push']
1500 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001501 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001502 ' '.join(receive_options))
1503 git_command += [remote, 'HEAD:refs/for/' + branch]
1504 RunGit(git_command)
1505 # TODO(ukai): parse Change-Id: and set issue number?
1506 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001507
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001508
ukai@chromium.orge8077812012-02-03 03:41:46 +00001509def RietveldUpload(options, args, cl):
1510 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001511 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1512 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001513 if options.emulate_svn_auto_props:
1514 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515
1516 change_desc = None
1517
pgervais@chromium.org91141372014-01-09 23:27:20 +00001518 if options.email is not None:
1519 upload_args.extend(['--email', options.email])
1520
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001521 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001522 if options.title:
1523 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001524 if options.message:
1525 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001526 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527 print ("This branch is associated with issue %s. "
1528 "Adding patch to that issue." % cl.GetIssue())
1529 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001530 if options.title:
1531 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001532 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001533 change_desc = ChangeDescription(message)
1534 if options.reviewers:
1535 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001536 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001537 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001538
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001539 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001540 print "Description is empty; aborting."
1541 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001542
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001543 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001544 if change_desc.get_reviewers():
1545 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001546 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001547 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001548 DieWithError("Must specify reviewers to send email.")
1549 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001550
1551 # We check this before applying rietveld.private assuming that in
1552 # rietveld.cc only addresses which we can send private CLs to are listed
1553 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1554 # --private is specified explicitly on the command line.
1555 if options.private:
1556 logging.warn('rietveld.cc is ignored since private flag is specified. '
1557 'You need to review and add them manually if necessary.')
1558 cc = cl.GetCCListWithoutDefault()
1559 else:
1560 cc = cl.GetCCList()
1561 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001562 if cc:
1563 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001564
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001565 if options.private or settings.GetDefaultPrivateFlag() == "True":
1566 upload_args.append('--private')
1567
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001568 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001569 if not options.find_copies:
1570 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001571
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001572 # Include the upstream repo's URL in the change -- this is useful for
1573 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001574 remote_url = cl.GetGitBaseUrlFromConfig()
1575 if not remote_url:
1576 if settings.GetIsGitSvn():
1577 # URL is dependent on the current directory.
1578 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1579 if data:
1580 keys = dict(line.split(': ', 1) for line in data.splitlines()
1581 if ': ' in line)
1582 remote_url = keys.get('URL', None)
1583 else:
1584 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1585 remote_url = (cl.GetRemoteUrl() + '@'
1586 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001587 if remote_url:
1588 upload_args.extend(['--base_url', remote_url])
1589
1590 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001591 upload_args = ['upload'] + upload_args + args
1592 logging.info('upload.RealMain(%s)', upload_args)
1593 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001594 issue = int(issue)
1595 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001596 except KeyboardInterrupt:
1597 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001598 except:
1599 # If we got an exception after the user typed a description for their
1600 # change, back up the description before re-raising.
1601 if change_desc:
1602 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1603 print '\nGot exception while uploading -- saving description to %s\n' \
1604 % backup_path
1605 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001606 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001607 backup_file.close()
1608 raise
1609
1610 if not cl.GetIssue():
1611 cl.SetIssue(issue)
1612 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001613
1614 if options.use_commit_queue:
1615 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616 return 0
1617
1618
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001619def cleanup_list(l):
1620 """Fixes a list so that comma separated items are put as individual items.
1621
1622 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1623 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1624 """
1625 items = sum((i.split(',') for i in l), [])
1626 stripped_items = (i.strip() for i in items)
1627 return sorted(filter(None, stripped_items))
1628
1629
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001630@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001631def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001632 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001633 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1634 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001635 parser.add_option('--bypass-watchlists', action='store_true',
1636 dest='bypass_watchlists',
1637 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001638 parser.add_option('-f', action='store_true', dest='force',
1639 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001640 parser.add_option('-m', dest='message', help='message for patchset')
1641 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001642 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001643 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001644 help='reviewer email addresses')
1645 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001646 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001647 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001648 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001649 help='send email to reviewer immediately')
1650 parser.add_option("--emulate_svn_auto_props", action="store_true",
1651 dest="emulate_svn_auto_props",
1652 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001653 parser.add_option('-c', '--use-commit-queue', action='store_true',
1654 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001655 parser.add_option('--private', action='store_true',
1656 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001657 parser.add_option('--target_branch',
1658 help='When uploading to gerrit, remote branch to '
1659 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001660 parser.add_option('--email', default=None,
1661 help='email address to use to connect to Rietveld')
1662
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001663 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001664 (options, args) = parser.parse_args(args)
1665
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001666 if options.target_branch and not settings.GetIsGerrit():
1667 parser.error('Use --target_branch for non gerrit repository.')
1668
ukai@chromium.org259e4682012-10-25 07:36:33 +00001669 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001670 return 1
1671
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001672 options.reviewers = cleanup_list(options.reviewers)
1673 options.cc = cleanup_list(options.cc)
1674
ukai@chromium.orge8077812012-02-03 03:41:46 +00001675 cl = Changelist()
1676 if args:
1677 # TODO(ukai): is it ok for gerrit case?
1678 base_branch = args[0]
1679 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001680 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001681 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001682 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001683
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001684 # Apply watchlists on upload.
1685 change = cl.GetChange(base_branch, None)
1686 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1687 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001688 if not options.bypass_watchlists:
1689 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001690
ukai@chromium.orge8077812012-02-03 03:41:46 +00001691 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001692 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001693 may_prompt=not options.force,
1694 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001695 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001696 if not hook_results.should_continue():
1697 return 1
1698 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001699 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001700
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001701 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001702 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001703 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001704 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001705 print ('The last upload made from this repository was patchset #%d but '
1706 'the most recent patchset on the server is #%d.'
1707 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001708 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1709 'from another machine or branch the patch you\'re uploading now '
1710 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001711 ask_for_data('About to upload; enter to confirm.')
1712
iannucci@chromium.org79540052012-10-19 23:15:26 +00001713 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001714 if settings.GetIsGerrit():
1715 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001716 ret = RietveldUpload(options, args, cl)
1717 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001718 git_set_branch_value('last-upload-hash',
1719 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001720
1721 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001722
1723
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001724def IsSubmoduleMergeCommit(ref):
1725 # When submodules are added to the repo, we expect there to be a single
1726 # non-git-svn merge commit at remote HEAD with a signature comment.
1727 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001728 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001729 return RunGit(cmd) != ''
1730
1731
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001732def SendUpstream(parser, args, cmd):
1733 """Common code for CmdPush and CmdDCommit
1734
1735 Squashed commit into a single.
1736 Updates changelog with metadata (e.g. pointer to review).
1737 Pushes/dcommits the code upstream.
1738 Updates review and closes.
1739 """
1740 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1741 help='bypass upload presubmit hook')
1742 parser.add_option('-m', dest='message',
1743 help="override review description")
1744 parser.add_option('-f', action='store_true', dest='force',
1745 help="force yes to questions (don't prompt)")
1746 parser.add_option('-c', dest='contributor',
1747 help="external contributor for patch (appended to " +
1748 "description and used as author for git). Should be " +
1749 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001750 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001751 (options, args) = parser.parse_args(args)
1752 cl = Changelist()
1753
1754 if not args or cmd == 'push':
1755 # Default to merging against our best guess of the upstream branch.
1756 args = [cl.GetUpstreamBranch()]
1757
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001758 if options.contributor:
1759 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1760 print "Please provide contibutor as 'First Last <email@example.com>'"
1761 return 1
1762
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001763 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001764 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765
ukai@chromium.org259e4682012-10-25 07:36:33 +00001766 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001767 return 1
1768
1769 # This rev-list syntax means "show all commits not in my branch that
1770 # are in base_branch".
1771 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1772 base_branch]).splitlines()
1773 if upstream_commits:
1774 print ('Base branch "%s" has %d commits '
1775 'not in this branch.' % (base_branch, len(upstream_commits)))
1776 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1777 return 1
1778
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001779 # This is the revision `svn dcommit` will commit on top of.
1780 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1781 '--pretty=format:%H'])
1782
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001783 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001784 # If the base_head is a submodule merge commit, the first parent of the
1785 # base_head should be a git-svn commit, which is what we're interested in.
1786 base_svn_head = base_branch
1787 if base_has_submodules:
1788 base_svn_head += '^1'
1789
1790 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001791 if extra_commits:
1792 print ('This branch has %d additional commits not upstreamed yet.'
1793 % len(extra_commits.splitlines()))
1794 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1795 'before attempting to %s.' % (base_branch, cmd))
1796 return 1
1797
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001798 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001799 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001800 author = None
1801 if options.contributor:
1802 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001803 hook_results = cl.RunHook(
1804 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001805 may_prompt=not options.force,
1806 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001807 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001808 if not hook_results.should_continue():
1809 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001810
1811 if cmd == 'dcommit':
1812 # Check the tree status if the tree status URL is set.
1813 status = GetTreeStatus()
1814 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001815 print('The tree is closed. Please wait for it to reopen. Use '
1816 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001817 return 1
1818 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001819 print('Unable to determine tree status. Please verify manually and '
1820 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001821 else:
1822 breakpad.SendStack(
1823 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001824 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1825 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001826 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001827
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001828 change_desc = ChangeDescription(options.message)
1829 if not change_desc.description and cl.GetIssue():
1830 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001831
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001832 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001833 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001834 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001835 else:
1836 print 'No description set.'
1837 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1838 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001839
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001840 # Keep a separate copy for the commit message, because the commit message
1841 # contains the link to the Rietveld issue, while the Rietveld message contains
1842 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001843 # Keep a separate copy for the commit message.
1844 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001845 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001846
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001847 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001848 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001849 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001850 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001851 commit_desc.append_footer('Patch from %s.' % options.contributor)
1852
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001853 print('Description:')
1854 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001855
1856 branches = [base_branch, cl.GetBranchRef()]
1857 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001858 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001859 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001860
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001861 # We want to squash all this branch's commits into one commit with the proper
1862 # description. We do this by doing a "reset --soft" to the base branch (which
1863 # keeps the working copy the same), then dcommitting that. If origin/master
1864 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1865 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001866 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001867 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1868 # Delete the branches if they exist.
1869 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1870 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1871 result = RunGitWithCode(showref_cmd)
1872 if result[0] == 0:
1873 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001874
1875 # We might be in a directory that's present in this branch but not in the
1876 # trunk. Move up to the top of the tree so that git commands that expect a
1877 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001878 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001879 if rel_base_path:
1880 os.chdir(rel_base_path)
1881
1882 # Stuff our change into the merge branch.
1883 # We wrap in a try...finally block so if anything goes wrong,
1884 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001885 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001886 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001887 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1888 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001889 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001890 RunGit(
1891 [
1892 'commit', '--author', options.contributor,
1893 '-m', commit_desc.description,
1894 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001895 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001896 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001897 if base_has_submodules:
1898 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1899 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1900 RunGit(['checkout', CHERRY_PICK_BRANCH])
1901 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001902 if cmd == 'push':
1903 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001904 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001905 retcode, output = RunGitWithCode(
1906 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1907 logging.debug(output)
1908 else:
1909 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001910 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001911 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001912 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 finally:
1914 # And then swap back to the original branch and clean up.
1915 RunGit(['checkout', '-q', cl.GetBranch()])
1916 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001917 if base_has_submodules:
1918 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919
1920 if cl.GetIssue():
1921 if cmd == 'dcommit' and 'Committed r' in output:
1922 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1923 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001924 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1925 for l in output.splitlines(False))
1926 match = filter(None, match)
1927 if len(match) != 1:
1928 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1929 output)
1930 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001931 else:
1932 return 1
1933 viewvc_url = settings.GetViewVCUrl()
1934 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001935 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001936 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001937 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938 print ('Closing issue '
1939 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001940 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001941 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001942 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001943 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001944 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001945 if options.bypass_hooks:
1946 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1947 else:
1948 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001949 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001950 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001951
1952 if retcode == 0:
1953 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1954 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001955 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001956
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001957 return 0
1958
1959
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001960@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001961def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001962 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001964 message = """This doesn't appear to be an SVN repository.
1965If your project has a git mirror with an upstream SVN master, you probably need
1966to run 'git svn init', see your project's git mirror documentation.
1967If your project has a true writeable upstream repository, you probably want
1968to run 'git cl push' instead.
1969Choose wisely, if you get this wrong, your commit might appear to succeed but
1970will instead be silently ignored."""
1971 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001972 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973 return SendUpstream(parser, args, 'dcommit')
1974
1975
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001976@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001978 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979 if settings.GetIsGitSvn():
1980 print('This appears to be an SVN repository.')
1981 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001982 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001983 return SendUpstream(parser, args, 'push')
1984
1985
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001986@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001988 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989 parser.add_option('-b', dest='newbranch',
1990 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001991 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001993 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1994 help='Change to the directory DIR immediately, '
1995 'before doing anything else.')
1996 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001997 help='failed patches spew .rej files rather than '
1998 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2000 help="don't commit after patch applies")
2001 (options, args) = parser.parse_args(args)
2002 if len(args) != 1:
2003 parser.print_help()
2004 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002005 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002007 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002008 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002009
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002010 if options.newbranch:
2011 if options.force:
2012 RunGit(['branch', '-D', options.newbranch],
2013 stderr=subprocess2.PIPE, error_ok=True)
2014 RunGit(['checkout', '-b', options.newbranch,
2015 Changelist().GetUpstreamBranch()])
2016
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002017 return PatchIssue(issue_arg, options.reject, options.nocommit,
2018 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002019
2020
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002021def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002022 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002024 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002025 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002026 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002027 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002029 # Assume it's a URL to the patch. Default to https.
2030 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002031 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002032 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002033 DieWithError('Must pass an issue ID or full URL for '
2034 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002035 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002036 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002037 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039 # Switch up to the top-level directory, if necessary, in preparation for
2040 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002041 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042 if top:
2043 os.chdir(top)
2044
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002045 # Git patches have a/ at the beginning of source paths. We strip that out
2046 # with a sed script rather than the -p flag to patch so we can feed either
2047 # Git or svn-style patches into the same apply command.
2048 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002049 try:
2050 patch_data = subprocess2.check_output(
2051 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2052 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002053 DieWithError('Git patch mungling failed.')
2054 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002055
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056 # We use "git apply" to apply the patch instead of "patch" so that we can
2057 # pick up file adds.
2058 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002059 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002060 if directory:
2061 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002062 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002063 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002064 elif IsGitVersionAtLeast('1.7.12'):
2065 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002066 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002067 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002068 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002069 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070 DieWithError('Failed to apply the patch')
2071
2072 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002073 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002074 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2075 cl = Changelist()
2076 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002077 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002078 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002079 else:
2080 print "Patch applied to index."
2081 return 0
2082
2083
2084def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002085 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002086 # Provide a wrapper for git svn rebase to help avoid accidental
2087 # git svn dcommit.
2088 # It's the only command that doesn't use parser at all since we just defer
2089 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002090
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002091 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002092
2093
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002094def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002095 """Fetches the tree status and returns either 'open', 'closed',
2096 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002097 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098 if url:
2099 status = urllib2.urlopen(url).read().lower()
2100 if status.find('closed') != -1 or status == '0':
2101 return 'closed'
2102 elif status.find('open') != -1 or status == '1':
2103 return 'open'
2104 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002105 return 'unset'
2106
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002107
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002108def GetTreeStatusReason():
2109 """Fetches the tree status from a json url and returns the message
2110 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002111 url = settings.GetTreeStatusUrl()
2112 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113 connection = urllib2.urlopen(json_url)
2114 status = json.loads(connection.read())
2115 connection.close()
2116 return status['message']
2117
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002119def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002120 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002121 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122 status = GetTreeStatus()
2123 if 'unset' == status:
2124 print 'You must configure your tree status URL by running "git cl config".'
2125 return 2
2126
2127 print "The tree is %s" % status
2128 print
2129 print GetTreeStatusReason()
2130 if status != 'open':
2131 return 1
2132 return 0
2133
2134
maruel@chromium.org15192402012-09-06 12:38:29 +00002135def CMDtry(parser, args):
2136 """Triggers a try job through Rietveld."""
2137 group = optparse.OptionGroup(parser, "Try job options")
2138 group.add_option(
2139 "-b", "--bot", action="append",
2140 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2141 "times to specify multiple builders. ex: "
2142 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2143 "the try server waterfall for the builders name and the tests "
2144 "available. Can also be used to specify gtest_filter, e.g. "
2145 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2146 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002147 "-m", "--master", default='',
2148 help=("Specify a try master where to run the tries."))
2149 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002150 "-r", "--revision",
2151 help="Revision to use for the try job; default: the "
2152 "revision will be determined by the try server; see "
2153 "its waterfall for more info")
2154 group.add_option(
2155 "-c", "--clobber", action="store_true", default=False,
2156 help="Force a clobber before building; e.g. don't do an "
2157 "incremental build")
2158 group.add_option(
2159 "--project",
2160 help="Override which project to use. Projects are defined "
2161 "server-side to define what default bot set to use")
2162 group.add_option(
2163 "-t", "--testfilter", action="append", default=[],
2164 help=("Apply a testfilter to all the selected builders. Unless the "
2165 "builders configurations are similar, use multiple "
2166 "--bot <builder>:<test> arguments."))
2167 group.add_option(
2168 "-n", "--name", help="Try job name; default to current branch name")
2169 parser.add_option_group(group)
2170 options, args = parser.parse_args(args)
2171
2172 if args:
2173 parser.error('Unknown arguments: %s' % args)
2174
2175 cl = Changelist()
2176 if not cl.GetIssue():
2177 parser.error('Need to upload first')
2178
2179 if not options.name:
2180 options.name = cl.GetBranch()
2181
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002182 def GetMasterMap():
2183 # Process --bot and --testfilter.
2184 if not options.bot:
2185 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002186
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002187 # Get try masters from PRESUBMIT.py files.
2188 masters = presubmit_support.DoGetTryMasters(
2189 change,
2190 change.LocalPaths(),
2191 settings.GetRoot(),
2192 None,
2193 None,
2194 options.verbose,
2195 sys.stdout)
2196 if masters:
2197 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002198
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002199 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2200 options.bot = presubmit_support.DoGetTrySlaves(
2201 change,
2202 change.LocalPaths(),
2203 settings.GetRoot(),
2204 None,
2205 None,
2206 options.verbose,
2207 sys.stdout)
2208 if not options.bot:
2209 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002210
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002211 builders_and_tests = {}
2212 # TODO(machenbach): The old style command-line options don't support
2213 # multiple try masters yet.
2214 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2215 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2216
2217 for bot in old_style:
2218 if ':' in bot:
2219 builder, tests = bot.split(':', 1)
2220 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2221 elif ',' in bot:
2222 parser.error('Specify one bot per --bot flag')
2223 else:
2224 builders_and_tests.setdefault(bot, []).append('defaulttests')
2225
2226 for bot, tests in new_style:
2227 builders_and_tests.setdefault(bot, []).extend(tests)
2228
2229 # Return a master map with one master to be backwards compatible. The
2230 # master name defaults to an empty string, which will cause the master
2231 # not to be set on rietveld (deprecated).
2232 return {options.master: builders_and_tests}
2233
2234 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002235
maruel@chromium.org15192402012-09-06 12:38:29 +00002236 if options.testfilter:
2237 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002238 masters = dict((master, dict(
2239 (b, forced_tests) for b, t in slaves.iteritems()
2240 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002241
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002242 for builders in masters.itervalues():
2243 if any('triggered' in b for b in builders):
2244 print >> sys.stderr, (
2245 'ERROR You are trying to send a job to a triggered bot. This type of'
2246 ' bot requires an\ninitial job from a parent (usually a builder). '
2247 'Instead send your job to the parent.\n'
2248 'Bot list: %s' % builders)
2249 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002250
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002251 patchset = cl.GetMostRecentPatchset()
2252 if patchset and patchset != cl.GetPatchset():
2253 print(
2254 '\nWARNING Mismatch between local config and server. Did a previous '
2255 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2256 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002257 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002258 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002259 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002260 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002261 except urllib2.HTTPError, e:
2262 if e.code == 404:
2263 print('404 from rietveld; '
2264 'did you mean to use "git try" instead of "git cl try"?')
2265 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002266 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002267
2268 for (master, builders) in masters.iteritems():
2269 if master:
2270 print 'Master: %s' % master
2271 length = max(len(builder) for builder in builders)
2272 for builder in sorted(builders):
2273 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002274 return 0
2275
2276
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002277@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002278def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002279 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002280 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002281 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002282 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002283
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002284 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002285 if args:
2286 # One arg means set upstream branch.
2287 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2288 cl = Changelist()
2289 print "Upstream branch set to " + cl.GetUpstreamBranch()
2290 else:
2291 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002292 return 0
2293
2294
thestig@chromium.org00858c82013-12-02 23:08:03 +00002295def CMDweb(parser, args):
2296 """Opens the current CL in the web browser."""
2297 _, args = parser.parse_args(args)
2298 if args:
2299 parser.error('Unrecognized args: %s' % ' '.join(args))
2300
2301 issue_url = Changelist().GetIssueURL()
2302 if not issue_url:
2303 print >> sys.stderr, 'ERROR No issue to open'
2304 return 1
2305
2306 webbrowser.open(issue_url)
2307 return 0
2308
2309
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002310def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002311 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002312 _, args = parser.parse_args(args)
2313 if args:
2314 parser.error('Unrecognized args: %s' % ' '.join(args))
2315 cl = Changelist()
2316 cl.SetFlag('commit', '1')
2317 return 0
2318
2319
groby@chromium.org411034a2013-02-26 15:12:01 +00002320def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002321 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002322 _, args = parser.parse_args(args)
2323 if args:
2324 parser.error('Unrecognized args: %s' % ' '.join(args))
2325 cl = Changelist()
2326 # Ensure there actually is an issue to close.
2327 cl.GetDescription()
2328 cl.CloseIssue()
2329 return 0
2330
2331
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002332def CMDdiff(parser, args):
2333 """shows differences between local tree and last upload."""
2334 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002335 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002336 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002337 if not issue:
2338 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002339 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002340 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002341
2342 # Create a new branch based on the merge-base
2343 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2344 try:
2345 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002346 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002347 if rtn != 0:
2348 return rtn
2349
2350 # Switch back to starting brand and diff against the temporary
2351 # branch containing the latest rietveld patch.
2352 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2353 finally:
2354 RunGit(['checkout', '-q', branch])
2355 RunGit(['branch', '-D', TMP_BRANCH])
2356
2357 return 0
2358
2359
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002360def CMDowners(parser, args):
2361 """interactively find the owners for reviewing"""
2362 parser.add_option(
2363 '--no-color',
2364 action='store_true',
2365 help='Use this option to disable color output')
2366 options, args = parser.parse_args(args)
2367
2368 author = RunGit(['config', 'user.email']).strip() or None
2369
2370 cl = Changelist()
2371
2372 if args:
2373 if len(args) > 1:
2374 parser.error('Unknown args')
2375 base_branch = args[0]
2376 else:
2377 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002378 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002379
2380 change = cl.GetChange(base_branch, None)
2381 return owners_finder.OwnersFinder(
2382 [f.LocalPath() for f in
2383 cl.GetChange(base_branch, None).AffectedFiles()],
2384 change.RepositoryRoot(), author,
2385 fopen=file, os_path=os.path, glob=glob.glob,
2386 disable_color=options.no_color).run()
2387
2388
enne@chromium.org555cfe42014-01-29 18:21:39 +00002389@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002390def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002391 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002392 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002393 parser.add_option('--full', action='store_true',
2394 help='Reformat the full content of all touched files')
2395 parser.add_option('--dry-run', action='store_true',
2396 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002397 parser.add_option('--diff', action='store_true',
2398 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002399 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002400
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002401 # git diff generates paths against the root of the repository. Change
2402 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002403 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002404 if rel_base_path:
2405 os.chdir(rel_base_path)
2406
digit@chromium.org29e47272013-05-17 17:01:46 +00002407 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002408 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002409 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002410 # Only list the names of modified files.
2411 diff_cmd.append('--name-only')
2412 else:
2413 # Only generate context-less patches.
2414 diff_cmd.append('-U0')
2415
2416 # Grab the merge-base commit, i.e. the upstream commit of the current
2417 # branch when it was created or the last time it was rebased. This is
2418 # to cover the case where the user may have called "git fetch origin",
2419 # moving the origin branch to a newer commit, but hasn't rebased yet.
2420 upstream_commit = None
2421 cl = Changelist()
2422 upstream_branch = cl.GetUpstreamBranch()
2423 if upstream_branch:
2424 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2425 upstream_commit = upstream_commit.strip()
2426
2427 if not upstream_commit:
2428 DieWithError('Could not find base commit for this branch. '
2429 'Are you in detached state?')
2430
2431 diff_cmd.append(upstream_commit)
2432
2433 # Handle source file filtering.
2434 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002435 if args:
2436 for arg in args:
2437 if os.path.isdir(arg):
2438 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2439 elif os.path.isfile(arg):
2440 diff_cmd.append(arg)
2441 else:
2442 DieWithError('Argument "%s" is not a file or a directory' % arg)
2443 else:
2444 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002445 diff_output = RunGit(diff_cmd)
2446
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002447 top_dir = os.path.normpath(
2448 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2449
2450 # Locate the clang-format binary in the checkout
2451 try:
2452 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2453 except clang_format.NotFoundError, e:
2454 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002455
digit@chromium.org29e47272013-05-17 17:01:46 +00002456 if opts.full:
2457 # diff_output is a list of files to send to clang-format.
2458 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002459 if not files:
2460 print "Nothing to format."
2461 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002462 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002463 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002464 cmd.append('-i')
2465 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002466 if opts.diff:
2467 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002468 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002469 env = os.environ.copy()
2470 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002471 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002472 try:
2473 script = clang_format.FindClangFormatScriptInChromiumTree(
2474 'clang-format-diff.py')
2475 except clang_format.NotFoundError, e:
2476 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002477
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002478 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002479 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002480 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002481
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002482 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002483 if opts.diff:
2484 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002485 if opts.dry_run and len(stdout) > 0:
2486 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002487
2488 return 0
2489
2490
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002491class OptionParser(optparse.OptionParser):
2492 """Creates the option parse and add --verbose support."""
2493 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002494 optparse.OptionParser.__init__(
2495 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002496 self.add_option(
2497 '-v', '--verbose', action='count', default=0,
2498 help='Use 2 times for more debugging info')
2499
2500 def parse_args(self, args=None, values=None):
2501 options, args = optparse.OptionParser.parse_args(self, args, values)
2502 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2503 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2504 return options, args
2505
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002506
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002507def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002508 if sys.hexversion < 0x02060000:
2509 print >> sys.stderr, (
2510 '\nYour python version %s is unsupported, please upgrade.\n' %
2511 sys.version.split(' ', 1)[0])
2512 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002513
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002514 # Reload settings.
2515 global settings
2516 settings = Settings()
2517
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002518 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002519 dispatcher = subcommand.CommandDispatcher(__name__)
2520 try:
2521 return dispatcher.execute(OptionParser(), argv)
2522 except urllib2.HTTPError, e:
2523 if e.code != 500:
2524 raise
2525 DieWithError(
2526 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2527 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002528
2529
2530if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002531 # These affect sys.stdout so do it outside of main() to simplify mocks in
2532 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002533 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002534 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002535 sys.exit(main(sys.argv[1:]))