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