blob: bd58fc1727fd9ea6ab13ad21c035612f7691b859 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
thakis@chromium.org3421c992014-11-02 02:20:32 +000011import base64
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000012import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000013import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import logging
15import optparse
16import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000017import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000019import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000022import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000024import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000025import webbrowser
thakis@chromium.org3421c992014-11-02 02:20:32 +000026import zlib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027
28try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000029 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030except ImportError:
31 pass
32
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035from third_party import upload
36import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000037import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000038import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000039import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
piman@chromium.org336f9122014-09-04 02:16:55 +000041import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000042import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000044import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000046import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000047import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048import watchlists
49
maruel@chromium.org0633fb42013-08-16 20:06:14 +000050__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000051
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000052DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000053POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000055GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000056CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057
thestig@chromium.org44202a22014-03-11 19:22:18 +000058# Valid extensions for files we want to lint.
59DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
60DEFAULT_LINT_IGNORE_REGEX = r"$^"
61
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000062# Shortcut since it quickly becomes redundant.
63Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000064
maruel@chromium.orgddd59412011-11-30 14:20:38 +000065# Initialized in main()
66settings = None
67
68
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000070 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071 sys.exit(1)
72
73
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000074def GetNoGitPagerEnv():
75 env = os.environ.copy()
76 # 'cat' is a magical git string that disables pagers on all platforms.
77 env['GIT_PAGER'] = 'cat'
78 return env
79
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000080
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000081def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000082 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000083 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000084 except subprocess2.CalledProcessError as e:
85 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000086 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000087 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000088 'Command "%s" failed.\n%s' % (
89 ' '.join(args), error_message or e.stdout or ''))
90 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000091
92
93def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000094 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000095 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000096
97
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000098def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000099 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000100 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000101 if suppress_stderr:
102 stderr = subprocess2.VOID
103 else:
104 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000105 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000106 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000107 stdout=subprocess2.PIPE,
108 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000109 return code, out[0]
110 except ValueError:
111 # When the subprocess fails, it returns None. That triggers a ValueError
112 # when trying to unpack the return value into (out, code).
113 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000114
115
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000116def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000117 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000118 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000119 return (version.startswith(prefix) and
120 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000121
122
maruel@chromium.org90541732011-04-01 17:54:18 +0000123def ask_for_data(prompt):
124 try:
125 return raw_input(prompt)
126 except KeyboardInterrupt:
127 # Hide the exception.
128 sys.exit(1)
129
130
iannucci@chromium.org79540052012-10-19 23:15:26 +0000131def git_set_branch_value(key, value):
132 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000133 if not branch:
134 return
135
136 cmd = ['config']
137 if isinstance(value, int):
138 cmd.append('--int')
139 git_key = 'branch.%s.%s' % (branch, key)
140 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000141
142
143def git_get_branch_default(key, default):
144 branch = Changelist().GetBranch()
145 if branch:
146 git_key = 'branch.%s.%s' % (branch, key)
147 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
148 try:
149 return int(stdout.strip())
150 except ValueError:
151 pass
152 return default
153
154
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155def add_git_similarity(parser):
156 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000158 help='Sets the percentage that a pair of files need to match in order to'
159 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160 parser.add_option(
161 '--find-copies', action='store_true',
162 help='Allows git to look for copies.')
163 parser.add_option(
164 '--no-find-copies', action='store_false', dest='find_copies',
165 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000166
167 old_parser_args = parser.parse_args
168 def Parse(args):
169 options, args = old_parser_args(args)
170
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000171 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000173 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000174 print('Note: Saving similarity of %d%% in git config.'
175 % options.similarity)
176 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000177
iannucci@chromium.org79540052012-10-19 23:15:26 +0000178 options.similarity = max(0, min(options.similarity, 100))
179
180 if options.find_copies is None:
181 options.find_copies = bool(
182 git_get_branch_default('git-find-copies', True))
183 else:
184 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000185
186 print('Using %d%% similarity for rename/copy detection. '
187 'Override with --similarity.' % options.similarity)
188
189 return options, args
190 parser.parse_args = Parse
191
192
ukai@chromium.org259e4682012-10-25 07:36:33 +0000193def is_dirty_git_tree(cmd):
194 # Make sure index is up-to-date before running diff-index.
195 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
196 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
197 if dirty:
198 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
199 print 'Uncommitted files: (git diff-index --name-status HEAD)'
200 print dirty[:4096]
201 if len(dirty) > 4096:
202 print '... (run "git diff-index --name-status HEAD" to see full output).'
203 return True
204 return False
205
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000206
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000207def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
208 """Return the corresponding git ref if |base_url| together with |glob_spec|
209 matches the full |url|.
210
211 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
212 """
213 fetch_suburl, as_ref = glob_spec.split(':')
214 if allow_wildcards:
215 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
216 if glob_match:
217 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
218 # "branches/{472,597,648}/src:refs/remotes/svn/*".
219 branch_re = re.escape(base_url)
220 if glob_match.group(1):
221 branch_re += '/' + re.escape(glob_match.group(1))
222 wildcard = glob_match.group(2)
223 if wildcard == '*':
224 branch_re += '([^/]*)'
225 else:
226 # Escape and replace surrounding braces with parentheses and commas
227 # with pipe symbols.
228 wildcard = re.escape(wildcard)
229 wildcard = re.sub('^\\\\{', '(', wildcard)
230 wildcard = re.sub('\\\\,', '|', wildcard)
231 wildcard = re.sub('\\\\}$', ')', wildcard)
232 branch_re += wildcard
233 if glob_match.group(3):
234 branch_re += re.escape(glob_match.group(3))
235 match = re.match(branch_re, url)
236 if match:
237 return re.sub('\*$', match.group(1), as_ref)
238
239 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
240 if fetch_suburl:
241 full_url = base_url + '/' + fetch_suburl
242 else:
243 full_url = base_url
244 if full_url == url:
245 return as_ref
246 return None
247
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000248
iannucci@chromium.org79540052012-10-19 23:15:26 +0000249def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000250 """Prints statistics about the change to the user."""
251 # --no-ext-diff is broken in some versions of Git, so try to work around
252 # this by overriding the environment (but there is still a problem if the
253 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000254 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000255 if 'GIT_EXTERNAL_DIFF' in env:
256 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000257
258 if find_copies:
259 similarity_options = ['--find-copies-harder', '-l100000',
260 '-C%s' % similarity]
261 else:
262 similarity_options = ['-M%s' % similarity]
263
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000264 try:
265 stdout = sys.stdout.fileno()
266 except AttributeError:
267 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000268 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000269 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000270 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000271 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000272
273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000274class Settings(object):
275 def __init__(self):
276 self.default_server = None
277 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000278 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000279 self.is_git_svn = None
280 self.svn_branch = None
281 self.tree_status_url = None
282 self.viewvc_url = None
283 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000284 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000285 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000286 self.project = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000287 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000288
289 def LazyUpdateIfNeeded(self):
290 """Updates the settings from a codereview.settings file, if available."""
291 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000292 # The only value that actually changes the behavior is
293 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000294 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000295 error_ok=True
296 ).strip().lower()
297
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000298 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000299 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000300 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000301 # set updated to True to avoid infinite calling loop
302 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000303 self.updated = True
304 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000305 self.updated = True
306
307 def GetDefaultServerUrl(self, error_ok=False):
308 if not self.default_server:
309 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000310 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000311 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000312 if error_ok:
313 return self.default_server
314 if not self.default_server:
315 error_message = ('Could not find settings file. You must configure '
316 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000317 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000318 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000319 return self.default_server
320
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000321 @staticmethod
322 def GetRelativeRoot():
323 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000324
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000325 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000326 if self.root is None:
327 self.root = os.path.abspath(self.GetRelativeRoot())
328 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000329
330 def GetIsGitSvn(self):
331 """Return true if this repo looks like it's using git-svn."""
332 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000333 if self.GetPendingRefPrefix():
334 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
335 self.is_git_svn = False
336 else:
337 # If you have any "svn-remote.*" config keys, we think you're using svn.
338 self.is_git_svn = RunGitWithCode(
339 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000340 return self.is_git_svn
341
342 def GetSVNBranch(self):
343 if self.svn_branch is None:
344 if not self.GetIsGitSvn():
345 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
346
347 # Try to figure out which remote branch we're based on.
348 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000349 # 1) iterate through our branch history and find the svn URL.
350 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000351
352 # regexp matching the git-svn line that contains the URL.
353 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
354
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000355 # We don't want to go through all of history, so read a line from the
356 # pipe at a time.
357 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000358 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000359 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
360 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000361 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 for line in proc.stdout:
363 match = git_svn_re.match(line)
364 if match:
365 url = match.group(1)
366 proc.stdout.close() # Cut pipe.
367 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000368
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000369 if url:
370 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
371 remotes = RunGit(['config', '--get-regexp',
372 r'^svn-remote\..*\.url']).splitlines()
373 for remote in remotes:
374 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000375 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000376 remote = match.group(1)
377 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000378 rewrite_root = RunGit(
379 ['config', 'svn-remote.%s.rewriteRoot' % remote],
380 error_ok=True).strip()
381 if rewrite_root:
382 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000383 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000384 ['config', 'svn-remote.%s.fetch' % remote],
385 error_ok=True).strip()
386 if fetch_spec:
387 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
388 if self.svn_branch:
389 break
390 branch_spec = RunGit(
391 ['config', 'svn-remote.%s.branches' % remote],
392 error_ok=True).strip()
393 if branch_spec:
394 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
395 if self.svn_branch:
396 break
397 tag_spec = RunGit(
398 ['config', 'svn-remote.%s.tags' % remote],
399 error_ok=True).strip()
400 if tag_spec:
401 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
402 if self.svn_branch:
403 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000404
405 if not self.svn_branch:
406 DieWithError('Can\'t guess svn branch -- try specifying it on the '
407 'command line')
408
409 return self.svn_branch
410
411 def GetTreeStatusUrl(self, error_ok=False):
412 if not self.tree_status_url:
413 error_message = ('You must configure your tree status URL by running '
414 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000415 self.tree_status_url = self._GetRietveldConfig(
416 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000417 return self.tree_status_url
418
419 def GetViewVCUrl(self):
420 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000421 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000422 return self.viewvc_url
423
rmistry@google.com90752582014-01-14 21:04:50 +0000424 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000425 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000426
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000427 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000428 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000429
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000430 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000431 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000432
ukai@chromium.orge8077812012-02-03 03:41:46 +0000433 def GetIsGerrit(self):
434 """Return true if this repo is assosiated with gerrit code review system."""
435 if self.is_gerrit is None:
436 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
437 return self.is_gerrit
438
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000439 def GetGitEditor(self):
440 """Return the editor specified in the git config, or None if none is."""
441 if self.git_editor is None:
442 self.git_editor = self._GetConfig('core.editor', error_ok=True)
443 return self.git_editor or None
444
thestig@chromium.org44202a22014-03-11 19:22:18 +0000445 def GetLintRegex(self):
446 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
447 DEFAULT_LINT_REGEX)
448
449 def GetLintIgnoreRegex(self):
450 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
451 DEFAULT_LINT_IGNORE_REGEX)
452
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000453 def GetProject(self):
454 if not self.project:
455 self.project = self._GetRietveldConfig('project', error_ok=True)
456 return self.project
457
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000458 def GetPendingRefPrefix(self):
459 if not self.pending_ref_prefix:
460 self.pending_ref_prefix = self._GetRietveldConfig(
461 'pending-ref-prefix', error_ok=True)
462 return self.pending_ref_prefix
463
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000464 def _GetRietveldConfig(self, param, **kwargs):
465 return self._GetConfig('rietveld.' + param, **kwargs)
466
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000467 def _GetConfig(self, param, **kwargs):
468 self.LazyUpdateIfNeeded()
469 return RunGit(['config', param], **kwargs).strip()
470
471
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000472def ShortBranchName(branch):
473 """Convert a name like 'refs/heads/foo' to just 'foo'."""
474 return branch.replace('refs/heads/', '')
475
476
477class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000478 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000479 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000480 global settings
481 if not settings:
482 # Happens when git_cl.py is used as a utility library.
483 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484 settings.GetDefaultServerUrl()
485 self.branchref = branchref
486 if self.branchref:
487 self.branch = ShortBranchName(self.branchref)
488 else:
489 self.branch = None
490 self.rietveld_server = None
491 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000492 self.lookedup_issue = False
493 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 self.has_description = False
495 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000496 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000497 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000498 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000499 self.cc = None
500 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000501 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000502 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000503
504 def GetCCList(self):
505 """Return the users cc'd on this CL.
506
507 Return is a string suitable for passing to gcl with the --cc flag.
508 """
509 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000510 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000511 more_cc = ','.join(self.watchers)
512 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
513 return self.cc
514
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000515 def GetCCListWithoutDefault(self):
516 """Return the users cc'd on this CL excluding default ones."""
517 if self.cc is None:
518 self.cc = ','.join(self.watchers)
519 return self.cc
520
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000521 def SetWatchers(self, watchers):
522 """Set the list of email addresses that should be cc'd based on the changed
523 files in this CL.
524 """
525 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000526
527 def GetBranch(self):
528 """Returns the short branch name, e.g. 'master'."""
529 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000530 branchref = RunGit(['symbolic-ref', 'HEAD'],
531 stderr=subprocess2.VOID, error_ok=True).strip()
532 if not branchref:
533 return None
534 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000535 self.branch = ShortBranchName(self.branchref)
536 return self.branch
537
538 def GetBranchRef(self):
539 """Returns the full branch name, e.g. 'refs/heads/master'."""
540 self.GetBranch() # Poke the lazy loader.
541 return self.branchref
542
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000543 @staticmethod
544 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000545 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000546 e.g. 'origin', 'refs/heads/master'
547 """
548 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000549 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
550 error_ok=True).strip()
551 if upstream_branch:
552 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
553 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000554 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
555 error_ok=True).strip()
556 if upstream_branch:
557 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000558 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000559 # Fall back on trying a git-svn upstream branch.
560 if settings.GetIsGitSvn():
561 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000562 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000563 # Else, try to guess the origin remote.
564 remote_branches = RunGit(['branch', '-r']).split()
565 if 'origin/master' in remote_branches:
566 # Fall back on origin/master if it exits.
567 remote = 'origin'
568 upstream_branch = 'refs/heads/master'
569 elif 'origin/trunk' in remote_branches:
570 # Fall back on origin/trunk if it exists. Generally a shared
571 # git-svn clone
572 remote = 'origin'
573 upstream_branch = 'refs/heads/trunk'
574 else:
575 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000576Either pass complete "git diff"-style arguments, like
577 git cl upload origin/master
578or verify this branch is set up to track another (via the --track argument to
579"git checkout -b ...").""")
580
581 return remote, upstream_branch
582
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000583 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000584 return git_common.get_or_create_merge_base(self.GetBranch(),
585 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000586
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000587 def GetUpstreamBranch(self):
588 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000589 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000590 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000591 upstream_branch = upstream_branch.replace('refs/heads/',
592 'refs/remotes/%s/' % remote)
593 upstream_branch = upstream_branch.replace('refs/branch-heads/',
594 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000595 self.upstream_branch = upstream_branch
596 return self.upstream_branch
597
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000598 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000599 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000600 remote, branch = None, self.GetBranch()
601 seen_branches = set()
602 while branch not in seen_branches:
603 seen_branches.add(branch)
604 remote, branch = self.FetchUpstreamTuple(branch)
605 branch = ShortBranchName(branch)
606 if remote != '.' or branch.startswith('refs/remotes'):
607 break
608 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000609 remotes = RunGit(['remote'], error_ok=True).split()
610 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000611 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000612 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000613 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000614 logging.warning('Could not determine which remote this change is '
615 'associated with, so defaulting to "%s". This may '
616 'not be what you want. You may prevent this message '
617 'by running "git svn info" as documented here: %s',
618 self._remote,
619 GIT_INSTRUCTIONS_URL)
620 else:
621 logging.warn('Could not determine which remote this change is '
622 'associated with. You may prevent this message by '
623 'running "git svn info" as documented here: %s',
624 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000625 branch = 'HEAD'
626 if branch.startswith('refs/remotes'):
627 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000628 elif branch.startswith('refs/branch-heads/'):
629 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000630 else:
631 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000632 return self._remote
633
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000634 def GitSanityChecks(self, upstream_git_obj):
635 """Checks git repo status and ensures diff is from local commits."""
636
637 # Verify the commit we're diffing against is in our current branch.
638 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
639 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
640 if upstream_sha != common_ancestor:
641 print >> sys.stderr, (
642 'ERROR: %s is not in the current branch. You may need to rebase '
643 'your tracking branch' % upstream_sha)
644 return False
645
646 # List the commits inside the diff, and verify they are all local.
647 commits_in_diff = RunGit(
648 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
649 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
650 remote_branch = remote_branch.strip()
651 if code != 0:
652 _, remote_branch = self.GetRemoteBranch()
653
654 commits_in_remote = RunGit(
655 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
656
657 common_commits = set(commits_in_diff) & set(commits_in_remote)
658 if common_commits:
659 print >> sys.stderr, (
660 'ERROR: Your diff contains %d commits already in %s.\n'
661 'Run "git log --oneline %s..HEAD" to get a list of commits in '
662 'the diff. If you are using a custom git flow, you can override'
663 ' the reference used for this check with "git config '
664 'gitcl.remotebranch <git-ref>".' % (
665 len(common_commits), remote_branch, upstream_git_obj))
666 return False
667 return True
668
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000669 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000670 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000671
672 Returns None if it is not set.
673 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000674 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
675 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000676
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000677 def GetRemoteUrl(self):
678 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
679
680 Returns None if there is no remote.
681 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000682 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000683 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
684
685 # If URL is pointing to a local directory, it is probably a git cache.
686 if os.path.isdir(url):
687 url = RunGit(['config', 'remote.%s.url' % remote],
688 error_ok=True,
689 cwd=url).strip()
690 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691
692 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000693 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000694 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000696 self.issue = int(issue) or None if issue else None
697 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000698 return self.issue
699
700 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000701 if not self.rietveld_server:
702 # If we're on a branch then get the server potentially associated
703 # with that branch.
704 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000705 rietveld_server_config = self._RietveldServer()
706 if rietveld_server_config:
707 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
708 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000709 if not self.rietveld_server:
710 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711 return self.rietveld_server
712
713 def GetIssueURL(self):
714 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000715 if not self.GetIssue():
716 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
718
719 def GetDescription(self, pretty=False):
720 if not self.has_description:
721 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000722 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000723 try:
724 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000725 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000726 if e.code == 404:
727 DieWithError(
728 ('\nWhile fetching the description for issue %d, received a '
729 '404 (not found)\n'
730 'error. It is likely that you deleted this '
731 'issue on the server. If this is the\n'
732 'case, please run\n\n'
733 ' git cl issue 0\n\n'
734 'to clear the association with the deleted issue. Then run '
735 'this command again.') % issue)
736 else:
737 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000738 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000739 except urllib2.URLError as e:
740 print >> sys.stderr, (
741 'Warning: Failed to retrieve CL description due to network '
742 'failure.')
743 self.description = ''
744
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000745 self.has_description = True
746 if pretty:
747 wrapper = textwrap.TextWrapper()
748 wrapper.initial_indent = wrapper.subsequent_indent = ' '
749 return wrapper.fill(self.description)
750 return self.description
751
752 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000753 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000754 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 patchset = RunGit(['config', self._PatchsetSetting()],
756 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000757 self.patchset = int(patchset) or None if patchset else None
758 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000759 return self.patchset
760
761 def SetPatchset(self, patchset):
762 """Set this branch's patchset. If patchset=0, clears the patchset."""
763 if patchset:
764 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000765 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000766 else:
767 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000768 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000769 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000770
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000771 def GetMostRecentPatchset(self):
772 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000773
774 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000775 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000776 '/download/issue%s_%s.diff' % (issue, patchset))
777
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000778 def GetIssueProperties(self):
779 if self._props is None:
780 issue = self.GetIssue()
781 if not issue:
782 self._props = {}
783 else:
784 self._props = self.RpcServer().get_issue_properties(issue, True)
785 return self._props
786
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000787 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000788 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000789
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790 def SetIssue(self, issue):
791 """Set this branch's issue. If issue=0, clears the issue."""
792 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000793 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794 RunGit(['config', self._IssueSetting(), str(issue)])
795 if self.rietveld_server:
796 RunGit(['config', self._RietveldServer(), self.rietveld_server])
797 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000798 current_issue = self.GetIssue()
799 if current_issue:
800 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000801 self.issue = None
802 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000804 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000805 if not self.GitSanityChecks(upstream_branch):
806 DieWithError('\nGit sanity check failure')
807
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000808 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000809 if not root:
810 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000811 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000812
813 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000814 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000815 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000816 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000817 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000818 except subprocess2.CalledProcessError:
819 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000820 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000821 'This branch probably doesn\'t exist anymore. To reset the\n'
822 'tracking branch, please run\n'
823 ' git branch --set-upstream %s trunk\n'
824 'replacing trunk with origin/master or the relevant branch') %
825 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000826
maruel@chromium.org52424302012-08-29 15:14:30 +0000827 issue = self.GetIssue()
828 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000829 if issue:
830 description = self.GetDescription()
831 else:
832 # If the change was never uploaded, use the log messages of all commits
833 # up to the branch point, as git cl upload will prefill the description
834 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000835 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
836 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000837
838 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000839 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000840 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000841 name,
842 description,
843 absroot,
844 files,
845 issue,
846 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000847 author,
848 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000849
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +0000850 def GetStatus(self):
851 """Apply a rough heuristic to give a simple summary of an issue's review
852 or CQ status, assuming adherence to a common workflow.
853
854 Returns None if no issue for this branch, or one of the following keywords:
855 * 'error' - error from review tool (including deleted issues)
856 * 'unsent' - not sent for review
857 * 'waiting' - waiting for review
858 * 'reply' - waiting for owner to reply to review
859 * 'lgtm' - LGTM from at least one approved reviewer
860 * 'commit' - in the commit queue
861 * 'closed' - closed
862 """
863 if not self.GetIssue():
864 return None
865
866 try:
867 props = self.GetIssueProperties()
868 except urllib2.HTTPError:
869 return 'error'
870
871 if props.get('closed'):
872 # Issue is closed.
873 return 'closed'
874 if props.get('commit'):
875 # Issue is in the commit queue.
876 return 'commit'
877
878 try:
879 reviewers = self.GetApprovingReviewers()
880 except urllib2.HTTPError:
881 return 'error'
882
883 if reviewers:
884 # Was LGTM'ed.
885 return 'lgtm'
886
887 messages = props.get('messages') or []
888
889 if not messages:
890 # No message was sent.
891 return 'unsent'
892 if messages[-1]['sender'] != props.get('owner_email'):
893 # Non-LGTM reply from non-owner
894 return 'reply'
895 return 'waiting'
896
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000897 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000898 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000899
900 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000901 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000902 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000903 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000904 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000905 except presubmit_support.PresubmitFailure, e:
906 DieWithError(
907 ('%s\nMaybe your depot_tools is out of date?\n'
908 'If all fails, contact maruel@') % e)
909
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000910 def UpdateDescription(self, description):
911 self.description = description
912 return self.RpcServer().update_description(
913 self.GetIssue(), self.description)
914
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000916 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000917 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000918
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000919 def SetFlag(self, flag, value):
920 """Patchset must match."""
921 if not self.GetPatchset():
922 DieWithError('The patchset needs to match. Send another patchset.')
923 try:
924 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000925 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000926 except urllib2.HTTPError, e:
927 if e.code == 404:
928 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
929 if e.code == 403:
930 DieWithError(
931 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
932 'match?') % (self.GetIssue(), self.GetPatchset()))
933 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000935 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 """Returns an upload.RpcServer() to access this review's rietveld instance.
937 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000938 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000939 self._rpc_server = rietveld.CachingRietveld(
940 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000941 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000942
943 def _IssueSetting(self):
944 """Return the git setting that stores this change's issue."""
945 return 'branch.%s.rietveldissue' % self.GetBranch()
946
947 def _PatchsetSetting(self):
948 """Return the git setting that stores this change's most recent patchset."""
949 return 'branch.%s.rietveldpatchset' % self.GetBranch()
950
951 def _RietveldServer(self):
952 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000953 branch = self.GetBranch()
954 if branch:
955 return 'branch.%s.rietveldserver' % branch
956 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000957
958
959def GetCodereviewSettingsInteractively():
960 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000961 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962 server = settings.GetDefaultServerUrl(error_ok=True)
963 prompt = 'Rietveld server (host[:port])'
964 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000965 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000966 if not server and not newserver:
967 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000968 if newserver:
969 newserver = gclient_utils.UpgradeToHttps(newserver)
970 if newserver != server:
971 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000973 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974 prompt = caption
975 if initial:
976 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000977 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978 if new_val == 'x':
979 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000980 elif new_val:
981 if is_url:
982 new_val = gclient_utils.UpgradeToHttps(new_val)
983 if new_val != initial:
984 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000985
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000986 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000987 SetProperty(settings.GetDefaultPrivateFlag(),
988 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000990 'tree-status-url', False)
991 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000992 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000993
994 # TODO: configure a default branch to diff against, rather than this
995 # svn-based hackery.
996
997
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000998class ChangeDescription(object):
999 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001000 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001001 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001002
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001003 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001004 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001005
agable@chromium.org42c20792013-09-12 17:34:49 +00001006 @property # www.logilab.org/ticket/89786
1007 def description(self): # pylint: disable=E0202
1008 return '\n'.join(self._description_lines)
1009
1010 def set_description(self, desc):
1011 if isinstance(desc, basestring):
1012 lines = desc.splitlines()
1013 else:
1014 lines = [line.rstrip() for line in desc]
1015 while lines and not lines[0]:
1016 lines.pop(0)
1017 while lines and not lines[-1]:
1018 lines.pop(-1)
1019 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001020
piman@chromium.org336f9122014-09-04 02:16:55 +00001021 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001022 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001023 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001024 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001025 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001026 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001027
agable@chromium.org42c20792013-09-12 17:34:49 +00001028 # Get the set of R= and TBR= lines and remove them from the desciption.
1029 regexp = re.compile(self.R_LINE)
1030 matches = [regexp.match(line) for line in self._description_lines]
1031 new_desc = [l for i, l in enumerate(self._description_lines)
1032 if not matches[i]]
1033 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001034
agable@chromium.org42c20792013-09-12 17:34:49 +00001035 # Construct new unified R= and TBR= lines.
1036 r_names = []
1037 tbr_names = []
1038 for match in matches:
1039 if not match:
1040 continue
1041 people = cleanup_list([match.group(2).strip()])
1042 if match.group(1) == 'TBR':
1043 tbr_names.extend(people)
1044 else:
1045 r_names.extend(people)
1046 for name in r_names:
1047 if name not in reviewers:
1048 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001049 if add_owners_tbr:
1050 owners_db = owners.Database(change.RepositoryRoot(),
1051 fopen=file, os_path=os.path, glob=glob.glob)
1052 all_reviewers = set(tbr_names + reviewers)
1053 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1054 all_reviewers)
1055 tbr_names.extend(owners_db.reviewers_for(missing_files,
1056 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001057 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1058 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1059
1060 # Put the new lines in the description where the old first R= line was.
1061 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1062 if 0 <= line_loc < len(self._description_lines):
1063 if new_tbr_line:
1064 self._description_lines.insert(line_loc, new_tbr_line)
1065 if new_r_line:
1066 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001067 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001068 if new_r_line:
1069 self.append_footer(new_r_line)
1070 if new_tbr_line:
1071 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001072
1073 def prompt(self):
1074 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001075 self.set_description([
1076 '# Enter a description of the change.',
1077 '# This will be displayed on the codereview site.',
1078 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001079 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001080 '--------------------',
1081 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001082
agable@chromium.org42c20792013-09-12 17:34:49 +00001083 regexp = re.compile(self.BUG_LINE)
1084 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001085 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001086 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001087 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001088 if not content:
1089 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001090 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001091
1092 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001093 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1094 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001095 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001096 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001097
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001098 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001099 if self._description_lines:
1100 # Add an empty line if either the last line or the new line isn't a tag.
1101 last_line = self._description_lines[-1]
1102 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1103 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1104 self._description_lines.append('')
1105 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001106
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001107 def get_reviewers(self):
1108 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001109 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1110 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001111 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001112
1113
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001114def get_approving_reviewers(props):
1115 """Retrieves the reviewers that approved a CL from the issue properties with
1116 messages.
1117
1118 Note that the list may contain reviewers that are not committer, thus are not
1119 considered by the CQ.
1120 """
1121 return sorted(
1122 set(
1123 message['sender']
1124 for message in props['messages']
1125 if message['approval'] and message['sender'] in props['reviewers']
1126 )
1127 )
1128
1129
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130def FindCodereviewSettingsFile(filename='codereview.settings'):
1131 """Finds the given file starting in the cwd and going up.
1132
1133 Only looks up to the top of the repository unless an
1134 'inherit-review-settings-ok' file exists in the root of the repository.
1135 """
1136 inherit_ok_file = 'inherit-review-settings-ok'
1137 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001138 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1140 root = '/'
1141 while True:
1142 if filename in os.listdir(cwd):
1143 if os.path.isfile(os.path.join(cwd, filename)):
1144 return open(os.path.join(cwd, filename))
1145 if cwd == root:
1146 break
1147 cwd = os.path.dirname(cwd)
1148
1149
1150def LoadCodereviewSettingsFromFile(fileobj):
1151 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001152 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001153
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 def SetProperty(name, setting, unset_error_ok=False):
1155 fullname = 'rietveld.' + name
1156 if setting in keyvals:
1157 RunGit(['config', fullname, keyvals[setting]])
1158 else:
1159 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1160
1161 SetProperty('server', 'CODE_REVIEW_SERVER')
1162 # Only server setting is required. Other settings can be absent.
1163 # In that case, we ignore errors raised during option deletion attempt.
1164 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001165 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1167 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001168 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001169 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1170 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001171 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001172 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001174 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001175 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001176
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1178 #should be of the form
1179 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1180 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1181 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1182 keyvals['ORIGIN_URL_CONFIG']])
1183
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001185def urlretrieve(source, destination):
1186 """urllib is broken for SSL connections via a proxy therefore we
1187 can't use urllib.urlretrieve()."""
1188 with open(destination, 'w') as f:
1189 f.write(urllib2.urlopen(source).read())
1190
1191
ukai@chromium.org712d6102013-11-27 00:52:58 +00001192def hasSheBang(fname):
1193 """Checks fname is a #! script."""
1194 with open(fname) as f:
1195 return f.read(2).startswith('#!')
1196
1197
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001198def DownloadHooks(force):
1199 """downloads hooks
1200
1201 Args:
1202 force: True to update hooks. False to install hooks if not present.
1203 """
1204 if not settings.GetIsGerrit():
1205 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001206 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001207 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1208 if not os.access(dst, os.X_OK):
1209 if os.path.exists(dst):
1210 if not force:
1211 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001212 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001213 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001214 if not hasSheBang(dst):
1215 DieWithError('Not a script: %s\n'
1216 'You need to download from\n%s\n'
1217 'into .git/hooks/commit-msg and '
1218 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001219 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1220 except Exception:
1221 if os.path.exists(dst):
1222 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001223 DieWithError('\nFailed to download hooks.\n'
1224 'You need to download from\n%s\n'
1225 'into .git/hooks/commit-msg and '
1226 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001227
1228
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001229@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001231 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001233 parser.add_option('--activate-update', action='store_true',
1234 help='activate auto-updating [rietveld] section in '
1235 '.git/config')
1236 parser.add_option('--deactivate-update', action='store_true',
1237 help='deactivate auto-updating [rietveld] section in '
1238 '.git/config')
1239 options, args = parser.parse_args(args)
1240
1241 if options.deactivate_update:
1242 RunGit(['config', 'rietveld.autoupdate', 'false'])
1243 return
1244
1245 if options.activate_update:
1246 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1247 return
1248
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001249 if len(args) == 0:
1250 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001251 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 return 0
1253
1254 url = args[0]
1255 if not url.endswith('codereview.settings'):
1256 url = os.path.join(url, 'codereview.settings')
1257
1258 # Load code review settings and download hooks (if available).
1259 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001260 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 return 0
1262
1263
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001264def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001265 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001266 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1267 branch = ShortBranchName(branchref)
1268 _, args = parser.parse_args(args)
1269 if not args:
1270 print("Current base-url:")
1271 return RunGit(['config', 'branch.%s.base-url' % branch],
1272 error_ok=False).strip()
1273 else:
1274 print("Setting base-url to %s" % args[0])
1275 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1276 error_ok=False).strip()
1277
1278
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001279def color_for_status(status):
1280 """Maps a Changelist status to color, for CMDstatus and other tools."""
1281 return {
1282 'unsent': Fore.RED,
1283 'waiting': Fore.BLUE,
1284 'reply': Fore.YELLOW,
1285 'lgtm': Fore.GREEN,
1286 'commit': Fore.MAGENTA,
1287 'closed': Fore.CYAN,
1288 'error': Fore.WHITE,
1289 }.get(status, Fore.WHITE)
1290
1291
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001292def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001293 """Show status of changelists.
1294
1295 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001296 - Red not sent for review or broken
1297 - Blue waiting for review
1298 - Yellow waiting for you to reply to review
1299 - Green LGTM'ed
1300 - Magenta in the commit queue
1301 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001302
1303 Also see 'git cl comments'.
1304 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 parser.add_option('--field',
1306 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001307 parser.add_option('-f', '--fast', action='store_true',
1308 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001309 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001310 if args:
1311 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001314 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315 if options.field.startswith('desc'):
1316 print cl.GetDescription()
1317 elif options.field == 'id':
1318 issueid = cl.GetIssue()
1319 if issueid:
1320 print issueid
1321 elif options.field == 'patch':
1322 patchset = cl.GetPatchset()
1323 if patchset:
1324 print patchset
1325 elif options.field == 'url':
1326 url = cl.GetIssueURL()
1327 if url:
1328 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001329 return 0
1330
1331 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1332 if not branches:
1333 print('No local branch found.')
1334 return 0
1335
1336 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001337 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001338 alignment = max(5, max(len(b) for b in branches))
1339 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001340 # Adhoc thread pool to request data concurrently.
1341 output = Queue.Queue()
1342
1343 # Silence upload.py otherwise it becomes unweldly.
1344 upload.verbosity = 0
1345
1346 if not options.fast:
1347 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001348 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001349 c = Changelist(branchref=b)
1350 i = c.GetIssueURL()
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001351 status = c.GetStatus()
1352 color = color_for_status(status)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001353
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001354 if i and (not status or status == 'error'):
1355 # The issue probably doesn't exist anymore.
1356 i += ' (broken)'
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001357
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001358 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001359
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001360 # Process one branch synchronously to work through authentication, then
1361 # spawn threads to process all the other branches in parallel.
1362 if branches:
1363 fetch(branches[0])
1364 threads = [
1365 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001366 for t in threads:
1367 t.daemon = True
1368 t.start()
1369 else:
1370 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1371 for b in branches:
1372 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001373 url = c.GetIssueURL()
1374 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001375
1376 tmp = {}
1377 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001378 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001379 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001380 b, i, color = output.get()
1381 tmp[b] = (i, color)
1382 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001383 reset = Fore.RESET
1384 if not sys.stdout.isatty():
1385 color = ''
1386 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001387 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001388 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001389
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001390 cl = Changelist()
1391 print
1392 print 'Current branch:',
1393 if not cl.GetIssue():
1394 print 'no issue assigned.'
1395 return 0
1396 print cl.GetBranch()
1397 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001398 if not options.fast:
1399 print 'Issue description:'
1400 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 return 0
1402
1403
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001404def colorize_CMDstatus_doc():
1405 """To be called once in main() to add colors to git cl status help."""
1406 colors = [i for i in dir(Fore) if i[0].isupper()]
1407
1408 def colorize_line(line):
1409 for color in colors:
1410 if color in line.upper():
1411 # Extract whitespaces first and the leading '-'.
1412 indent = len(line) - len(line.lstrip(' ')) + 1
1413 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1414 return line
1415
1416 lines = CMDstatus.__doc__.splitlines()
1417 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1418
1419
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001420@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001422 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001423
1424 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001425 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001426 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
1428 cl = Changelist()
1429 if len(args) > 0:
1430 try:
1431 issue = int(args[0])
1432 except ValueError:
1433 DieWithError('Pass a number to set the issue or none to list it.\n'
1434 'Maybe you want to run git cl status?')
1435 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001436 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 return 0
1438
1439
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001440def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001441 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001442 (_, args) = parser.parse_args(args)
1443 if args:
1444 parser.error('Unsupported argument: %s' % args)
1445
1446 cl = Changelist()
1447 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001448 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001449 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001450 if message['disapproval']:
1451 color = Fore.RED
1452 elif message['approval']:
1453 color = Fore.GREEN
1454 elif message['sender'] == data['owner_email']:
1455 color = Fore.MAGENTA
1456 else:
1457 color = Fore.BLUE
1458 print '\n%s%s %s%s' % (
1459 color, message['date'].split('.', 1)[0], message['sender'],
1460 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001461 if message['text'].strip():
1462 print '\n'.join(' ' + l for l in message['text'].splitlines())
1463 return 0
1464
1465
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001466def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001467 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001468 cl = Changelist()
1469 if not cl.GetIssue():
1470 DieWithError('This branch has no associated changelist.')
1471 description = ChangeDescription(cl.GetDescription())
1472 description.prompt()
1473 cl.UpdateDescription(description.description)
1474 return 0
1475
1476
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001477def CreateDescriptionFromLog(args):
1478 """Pulls out the commit log to use as a base for the CL description."""
1479 log_args = []
1480 if len(args) == 1 and not args[0].endswith('.'):
1481 log_args = [args[0] + '..']
1482 elif len(args) == 1 and args[0].endswith('...'):
1483 log_args = [args[0][:-1]]
1484 elif len(args) == 2:
1485 log_args = [args[0] + '..' + args[1]]
1486 else:
1487 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001488 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001489
1490
thestig@chromium.org44202a22014-03-11 19:22:18 +00001491def CMDlint(parser, args):
1492 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001493 parser.add_option('--filter', action='append', metavar='-x,+y',
1494 help='Comma-separated list of cpplint\'s category-filters')
1495 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001496
1497 # Access to a protected member _XX of a client class
1498 # pylint: disable=W0212
1499 try:
1500 import cpplint
1501 import cpplint_chromium
1502 except ImportError:
1503 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1504 return 1
1505
1506 # Change the current working directory before calling lint so that it
1507 # shows the correct base.
1508 previous_cwd = os.getcwd()
1509 os.chdir(settings.GetRoot())
1510 try:
1511 cl = Changelist()
1512 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1513 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001514 if not files:
1515 print "Cannot lint an empty CL"
1516 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001517
1518 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001519 command = args + files
1520 if options.filter:
1521 command = ['--filter=' + ','.join(options.filter)] + command
1522 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001523
1524 white_regex = re.compile(settings.GetLintRegex())
1525 black_regex = re.compile(settings.GetLintIgnoreRegex())
1526 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1527 for filename in filenames:
1528 if white_regex.match(filename):
1529 if black_regex.match(filename):
1530 print "Ignoring file %s" % filename
1531 else:
1532 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1533 extra_check_functions)
1534 else:
1535 print "Skipping file %s" % filename
1536 finally:
1537 os.chdir(previous_cwd)
1538 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1539 if cpplint._cpplint_state.error_count != 0:
1540 return 1
1541 return 0
1542
1543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001545 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001546 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001548 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001549 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001550 (options, args) = parser.parse_args(args)
1551
ukai@chromium.org259e4682012-10-25 07:36:33 +00001552 if not options.force and is_dirty_git_tree('presubmit'):
1553 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001554 return 1
1555
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001556 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001557 if args:
1558 base_branch = args[0]
1559 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001560 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001561 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001563 cl.RunHook(
1564 committing=not options.upload,
1565 may_prompt=False,
1566 verbose=options.verbose,
1567 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001568 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001569
1570
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001571def AddChangeIdToCommitMessage(options, args):
1572 """Re-commits using the current message, assumes the commit hook is in
1573 place.
1574 """
1575 log_desc = options.message or CreateDescriptionFromLog(args)
1576 git_command = ['commit', '--amend', '-m', log_desc]
1577 RunGit(git_command)
1578 new_log_desc = CreateDescriptionFromLog(args)
1579 if CHANGE_ID in new_log_desc:
1580 print 'git-cl: Added Change-Id to commit message.'
1581 else:
1582 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1583
1584
piman@chromium.org336f9122014-09-04 02:16:55 +00001585def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001586 """upload the current branch to gerrit."""
1587 # We assume the remote called "origin" is the one we want.
1588 # It is probably not worthwhile to support different workflows.
1589 remote = 'origin'
1590 branch = 'master'
1591 if options.target_branch:
1592 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001593
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001594 change_desc = ChangeDescription(
1595 options.message or CreateDescriptionFromLog(args))
1596 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001597 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001598 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001599 if CHANGE_ID not in change_desc.description:
1600 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001601
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001602 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001603 if len(commits) > 1:
1604 print('WARNING: This will upload %d commits. Run the following command '
1605 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001606 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001607 print('You can also use `git squash-branch` to squash these into a single'
1608 'commit.')
1609 ask_for_data('About to upload; enter to confirm.')
1610
piman@chromium.org336f9122014-09-04 02:16:55 +00001611 if options.reviewers or options.tbr_owners:
1612 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001613
ukai@chromium.orge8077812012-02-03 03:41:46 +00001614 receive_options = []
1615 cc = cl.GetCCList().split(',')
1616 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001617 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001618 cc = filter(None, cc)
1619 if cc:
1620 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001621 if change_desc.get_reviewers():
1622 receive_options.extend(
1623 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001624
ukai@chromium.orge8077812012-02-03 03:41:46 +00001625 git_command = ['push']
1626 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001627 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001628 ' '.join(receive_options))
1629 git_command += [remote, 'HEAD:refs/for/' + branch]
1630 RunGit(git_command)
1631 # TODO(ukai): parse Change-Id: and set issue number?
1632 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001633
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001634
piman@chromium.org336f9122014-09-04 02:16:55 +00001635def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001636 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001637 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1638 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639 if options.emulate_svn_auto_props:
1640 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001641
1642 change_desc = None
1643
pgervais@chromium.org91141372014-01-09 23:27:20 +00001644 if options.email is not None:
1645 upload_args.extend(['--email', options.email])
1646
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001647 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001648 if options.title:
1649 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001650 if options.message:
1651 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001652 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653 print ("This branch is associated with issue %s. "
1654 "Adding patch to that issue." % cl.GetIssue())
1655 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001656 if options.title:
1657 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001658 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001659 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00001660 if options.reviewers or options.tbr_owners:
1661 change_desc.update_reviewers(options.reviewers,
1662 options.tbr_owners,
1663 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001664 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001665 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001666
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001667 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001668 print "Description is empty; aborting."
1669 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001670
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001671 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001672 if change_desc.get_reviewers():
1673 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001674 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001675 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001676 DieWithError("Must specify reviewers to send email.")
1677 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001678
1679 # We check this before applying rietveld.private assuming that in
1680 # rietveld.cc only addresses which we can send private CLs to are listed
1681 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1682 # --private is specified explicitly on the command line.
1683 if options.private:
1684 logging.warn('rietveld.cc is ignored since private flag is specified. '
1685 'You need to review and add them manually if necessary.')
1686 cc = cl.GetCCListWithoutDefault()
1687 else:
1688 cc = cl.GetCCList()
1689 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001690 if cc:
1691 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001692
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001693 if options.private or settings.GetDefaultPrivateFlag() == "True":
1694 upload_args.append('--private')
1695
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001696 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001697 if not options.find_copies:
1698 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001699
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001700 # Include the upstream repo's URL in the change -- this is useful for
1701 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001702 remote_url = cl.GetGitBaseUrlFromConfig()
1703 if not remote_url:
1704 if settings.GetIsGitSvn():
1705 # URL is dependent on the current directory.
1706 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1707 if data:
1708 keys = dict(line.split(': ', 1) for line in data.splitlines()
1709 if ': ' in line)
1710 remote_url = keys.get('URL', None)
1711 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00001712 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1713 remote_url = (cl.GetRemoteUrl() + '@'
1714 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001715 if remote_url:
1716 upload_args.extend(['--base_url', remote_url])
1717
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001718 project = settings.GetProject()
1719 if project:
1720 upload_args.extend(['--project', project])
1721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001723 upload_args = ['upload'] + upload_args + args
1724 logging.info('upload.RealMain(%s)', upload_args)
1725 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001726 issue = int(issue)
1727 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001728 except KeyboardInterrupt:
1729 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730 except:
1731 # If we got an exception after the user typed a description for their
1732 # change, back up the description before re-raising.
1733 if change_desc:
1734 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1735 print '\nGot exception while uploading -- saving description to %s\n' \
1736 % backup_path
1737 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001738 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001739 backup_file.close()
1740 raise
1741
1742 if not cl.GetIssue():
1743 cl.SetIssue(issue)
1744 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001745
1746 if options.use_commit_queue:
1747 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001748 return 0
1749
1750
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001751def cleanup_list(l):
1752 """Fixes a list so that comma separated items are put as individual items.
1753
1754 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1755 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1756 """
1757 items = sum((i.split(',') for i in l), [])
1758 stripped_items = (i.strip() for i in items)
1759 return sorted(filter(None, stripped_items))
1760
1761
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001762@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001763def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001764 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001765 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1766 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001767 parser.add_option('--bypass-watchlists', action='store_true',
1768 dest='bypass_watchlists',
1769 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001770 parser.add_option('-f', action='store_true', dest='force',
1771 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001772 parser.add_option('-m', dest='message', help='message for patchset')
1773 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001774 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001775 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001776 help='reviewer email addresses')
1777 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001778 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001779 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001780 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001781 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001782 parser.add_option('--emulate_svn_auto_props',
1783 '--emulate-svn-auto-props',
1784 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001785 dest="emulate_svn_auto_props",
1786 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001787 parser.add_option('-c', '--use-commit-queue', action='store_true',
1788 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001789 parser.add_option('--private', action='store_true',
1790 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001791 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001792 '--target-branch',
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001793 help='When uploading to gerrit, remote branch to '
1794 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001795 parser.add_option('--email', default=None,
1796 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00001797 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
1798 help='add a set of OWNERS to TBR')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001799
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001800 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001801 (options, args) = parser.parse_args(args)
1802
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001803 if options.target_branch and not settings.GetIsGerrit():
1804 parser.error('Use --target_branch for non gerrit repository.')
1805
ukai@chromium.org259e4682012-10-25 07:36:33 +00001806 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001807 return 1
1808
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001809 options.reviewers = cleanup_list(options.reviewers)
1810 options.cc = cleanup_list(options.cc)
1811
ukai@chromium.orge8077812012-02-03 03:41:46 +00001812 cl = Changelist()
1813 if args:
1814 # TODO(ukai): is it ok for gerrit case?
1815 base_branch = args[0]
1816 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001817 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001818 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001819 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001820
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001821 # Apply watchlists on upload.
1822 change = cl.GetChange(base_branch, None)
1823 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1824 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001825 if not options.bypass_watchlists:
1826 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001827
ukai@chromium.orge8077812012-02-03 03:41:46 +00001828 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00001829 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001830 # Set the reviewer list now so that presubmit checks can access it.
1831 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00001832 change_description.update_reviewers(options.reviewers,
1833 options.tbr_owners,
1834 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001835 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001836 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001837 may_prompt=not options.force,
1838 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001839 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001840 if not hook_results.should_continue():
1841 return 1
1842 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001843 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001844
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001845 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001846 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001847 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001848 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001849 print ('The last upload made from this repository was patchset #%d but '
1850 'the most recent patchset on the server is #%d.'
1851 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001852 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1853 'from another machine or branch the patch you\'re uploading now '
1854 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001855 ask_for_data('About to upload; enter to confirm.')
1856
iannucci@chromium.org79540052012-10-19 23:15:26 +00001857 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001858 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00001859 return GerritUpload(options, args, cl, change)
1860 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001861 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001862 git_set_branch_value('last-upload-hash',
1863 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001864
1865 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001866
1867
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001868def IsSubmoduleMergeCommit(ref):
1869 # When submodules are added to the repo, we expect there to be a single
1870 # non-git-svn merge commit at remote HEAD with a signature comment.
1871 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001872 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001873 return RunGit(cmd) != ''
1874
1875
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001877 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001878
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001879 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001880 Updates changelog with metadata (e.g. pointer to review).
1881 Pushes/dcommits the code upstream.
1882 Updates review and closes.
1883 """
1884 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1885 help='bypass upload presubmit hook')
1886 parser.add_option('-m', dest='message',
1887 help="override review description")
1888 parser.add_option('-f', action='store_true', dest='force',
1889 help="force yes to questions (don't prompt)")
1890 parser.add_option('-c', dest='contributor',
1891 help="external contributor for patch (appended to " +
1892 "description and used as author for git). Should be " +
1893 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001894 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001895 (options, args) = parser.parse_args(args)
1896 cl = Changelist()
1897
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001898 current = cl.GetBranch()
1899 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1900 if not settings.GetIsGitSvn() and remote == '.':
1901 print
1902 print 'Attempting to push branch %r into another local branch!' % current
1903 print
1904 print 'Either reparent this branch on top of origin/master:'
1905 print ' git reparent-branch --root'
1906 print
1907 print 'OR run `git rebase-update` if you think the parent branch is already'
1908 print 'committed.'
1909 print
1910 print ' Current parent: %r' % upstream_branch
1911 return 1
1912
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001913 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001914 # Default to merging against our best guess of the upstream branch.
1915 args = [cl.GetUpstreamBranch()]
1916
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001917 if options.contributor:
1918 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1919 print "Please provide contibutor as 'First Last <email@example.com>'"
1920 return 1
1921
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001922 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001923 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924
ukai@chromium.org259e4682012-10-25 07:36:33 +00001925 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001926 return 1
1927
1928 # This rev-list syntax means "show all commits not in my branch that
1929 # are in base_branch".
1930 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1931 base_branch]).splitlines()
1932 if upstream_commits:
1933 print ('Base branch "%s" has %d commits '
1934 'not in this branch.' % (base_branch, len(upstream_commits)))
1935 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1936 return 1
1937
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001938 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001939 svn_head = None
1940 if cmd == 'dcommit' or base_has_submodules:
1941 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1942 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001943
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001944 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001945 # If the base_head is a submodule merge commit, the first parent of the
1946 # base_head should be a git-svn commit, which is what we're interested in.
1947 base_svn_head = base_branch
1948 if base_has_submodules:
1949 base_svn_head += '^1'
1950
1951 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001952 if extra_commits:
1953 print ('This branch has %d additional commits not upstreamed yet.'
1954 % len(extra_commits.splitlines()))
1955 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1956 'before attempting to %s.' % (base_branch, cmd))
1957 return 1
1958
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001959 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001960 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001961 author = None
1962 if options.contributor:
1963 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001964 hook_results = cl.RunHook(
1965 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001966 may_prompt=not options.force,
1967 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001968 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001969 if not hook_results.should_continue():
1970 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001972 # Check the tree status if the tree status URL is set.
1973 status = GetTreeStatus()
1974 if 'closed' == status:
1975 print('The tree is closed. Please wait for it to reopen. Use '
1976 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1977 return 1
1978 elif 'unknown' == status:
1979 print('Unable to determine tree status. Please verify manually and '
1980 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1981 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00001982 else:
1983 breakpad.SendStack(
1984 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001985 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1986 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001987 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001988
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001989 change_desc = ChangeDescription(options.message)
1990 if not change_desc.description and cl.GetIssue():
1991 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001992
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001993 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001994 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001995 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001996 else:
1997 print 'No description set.'
1998 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1999 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002001 # Keep a separate copy for the commit message, because the commit message
2002 # contains the link to the Rietveld issue, while the Rietveld message contains
2003 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002004 # Keep a separate copy for the commit message.
2005 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002006 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002007
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002008 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002009 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002010 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002011 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002012 commit_desc.append_footer('Patch from %s.' % options.contributor)
2013
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002014 print('Description:')
2015 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002017 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002019 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002020
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002021 # We want to squash all this branch's commits into one commit with the proper
2022 # description. We do this by doing a "reset --soft" to the base branch (which
2023 # keeps the working copy the same), then dcommitting that. If origin/master
2024 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2025 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002026 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002027 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2028 # Delete the branches if they exist.
2029 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2030 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2031 result = RunGitWithCode(showref_cmd)
2032 if result[0] == 0:
2033 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002034
2035 # We might be in a directory that's present in this branch but not in the
2036 # trunk. Move up to the top of the tree so that git commands that expect a
2037 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002038 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002039 if rel_base_path:
2040 os.chdir(rel_base_path)
2041
2042 # Stuff our change into the merge branch.
2043 # We wrap in a try...finally block so if anything goes wrong,
2044 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002045 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002046 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002047 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002048 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002049 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002050 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002051 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002052 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002053 RunGit(
2054 [
2055 'commit', '--author', options.contributor,
2056 '-m', commit_desc.description,
2057 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002058 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002059 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002060 if base_has_submodules:
2061 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2062 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2063 RunGit(['checkout', CHERRY_PICK_BRANCH])
2064 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002065 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002066 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002067 pending_prefix = settings.GetPendingRefPrefix()
2068 if not pending_prefix or branch.startswith(pending_prefix):
2069 # If not using refs/pending/heads/* at all, or target ref is already set
2070 # to pending, then push to the target ref directly.
2071 retcode, output = RunGitWithCode(
2072 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002073 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002074 else:
2075 # Cherry-pick the change on top of pending ref and then push it.
2076 assert branch.startswith('refs/'), branch
2077 assert pending_prefix[-1] == '/', pending_prefix
2078 pending_ref = pending_prefix + branch[len('refs/'):]
2079 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002080 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002081 if retcode == 0:
2082 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002083 else:
2084 # dcommit the merge branch.
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002085 _, output = RunGitWithCode(['svn', 'dcommit',
2086 '-C%s' % options.similarity,
2087 '--no-rebase', '--rmdir'])
2088 if 'Committed r' in output:
2089 revision = re.match(
2090 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2091 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002092 finally:
2093 # And then swap back to the original branch and clean up.
2094 RunGit(['checkout', '-q', cl.GetBranch()])
2095 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002096 if base_has_submodules:
2097 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002099 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002100 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002101 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002102
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002103 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002104 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002105 try:
2106 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2107 # We set pushed_to_pending to False, since it made it all the way to the
2108 # real ref.
2109 pushed_to_pending = False
2110 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002111 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002112
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002114 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002116 if not to_pending:
2117 if viewvc_url and revision:
2118 change_desc.append_footer(
2119 'Committed: %s%s' % (viewvc_url, revision))
2120 elif revision:
2121 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002122 print ('Closing issue '
2123 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002124 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002126 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002127 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002128 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002129 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002130 if options.bypass_hooks:
2131 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2132 else:
2133 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002134 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002135 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002136
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002137 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002138 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2139 print 'The commit is in the pending queue (%s).' % pending_ref
2140 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002141 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002142 'footer.' % branch)
2143
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002144 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2145 if os.path.isfile(hook):
2146 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002147
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002148 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002149
2150
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002151def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2152 print
2153 print 'Waiting for commit to be landed on %s...' % real_ref
2154 print '(If you are impatient, you may Ctrl-C once without harm)'
2155 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2156 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2157
2158 loop = 0
2159 while True:
2160 sys.stdout.write('fetching (%d)... \r' % loop)
2161 sys.stdout.flush()
2162 loop += 1
2163
2164 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2165 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2166 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2167 for commit in commits.splitlines():
2168 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2169 print 'Found commit on %s' % real_ref
2170 return commit
2171
2172 current_rev = to_rev
2173
2174
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002175def PushToGitPending(remote, pending_ref, upstream_ref):
2176 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2177
2178 Returns:
2179 (retcode of last operation, output log of last operation).
2180 """
2181 assert pending_ref.startswith('refs/'), pending_ref
2182 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2183 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2184 code = 0
2185 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002186 max_attempts = 3
2187 attempts_left = max_attempts
2188 while attempts_left:
2189 if attempts_left != max_attempts:
2190 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2191 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002192
2193 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002194 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002195 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002196 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002197 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002198 print 'Fetch failed with exit code %d.' % code
2199 if out.strip():
2200 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002201 continue
2202
2203 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002204 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002205 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002206 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002207 if code:
2208 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002209 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2210 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002211 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2212 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002213 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002214 return code, out
2215
2216 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002217 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002218 code, out = RunGitWithCode(
2219 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2220 if code == 0:
2221 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002222 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002223 return code, out
2224
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002225 print 'Push failed with exit code %d.' % code
2226 if out.strip():
2227 print out.strip()
2228 if IsFatalPushFailure(out):
2229 print (
2230 'Fatal push error. Make sure your .netrc credentials and git '
2231 'user.email are correct and you have push access to the repo.')
2232 return code, out
2233
2234 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002235 return code, out
2236
2237
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002238def IsFatalPushFailure(push_stdout):
2239 """True if retrying push won't help."""
2240 return '(prohibited by Gerrit)' in push_stdout
2241
2242
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002243@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002244def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002245 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002246 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002247 message = """This doesn't appear to be an SVN repository.
2248If your project has a git mirror with an upstream SVN master, you probably need
2249to run 'git svn init', see your project's git mirror documentation.
2250If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002251to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002252Choose wisely, if you get this wrong, your commit might appear to succeed but
2253will instead be silently ignored."""
2254 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002255 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002256 return SendUpstream(parser, args, 'dcommit')
2257
2258
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002259@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002260def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002261 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002262 if settings.GetIsGitSvn():
2263 print('This appears to be an SVN repository.')
2264 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002265 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002266 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002267
2268
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002269@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002270def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002271 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002272 parser.add_option('-b', dest='newbranch',
2273 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002274 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002275 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002276 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2277 help='Change to the directory DIR immediately, '
2278 'before doing anything else.')
2279 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002280 help='failed patches spew .rej files rather than '
2281 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002282 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2283 help="don't commit after patch applies")
2284 (options, args) = parser.parse_args(args)
2285 if len(args) != 1:
2286 parser.print_help()
2287 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002288 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002289
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002290 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002291 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002292
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002293 if options.newbranch:
2294 if options.force:
2295 RunGit(['branch', '-D', options.newbranch],
2296 stderr=subprocess2.PIPE, error_ok=True)
2297 RunGit(['checkout', '-b', options.newbranch,
2298 Changelist().GetUpstreamBranch()])
2299
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002300 return PatchIssue(issue_arg, options.reject, options.nocommit,
2301 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002302
2303
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002304def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002305 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002306 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002307 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002308 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002309 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002310 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002311 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002312 # Assume it's a URL to the patch. Default to https.
2313 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002314 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002315 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002316 DieWithError('Must pass an issue ID or full URL for '
2317 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002318 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002319 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002320 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002322 # Switch up to the top-level directory, if necessary, in preparation for
2323 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002324 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002325 if top:
2326 os.chdir(top)
2327
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002328 # Git patches have a/ at the beginning of source paths. We strip that out
2329 # with a sed script rather than the -p flag to patch so we can feed either
2330 # Git or svn-style patches into the same apply command.
2331 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002332 try:
2333 patch_data = subprocess2.check_output(
2334 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2335 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002336 DieWithError('Git patch mungling failed.')
2337 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002338
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002339 # We use "git apply" to apply the patch instead of "patch" so that we can
2340 # pick up file adds.
2341 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002342 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002343 if directory:
2344 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002345 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002346 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002347 elif IsGitVersionAtLeast('1.7.12'):
2348 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002349 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002350 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002351 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002352 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002353 DieWithError('Failed to apply the patch')
2354
2355 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002356 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002357 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2358 cl = Changelist()
2359 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002360 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002361 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002362 else:
2363 print "Patch applied to index."
2364 return 0
2365
2366
2367def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002368 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002369 # Provide a wrapper for git svn rebase to help avoid accidental
2370 # git svn dcommit.
2371 # It's the only command that doesn't use parser at all since we just defer
2372 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002373
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002374 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002375
2376
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002377def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002378 """Fetches the tree status and returns either 'open', 'closed',
2379 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002380 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002381 if url:
2382 status = urllib2.urlopen(url).read().lower()
2383 if status.find('closed') != -1 or status == '0':
2384 return 'closed'
2385 elif status.find('open') != -1 or status == '1':
2386 return 'open'
2387 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002388 return 'unset'
2389
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002390
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002391def GetTreeStatusReason():
2392 """Fetches the tree status from a json url and returns the message
2393 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002394 url = settings.GetTreeStatusUrl()
2395 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002396 connection = urllib2.urlopen(json_url)
2397 status = json.loads(connection.read())
2398 connection.close()
2399 return status['message']
2400
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002401
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002402def GetBuilderMaster(bot_list):
2403 """For a given builder, fetch the master from AE if available."""
2404 map_url = 'https://builders-map.appspot.com/'
2405 try:
2406 master_map = json.load(urllib2.urlopen(map_url))
2407 except urllib2.URLError as e:
2408 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2409 (map_url, e))
2410 except ValueError as e:
2411 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2412 if not master_map:
2413 return None, 'Failed to build master map.'
2414
2415 result_master = ''
2416 for bot in bot_list:
2417 builder = bot.split(':', 1)[0]
2418 master_list = master_map.get(builder, [])
2419 if not master_list:
2420 return None, ('No matching master for builder %s.' % builder)
2421 elif len(master_list) > 1:
2422 return None, ('The builder name %s exists in multiple masters %s.' %
2423 (builder, master_list))
2424 else:
2425 cur_master = master_list[0]
2426 if not result_master:
2427 result_master = cur_master
2428 elif result_master != cur_master:
2429 return None, 'The builders do not belong to the same master.'
2430 return result_master, None
2431
2432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002433def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002434 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002435 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002436 status = GetTreeStatus()
2437 if 'unset' == status:
2438 print 'You must configure your tree status URL by running "git cl config".'
2439 return 2
2440
2441 print "The tree is %s" % status
2442 print
2443 print GetTreeStatusReason()
2444 if status != 'open':
2445 return 1
2446 return 0
2447
2448
maruel@chromium.org15192402012-09-06 12:38:29 +00002449def CMDtry(parser, args):
2450 """Triggers a try job through Rietveld."""
2451 group = optparse.OptionGroup(parser, "Try job options")
2452 group.add_option(
2453 "-b", "--bot", action="append",
2454 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2455 "times to specify multiple builders. ex: "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002456 "'-b win_rel:ui_tests,webkit_unit_tests -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002457 "the try server waterfall for the builders name and the tests "
2458 "available. Can also be used to specify gtest_filter, e.g. "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002459 "-b win_rel:base_unittests:ValuesTest.*Value"))
maruel@chromium.org15192402012-09-06 12:38:29 +00002460 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002461 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002462 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002463 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002464 "-r", "--revision",
2465 help="Revision to use for the try job; default: the "
2466 "revision will be determined by the try server; see "
2467 "its waterfall for more info")
2468 group.add_option(
2469 "-c", "--clobber", action="store_true", default=False,
2470 help="Force a clobber before building; e.g. don't do an "
2471 "incremental build")
2472 group.add_option(
2473 "--project",
2474 help="Override which project to use. Projects are defined "
2475 "server-side to define what default bot set to use")
2476 group.add_option(
2477 "-t", "--testfilter", action="append", default=[],
2478 help=("Apply a testfilter to all the selected builders. Unless the "
2479 "builders configurations are similar, use multiple "
2480 "--bot <builder>:<test> arguments."))
2481 group.add_option(
2482 "-n", "--name", help="Try job name; default to current branch name")
2483 parser.add_option_group(group)
2484 options, args = parser.parse_args(args)
2485
2486 if args:
2487 parser.error('Unknown arguments: %s' % args)
2488
2489 cl = Changelist()
2490 if not cl.GetIssue():
2491 parser.error('Need to upload first')
2492
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002493 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002494 if props.get('closed'):
2495 parser.error('Cannot send tryjobs for a closed CL')
2496
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002497 if props.get('private'):
2498 parser.error('Cannot use trybots with private issue')
2499
maruel@chromium.org15192402012-09-06 12:38:29 +00002500 if not options.name:
2501 options.name = cl.GetBranch()
2502
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002503 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002504 options.master, err_msg = GetBuilderMaster(options.bot)
2505 if err_msg:
2506 parser.error('Tryserver master cannot be found because: %s\n'
2507 'Please manually specify the tryserver master'
2508 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002509
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002510 def GetMasterMap():
2511 # Process --bot and --testfilter.
2512 if not options.bot:
2513 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002514
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002515 # Get try masters from PRESUBMIT.py files.
2516 masters = presubmit_support.DoGetTryMasters(
2517 change,
2518 change.LocalPaths(),
2519 settings.GetRoot(),
2520 None,
2521 None,
2522 options.verbose,
2523 sys.stdout)
2524 if masters:
2525 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002526
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002527 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2528 options.bot = presubmit_support.DoGetTrySlaves(
2529 change,
2530 change.LocalPaths(),
2531 settings.GetRoot(),
2532 None,
2533 None,
2534 options.verbose,
2535 sys.stdout)
2536 if not options.bot:
2537 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002538
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002539 builders_and_tests = {}
2540 # TODO(machenbach): The old style command-line options don't support
2541 # multiple try masters yet.
2542 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2543 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2544
2545 for bot in old_style:
2546 if ':' in bot:
2547 builder, tests = bot.split(':', 1)
2548 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2549 elif ',' in bot:
2550 parser.error('Specify one bot per --bot flag')
2551 else:
2552 builders_and_tests.setdefault(bot, []).append('defaulttests')
2553
2554 for bot, tests in new_style:
2555 builders_and_tests.setdefault(bot, []).extend(tests)
2556
2557 # Return a master map with one master to be backwards compatible. The
2558 # master name defaults to an empty string, which will cause the master
2559 # not to be set on rietveld (deprecated).
2560 return {options.master: builders_and_tests}
2561
2562 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002563
maruel@chromium.org15192402012-09-06 12:38:29 +00002564 if options.testfilter:
2565 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002566 masters = dict((master, dict(
2567 (b, forced_tests) for b, t in slaves.iteritems()
2568 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002569
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002570 for builders in masters.itervalues():
2571 if any('triggered' in b for b in builders):
2572 print >> sys.stderr, (
2573 'ERROR You are trying to send a job to a triggered bot. This type of'
2574 ' bot requires an\ninitial job from a parent (usually a builder). '
2575 'Instead send your job to the parent.\n'
2576 'Bot list: %s' % builders)
2577 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002578
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002579 patchset = cl.GetMostRecentPatchset()
2580 if patchset and patchset != cl.GetPatchset():
2581 print(
2582 '\nWARNING Mismatch between local config and server. Did a previous '
2583 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2584 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002585 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002586 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002587 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002588 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002589 except urllib2.HTTPError, e:
2590 if e.code == 404:
2591 print('404 from rietveld; '
2592 'did you mean to use "git try" instead of "git cl try"?')
2593 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002594 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002595
2596 for (master, builders) in masters.iteritems():
2597 if master:
2598 print 'Master: %s' % master
2599 length = max(len(builder) for builder in builders)
2600 for builder in sorted(builders):
2601 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002602 return 0
2603
2604
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002605@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002606def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002607 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002608 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002609 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002610 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002612 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002613 if args:
2614 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002615 branch = cl.GetBranch()
2616 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002617 cl = Changelist()
2618 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002619
2620 # Clear configured merge-base, if there is one.
2621 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002622 else:
2623 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002624 return 0
2625
2626
thestig@chromium.org00858c82013-12-02 23:08:03 +00002627def CMDweb(parser, args):
2628 """Opens the current CL in the web browser."""
2629 _, args = parser.parse_args(args)
2630 if args:
2631 parser.error('Unrecognized args: %s' % ' '.join(args))
2632
2633 issue_url = Changelist().GetIssueURL()
2634 if not issue_url:
2635 print >> sys.stderr, 'ERROR No issue to open'
2636 return 1
2637
2638 webbrowser.open(issue_url)
2639 return 0
2640
2641
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002642def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002643 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002644 _, args = parser.parse_args(args)
2645 if args:
2646 parser.error('Unrecognized args: %s' % ' '.join(args))
2647 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002648 props = cl.GetIssueProperties()
2649 if props.get('private'):
2650 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002651 cl.SetFlag('commit', '1')
2652 return 0
2653
2654
groby@chromium.org411034a2013-02-26 15:12:01 +00002655def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002656 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002657 _, args = parser.parse_args(args)
2658 if args:
2659 parser.error('Unrecognized args: %s' % ' '.join(args))
2660 cl = Changelist()
2661 # Ensure there actually is an issue to close.
2662 cl.GetDescription()
2663 cl.CloseIssue()
2664 return 0
2665
2666
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002667def CMDdiff(parser, args):
2668 """shows differences between local tree and last upload."""
2669 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002670 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002671 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002672 if not issue:
2673 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002674 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002675 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002676
2677 # Create a new branch based on the merge-base
2678 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2679 try:
2680 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002681 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002682 if rtn != 0:
2683 return rtn
2684
2685 # Switch back to starting brand and diff against the temporary
2686 # branch containing the latest rietveld patch.
2687 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2688 finally:
2689 RunGit(['checkout', '-q', branch])
2690 RunGit(['branch', '-D', TMP_BRANCH])
2691
2692 return 0
2693
2694
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002695def CMDowners(parser, args):
2696 """interactively find the owners for reviewing"""
2697 parser.add_option(
2698 '--no-color',
2699 action='store_true',
2700 help='Use this option to disable color output')
2701 options, args = parser.parse_args(args)
2702
2703 author = RunGit(['config', 'user.email']).strip() or None
2704
2705 cl = Changelist()
2706
2707 if args:
2708 if len(args) > 1:
2709 parser.error('Unknown args')
2710 base_branch = args[0]
2711 else:
2712 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002713 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002714
2715 change = cl.GetChange(base_branch, None)
2716 return owners_finder.OwnersFinder(
2717 [f.LocalPath() for f in
2718 cl.GetChange(base_branch, None).AffectedFiles()],
2719 change.RepositoryRoot(), author,
2720 fopen=file, os_path=os.path, glob=glob.glob,
2721 disable_color=options.no_color).run()
2722
2723
enne@chromium.org555cfe42014-01-29 18:21:39 +00002724@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002725def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002726 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002727 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002728 parser.add_option('--full', action='store_true',
2729 help='Reformat the full content of all touched files')
2730 parser.add_option('--dry-run', action='store_true',
2731 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002732 parser.add_option('--diff', action='store_true',
2733 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002734 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002735
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002736 # git diff generates paths against the root of the repository. Change
2737 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002738 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002739 if rel_base_path:
2740 os.chdir(rel_base_path)
2741
digit@chromium.org29e47272013-05-17 17:01:46 +00002742 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002743 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002744 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002745 # Only list the names of modified files.
2746 diff_cmd.append('--name-only')
2747 else:
2748 # Only generate context-less patches.
2749 diff_cmd.append('-U0')
2750
2751 # Grab the merge-base commit, i.e. the upstream commit of the current
2752 # branch when it was created or the last time it was rebased. This is
2753 # to cover the case where the user may have called "git fetch origin",
2754 # moving the origin branch to a newer commit, but hasn't rebased yet.
2755 upstream_commit = None
2756 cl = Changelist()
2757 upstream_branch = cl.GetUpstreamBranch()
2758 if upstream_branch:
2759 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2760 upstream_commit = upstream_commit.strip()
2761
2762 if not upstream_commit:
2763 DieWithError('Could not find base commit for this branch. '
2764 'Are you in detached state?')
2765
2766 diff_cmd.append(upstream_commit)
2767
2768 # Handle source file filtering.
2769 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002770 if args:
2771 for arg in args:
2772 if os.path.isdir(arg):
2773 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2774 elif os.path.isfile(arg):
2775 diff_cmd.append(arg)
2776 else:
2777 DieWithError('Argument "%s" is not a file or a directory' % arg)
2778 else:
2779 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002780 diff_output = RunGit(diff_cmd)
2781
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002782 top_dir = os.path.normpath(
2783 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2784
2785 # Locate the clang-format binary in the checkout
2786 try:
2787 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2788 except clang_format.NotFoundError, e:
2789 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002790
digit@chromium.org29e47272013-05-17 17:01:46 +00002791 if opts.full:
2792 # diff_output is a list of files to send to clang-format.
2793 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002794 if not files:
2795 print "Nothing to format."
2796 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002797 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002798 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002799 cmd.append('-i')
2800 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002801 if opts.diff:
2802 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002803 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002804 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00002805 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00002806 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002807 try:
2808 script = clang_format.FindClangFormatScriptInChromiumTree(
2809 'clang-format-diff.py')
2810 except clang_format.NotFoundError, e:
2811 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002812
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002813 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002814 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002815 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002816
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002817 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002818 if opts.diff:
2819 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002820 if opts.dry_run and len(stdout) > 0:
2821 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002822
2823 return 0
2824
2825
maruel@chromium.org29404b52014-09-08 22:58:00 +00002826def CMDlol(parser, args):
2827 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00002828 print zlib.decompress(base64.b64decode(
2829 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
2830 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
2831 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
2832 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00002833 return 0
2834
2835
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002836class OptionParser(optparse.OptionParser):
2837 """Creates the option parse and add --verbose support."""
2838 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002839 optparse.OptionParser.__init__(
2840 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002841 self.add_option(
2842 '-v', '--verbose', action='count', default=0,
2843 help='Use 2 times for more debugging info')
2844
2845 def parse_args(self, args=None, values=None):
2846 options, args = optparse.OptionParser.parse_args(self, args, values)
2847 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2848 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2849 return options, args
2850
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002851
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002852def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002853 if sys.hexversion < 0x02060000:
2854 print >> sys.stderr, (
2855 '\nYour python version %s is unsupported, please upgrade.\n' %
2856 sys.version.split(' ', 1)[0])
2857 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002858
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002859 # Reload settings.
2860 global settings
2861 settings = Settings()
2862
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002863 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002864 dispatcher = subcommand.CommandDispatcher(__name__)
2865 try:
2866 return dispatcher.execute(OptionParser(), argv)
2867 except urllib2.HTTPError, e:
2868 if e.code != 500:
2869 raise
2870 DieWithError(
2871 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2872 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002873
2874
2875if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002876 # These affect sys.stdout so do it outside of main() to simplify mocks in
2877 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002878 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002879 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002880 sys.exit(main(sys.argv[1:]))