blob: 48f6c26d4d8e7f729c5ca6c8d6951cc4ee05e226 [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
apavlov@chromium.orge4efd512014-11-05 09:05:29 +0000790 def AddComment(self, message):
791 return self.RpcServer().add_comment(self.GetIssue(), message)
792
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000793 def SetIssue(self, issue):
794 """Set this branch's issue. If issue=0, clears the issue."""
795 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000796 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797 RunGit(['config', self._IssueSetting(), str(issue)])
798 if self.rietveld_server:
799 RunGit(['config', self._RietveldServer(), self.rietveld_server])
800 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000801 current_issue = self.GetIssue()
802 if current_issue:
803 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000804 self.issue = None
805 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000806
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000807 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000808 if not self.GitSanityChecks(upstream_branch):
809 DieWithError('\nGit sanity check failure')
810
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000811 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000812 if not root:
813 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000814 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000815
816 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000817 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000818 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000819 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000820 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000821 except subprocess2.CalledProcessError:
822 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000823 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000824 'This branch probably doesn\'t exist anymore. To reset the\n'
825 'tracking branch, please run\n'
826 ' git branch --set-upstream %s trunk\n'
827 'replacing trunk with origin/master or the relevant branch') %
828 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000829
maruel@chromium.org52424302012-08-29 15:14:30 +0000830 issue = self.GetIssue()
831 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000832 if issue:
833 description = self.GetDescription()
834 else:
835 # If the change was never uploaded, use the log messages of all commits
836 # up to the branch point, as git cl upload will prefill the description
837 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000838 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
839 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000840
841 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000842 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000843 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000844 name,
845 description,
846 absroot,
847 files,
848 issue,
849 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000850 author,
851 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000852
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +0000853 def GetStatus(self):
854 """Apply a rough heuristic to give a simple summary of an issue's review
855 or CQ status, assuming adherence to a common workflow.
856
857 Returns None if no issue for this branch, or one of the following keywords:
858 * 'error' - error from review tool (including deleted issues)
859 * 'unsent' - not sent for review
860 * 'waiting' - waiting for review
861 * 'reply' - waiting for owner to reply to review
862 * 'lgtm' - LGTM from at least one approved reviewer
863 * 'commit' - in the commit queue
864 * 'closed' - closed
865 """
866 if not self.GetIssue():
867 return None
868
869 try:
870 props = self.GetIssueProperties()
871 except urllib2.HTTPError:
872 return 'error'
873
874 if props.get('closed'):
875 # Issue is closed.
876 return 'closed'
877 if props.get('commit'):
878 # Issue is in the commit queue.
879 return 'commit'
880
881 try:
882 reviewers = self.GetApprovingReviewers()
883 except urllib2.HTTPError:
884 return 'error'
885
886 if reviewers:
887 # Was LGTM'ed.
888 return 'lgtm'
889
890 messages = props.get('messages') or []
891
892 if not messages:
893 # No message was sent.
894 return 'unsent'
895 if messages[-1]['sender'] != props.get('owner_email'):
896 # Non-LGTM reply from non-owner
897 return 'reply'
898 return 'waiting'
899
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000900 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000901 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000902
903 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000904 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000905 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000906 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000907 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000908 except presubmit_support.PresubmitFailure, e:
909 DieWithError(
910 ('%s\nMaybe your depot_tools is out of date?\n'
911 'If all fails, contact maruel@') % e)
912
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000913 def UpdateDescription(self, description):
914 self.description = description
915 return self.RpcServer().update_description(
916 self.GetIssue(), self.description)
917
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000918 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000919 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000920 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000922 def SetFlag(self, flag, value):
923 """Patchset must match."""
924 if not self.GetPatchset():
925 DieWithError('The patchset needs to match. Send another patchset.')
926 try:
927 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000928 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000929 except urllib2.HTTPError, e:
930 if e.code == 404:
931 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
932 if e.code == 403:
933 DieWithError(
934 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
935 'match?') % (self.GetIssue(), self.GetPatchset()))
936 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000938 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939 """Returns an upload.RpcServer() to access this review's rietveld instance.
940 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000941 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000942 self._rpc_server = rietveld.CachingRietveld(
943 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000944 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000945
946 def _IssueSetting(self):
947 """Return the git setting that stores this change's issue."""
948 return 'branch.%s.rietveldissue' % self.GetBranch()
949
950 def _PatchsetSetting(self):
951 """Return the git setting that stores this change's most recent patchset."""
952 return 'branch.%s.rietveldpatchset' % self.GetBranch()
953
954 def _RietveldServer(self):
955 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000956 branch = self.GetBranch()
957 if branch:
958 return 'branch.%s.rietveldserver' % branch
959 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960
961
962def GetCodereviewSettingsInteractively():
963 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000964 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000965 server = settings.GetDefaultServerUrl(error_ok=True)
966 prompt = 'Rietveld server (host[:port])'
967 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000968 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 if not server and not newserver:
970 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000971 if newserver:
972 newserver = gclient_utils.UpgradeToHttps(newserver)
973 if newserver != server:
974 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000976 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977 prompt = caption
978 if initial:
979 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000980 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000981 if new_val == 'x':
982 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000983 elif new_val:
984 if is_url:
985 new_val = gclient_utils.UpgradeToHttps(new_val)
986 if new_val != initial:
987 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000988
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000989 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000990 SetProperty(settings.GetDefaultPrivateFlag(),
991 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000992 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000993 'tree-status-url', False)
994 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000995 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996
997 # TODO: configure a default branch to diff against, rather than this
998 # svn-based hackery.
999
1000
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001001class ChangeDescription(object):
1002 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +00001003 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +00001004 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001005
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001006 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001007 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001008
agable@chromium.org42c20792013-09-12 17:34:49 +00001009 @property # www.logilab.org/ticket/89786
1010 def description(self): # pylint: disable=E0202
1011 return '\n'.join(self._description_lines)
1012
1013 def set_description(self, desc):
1014 if isinstance(desc, basestring):
1015 lines = desc.splitlines()
1016 else:
1017 lines = [line.rstrip() for line in desc]
1018 while lines and not lines[0]:
1019 lines.pop(0)
1020 while lines and not lines[-1]:
1021 lines.pop(-1)
1022 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001023
piman@chromium.org336f9122014-09-04 02:16:55 +00001024 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001025 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001026 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001027 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001028 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001029 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001030
agable@chromium.org42c20792013-09-12 17:34:49 +00001031 # Get the set of R= and TBR= lines and remove them from the desciption.
1032 regexp = re.compile(self.R_LINE)
1033 matches = [regexp.match(line) for line in self._description_lines]
1034 new_desc = [l for i, l in enumerate(self._description_lines)
1035 if not matches[i]]
1036 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001037
agable@chromium.org42c20792013-09-12 17:34:49 +00001038 # Construct new unified R= and TBR= lines.
1039 r_names = []
1040 tbr_names = []
1041 for match in matches:
1042 if not match:
1043 continue
1044 people = cleanup_list([match.group(2).strip()])
1045 if match.group(1) == 'TBR':
1046 tbr_names.extend(people)
1047 else:
1048 r_names.extend(people)
1049 for name in r_names:
1050 if name not in reviewers:
1051 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001052 if add_owners_tbr:
1053 owners_db = owners.Database(change.RepositoryRoot(),
1054 fopen=file, os_path=os.path, glob=glob.glob)
1055 all_reviewers = set(tbr_names + reviewers)
1056 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1057 all_reviewers)
1058 tbr_names.extend(owners_db.reviewers_for(missing_files,
1059 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001060 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1061 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1062
1063 # Put the new lines in the description where the old first R= line was.
1064 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1065 if 0 <= line_loc < len(self._description_lines):
1066 if new_tbr_line:
1067 self._description_lines.insert(line_loc, new_tbr_line)
1068 if new_r_line:
1069 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001070 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001071 if new_r_line:
1072 self.append_footer(new_r_line)
1073 if new_tbr_line:
1074 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001075
1076 def prompt(self):
1077 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001078 self.set_description([
1079 '# Enter a description of the change.',
1080 '# This will be displayed on the codereview site.',
1081 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001082 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001083 '--------------------',
1084 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001085
agable@chromium.org42c20792013-09-12 17:34:49 +00001086 regexp = re.compile(self.BUG_LINE)
1087 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001088 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001089 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001090 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001091 if not content:
1092 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001093 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001094
1095 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001096 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1097 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001098 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001099 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001100
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001101 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001102 if self._description_lines:
1103 # Add an empty line if either the last line or the new line isn't a tag.
1104 last_line = self._description_lines[-1]
1105 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1106 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1107 self._description_lines.append('')
1108 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001109
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001110 def get_reviewers(self):
1111 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001112 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1113 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001114 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001115
1116
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001117def get_approving_reviewers(props):
1118 """Retrieves the reviewers that approved a CL from the issue properties with
1119 messages.
1120
1121 Note that the list may contain reviewers that are not committer, thus are not
1122 considered by the CQ.
1123 """
1124 return sorted(
1125 set(
1126 message['sender']
1127 for message in props['messages']
1128 if message['approval'] and message['sender'] in props['reviewers']
1129 )
1130 )
1131
1132
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001133def FindCodereviewSettingsFile(filename='codereview.settings'):
1134 """Finds the given file starting in the cwd and going up.
1135
1136 Only looks up to the top of the repository unless an
1137 'inherit-review-settings-ok' file exists in the root of the repository.
1138 """
1139 inherit_ok_file = 'inherit-review-settings-ok'
1140 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001141 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1143 root = '/'
1144 while True:
1145 if filename in os.listdir(cwd):
1146 if os.path.isfile(os.path.join(cwd, filename)):
1147 return open(os.path.join(cwd, filename))
1148 if cwd == root:
1149 break
1150 cwd = os.path.dirname(cwd)
1151
1152
1153def LoadCodereviewSettingsFromFile(fileobj):
1154 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001155 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001157 def SetProperty(name, setting, unset_error_ok=False):
1158 fullname = 'rietveld.' + name
1159 if setting in keyvals:
1160 RunGit(['config', fullname, keyvals[setting]])
1161 else:
1162 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1163
1164 SetProperty('server', 'CODE_REVIEW_SERVER')
1165 # Only server setting is required. Other settings can be absent.
1166 # In that case, we ignore errors raised during option deletion attempt.
1167 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001168 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1170 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001171 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001172 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1173 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001174 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001175 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001177 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001178 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001179
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1181 #should be of the form
1182 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1183 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1184 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1185 keyvals['ORIGIN_URL_CONFIG']])
1186
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001188def urlretrieve(source, destination):
1189 """urllib is broken for SSL connections via a proxy therefore we
1190 can't use urllib.urlretrieve()."""
1191 with open(destination, 'w') as f:
1192 f.write(urllib2.urlopen(source).read())
1193
1194
ukai@chromium.org712d6102013-11-27 00:52:58 +00001195def hasSheBang(fname):
1196 """Checks fname is a #! script."""
1197 with open(fname) as f:
1198 return f.read(2).startswith('#!')
1199
1200
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001201def DownloadHooks(force):
1202 """downloads hooks
1203
1204 Args:
1205 force: True to update hooks. False to install hooks if not present.
1206 """
1207 if not settings.GetIsGerrit():
1208 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001209 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001210 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1211 if not os.access(dst, os.X_OK):
1212 if os.path.exists(dst):
1213 if not force:
1214 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001215 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001216 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001217 if not hasSheBang(dst):
1218 DieWithError('Not a script: %s\n'
1219 'You need to download from\n%s\n'
1220 'into .git/hooks/commit-msg and '
1221 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001222 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1223 except Exception:
1224 if os.path.exists(dst):
1225 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001226 DieWithError('\nFailed to download hooks.\n'
1227 'You need to download from\n%s\n'
1228 'into .git/hooks/commit-msg and '
1229 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001230
1231
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001232@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001234 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001236 parser.add_option('--activate-update', action='store_true',
1237 help='activate auto-updating [rietveld] section in '
1238 '.git/config')
1239 parser.add_option('--deactivate-update', action='store_true',
1240 help='deactivate auto-updating [rietveld] section in '
1241 '.git/config')
1242 options, args = parser.parse_args(args)
1243
1244 if options.deactivate_update:
1245 RunGit(['config', 'rietveld.autoupdate', 'false'])
1246 return
1247
1248 if options.activate_update:
1249 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1250 return
1251
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 if len(args) == 0:
1253 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001254 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 return 0
1256
1257 url = args[0]
1258 if not url.endswith('codereview.settings'):
1259 url = os.path.join(url, 'codereview.settings')
1260
1261 # Load code review settings and download hooks (if available).
1262 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001263 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001264 return 0
1265
1266
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001267def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001268 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001269 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1270 branch = ShortBranchName(branchref)
1271 _, args = parser.parse_args(args)
1272 if not args:
1273 print("Current base-url:")
1274 return RunGit(['config', 'branch.%s.base-url' % branch],
1275 error_ok=False).strip()
1276 else:
1277 print("Setting base-url to %s" % args[0])
1278 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1279 error_ok=False).strip()
1280
1281
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001282def color_for_status(status):
1283 """Maps a Changelist status to color, for CMDstatus and other tools."""
1284 return {
1285 'unsent': Fore.RED,
1286 'waiting': Fore.BLUE,
1287 'reply': Fore.YELLOW,
1288 'lgtm': Fore.GREEN,
1289 'commit': Fore.MAGENTA,
1290 'closed': Fore.CYAN,
1291 'error': Fore.WHITE,
1292 }.get(status, Fore.WHITE)
1293
1294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001296 """Show status of changelists.
1297
1298 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001299 - Red not sent for review or broken
1300 - Blue waiting for review
1301 - Yellow waiting for you to reply to review
1302 - Green LGTM'ed
1303 - Magenta in the commit queue
1304 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001305
1306 Also see 'git cl comments'.
1307 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 parser.add_option('--field',
1309 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001310 parser.add_option('-f', '--fast', action='store_true',
1311 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001313 if args:
1314 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001317 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001318 if options.field.startswith('desc'):
1319 print cl.GetDescription()
1320 elif options.field == 'id':
1321 issueid = cl.GetIssue()
1322 if issueid:
1323 print issueid
1324 elif options.field == 'patch':
1325 patchset = cl.GetPatchset()
1326 if patchset:
1327 print patchset
1328 elif options.field == 'url':
1329 url = cl.GetIssueURL()
1330 if url:
1331 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001332 return 0
1333
1334 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1335 if not branches:
1336 print('No local branch found.')
1337 return 0
1338
1339 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001340 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001341 alignment = max(5, max(len(b) for b in branches))
1342 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001343 # Adhoc thread pool to request data concurrently.
1344 output = Queue.Queue()
1345
1346 # Silence upload.py otherwise it becomes unweldly.
1347 upload.verbosity = 0
1348
1349 if not options.fast:
1350 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001351 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001352 c = Changelist(branchref=b)
1353 i = c.GetIssueURL()
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001354 status = c.GetStatus()
1355 color = color_for_status(status)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001356
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001357 if i and (not status or status == 'error'):
1358 # The issue probably doesn't exist anymore.
1359 i += ' (broken)'
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001360
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001361 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001362
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001363 # Process one branch synchronously to work through authentication, then
1364 # spawn threads to process all the other branches in parallel.
1365 if branches:
1366 fetch(branches[0])
1367 threads = [
1368 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001369 for t in threads:
1370 t.daemon = True
1371 t.start()
1372 else:
1373 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1374 for b in branches:
1375 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001376 url = c.GetIssueURL()
1377 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001378
1379 tmp = {}
1380 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001381 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001382 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001383 b, i, color = output.get()
1384 tmp[b] = (i, color)
1385 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001386 reset = Fore.RESET
1387 if not sys.stdout.isatty():
1388 color = ''
1389 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001390 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001391 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001392
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001393 cl = Changelist()
1394 print
1395 print 'Current branch:',
1396 if not cl.GetIssue():
1397 print 'no issue assigned.'
1398 return 0
1399 print cl.GetBranch()
1400 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001401 if not options.fast:
1402 print 'Issue description:'
1403 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 return 0
1405
1406
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001407def colorize_CMDstatus_doc():
1408 """To be called once in main() to add colors to git cl status help."""
1409 colors = [i for i in dir(Fore) if i[0].isupper()]
1410
1411 def colorize_line(line):
1412 for color in colors:
1413 if color in line.upper():
1414 # Extract whitespaces first and the leading '-'.
1415 indent = len(line) - len(line.lstrip(' ')) + 1
1416 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1417 return line
1418
1419 lines = CMDstatus.__doc__.splitlines()
1420 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1421
1422
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001423@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001424def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001425 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426
1427 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001428 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001429 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430
1431 cl = Changelist()
1432 if len(args) > 0:
1433 try:
1434 issue = int(args[0])
1435 except ValueError:
1436 DieWithError('Pass a number to set the issue or none to list it.\n'
1437 'Maybe you want to run git cl status?')
1438 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001439 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 return 0
1441
1442
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001443def CMDcomments(parser, args):
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001444 """Shows or posts review comments for any changelist."""
1445 parser.add_option('-a', '--add-comment', dest='comment',
1446 help='comment to add to an issue')
1447 parser.add_option('-i', dest='issue',
1448 help="review issue id (defaults to current issue)")
1449 options, args = parser.parse_args(args)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001450
apavlov@chromium.orge4efd512014-11-05 09:05:29 +00001451 issue = None
1452 if options.issue:
1453 try:
1454 issue = int(options.issue)
1455 except ValueError:
1456 DieWithError('A review issue id is expected to be a number')
1457
1458 cl = Changelist(issue=issue)
1459
1460 if options.comment:
1461 cl.AddComment(options.comment)
1462 return 0
1463
1464 data = cl.GetIssueProperties()
1465 for message in sorted(data['messages'], key=lambda x: x['date']):
1466 if message['disapproval']:
1467 color = Fore.RED
1468 elif message['approval']:
1469 color = Fore.GREEN
1470 elif message['sender'] == data['owner_email']:
1471 color = Fore.MAGENTA
1472 else:
1473 color = Fore.BLUE
1474 print '\n%s%s %s%s' % (
1475 color, message['date'].split('.', 1)[0], message['sender'],
1476 Fore.RESET)
1477 if message['text'].strip():
1478 print '\n'.join(' ' + l for l in message['text'].splitlines())
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001479 return 0
1480
1481
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001482def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001483 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001484 cl = Changelist()
1485 if not cl.GetIssue():
1486 DieWithError('This branch has no associated changelist.')
1487 description = ChangeDescription(cl.GetDescription())
1488 description.prompt()
1489 cl.UpdateDescription(description.description)
1490 return 0
1491
1492
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493def CreateDescriptionFromLog(args):
1494 """Pulls out the commit log to use as a base for the CL description."""
1495 log_args = []
1496 if len(args) == 1 and not args[0].endswith('.'):
1497 log_args = [args[0] + '..']
1498 elif len(args) == 1 and args[0].endswith('...'):
1499 log_args = [args[0][:-1]]
1500 elif len(args) == 2:
1501 log_args = [args[0] + '..' + args[1]]
1502 else:
1503 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001504 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505
1506
thestig@chromium.org44202a22014-03-11 19:22:18 +00001507def CMDlint(parser, args):
1508 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001509 parser.add_option('--filter', action='append', metavar='-x,+y',
1510 help='Comma-separated list of cpplint\'s category-filters')
1511 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001512
1513 # Access to a protected member _XX of a client class
1514 # pylint: disable=W0212
1515 try:
1516 import cpplint
1517 import cpplint_chromium
1518 except ImportError:
1519 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1520 return 1
1521
1522 # Change the current working directory before calling lint so that it
1523 # shows the correct base.
1524 previous_cwd = os.getcwd()
1525 os.chdir(settings.GetRoot())
1526 try:
1527 cl = Changelist()
1528 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1529 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001530 if not files:
1531 print "Cannot lint an empty CL"
1532 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001533
1534 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001535 command = args + files
1536 if options.filter:
1537 command = ['--filter=' + ','.join(options.filter)] + command
1538 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001539
1540 white_regex = re.compile(settings.GetLintRegex())
1541 black_regex = re.compile(settings.GetLintIgnoreRegex())
1542 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1543 for filename in filenames:
1544 if white_regex.match(filename):
1545 if black_regex.match(filename):
1546 print "Ignoring file %s" % filename
1547 else:
1548 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1549 extra_check_functions)
1550 else:
1551 print "Skipping file %s" % filename
1552 finally:
1553 os.chdir(previous_cwd)
1554 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1555 if cpplint._cpplint_state.error_count != 0:
1556 return 1
1557 return 0
1558
1559
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001560def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001561 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001562 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001563 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001564 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001565 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001566 (options, args) = parser.parse_args(args)
1567
ukai@chromium.org259e4682012-10-25 07:36:33 +00001568 if not options.force and is_dirty_git_tree('presubmit'):
1569 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001570 return 1
1571
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001572 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001573 if args:
1574 base_branch = args[0]
1575 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001576 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001577 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001578
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001579 cl.RunHook(
1580 committing=not options.upload,
1581 may_prompt=False,
1582 verbose=options.verbose,
1583 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001584 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001585
1586
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001587def AddChangeIdToCommitMessage(options, args):
1588 """Re-commits using the current message, assumes the commit hook is in
1589 place.
1590 """
1591 log_desc = options.message or CreateDescriptionFromLog(args)
1592 git_command = ['commit', '--amend', '-m', log_desc]
1593 RunGit(git_command)
1594 new_log_desc = CreateDescriptionFromLog(args)
1595 if CHANGE_ID in new_log_desc:
1596 print 'git-cl: Added Change-Id to commit message.'
1597 else:
1598 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1599
1600
piman@chromium.org336f9122014-09-04 02:16:55 +00001601def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001602 """upload the current branch to gerrit."""
1603 # We assume the remote called "origin" is the one we want.
1604 # It is probably not worthwhile to support different workflows.
1605 remote = 'origin'
1606 branch = 'master'
1607 if options.target_branch:
1608 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001610 change_desc = ChangeDescription(
1611 options.message or CreateDescriptionFromLog(args))
1612 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001613 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001614 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001615 if CHANGE_ID not in change_desc.description:
1616 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001617
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001618 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001619 if len(commits) > 1:
1620 print('WARNING: This will upload %d commits. Run the following command '
1621 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001622 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001623 print('You can also use `git squash-branch` to squash these into a single'
1624 'commit.')
1625 ask_for_data('About to upload; enter to confirm.')
1626
piman@chromium.org336f9122014-09-04 02:16:55 +00001627 if options.reviewers or options.tbr_owners:
1628 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001629
ukai@chromium.orge8077812012-02-03 03:41:46 +00001630 receive_options = []
1631 cc = cl.GetCCList().split(',')
1632 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001633 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001634 cc = filter(None, cc)
1635 if cc:
1636 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001637 if change_desc.get_reviewers():
1638 receive_options.extend(
1639 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001640
ukai@chromium.orge8077812012-02-03 03:41:46 +00001641 git_command = ['push']
1642 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001643 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001644 ' '.join(receive_options))
1645 git_command += [remote, 'HEAD:refs/for/' + branch]
1646 RunGit(git_command)
1647 # TODO(ukai): parse Change-Id: and set issue number?
1648 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001649
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001650
piman@chromium.org336f9122014-09-04 02:16:55 +00001651def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001652 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001653 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1654 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001655 if options.emulate_svn_auto_props:
1656 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001657
1658 change_desc = None
1659
pgervais@chromium.org91141372014-01-09 23:27:20 +00001660 if options.email is not None:
1661 upload_args.extend(['--email', options.email])
1662
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001663 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001664 if options.title:
1665 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001666 if options.message:
1667 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001668 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001669 print ("This branch is associated with issue %s. "
1670 "Adding patch to that issue." % cl.GetIssue())
1671 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001672 if options.title:
1673 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001674 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001675 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00001676 if options.reviewers or options.tbr_owners:
1677 change_desc.update_reviewers(options.reviewers,
1678 options.tbr_owners,
1679 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001680 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001681 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001682
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001683 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001684 print "Description is empty; aborting."
1685 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001686
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001687 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001688 if change_desc.get_reviewers():
1689 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001690 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001691 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001692 DieWithError("Must specify reviewers to send email.")
1693 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001694
1695 # We check this before applying rietveld.private assuming that in
1696 # rietveld.cc only addresses which we can send private CLs to are listed
1697 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1698 # --private is specified explicitly on the command line.
1699 if options.private:
1700 logging.warn('rietveld.cc is ignored since private flag is specified. '
1701 'You need to review and add them manually if necessary.')
1702 cc = cl.GetCCListWithoutDefault()
1703 else:
1704 cc = cl.GetCCList()
1705 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001706 if cc:
1707 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001708
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001709 if options.private or settings.GetDefaultPrivateFlag() == "True":
1710 upload_args.append('--private')
1711
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001712 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001713 if not options.find_copies:
1714 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001715
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001716 # Include the upstream repo's URL in the change -- this is useful for
1717 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001718 remote_url = cl.GetGitBaseUrlFromConfig()
1719 if not remote_url:
1720 if settings.GetIsGitSvn():
1721 # URL is dependent on the current directory.
1722 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1723 if data:
1724 keys = dict(line.split(': ', 1) for line in data.splitlines()
1725 if ': ' in line)
1726 remote_url = keys.get('URL', None)
1727 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00001728 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1729 remote_url = (cl.GetRemoteUrl() + '@'
1730 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731 if remote_url:
1732 upload_args.extend(['--base_url', remote_url])
1733
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001734 project = settings.GetProject()
1735 if project:
1736 upload_args.extend(['--project', project])
1737
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001739 upload_args = ['upload'] + upload_args + args
1740 logging.info('upload.RealMain(%s)', upload_args)
1741 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001742 issue = int(issue)
1743 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001744 except KeyboardInterrupt:
1745 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001746 except:
1747 # If we got an exception after the user typed a description for their
1748 # change, back up the description before re-raising.
1749 if change_desc:
1750 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1751 print '\nGot exception while uploading -- saving description to %s\n' \
1752 % backup_path
1753 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001754 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755 backup_file.close()
1756 raise
1757
1758 if not cl.GetIssue():
1759 cl.SetIssue(issue)
1760 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001761
1762 if options.use_commit_queue:
1763 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001764 return 0
1765
1766
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001767def cleanup_list(l):
1768 """Fixes a list so that comma separated items are put as individual items.
1769
1770 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1771 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1772 """
1773 items = sum((i.split(',') for i in l), [])
1774 stripped_items = (i.strip() for i in items)
1775 return sorted(filter(None, stripped_items))
1776
1777
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001778@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001779def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001780 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001781 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1782 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001783 parser.add_option('--bypass-watchlists', action='store_true',
1784 dest='bypass_watchlists',
1785 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001786 parser.add_option('-f', action='store_true', dest='force',
1787 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001788 parser.add_option('-m', dest='message', help='message for patchset')
1789 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001790 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001791 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001792 help='reviewer email addresses')
1793 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001794 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001795 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001796 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001797 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001798 parser.add_option('--emulate_svn_auto_props',
1799 '--emulate-svn-auto-props',
1800 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001801 dest="emulate_svn_auto_props",
1802 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001803 parser.add_option('-c', '--use-commit-queue', action='store_true',
1804 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001805 parser.add_option('--private', action='store_true',
1806 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001807 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001808 '--target-branch',
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001809 help='When uploading to gerrit, remote branch to '
1810 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001811 parser.add_option('--email', default=None,
1812 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00001813 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
1814 help='add a set of OWNERS to TBR')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001815
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001816 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001817 (options, args) = parser.parse_args(args)
1818
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001819 if options.target_branch and not settings.GetIsGerrit():
1820 parser.error('Use --target_branch for non gerrit repository.')
1821
ukai@chromium.org259e4682012-10-25 07:36:33 +00001822 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001823 return 1
1824
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001825 options.reviewers = cleanup_list(options.reviewers)
1826 options.cc = cleanup_list(options.cc)
1827
ukai@chromium.orge8077812012-02-03 03:41:46 +00001828 cl = Changelist()
1829 if args:
1830 # TODO(ukai): is it ok for gerrit case?
1831 base_branch = args[0]
1832 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001833 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001834 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001835 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001836
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001837 # Apply watchlists on upload.
1838 change = cl.GetChange(base_branch, None)
1839 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1840 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001841 if not options.bypass_watchlists:
1842 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001843
ukai@chromium.orge8077812012-02-03 03:41:46 +00001844 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00001845 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001846 # Set the reviewer list now so that presubmit checks can access it.
1847 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00001848 change_description.update_reviewers(options.reviewers,
1849 options.tbr_owners,
1850 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001851 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001852 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001853 may_prompt=not options.force,
1854 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001855 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001856 if not hook_results.should_continue():
1857 return 1
1858 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001859 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001860
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001861 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001862 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001863 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001864 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001865 print ('The last upload made from this repository was patchset #%d but '
1866 'the most recent patchset on the server is #%d.'
1867 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001868 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1869 'from another machine or branch the patch you\'re uploading now '
1870 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001871 ask_for_data('About to upload; enter to confirm.')
1872
iannucci@chromium.org79540052012-10-19 23:15:26 +00001873 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001874 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00001875 return GerritUpload(options, args, cl, change)
1876 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001877 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001878 git_set_branch_value('last-upload-hash',
1879 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001880
1881 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001882
1883
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001884def IsSubmoduleMergeCommit(ref):
1885 # When submodules are added to the repo, we expect there to be a single
1886 # non-git-svn merge commit at remote HEAD with a signature comment.
1887 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001888 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001889 return RunGit(cmd) != ''
1890
1891
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001892def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001893 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001894
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001895 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001896 Updates changelog with metadata (e.g. pointer to review).
1897 Pushes/dcommits the code upstream.
1898 Updates review and closes.
1899 """
1900 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1901 help='bypass upload presubmit hook')
1902 parser.add_option('-m', dest='message',
1903 help="override review description")
1904 parser.add_option('-f', action='store_true', dest='force',
1905 help="force yes to questions (don't prompt)")
1906 parser.add_option('-c', dest='contributor',
1907 help="external contributor for patch (appended to " +
1908 "description and used as author for git). Should be " +
1909 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001910 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911 (options, args) = parser.parse_args(args)
1912 cl = Changelist()
1913
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001914 current = cl.GetBranch()
1915 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1916 if not settings.GetIsGitSvn() and remote == '.':
1917 print
1918 print 'Attempting to push branch %r into another local branch!' % current
1919 print
1920 print 'Either reparent this branch on top of origin/master:'
1921 print ' git reparent-branch --root'
1922 print
1923 print 'OR run `git rebase-update` if you think the parent branch is already'
1924 print 'committed.'
1925 print
1926 print ' Current parent: %r' % upstream_branch
1927 return 1
1928
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001929 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001930 # Default to merging against our best guess of the upstream branch.
1931 args = [cl.GetUpstreamBranch()]
1932
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001933 if options.contributor:
1934 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1935 print "Please provide contibutor as 'First Last <email@example.com>'"
1936 return 1
1937
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001939 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001940
ukai@chromium.org259e4682012-10-25 07:36:33 +00001941 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942 return 1
1943
1944 # This rev-list syntax means "show all commits not in my branch that
1945 # are in base_branch".
1946 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1947 base_branch]).splitlines()
1948 if upstream_commits:
1949 print ('Base branch "%s" has %d commits '
1950 'not in this branch.' % (base_branch, len(upstream_commits)))
1951 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1952 return 1
1953
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001954 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001955 svn_head = None
1956 if cmd == 'dcommit' or base_has_submodules:
1957 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1958 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001959
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001960 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001961 # If the base_head is a submodule merge commit, the first parent of the
1962 # base_head should be a git-svn commit, which is what we're interested in.
1963 base_svn_head = base_branch
1964 if base_has_submodules:
1965 base_svn_head += '^1'
1966
1967 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968 if extra_commits:
1969 print ('This branch has %d additional commits not upstreamed yet.'
1970 % len(extra_commits.splitlines()))
1971 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1972 'before attempting to %s.' % (base_branch, cmd))
1973 return 1
1974
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001975 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001976 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001977 author = None
1978 if options.contributor:
1979 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001980 hook_results = cl.RunHook(
1981 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001982 may_prompt=not options.force,
1983 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001984 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001985 if not hook_results.should_continue():
1986 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001988 # Check the tree status if the tree status URL is set.
1989 status = GetTreeStatus()
1990 if 'closed' == status:
1991 print('The tree is closed. Please wait for it to reopen. Use '
1992 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1993 return 1
1994 elif 'unknown' == status:
1995 print('Unable to determine tree status. Please verify manually and '
1996 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1997 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00001998 else:
1999 breakpad.SendStack(
2000 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002001 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
2002 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00002003 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002004
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002005 change_desc = ChangeDescription(options.message)
2006 if not change_desc.description and cl.GetIssue():
2007 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002008
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002009 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00002010 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002011 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00002012 else:
2013 print 'No description set.'
2014 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
2015 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002017 # Keep a separate copy for the commit message, because the commit message
2018 # contains the link to the Rietveld issue, while the Rietveld message contains
2019 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002020 # Keep a separate copy for the commit message.
2021 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002022 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002023
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002024 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002025 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002026 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002027 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002028 commit_desc.append_footer('Patch from %s.' % options.contributor)
2029
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002030 print('Description:')
2031 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002033 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002034 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002035 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002036
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002037 # We want to squash all this branch's commits into one commit with the proper
2038 # description. We do this by doing a "reset --soft" to the base branch (which
2039 # keeps the working copy the same), then dcommitting that. If origin/master
2040 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2041 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002042 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002043 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2044 # Delete the branches if they exist.
2045 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2046 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2047 result = RunGitWithCode(showref_cmd)
2048 if result[0] == 0:
2049 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002050
2051 # We might be in a directory that's present in this branch but not in the
2052 # trunk. Move up to the top of the tree so that git commands that expect a
2053 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002054 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002055 if rel_base_path:
2056 os.chdir(rel_base_path)
2057
2058 # Stuff our change into the merge branch.
2059 # We wrap in a try...finally block so if anything goes wrong,
2060 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002061 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002062 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002063 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002064 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002066 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002067 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002069 RunGit(
2070 [
2071 'commit', '--author', options.contributor,
2072 '-m', commit_desc.description,
2073 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002074 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002075 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002076 if base_has_submodules:
2077 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2078 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2079 RunGit(['checkout', CHERRY_PICK_BRANCH])
2080 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002081 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002082 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002083 pending_prefix = settings.GetPendingRefPrefix()
2084 if not pending_prefix or branch.startswith(pending_prefix):
2085 # If not using refs/pending/heads/* at all, or target ref is already set
2086 # to pending, then push to the target ref directly.
2087 retcode, output = RunGitWithCode(
2088 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002089 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002090 else:
2091 # Cherry-pick the change on top of pending ref and then push it.
2092 assert branch.startswith('refs/'), branch
2093 assert pending_prefix[-1] == '/', pending_prefix
2094 pending_ref = pending_prefix + branch[len('refs/'):]
2095 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002096 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002097 if retcode == 0:
2098 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002099 else:
2100 # dcommit the merge branch.
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002101 _, output = RunGitWithCode(['svn', 'dcommit',
2102 '-C%s' % options.similarity,
2103 '--no-rebase', '--rmdir'])
2104 if 'Committed r' in output:
2105 revision = re.match(
2106 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2107 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002108 finally:
2109 # And then swap back to the original branch and clean up.
2110 RunGit(['checkout', '-q', cl.GetBranch()])
2111 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002112 if base_has_submodules:
2113 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002114
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002115 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002116 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002117 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002118
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002119 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002120 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002121 try:
2122 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2123 # We set pushed_to_pending to False, since it made it all the way to the
2124 # real ref.
2125 pushed_to_pending = False
2126 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002127 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002128
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002129 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002130 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002131 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002132 if not to_pending:
2133 if viewvc_url and revision:
2134 change_desc.append_footer(
2135 'Committed: %s%s' % (viewvc_url, revision))
2136 elif revision:
2137 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138 print ('Closing issue '
2139 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002140 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002141 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002142 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002143 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002144 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002145 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002146 if options.bypass_hooks:
2147 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2148 else:
2149 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002150 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002151 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002152
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002153 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002154 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2155 print 'The commit is in the pending queue (%s).' % pending_ref
2156 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002157 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002158 'footer.' % branch)
2159
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002160 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2161 if os.path.isfile(hook):
2162 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002163
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002164 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002165
2166
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002167def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2168 print
2169 print 'Waiting for commit to be landed on %s...' % real_ref
2170 print '(If you are impatient, you may Ctrl-C once without harm)'
2171 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2172 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2173
2174 loop = 0
2175 while True:
2176 sys.stdout.write('fetching (%d)... \r' % loop)
2177 sys.stdout.flush()
2178 loop += 1
2179
2180 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2181 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2182 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2183 for commit in commits.splitlines():
2184 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2185 print 'Found commit on %s' % real_ref
2186 return commit
2187
2188 current_rev = to_rev
2189
2190
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002191def PushToGitPending(remote, pending_ref, upstream_ref):
2192 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2193
2194 Returns:
2195 (retcode of last operation, output log of last operation).
2196 """
2197 assert pending_ref.startswith('refs/'), pending_ref
2198 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2199 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2200 code = 0
2201 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002202 max_attempts = 3
2203 attempts_left = max_attempts
2204 while attempts_left:
2205 if attempts_left != max_attempts:
2206 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2207 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002208
2209 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002210 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002211 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002212 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002213 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002214 print 'Fetch failed with exit code %d.' % code
2215 if out.strip():
2216 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002217 continue
2218
2219 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002220 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002221 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002222 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002223 if code:
2224 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002225 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2226 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002227 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2228 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002229 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002230 return code, out
2231
2232 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002233 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002234 code, out = RunGitWithCode(
2235 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2236 if code == 0:
2237 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002238 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002239 return code, out
2240
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002241 print 'Push failed with exit code %d.' % code
2242 if out.strip():
2243 print out.strip()
2244 if IsFatalPushFailure(out):
2245 print (
2246 'Fatal push error. Make sure your .netrc credentials and git '
2247 'user.email are correct and you have push access to the repo.')
2248 return code, out
2249
2250 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002251 return code, out
2252
2253
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002254def IsFatalPushFailure(push_stdout):
2255 """True if retrying push won't help."""
2256 return '(prohibited by Gerrit)' in push_stdout
2257
2258
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002259@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002260def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002261 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002262 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002263 message = """This doesn't appear to be an SVN repository.
2264If your project has a git mirror with an upstream SVN master, you probably need
2265to run 'git svn init', see your project's git mirror documentation.
2266If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002267to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002268Choose wisely, if you get this wrong, your commit might appear to succeed but
2269will instead be silently ignored."""
2270 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002271 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002272 return SendUpstream(parser, args, 'dcommit')
2273
2274
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002275@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002276def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002277 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002278 if settings.GetIsGitSvn():
2279 print('This appears to be an SVN repository.')
2280 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002281 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002282 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002283
2284
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002285@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002286def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002287 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002288 parser.add_option('-b', dest='newbranch',
2289 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002290 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002291 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002292 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2293 help='Change to the directory DIR immediately, '
2294 'before doing anything else.')
2295 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002296 help='failed patches spew .rej files rather than '
2297 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002298 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2299 help="don't commit after patch applies")
2300 (options, args) = parser.parse_args(args)
2301 if len(args) != 1:
2302 parser.print_help()
2303 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002304 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002305
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002306 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002307 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002308
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002309 if options.newbranch:
2310 if options.force:
2311 RunGit(['branch', '-D', options.newbranch],
2312 stderr=subprocess2.PIPE, error_ok=True)
2313 RunGit(['checkout', '-b', options.newbranch,
2314 Changelist().GetUpstreamBranch()])
2315
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002316 return PatchIssue(issue_arg, options.reject, options.nocommit,
2317 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002318
2319
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002320def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002321 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002322 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002323 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002324 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002325 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002326 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002327 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002328 # Assume it's a URL to the patch. Default to https.
2329 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002330 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002331 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002332 DieWithError('Must pass an issue ID or full URL for '
2333 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002334 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002335 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002336 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002337
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002338 # Switch up to the top-level directory, if necessary, in preparation for
2339 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002340 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002341 if top:
2342 os.chdir(top)
2343
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002344 # Git patches have a/ at the beginning of source paths. We strip that out
2345 # with a sed script rather than the -p flag to patch so we can feed either
2346 # Git or svn-style patches into the same apply command.
2347 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002348 try:
2349 patch_data = subprocess2.check_output(
2350 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2351 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002352 DieWithError('Git patch mungling failed.')
2353 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002355 # We use "git apply" to apply the patch instead of "patch" so that we can
2356 # pick up file adds.
2357 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002358 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002359 if directory:
2360 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002361 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002362 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002363 elif IsGitVersionAtLeast('1.7.12'):
2364 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002365 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002366 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002367 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002368 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002369 DieWithError('Failed to apply the patch')
2370
2371 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002372 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002373 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2374 cl = Changelist()
2375 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002376 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002377 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002378 else:
2379 print "Patch applied to index."
2380 return 0
2381
2382
2383def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002384 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002385 # Provide a wrapper for git svn rebase to help avoid accidental
2386 # git svn dcommit.
2387 # It's the only command that doesn't use parser at all since we just defer
2388 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002389
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002390 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002391
2392
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002393def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002394 """Fetches the tree status and returns either 'open', 'closed',
2395 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002396 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002397 if url:
2398 status = urllib2.urlopen(url).read().lower()
2399 if status.find('closed') != -1 or status == '0':
2400 return 'closed'
2401 elif status.find('open') != -1 or status == '1':
2402 return 'open'
2403 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002404 return 'unset'
2405
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002406
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002407def GetTreeStatusReason():
2408 """Fetches the tree status from a json url and returns the message
2409 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002410 url = settings.GetTreeStatusUrl()
2411 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002412 connection = urllib2.urlopen(json_url)
2413 status = json.loads(connection.read())
2414 connection.close()
2415 return status['message']
2416
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002417
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002418def GetBuilderMaster(bot_list):
2419 """For a given builder, fetch the master from AE if available."""
2420 map_url = 'https://builders-map.appspot.com/'
2421 try:
2422 master_map = json.load(urllib2.urlopen(map_url))
2423 except urllib2.URLError as e:
2424 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2425 (map_url, e))
2426 except ValueError as e:
2427 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2428 if not master_map:
2429 return None, 'Failed to build master map.'
2430
2431 result_master = ''
2432 for bot in bot_list:
2433 builder = bot.split(':', 1)[0]
2434 master_list = master_map.get(builder, [])
2435 if not master_list:
2436 return None, ('No matching master for builder %s.' % builder)
2437 elif len(master_list) > 1:
2438 return None, ('The builder name %s exists in multiple masters %s.' %
2439 (builder, master_list))
2440 else:
2441 cur_master = master_list[0]
2442 if not result_master:
2443 result_master = cur_master
2444 elif result_master != cur_master:
2445 return None, 'The builders do not belong to the same master.'
2446 return result_master, None
2447
2448
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002449def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002450 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002451 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002452 status = GetTreeStatus()
2453 if 'unset' == status:
2454 print 'You must configure your tree status URL by running "git cl config".'
2455 return 2
2456
2457 print "The tree is %s" % status
2458 print
2459 print GetTreeStatusReason()
2460 if status != 'open':
2461 return 1
2462 return 0
2463
2464
maruel@chromium.org15192402012-09-06 12:38:29 +00002465def CMDtry(parser, args):
2466 """Triggers a try job through Rietveld."""
2467 group = optparse.OptionGroup(parser, "Try job options")
2468 group.add_option(
2469 "-b", "--bot", action="append",
2470 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2471 "times to specify multiple builders. ex: "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002472 "'-b win_rel:ui_tests,webkit_unit_tests -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002473 "the try server waterfall for the builders name and the tests "
2474 "available. Can also be used to specify gtest_filter, e.g. "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002475 "-b win_rel:base_unittests:ValuesTest.*Value"))
maruel@chromium.org15192402012-09-06 12:38:29 +00002476 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002477 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002478 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002479 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002480 "-r", "--revision",
2481 help="Revision to use for the try job; default: the "
2482 "revision will be determined by the try server; see "
2483 "its waterfall for more info")
2484 group.add_option(
2485 "-c", "--clobber", action="store_true", default=False,
2486 help="Force a clobber before building; e.g. don't do an "
2487 "incremental build")
2488 group.add_option(
2489 "--project",
2490 help="Override which project to use. Projects are defined "
2491 "server-side to define what default bot set to use")
2492 group.add_option(
2493 "-t", "--testfilter", action="append", default=[],
2494 help=("Apply a testfilter to all the selected builders. Unless the "
2495 "builders configurations are similar, use multiple "
2496 "--bot <builder>:<test> arguments."))
2497 group.add_option(
2498 "-n", "--name", help="Try job name; default to current branch name")
2499 parser.add_option_group(group)
2500 options, args = parser.parse_args(args)
2501
2502 if args:
2503 parser.error('Unknown arguments: %s' % args)
2504
2505 cl = Changelist()
2506 if not cl.GetIssue():
2507 parser.error('Need to upload first')
2508
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002509 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002510 if props.get('closed'):
2511 parser.error('Cannot send tryjobs for a closed CL')
2512
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002513 if props.get('private'):
2514 parser.error('Cannot use trybots with private issue')
2515
maruel@chromium.org15192402012-09-06 12:38:29 +00002516 if not options.name:
2517 options.name = cl.GetBranch()
2518
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002519 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002520 options.master, err_msg = GetBuilderMaster(options.bot)
2521 if err_msg:
2522 parser.error('Tryserver master cannot be found because: %s\n'
2523 'Please manually specify the tryserver master'
2524 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002525
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002526 def GetMasterMap():
2527 # Process --bot and --testfilter.
2528 if not options.bot:
2529 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002530
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002531 # Get try masters from PRESUBMIT.py files.
2532 masters = presubmit_support.DoGetTryMasters(
2533 change,
2534 change.LocalPaths(),
2535 settings.GetRoot(),
2536 None,
2537 None,
2538 options.verbose,
2539 sys.stdout)
2540 if masters:
2541 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002542
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002543 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2544 options.bot = presubmit_support.DoGetTrySlaves(
2545 change,
2546 change.LocalPaths(),
2547 settings.GetRoot(),
2548 None,
2549 None,
2550 options.verbose,
2551 sys.stdout)
2552 if not options.bot:
2553 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002554
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002555 builders_and_tests = {}
2556 # TODO(machenbach): The old style command-line options don't support
2557 # multiple try masters yet.
2558 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2559 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2560
2561 for bot in old_style:
2562 if ':' in bot:
2563 builder, tests = bot.split(':', 1)
2564 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2565 elif ',' in bot:
2566 parser.error('Specify one bot per --bot flag')
2567 else:
2568 builders_and_tests.setdefault(bot, []).append('defaulttests')
2569
2570 for bot, tests in new_style:
2571 builders_and_tests.setdefault(bot, []).extend(tests)
2572
2573 # Return a master map with one master to be backwards compatible. The
2574 # master name defaults to an empty string, which will cause the master
2575 # not to be set on rietveld (deprecated).
2576 return {options.master: builders_and_tests}
2577
2578 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002579
maruel@chromium.org15192402012-09-06 12:38:29 +00002580 if options.testfilter:
2581 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002582 masters = dict((master, dict(
2583 (b, forced_tests) for b, t in slaves.iteritems()
2584 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002585
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002586 for builders in masters.itervalues():
2587 if any('triggered' in b for b in builders):
2588 print >> sys.stderr, (
2589 'ERROR You are trying to send a job to a triggered bot. This type of'
2590 ' bot requires an\ninitial job from a parent (usually a builder). '
2591 'Instead send your job to the parent.\n'
2592 'Bot list: %s' % builders)
2593 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002594
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002595 patchset = cl.GetMostRecentPatchset()
2596 if patchset and patchset != cl.GetPatchset():
2597 print(
2598 '\nWARNING Mismatch between local config and server. Did a previous '
2599 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2600 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002601 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002602 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002603 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002604 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002605 except urllib2.HTTPError, e:
2606 if e.code == 404:
2607 print('404 from rietveld; '
2608 'did you mean to use "git try" instead of "git cl try"?')
2609 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002610 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002611
2612 for (master, builders) in masters.iteritems():
2613 if master:
2614 print 'Master: %s' % master
2615 length = max(len(builder) for builder in builders)
2616 for builder in sorted(builders):
2617 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002618 return 0
2619
2620
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002621@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002623 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002624 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002625 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002626 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002627
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002628 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002629 if args:
2630 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002631 branch = cl.GetBranch()
2632 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002633 cl = Changelist()
2634 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002635
2636 # Clear configured merge-base, if there is one.
2637 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002638 else:
2639 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002640 return 0
2641
2642
thestig@chromium.org00858c82013-12-02 23:08:03 +00002643def CMDweb(parser, args):
2644 """Opens the current CL in the web browser."""
2645 _, args = parser.parse_args(args)
2646 if args:
2647 parser.error('Unrecognized args: %s' % ' '.join(args))
2648
2649 issue_url = Changelist().GetIssueURL()
2650 if not issue_url:
2651 print >> sys.stderr, 'ERROR No issue to open'
2652 return 1
2653
2654 webbrowser.open(issue_url)
2655 return 0
2656
2657
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002658def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002659 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002660 _, args = parser.parse_args(args)
2661 if args:
2662 parser.error('Unrecognized args: %s' % ' '.join(args))
2663 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002664 props = cl.GetIssueProperties()
2665 if props.get('private'):
2666 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002667 cl.SetFlag('commit', '1')
2668 return 0
2669
2670
groby@chromium.org411034a2013-02-26 15:12:01 +00002671def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002672 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002673 _, args = parser.parse_args(args)
2674 if args:
2675 parser.error('Unrecognized args: %s' % ' '.join(args))
2676 cl = Changelist()
2677 # Ensure there actually is an issue to close.
2678 cl.GetDescription()
2679 cl.CloseIssue()
2680 return 0
2681
2682
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002683def CMDdiff(parser, args):
2684 """shows differences between local tree and last upload."""
2685 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002686 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002687 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002688 if not issue:
2689 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002690 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002691 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002692
2693 # Create a new branch based on the merge-base
2694 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2695 try:
2696 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002697 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002698 if rtn != 0:
2699 return rtn
2700
2701 # Switch back to starting brand and diff against the temporary
2702 # branch containing the latest rietveld patch.
2703 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2704 finally:
2705 RunGit(['checkout', '-q', branch])
2706 RunGit(['branch', '-D', TMP_BRANCH])
2707
2708 return 0
2709
2710
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002711def CMDowners(parser, args):
2712 """interactively find the owners for reviewing"""
2713 parser.add_option(
2714 '--no-color',
2715 action='store_true',
2716 help='Use this option to disable color output')
2717 options, args = parser.parse_args(args)
2718
2719 author = RunGit(['config', 'user.email']).strip() or None
2720
2721 cl = Changelist()
2722
2723 if args:
2724 if len(args) > 1:
2725 parser.error('Unknown args')
2726 base_branch = args[0]
2727 else:
2728 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002729 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002730
2731 change = cl.GetChange(base_branch, None)
2732 return owners_finder.OwnersFinder(
2733 [f.LocalPath() for f in
2734 cl.GetChange(base_branch, None).AffectedFiles()],
2735 change.RepositoryRoot(), author,
2736 fopen=file, os_path=os.path, glob=glob.glob,
2737 disable_color=options.no_color).run()
2738
2739
enne@chromium.org555cfe42014-01-29 18:21:39 +00002740@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002741def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002742 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002743 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002744 parser.add_option('--full', action='store_true',
2745 help='Reformat the full content of all touched files')
2746 parser.add_option('--dry-run', action='store_true',
2747 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002748 parser.add_option('--diff', action='store_true',
2749 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002750 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002751
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002752 # git diff generates paths against the root of the repository. Change
2753 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002754 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002755 if rel_base_path:
2756 os.chdir(rel_base_path)
2757
digit@chromium.org29e47272013-05-17 17:01:46 +00002758 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002759 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002760 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002761 # Only list the names of modified files.
2762 diff_cmd.append('--name-only')
2763 else:
2764 # Only generate context-less patches.
2765 diff_cmd.append('-U0')
2766
2767 # Grab the merge-base commit, i.e. the upstream commit of the current
2768 # branch when it was created or the last time it was rebased. This is
2769 # to cover the case where the user may have called "git fetch origin",
2770 # moving the origin branch to a newer commit, but hasn't rebased yet.
2771 upstream_commit = None
2772 cl = Changelist()
2773 upstream_branch = cl.GetUpstreamBranch()
2774 if upstream_branch:
2775 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2776 upstream_commit = upstream_commit.strip()
2777
2778 if not upstream_commit:
2779 DieWithError('Could not find base commit for this branch. '
2780 'Are you in detached state?')
2781
2782 diff_cmd.append(upstream_commit)
2783
2784 # Handle source file filtering.
2785 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002786 if args:
2787 for arg in args:
2788 if os.path.isdir(arg):
2789 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2790 elif os.path.isfile(arg):
2791 diff_cmd.append(arg)
2792 else:
2793 DieWithError('Argument "%s" is not a file or a directory' % arg)
2794 else:
2795 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002796 diff_output = RunGit(diff_cmd)
2797
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002798 top_dir = os.path.normpath(
2799 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2800
2801 # Locate the clang-format binary in the checkout
2802 try:
2803 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2804 except clang_format.NotFoundError, e:
2805 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002806
digit@chromium.org29e47272013-05-17 17:01:46 +00002807 if opts.full:
2808 # diff_output is a list of files to send to clang-format.
2809 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002810 if not files:
2811 print "Nothing to format."
2812 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002813 cmd = [clang_format_tool]
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')
2816 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002817 if opts.diff:
2818 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002819 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002820 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00002821 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00002822 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002823 try:
2824 script = clang_format.FindClangFormatScriptInChromiumTree(
2825 'clang-format-diff.py')
2826 except clang_format.NotFoundError, e:
2827 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002828
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002829 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002830 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002831 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002832
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002833 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002834 if opts.diff:
2835 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002836 if opts.dry_run and len(stdout) > 0:
2837 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002838
2839 return 0
2840
2841
maruel@chromium.org29404b52014-09-08 22:58:00 +00002842def CMDlol(parser, args):
2843 # This command is intentionally undocumented.
thakis@chromium.org3421c992014-11-02 02:20:32 +00002844 print zlib.decompress(base64.b64decode(
2845 'eNptkLEOwyAMRHe+wupCIqW57v0Vq84WqWtXyrcXnCBsmgMJ+/SSAxMZgRB6NzE'
2846 'E2ObgCKJooYdu4uAQVffUEoE1sRQLxAcqzd7uK2gmStrll1ucV3uZyaY5sXyDd9'
2847 'JAnN+lAXsOMJ90GANAi43mq5/VeeacylKVgi8o6F1SC63FxnagHfJUTfUYdCR/W'
2848 'Ofe+0dHL7PicpytKP750Fh1q2qnLVof4w8OZWNY'))
maruel@chromium.org29404b52014-09-08 22:58:00 +00002849 return 0
2850
2851
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002852class OptionParser(optparse.OptionParser):
2853 """Creates the option parse and add --verbose support."""
2854 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002855 optparse.OptionParser.__init__(
2856 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002857 self.add_option(
2858 '-v', '--verbose', action='count', default=0,
2859 help='Use 2 times for more debugging info')
2860
2861 def parse_args(self, args=None, values=None):
2862 options, args = optparse.OptionParser.parse_args(self, args, values)
2863 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2864 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2865 return options, args
2866
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002867
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002868def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002869 if sys.hexversion < 0x02060000:
2870 print >> sys.stderr, (
2871 '\nYour python version %s is unsupported, please upgrade.\n' %
2872 sys.version.split(' ', 1)[0])
2873 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002874
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002875 # Reload settings.
2876 global settings
2877 settings = Settings()
2878
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002879 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002880 dispatcher = subcommand.CommandDispatcher(__name__)
2881 try:
2882 return dispatcher.execute(OptionParser(), argv)
2883 except urllib2.HTTPError, e:
2884 if e.code != 500:
2885 raise
2886 DieWithError(
2887 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2888 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002889
2890
2891if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002892 # These affect sys.stdout so do it outside of main() to simplify mocks in
2893 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002894 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002895 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002896 sys.exit(main(sys.argv[1:]))