blob: 573245f14fe2bd026db58c8b1a893f93d5de616d [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000035import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000036import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000037import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000038import git_common
39import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000041import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000043import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000044import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import watchlists
46
maruel@chromium.org0633fb42013-08-16 20:06:14 +000047__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000048
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000049DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000050POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000052GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000053CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054
thestig@chromium.org44202a22014-03-11 19:22:18 +000055# Valid extensions for files we want to lint.
56DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
57DEFAULT_LINT_IGNORE_REGEX = r"$^"
58
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000059# Shortcut since it quickly becomes redundant.
60Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000061
maruel@chromium.orgddd59412011-11-30 14:20:38 +000062# Initialized in main()
63settings = None
64
65
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000066def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000067 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068 sys.exit(1)
69
70
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000071def GetNoGitPagerEnv():
72 env = os.environ.copy()
73 # 'cat' is a magical git string that disables pagers on all platforms.
74 env['GIT_PAGER'] = 'cat'
75 return env
76
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000077
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000078def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000080 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000081 except subprocess2.CalledProcessError as e:
82 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000083 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000084 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000085 'Command "%s" failed.\n%s' % (
86 ' '.join(args), error_message or e.stdout or ''))
87 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000088
89
90def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000091 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000092 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000093
94
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000095def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000096 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000097 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000098 if suppress_stderr:
99 stderr = subprocess2.VOID
100 else:
101 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000102 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000103 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000104 stdout=subprocess2.PIPE,
105 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000106 return code, out[0]
107 except ValueError:
108 # When the subprocess fails, it returns None. That triggers a ValueError
109 # when trying to unpack the return value into (out, code).
110 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000111
112
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000113def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000114 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000115 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000116 return (version.startswith(prefix) and
117 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000118
119
maruel@chromium.org90541732011-04-01 17:54:18 +0000120def ask_for_data(prompt):
121 try:
122 return raw_input(prompt)
123 except KeyboardInterrupt:
124 # Hide the exception.
125 sys.exit(1)
126
127
iannucci@chromium.org79540052012-10-19 23:15:26 +0000128def git_set_branch_value(key, value):
129 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000130 if not branch:
131 return
132
133 cmd = ['config']
134 if isinstance(value, int):
135 cmd.append('--int')
136 git_key = 'branch.%s.%s' % (branch, key)
137 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138
139
140def git_get_branch_default(key, default):
141 branch = Changelist().GetBranch()
142 if branch:
143 git_key = 'branch.%s.%s' % (branch, key)
144 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
145 try:
146 return int(stdout.strip())
147 except ValueError:
148 pass
149 return default
150
151
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000152def add_git_similarity(parser):
153 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000154 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000155 help='Sets the percentage that a pair of files need to match in order to'
156 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000157 parser.add_option(
158 '--find-copies', action='store_true',
159 help='Allows git to look for copies.')
160 parser.add_option(
161 '--no-find-copies', action='store_false', dest='find_copies',
162 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000163
164 old_parser_args = parser.parse_args
165 def Parse(args):
166 options, args = old_parser_args(args)
167
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000168 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000169 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000170 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000171 print('Note: Saving similarity of %d%% in git config.'
172 % options.similarity)
173 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000174
iannucci@chromium.org79540052012-10-19 23:15:26 +0000175 options.similarity = max(0, min(options.similarity, 100))
176
177 if options.find_copies is None:
178 options.find_copies = bool(
179 git_get_branch_default('git-find-copies', True))
180 else:
181 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000182
183 print('Using %d%% similarity for rename/copy detection. '
184 'Override with --similarity.' % options.similarity)
185
186 return options, args
187 parser.parse_args = Parse
188
189
ukai@chromium.org259e4682012-10-25 07:36:33 +0000190def is_dirty_git_tree(cmd):
191 # Make sure index is up-to-date before running diff-index.
192 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
193 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
194 if dirty:
195 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
196 print 'Uncommitted files: (git diff-index --name-status HEAD)'
197 print dirty[:4096]
198 if len(dirty) > 4096:
199 print '... (run "git diff-index --name-status HEAD" to see full output).'
200 return True
201 return False
202
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000203
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000204def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
205 """Return the corresponding git ref if |base_url| together with |glob_spec|
206 matches the full |url|.
207
208 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
209 """
210 fetch_suburl, as_ref = glob_spec.split(':')
211 if allow_wildcards:
212 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
213 if glob_match:
214 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
215 # "branches/{472,597,648}/src:refs/remotes/svn/*".
216 branch_re = re.escape(base_url)
217 if glob_match.group(1):
218 branch_re += '/' + re.escape(glob_match.group(1))
219 wildcard = glob_match.group(2)
220 if wildcard == '*':
221 branch_re += '([^/]*)'
222 else:
223 # Escape and replace surrounding braces with parentheses and commas
224 # with pipe symbols.
225 wildcard = re.escape(wildcard)
226 wildcard = re.sub('^\\\\{', '(', wildcard)
227 wildcard = re.sub('\\\\,', '|', wildcard)
228 wildcard = re.sub('\\\\}$', ')', wildcard)
229 branch_re += wildcard
230 if glob_match.group(3):
231 branch_re += re.escape(glob_match.group(3))
232 match = re.match(branch_re, url)
233 if match:
234 return re.sub('\*$', match.group(1), as_ref)
235
236 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
237 if fetch_suburl:
238 full_url = base_url + '/' + fetch_suburl
239 else:
240 full_url = base_url
241 if full_url == url:
242 return as_ref
243 return None
244
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000245
iannucci@chromium.org79540052012-10-19 23:15:26 +0000246def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000247 """Prints statistics about the change to the user."""
248 # --no-ext-diff is broken in some versions of Git, so try to work around
249 # this by overriding the environment (but there is still a problem if the
250 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000251 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000252 if 'GIT_EXTERNAL_DIFF' in env:
253 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000254
255 if find_copies:
256 similarity_options = ['--find-copies-harder', '-l100000',
257 '-C%s' % similarity]
258 else:
259 similarity_options = ['-M%s' % similarity]
260
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000261 try:
262 stdout = sys.stdout.fileno()
263 except AttributeError:
264 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000265 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000266 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000267 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000268 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000269
270
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000271class Settings(object):
272 def __init__(self):
273 self.default_server = None
274 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000275 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000276 self.is_git_svn = None
277 self.svn_branch = None
278 self.tree_status_url = None
279 self.viewvc_url = None
280 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000281 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000282 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000283 self.project = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000284 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000285
286 def LazyUpdateIfNeeded(self):
287 """Updates the settings from a codereview.settings file, if available."""
288 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000289 # The only value that actually changes the behavior is
290 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000291 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000292 error_ok=True
293 ).strip().lower()
294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000296 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000297 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000298 # set updated to True to avoid infinite calling loop
299 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000300 self.updated = True
301 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302 self.updated = True
303
304 def GetDefaultServerUrl(self, error_ok=False):
305 if not self.default_server:
306 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000307 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000308 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000309 if error_ok:
310 return self.default_server
311 if not self.default_server:
312 error_message = ('Could not find settings file. You must configure '
313 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000314 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000315 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316 return self.default_server
317
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000318 @staticmethod
319 def GetRelativeRoot():
320 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000322 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000323 if self.root is None:
324 self.root = os.path.abspath(self.GetRelativeRoot())
325 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326
327 def GetIsGitSvn(self):
328 """Return true if this repo looks like it's using git-svn."""
329 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000330 if self.GetPendingRefPrefix():
331 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
332 self.is_git_svn = False
333 else:
334 # If you have any "svn-remote.*" config keys, we think you're using svn.
335 self.is_git_svn = RunGitWithCode(
336 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000337 return self.is_git_svn
338
339 def GetSVNBranch(self):
340 if self.svn_branch is None:
341 if not self.GetIsGitSvn():
342 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
343
344 # Try to figure out which remote branch we're based on.
345 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000346 # 1) iterate through our branch history and find the svn URL.
347 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000348
349 # regexp matching the git-svn line that contains the URL.
350 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
351
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000352 # We don't want to go through all of history, so read a line from the
353 # pipe at a time.
354 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000355 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000356 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
357 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000358 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000359 for line in proc.stdout:
360 match = git_svn_re.match(line)
361 if match:
362 url = match.group(1)
363 proc.stdout.close() # Cut pipe.
364 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000365
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000366 if url:
367 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
368 remotes = RunGit(['config', '--get-regexp',
369 r'^svn-remote\..*\.url']).splitlines()
370 for remote in remotes:
371 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000372 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000373 remote = match.group(1)
374 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000375 rewrite_root = RunGit(
376 ['config', 'svn-remote.%s.rewriteRoot' % remote],
377 error_ok=True).strip()
378 if rewrite_root:
379 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000380 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000381 ['config', 'svn-remote.%s.fetch' % remote],
382 error_ok=True).strip()
383 if fetch_spec:
384 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
385 if self.svn_branch:
386 break
387 branch_spec = RunGit(
388 ['config', 'svn-remote.%s.branches' % remote],
389 error_ok=True).strip()
390 if branch_spec:
391 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
392 if self.svn_branch:
393 break
394 tag_spec = RunGit(
395 ['config', 'svn-remote.%s.tags' % remote],
396 error_ok=True).strip()
397 if tag_spec:
398 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
399 if self.svn_branch:
400 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000401
402 if not self.svn_branch:
403 DieWithError('Can\'t guess svn branch -- try specifying it on the '
404 'command line')
405
406 return self.svn_branch
407
408 def GetTreeStatusUrl(self, error_ok=False):
409 if not self.tree_status_url:
410 error_message = ('You must configure your tree status URL by running '
411 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000412 self.tree_status_url = self._GetRietveldConfig(
413 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000414 return self.tree_status_url
415
416 def GetViewVCUrl(self):
417 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000418 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000419 return self.viewvc_url
420
rmistry@google.com90752582014-01-14 21:04:50 +0000421 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000422 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000423
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000424 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000425 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000426
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000427 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000428 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000429
ukai@chromium.orge8077812012-02-03 03:41:46 +0000430 def GetIsGerrit(self):
431 """Return true if this repo is assosiated with gerrit code review system."""
432 if self.is_gerrit is None:
433 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
434 return self.is_gerrit
435
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000436 def GetGitEditor(self):
437 """Return the editor specified in the git config, or None if none is."""
438 if self.git_editor is None:
439 self.git_editor = self._GetConfig('core.editor', error_ok=True)
440 return self.git_editor or None
441
thestig@chromium.org44202a22014-03-11 19:22:18 +0000442 def GetLintRegex(self):
443 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
444 DEFAULT_LINT_REGEX)
445
446 def GetLintIgnoreRegex(self):
447 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
448 DEFAULT_LINT_IGNORE_REGEX)
449
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000450 def GetProject(self):
451 if not self.project:
452 self.project = self._GetRietveldConfig('project', error_ok=True)
453 return self.project
454
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000455 def GetPendingRefPrefix(self):
456 if not self.pending_ref_prefix:
457 self.pending_ref_prefix = self._GetRietveldConfig(
458 'pending-ref-prefix', error_ok=True)
459 return self.pending_ref_prefix
460
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000461 def _GetRietveldConfig(self, param, **kwargs):
462 return self._GetConfig('rietveld.' + param, **kwargs)
463
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000464 def _GetConfig(self, param, **kwargs):
465 self.LazyUpdateIfNeeded()
466 return RunGit(['config', param], **kwargs).strip()
467
468
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469def ShortBranchName(branch):
470 """Convert a name like 'refs/heads/foo' to just 'foo'."""
471 return branch.replace('refs/heads/', '')
472
473
474class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000475 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000476 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000477 global settings
478 if not settings:
479 # Happens when git_cl.py is used as a utility library.
480 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000481 settings.GetDefaultServerUrl()
482 self.branchref = branchref
483 if self.branchref:
484 self.branch = ShortBranchName(self.branchref)
485 else:
486 self.branch = None
487 self.rietveld_server = None
488 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000489 self.lookedup_issue = False
490 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 self.has_description = False
492 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000493 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000495 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000496 self.cc = None
497 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000498 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000499 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000500
501 def GetCCList(self):
502 """Return the users cc'd on this CL.
503
504 Return is a string suitable for passing to gcl with the --cc flag.
505 """
506 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000507 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000508 more_cc = ','.join(self.watchers)
509 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
510 return self.cc
511
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000512 def GetCCListWithoutDefault(self):
513 """Return the users cc'd on this CL excluding default ones."""
514 if self.cc is None:
515 self.cc = ','.join(self.watchers)
516 return self.cc
517
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000518 def SetWatchers(self, watchers):
519 """Set the list of email addresses that should be cc'd based on the changed
520 files in this CL.
521 """
522 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000523
524 def GetBranch(self):
525 """Returns the short branch name, e.g. 'master'."""
526 if not self.branch:
527 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
528 self.branch = ShortBranchName(self.branchref)
529 return self.branch
530
531 def GetBranchRef(self):
532 """Returns the full branch name, e.g. 'refs/heads/master'."""
533 self.GetBranch() # Poke the lazy loader.
534 return self.branchref
535
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000536 @staticmethod
537 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000538 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000539 e.g. 'origin', 'refs/heads/master'
540 """
541 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000542 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
543 error_ok=True).strip()
544 if upstream_branch:
545 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
546 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000547 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
548 error_ok=True).strip()
549 if upstream_branch:
550 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000551 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000552 # Fall back on trying a git-svn upstream branch.
553 if settings.GetIsGitSvn():
554 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000555 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000556 # Else, try to guess the origin remote.
557 remote_branches = RunGit(['branch', '-r']).split()
558 if 'origin/master' in remote_branches:
559 # Fall back on origin/master if it exits.
560 remote = 'origin'
561 upstream_branch = 'refs/heads/master'
562 elif 'origin/trunk' in remote_branches:
563 # Fall back on origin/trunk if it exists. Generally a shared
564 # git-svn clone
565 remote = 'origin'
566 upstream_branch = 'refs/heads/trunk'
567 else:
568 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569Either pass complete "git diff"-style arguments, like
570 git cl upload origin/master
571or verify this branch is set up to track another (via the --track argument to
572"git checkout -b ...").""")
573
574 return remote, upstream_branch
575
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000576 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000577 return git_common.get_or_create_merge_base(self.GetBranch(),
578 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000579
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000580 def GetUpstreamBranch(self):
581 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000582 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000584 upstream_branch = upstream_branch.replace('refs/heads/',
585 'refs/remotes/%s/' % remote)
586 upstream_branch = upstream_branch.replace('refs/branch-heads/',
587 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 self.upstream_branch = upstream_branch
589 return self.upstream_branch
590
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000591 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000592 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000593 remote, branch = None, self.GetBranch()
594 seen_branches = set()
595 while branch not in seen_branches:
596 seen_branches.add(branch)
597 remote, branch = self.FetchUpstreamTuple(branch)
598 branch = ShortBranchName(branch)
599 if remote != '.' or branch.startswith('refs/remotes'):
600 break
601 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000602 remotes = RunGit(['remote'], error_ok=True).split()
603 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000604 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000605 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000606 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000607 logging.warning('Could not determine which remote this change is '
608 'associated with, so defaulting to "%s". This may '
609 'not be what you want. You may prevent this message '
610 'by running "git svn info" as documented here: %s',
611 self._remote,
612 GIT_INSTRUCTIONS_URL)
613 else:
614 logging.warn('Could not determine which remote this change is '
615 'associated with. You may prevent this message by '
616 'running "git svn info" as documented here: %s',
617 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000618 branch = 'HEAD'
619 if branch.startswith('refs/remotes'):
620 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000621 elif branch.startswith('refs/branch-heads/'):
622 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000623 else:
624 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000625 return self._remote
626
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000627 def GitSanityChecks(self, upstream_git_obj):
628 """Checks git repo status and ensures diff is from local commits."""
629
630 # Verify the commit we're diffing against is in our current branch.
631 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
632 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
633 if upstream_sha != common_ancestor:
634 print >> sys.stderr, (
635 'ERROR: %s is not in the current branch. You may need to rebase '
636 'your tracking branch' % upstream_sha)
637 return False
638
639 # List the commits inside the diff, and verify they are all local.
640 commits_in_diff = RunGit(
641 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
642 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
643 remote_branch = remote_branch.strip()
644 if code != 0:
645 _, remote_branch = self.GetRemoteBranch()
646
647 commits_in_remote = RunGit(
648 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
649
650 common_commits = set(commits_in_diff) & set(commits_in_remote)
651 if common_commits:
652 print >> sys.stderr, (
653 'ERROR: Your diff contains %d commits already in %s.\n'
654 'Run "git log --oneline %s..HEAD" to get a list of commits in '
655 'the diff. If you are using a custom git flow, you can override'
656 ' the reference used for this check with "git config '
657 'gitcl.remotebranch <git-ref>".' % (
658 len(common_commits), remote_branch, upstream_git_obj))
659 return False
660 return True
661
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000662 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000663 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000664
665 Returns None if it is not set.
666 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000667 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
668 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000669
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000670 def GetRemoteUrl(self):
671 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
672
673 Returns None if there is no remote.
674 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000675 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000676 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
677
678 # If URL is pointing to a local directory, it is probably a git cache.
679 if os.path.isdir(url):
680 url = RunGit(['config', 'remote.%s.url' % remote],
681 error_ok=True,
682 cwd=url).strip()
683 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684
685 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000686 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000687 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000688 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000689 self.issue = int(issue) or None if issue else None
690 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691 return self.issue
692
693 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000694 if not self.rietveld_server:
695 # If we're on a branch then get the server potentially associated
696 # with that branch.
697 if self.GetIssue():
698 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
699 ['config', self._RietveldServer()], error_ok=True).strip())
700 if not self.rietveld_server:
701 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702 return self.rietveld_server
703
704 def GetIssueURL(self):
705 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000706 if not self.GetIssue():
707 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000708 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
709
710 def GetDescription(self, pretty=False):
711 if not self.has_description:
712 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000713 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000714 try:
715 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000716 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000717 if e.code == 404:
718 DieWithError(
719 ('\nWhile fetching the description for issue %d, received a '
720 '404 (not found)\n'
721 'error. It is likely that you deleted this '
722 'issue on the server. If this is the\n'
723 'case, please run\n\n'
724 ' git cl issue 0\n\n'
725 'to clear the association with the deleted issue. Then run '
726 'this command again.') % issue)
727 else:
728 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000729 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000730 except urllib2.URLError as e:
731 print >> sys.stderr, (
732 'Warning: Failed to retrieve CL description due to network '
733 'failure.')
734 self.description = ''
735
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000736 self.has_description = True
737 if pretty:
738 wrapper = textwrap.TextWrapper()
739 wrapper.initial_indent = wrapper.subsequent_indent = ' '
740 return wrapper.fill(self.description)
741 return self.description
742
743 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000744 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000745 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 patchset = RunGit(['config', self._PatchsetSetting()],
747 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000748 self.patchset = int(patchset) or None if patchset else None
749 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750 return self.patchset
751
752 def SetPatchset(self, patchset):
753 """Set this branch's patchset. If patchset=0, clears the patchset."""
754 if patchset:
755 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000756 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757 else:
758 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000759 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000760 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000761
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000762 def GetMostRecentPatchset(self):
763 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000764
765 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000766 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000767 '/download/issue%s_%s.diff' % (issue, patchset))
768
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000769 def GetIssueProperties(self):
770 if self._props is None:
771 issue = self.GetIssue()
772 if not issue:
773 self._props = {}
774 else:
775 self._props = self.RpcServer().get_issue_properties(issue, True)
776 return self._props
777
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000778 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000779 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000780
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781 def SetIssue(self, issue):
782 """Set this branch's issue. If issue=0, clears the issue."""
783 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000784 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000785 RunGit(['config', self._IssueSetting(), str(issue)])
786 if self.rietveld_server:
787 RunGit(['config', self._RietveldServer(), self.rietveld_server])
788 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000789 current_issue = self.GetIssue()
790 if current_issue:
791 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000792 self.issue = None
793 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000795 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000796 if not self.GitSanityChecks(upstream_branch):
797 DieWithError('\nGit sanity check failure')
798
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000799 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000800 if not root:
801 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000802 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000803
804 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000805 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000806 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000807 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000808 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000809 except subprocess2.CalledProcessError:
810 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000811 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000812 'This branch probably doesn\'t exist anymore. To reset the\n'
813 'tracking branch, please run\n'
814 ' git branch --set-upstream %s trunk\n'
815 'replacing trunk with origin/master or the relevant branch') %
816 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000817
maruel@chromium.org52424302012-08-29 15:14:30 +0000818 issue = self.GetIssue()
819 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000820 if issue:
821 description = self.GetDescription()
822 else:
823 # If the change was never uploaded, use the log messages of all commits
824 # up to the branch point, as git cl upload will prefill the description
825 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000826 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
827 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000828
829 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000830 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000831 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000832 name,
833 description,
834 absroot,
835 files,
836 issue,
837 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000838 author,
839 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000840
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000841 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000842 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000843
844 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000845 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000846 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000847 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000848 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000849 except presubmit_support.PresubmitFailure, e:
850 DieWithError(
851 ('%s\nMaybe your depot_tools is out of date?\n'
852 'If all fails, contact maruel@') % e)
853
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000854 def UpdateDescription(self, description):
855 self.description = description
856 return self.RpcServer().update_description(
857 self.GetIssue(), self.description)
858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000860 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000861 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000862
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000863 def SetFlag(self, flag, value):
864 """Patchset must match."""
865 if not self.GetPatchset():
866 DieWithError('The patchset needs to match. Send another patchset.')
867 try:
868 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000869 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000870 except urllib2.HTTPError, e:
871 if e.code == 404:
872 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
873 if e.code == 403:
874 DieWithError(
875 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
876 'match?') % (self.GetIssue(), self.GetPatchset()))
877 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000879 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 """Returns an upload.RpcServer() to access this review's rietveld instance.
881 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000882 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000883 self._rpc_server = rietveld.CachingRietveld(
884 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000885 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000886
887 def _IssueSetting(self):
888 """Return the git setting that stores this change's issue."""
889 return 'branch.%s.rietveldissue' % self.GetBranch()
890
891 def _PatchsetSetting(self):
892 """Return the git setting that stores this change's most recent patchset."""
893 return 'branch.%s.rietveldpatchset' % self.GetBranch()
894
895 def _RietveldServer(self):
896 """Returns the git setting that stores this change's rietveld server."""
897 return 'branch.%s.rietveldserver' % self.GetBranch()
898
899
900def GetCodereviewSettingsInteractively():
901 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000902 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903 server = settings.GetDefaultServerUrl(error_ok=True)
904 prompt = 'Rietveld server (host[:port])'
905 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000906 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 if not server and not newserver:
908 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000909 if newserver:
910 newserver = gclient_utils.UpgradeToHttps(newserver)
911 if newserver != server:
912 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000914 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915 prompt = caption
916 if initial:
917 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000918 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919 if new_val == 'x':
920 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000921 elif new_val:
922 if is_url:
923 new_val = gclient_utils.UpgradeToHttps(new_val)
924 if new_val != initial:
925 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000927 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000928 SetProperty(settings.GetDefaultPrivateFlag(),
929 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000930 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000931 'tree-status-url', False)
932 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000933 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934
935 # TODO: configure a default branch to diff against, rather than this
936 # svn-based hackery.
937
938
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000939class ChangeDescription(object):
940 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000941 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000943
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000944 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000945 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000946
agable@chromium.org42c20792013-09-12 17:34:49 +0000947 @property # www.logilab.org/ticket/89786
948 def description(self): # pylint: disable=E0202
949 return '\n'.join(self._description_lines)
950
951 def set_description(self, desc):
952 if isinstance(desc, basestring):
953 lines = desc.splitlines()
954 else:
955 lines = [line.rstrip() for line in desc]
956 while lines and not lines[0]:
957 lines.pop(0)
958 while lines and not lines[-1]:
959 lines.pop(-1)
960 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000961
962 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000963 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000964 assert isinstance(reviewers, list), reviewers
965 if not reviewers:
966 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000967 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000968
agable@chromium.org42c20792013-09-12 17:34:49 +0000969 # Get the set of R= and TBR= lines and remove them from the desciption.
970 regexp = re.compile(self.R_LINE)
971 matches = [regexp.match(line) for line in self._description_lines]
972 new_desc = [l for i, l in enumerate(self._description_lines)
973 if not matches[i]]
974 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000975
agable@chromium.org42c20792013-09-12 17:34:49 +0000976 # Construct new unified R= and TBR= lines.
977 r_names = []
978 tbr_names = []
979 for match in matches:
980 if not match:
981 continue
982 people = cleanup_list([match.group(2).strip()])
983 if match.group(1) == 'TBR':
984 tbr_names.extend(people)
985 else:
986 r_names.extend(people)
987 for name in r_names:
988 if name not in reviewers:
989 reviewers.append(name)
990 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
991 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
992
993 # Put the new lines in the description where the old first R= line was.
994 line_loc = next((i for i, match in enumerate(matches) if match), -1)
995 if 0 <= line_loc < len(self._description_lines):
996 if new_tbr_line:
997 self._description_lines.insert(line_loc, new_tbr_line)
998 if new_r_line:
999 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001000 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001001 if new_r_line:
1002 self.append_footer(new_r_line)
1003 if new_tbr_line:
1004 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001005
1006 def prompt(self):
1007 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001008 self.set_description([
1009 '# Enter a description of the change.',
1010 '# This will be displayed on the codereview site.',
1011 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001012 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001013 '--------------------',
1014 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001015
agable@chromium.org42c20792013-09-12 17:34:49 +00001016 regexp = re.compile(self.BUG_LINE)
1017 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001018 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001019 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001020 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001021 if not content:
1022 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001023 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001024
1025 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001026 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1027 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001028 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001029 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001030
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001031 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001032 if self._description_lines:
1033 # Add an empty line if either the last line or the new line isn't a tag.
1034 last_line = self._description_lines[-1]
1035 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1036 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1037 self._description_lines.append('')
1038 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001039
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001040 def get_reviewers(self):
1041 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001042 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1043 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001044 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001045
1046
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001047def get_approving_reviewers(props):
1048 """Retrieves the reviewers that approved a CL from the issue properties with
1049 messages.
1050
1051 Note that the list may contain reviewers that are not committer, thus are not
1052 considered by the CQ.
1053 """
1054 return sorted(
1055 set(
1056 message['sender']
1057 for message in props['messages']
1058 if message['approval'] and message['sender'] in props['reviewers']
1059 )
1060 )
1061
1062
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001063def FindCodereviewSettingsFile(filename='codereview.settings'):
1064 """Finds the given file starting in the cwd and going up.
1065
1066 Only looks up to the top of the repository unless an
1067 'inherit-review-settings-ok' file exists in the root of the repository.
1068 """
1069 inherit_ok_file = 'inherit-review-settings-ok'
1070 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001071 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001072 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1073 root = '/'
1074 while True:
1075 if filename in os.listdir(cwd):
1076 if os.path.isfile(os.path.join(cwd, filename)):
1077 return open(os.path.join(cwd, filename))
1078 if cwd == root:
1079 break
1080 cwd = os.path.dirname(cwd)
1081
1082
1083def LoadCodereviewSettingsFromFile(fileobj):
1084 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001085 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001087 def SetProperty(name, setting, unset_error_ok=False):
1088 fullname = 'rietveld.' + name
1089 if setting in keyvals:
1090 RunGit(['config', fullname, keyvals[setting]])
1091 else:
1092 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1093
1094 SetProperty('server', 'CODE_REVIEW_SERVER')
1095 # Only server setting is required. Other settings can be absent.
1096 # In that case, we ignore errors raised during option deletion attempt.
1097 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001098 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1100 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001101 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001102 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1103 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001104 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001105 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001107 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001108 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001109
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1111 #should be of the form
1112 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1113 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1114 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1115 keyvals['ORIGIN_URL_CONFIG']])
1116
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001117
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001118def urlretrieve(source, destination):
1119 """urllib is broken for SSL connections via a proxy therefore we
1120 can't use urllib.urlretrieve()."""
1121 with open(destination, 'w') as f:
1122 f.write(urllib2.urlopen(source).read())
1123
1124
ukai@chromium.org712d6102013-11-27 00:52:58 +00001125def hasSheBang(fname):
1126 """Checks fname is a #! script."""
1127 with open(fname) as f:
1128 return f.read(2).startswith('#!')
1129
1130
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001131def DownloadHooks(force):
1132 """downloads hooks
1133
1134 Args:
1135 force: True to update hooks. False to install hooks if not present.
1136 """
1137 if not settings.GetIsGerrit():
1138 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001139 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001140 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1141 if not os.access(dst, os.X_OK):
1142 if os.path.exists(dst):
1143 if not force:
1144 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001145 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001146 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001147 if not hasSheBang(dst):
1148 DieWithError('Not a script: %s\n'
1149 'You need to download from\n%s\n'
1150 'into .git/hooks/commit-msg and '
1151 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001152 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1153 except Exception:
1154 if os.path.exists(dst):
1155 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001156 DieWithError('\nFailed to download hooks.\n'
1157 'You need to download from\n%s\n'
1158 'into .git/hooks/commit-msg and '
1159 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001160
1161
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001162@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001163def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001164 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001166 parser.add_option('--activate-update', action='store_true',
1167 help='activate auto-updating [rietveld] section in '
1168 '.git/config')
1169 parser.add_option('--deactivate-update', action='store_true',
1170 help='deactivate auto-updating [rietveld] section in '
1171 '.git/config')
1172 options, args = parser.parse_args(args)
1173
1174 if options.deactivate_update:
1175 RunGit(['config', 'rietveld.autoupdate', 'false'])
1176 return
1177
1178 if options.activate_update:
1179 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1180 return
1181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182 if len(args) == 0:
1183 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001184 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 return 0
1186
1187 url = args[0]
1188 if not url.endswith('codereview.settings'):
1189 url = os.path.join(url, 'codereview.settings')
1190
1191 # Load code review settings and download hooks (if available).
1192 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001193 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001194 return 0
1195
1196
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001197def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001198 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001199 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1200 branch = ShortBranchName(branchref)
1201 _, args = parser.parse_args(args)
1202 if not args:
1203 print("Current base-url:")
1204 return RunGit(['config', 'branch.%s.base-url' % branch],
1205 error_ok=False).strip()
1206 else:
1207 print("Setting base-url to %s" % args[0])
1208 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1209 error_ok=False).strip()
1210
1211
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001213 """Show status of changelists.
1214
1215 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001216 - Red not sent for review or broken
1217 - Blue waiting for review
1218 - Yellow waiting for you to reply to review
1219 - Green LGTM'ed
1220 - Magenta in the commit queue
1221 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001222
1223 Also see 'git cl comments'.
1224 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 parser.add_option('--field',
1226 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001227 parser.add_option('-f', '--fast', action='store_true',
1228 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001230 if args:
1231 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001232
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001234 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235 if options.field.startswith('desc'):
1236 print cl.GetDescription()
1237 elif options.field == 'id':
1238 issueid = cl.GetIssue()
1239 if issueid:
1240 print issueid
1241 elif options.field == 'patch':
1242 patchset = cl.GetPatchset()
1243 if patchset:
1244 print patchset
1245 elif options.field == 'url':
1246 url = cl.GetIssueURL()
1247 if url:
1248 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001249 return 0
1250
1251 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1252 if not branches:
1253 print('No local branch found.')
1254 return 0
1255
1256 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001257 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001258 alignment = max(5, max(len(b) for b in branches))
1259 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001260 # Adhoc thread pool to request data concurrently.
1261 output = Queue.Queue()
1262
1263 # Silence upload.py otherwise it becomes unweldly.
1264 upload.verbosity = 0
1265
1266 if not options.fast:
1267 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001268 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001269 c = Changelist(branchref=b)
1270 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001271 props = {}
1272 r = None
1273 if i:
1274 try:
1275 props = c.GetIssueProperties()
1276 r = c.GetApprovingReviewers() if i else None
1277 except urllib2.HTTPError:
1278 # The issue probably doesn't exist anymore.
1279 i += ' (broken)'
1280
1281 msgs = props.get('messages') or []
1282
1283 if not i:
1284 color = Fore.WHITE
1285 elif props.get('closed'):
1286 # Issue is closed.
1287 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001288 elif props.get('commit'):
1289 # Issue is in the commit queue.
1290 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001291 elif r:
1292 # Was LGTM'ed.
1293 color = Fore.GREEN
1294 elif not msgs:
1295 # No message was sent.
1296 color = Fore.RED
1297 elif msgs[-1]['sender'] != props.get('owner_email'):
1298 color = Fore.YELLOW
1299 else:
1300 color = Fore.BLUE
1301 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001302
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001303 # Process one branch synchronously to work through authentication, then
1304 # spawn threads to process all the other branches in parallel.
1305 if branches:
1306 fetch(branches[0])
1307 threads = [
1308 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001309 for t in threads:
1310 t.daemon = True
1311 t.start()
1312 else:
1313 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1314 for b in branches:
1315 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001316 url = c.GetIssueURL()
1317 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001318
1319 tmp = {}
1320 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001321 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001322 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001323 b, i, color = output.get()
1324 tmp[b] = (i, color)
1325 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001326 reset = Fore.RESET
1327 if not sys.stdout.isatty():
1328 color = ''
1329 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001330 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001331 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001332
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001333 cl = Changelist()
1334 print
1335 print 'Current branch:',
1336 if not cl.GetIssue():
1337 print 'no issue assigned.'
1338 return 0
1339 print cl.GetBranch()
1340 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001341 if not options.fast:
1342 print 'Issue description:'
1343 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344 return 0
1345
1346
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001347def colorize_CMDstatus_doc():
1348 """To be called once in main() to add colors to git cl status help."""
1349 colors = [i for i in dir(Fore) if i[0].isupper()]
1350
1351 def colorize_line(line):
1352 for color in colors:
1353 if color in line.upper():
1354 # Extract whitespaces first and the leading '-'.
1355 indent = len(line) - len(line.lstrip(' ')) + 1
1356 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1357 return line
1358
1359 lines = CMDstatus.__doc__.splitlines()
1360 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1361
1362
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001363@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001364def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001365 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
1367 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001368 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001369 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
1371 cl = Changelist()
1372 if len(args) > 0:
1373 try:
1374 issue = int(args[0])
1375 except ValueError:
1376 DieWithError('Pass a number to set the issue or none to list it.\n'
1377 'Maybe you want to run git cl status?')
1378 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001379 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380 return 0
1381
1382
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001383def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001384 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001385 (_, args) = parser.parse_args(args)
1386 if args:
1387 parser.error('Unsupported argument: %s' % args)
1388
1389 cl = Changelist()
1390 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001391 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001392 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001393 if message['disapproval']:
1394 color = Fore.RED
1395 elif message['approval']:
1396 color = Fore.GREEN
1397 elif message['sender'] == data['owner_email']:
1398 color = Fore.MAGENTA
1399 else:
1400 color = Fore.BLUE
1401 print '\n%s%s %s%s' % (
1402 color, message['date'].split('.', 1)[0], message['sender'],
1403 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001404 if message['text'].strip():
1405 print '\n'.join(' ' + l for l in message['text'].splitlines())
1406 return 0
1407
1408
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001409def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001410 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001411 cl = Changelist()
1412 if not cl.GetIssue():
1413 DieWithError('This branch has no associated changelist.')
1414 description = ChangeDescription(cl.GetDescription())
1415 description.prompt()
1416 cl.UpdateDescription(description.description)
1417 return 0
1418
1419
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420def CreateDescriptionFromLog(args):
1421 """Pulls out the commit log to use as a base for the CL description."""
1422 log_args = []
1423 if len(args) == 1 and not args[0].endswith('.'):
1424 log_args = [args[0] + '..']
1425 elif len(args) == 1 and args[0].endswith('...'):
1426 log_args = [args[0][:-1]]
1427 elif len(args) == 2:
1428 log_args = [args[0] + '..' + args[1]]
1429 else:
1430 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001431 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001432
1433
thestig@chromium.org44202a22014-03-11 19:22:18 +00001434def CMDlint(parser, args):
1435 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001436 parser.add_option('--filter', action='append', metavar='-x,+y',
1437 help='Comma-separated list of cpplint\'s category-filters')
1438 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001439
1440 # Access to a protected member _XX of a client class
1441 # pylint: disable=W0212
1442 try:
1443 import cpplint
1444 import cpplint_chromium
1445 except ImportError:
1446 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1447 return 1
1448
1449 # Change the current working directory before calling lint so that it
1450 # shows the correct base.
1451 previous_cwd = os.getcwd()
1452 os.chdir(settings.GetRoot())
1453 try:
1454 cl = Changelist()
1455 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1456 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001457 if not files:
1458 print "Cannot lint an empty CL"
1459 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001460
1461 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001462 command = args + files
1463 if options.filter:
1464 command = ['--filter=' + ','.join(options.filter)] + command
1465 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001466
1467 white_regex = re.compile(settings.GetLintRegex())
1468 black_regex = re.compile(settings.GetLintIgnoreRegex())
1469 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1470 for filename in filenames:
1471 if white_regex.match(filename):
1472 if black_regex.match(filename):
1473 print "Ignoring file %s" % filename
1474 else:
1475 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1476 extra_check_functions)
1477 else:
1478 print "Skipping file %s" % filename
1479 finally:
1480 os.chdir(previous_cwd)
1481 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1482 if cpplint._cpplint_state.error_count != 0:
1483 return 1
1484 return 0
1485
1486
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001488 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001489 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001491 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001492 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001493 (options, args) = parser.parse_args(args)
1494
ukai@chromium.org259e4682012-10-25 07:36:33 +00001495 if not options.force and is_dirty_git_tree('presubmit'):
1496 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497 return 1
1498
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001499 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500 if args:
1501 base_branch = args[0]
1502 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001503 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001504 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001506 cl.RunHook(
1507 committing=not options.upload,
1508 may_prompt=False,
1509 verbose=options.verbose,
1510 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001511 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001512
1513
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001514def AddChangeIdToCommitMessage(options, args):
1515 """Re-commits using the current message, assumes the commit hook is in
1516 place.
1517 """
1518 log_desc = options.message or CreateDescriptionFromLog(args)
1519 git_command = ['commit', '--amend', '-m', log_desc]
1520 RunGit(git_command)
1521 new_log_desc = CreateDescriptionFromLog(args)
1522 if CHANGE_ID in new_log_desc:
1523 print 'git-cl: Added Change-Id to commit message.'
1524 else:
1525 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1526
1527
ukai@chromium.orge8077812012-02-03 03:41:46 +00001528def GerritUpload(options, args, cl):
1529 """upload the current branch to gerrit."""
1530 # We assume the remote called "origin" is the one we want.
1531 # It is probably not worthwhile to support different workflows.
1532 remote = 'origin'
1533 branch = 'master'
1534 if options.target_branch:
1535 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001537 change_desc = ChangeDescription(
1538 options.message or CreateDescriptionFromLog(args))
1539 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001540 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001541 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001542 if CHANGE_ID not in change_desc.description:
1543 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001544
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001545 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001546 if len(commits) > 1:
1547 print('WARNING: This will upload %d commits. Run the following command '
1548 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001549 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001550 print('You can also use `git squash-branch` to squash these into a single'
1551 'commit.')
1552 ask_for_data('About to upload; enter to confirm.')
1553
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001554 if options.reviewers:
1555 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556
ukai@chromium.orge8077812012-02-03 03:41:46 +00001557 receive_options = []
1558 cc = cl.GetCCList().split(',')
1559 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001560 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001561 cc = filter(None, cc)
1562 if cc:
1563 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001564 if change_desc.get_reviewers():
1565 receive_options.extend(
1566 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567
ukai@chromium.orge8077812012-02-03 03:41:46 +00001568 git_command = ['push']
1569 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001570 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001571 ' '.join(receive_options))
1572 git_command += [remote, 'HEAD:refs/for/' + branch]
1573 RunGit(git_command)
1574 # TODO(ukai): parse Change-Id: and set issue number?
1575 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001576
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577
jam@chromium.org82114c02014-08-22 22:13:21 +00001578def RietveldUpload(options, args, cl):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001579 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001580 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1581 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582 if options.emulate_svn_auto_props:
1583 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001584
1585 change_desc = None
1586
pgervais@chromium.org91141372014-01-09 23:27:20 +00001587 if options.email is not None:
1588 upload_args.extend(['--email', options.email])
1589
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001590 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001591 if options.title:
1592 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001593 if options.message:
1594 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001595 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001596 print ("This branch is associated with issue %s. "
1597 "Adding patch to that issue." % cl.GetIssue())
1598 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001599 if options.title:
1600 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001601 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001602 change_desc = ChangeDescription(message)
1603 if options.reviewers:
1604 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001605 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001606 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001607
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001608 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001609 print "Description is empty; aborting."
1610 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001611
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001612 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001613 if change_desc.get_reviewers():
1614 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001615 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001616 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001617 DieWithError("Must specify reviewers to send email.")
1618 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001619
1620 # We check this before applying rietveld.private assuming that in
1621 # rietveld.cc only addresses which we can send private CLs to are listed
1622 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1623 # --private is specified explicitly on the command line.
1624 if options.private:
1625 logging.warn('rietveld.cc is ignored since private flag is specified. '
1626 'You need to review and add them manually if necessary.')
1627 cc = cl.GetCCListWithoutDefault()
1628 else:
1629 cc = cl.GetCCList()
1630 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001631 if cc:
1632 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001633
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001634 if options.private or settings.GetDefaultPrivateFlag() == "True":
1635 upload_args.append('--private')
1636
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001637 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001638 if not options.find_copies:
1639 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001640
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001641 # Include the upstream repo's URL in the change -- this is useful for
1642 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001643 remote_url = cl.GetGitBaseUrlFromConfig()
1644 if not remote_url:
1645 if settings.GetIsGitSvn():
1646 # URL is dependent on the current directory.
1647 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1648 if data:
1649 keys = dict(line.split(': ', 1) for line in data.splitlines()
1650 if ': ' in line)
1651 remote_url = keys.get('URL', None)
1652 else:
1653 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1654 remote_url = (cl.GetRemoteUrl() + '@'
1655 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001656 if remote_url:
1657 upload_args.extend(['--base_url', remote_url])
1658
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001659 project = settings.GetProject()
1660 if project:
1661 upload_args.extend(['--project', project])
1662
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001663 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001664 upload_args = ['upload'] + upload_args + args
1665 logging.info('upload.RealMain(%s)', upload_args)
1666 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001667 issue = int(issue)
1668 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001669 except KeyboardInterrupt:
1670 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001671 except:
1672 # If we got an exception after the user typed a description for their
1673 # change, back up the description before re-raising.
1674 if change_desc:
1675 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1676 print '\nGot exception while uploading -- saving description to %s\n' \
1677 % backup_path
1678 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001679 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680 backup_file.close()
1681 raise
1682
1683 if not cl.GetIssue():
1684 cl.SetIssue(issue)
1685 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001686
1687 if options.use_commit_queue:
1688 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689 return 0
1690
1691
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001692def cleanup_list(l):
1693 """Fixes a list so that comma separated items are put as individual items.
1694
1695 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1696 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1697 """
1698 items = sum((i.split(',') for i in l), [])
1699 stripped_items = (i.strip() for i in items)
1700 return sorted(filter(None, stripped_items))
1701
1702
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001703@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001704def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001705 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001706 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1707 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001708 parser.add_option('--bypass-watchlists', action='store_true',
1709 dest='bypass_watchlists',
1710 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001711 parser.add_option('-f', action='store_true', dest='force',
1712 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001713 parser.add_option('-m', dest='message', help='message for patchset')
1714 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001715 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001716 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001717 help='reviewer email addresses')
1718 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001719 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001720 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001721 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001722 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001723 parser.add_option('--emulate_svn_auto_props',
1724 '--emulate-svn-auto-props',
1725 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001726 dest="emulate_svn_auto_props",
1727 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001728 parser.add_option('-c', '--use-commit-queue', action='store_true',
1729 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001730 parser.add_option('--private', action='store_true',
1731 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001732 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001733 '--target-branch',
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001734 help='When uploading to gerrit, remote branch to '
1735 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001736 parser.add_option('--email', default=None,
1737 help='email address to use to connect to Rietveld')
1738
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001739 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001740 (options, args) = parser.parse_args(args)
1741
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001742 if options.target_branch and not settings.GetIsGerrit():
1743 parser.error('Use --target_branch for non gerrit repository.')
1744
ukai@chromium.org259e4682012-10-25 07:36:33 +00001745 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001746 return 1
1747
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001748 options.reviewers = cleanup_list(options.reviewers)
1749 options.cc = cleanup_list(options.cc)
1750
ukai@chromium.orge8077812012-02-03 03:41:46 +00001751 cl = Changelist()
1752 if args:
1753 # TODO(ukai): is it ok for gerrit case?
1754 base_branch = args[0]
1755 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001756 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001757 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001758 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001759
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001760 # Apply watchlists on upload.
1761 change = cl.GetChange(base_branch, None)
1762 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1763 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001764 if not options.bypass_watchlists:
1765 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001766
ukai@chromium.orge8077812012-02-03 03:41:46 +00001767 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001768 if options.reviewers:
1769 # Set the reviewer list now so that presubmit checks can access it.
1770 change_description = ChangeDescription(change.FullDescriptionText())
1771 change_description.update_reviewers(options.reviewers)
1772 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001773 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001774 may_prompt=not options.force,
1775 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001776 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001777 if not hook_results.should_continue():
1778 return 1
1779 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001780 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001781
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001782 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001783 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001784 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001785 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001786 print ('The last upload made from this repository was patchset #%d but '
1787 'the most recent patchset on the server is #%d.'
1788 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001789 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1790 'from another machine or branch the patch you\'re uploading now '
1791 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001792 ask_for_data('About to upload; enter to confirm.')
1793
iannucci@chromium.org79540052012-10-19 23:15:26 +00001794 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001795 if settings.GetIsGerrit():
1796 return GerritUpload(options, args, cl)
jam@chromium.org82114c02014-08-22 22:13:21 +00001797 ret = RietveldUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001798 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001799 git_set_branch_value('last-upload-hash',
1800 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001801
1802 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001803
1804
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001805def IsSubmoduleMergeCommit(ref):
1806 # When submodules are added to the repo, we expect there to be a single
1807 # non-git-svn merge commit at remote HEAD with a signature comment.
1808 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001809 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001810 return RunGit(cmd) != ''
1811
1812
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001813def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001814 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001816 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001817 Updates changelog with metadata (e.g. pointer to review).
1818 Pushes/dcommits the code upstream.
1819 Updates review and closes.
1820 """
1821 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1822 help='bypass upload presubmit hook')
1823 parser.add_option('-m', dest='message',
1824 help="override review description")
1825 parser.add_option('-f', action='store_true', dest='force',
1826 help="force yes to questions (don't prompt)")
1827 parser.add_option('-c', dest='contributor',
1828 help="external contributor for patch (appended to " +
1829 "description and used as author for git). Should be " +
1830 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001831 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832 (options, args) = parser.parse_args(args)
1833 cl = Changelist()
1834
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001835 current = cl.GetBranch()
1836 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1837 if not settings.GetIsGitSvn() and remote == '.':
1838 print
1839 print 'Attempting to push branch %r into another local branch!' % current
1840 print
1841 print 'Either reparent this branch on top of origin/master:'
1842 print ' git reparent-branch --root'
1843 print
1844 print 'OR run `git rebase-update` if you think the parent branch is already'
1845 print 'committed.'
1846 print
1847 print ' Current parent: %r' % upstream_branch
1848 return 1
1849
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001850 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001851 # Default to merging against our best guess of the upstream branch.
1852 args = [cl.GetUpstreamBranch()]
1853
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001854 if options.contributor:
1855 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1856 print "Please provide contibutor as 'First Last <email@example.com>'"
1857 return 1
1858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001860 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001861
ukai@chromium.org259e4682012-10-25 07:36:33 +00001862 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001863 return 1
1864
1865 # This rev-list syntax means "show all commits not in my branch that
1866 # are in base_branch".
1867 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1868 base_branch]).splitlines()
1869 if upstream_commits:
1870 print ('Base branch "%s" has %d commits '
1871 'not in this branch.' % (base_branch, len(upstream_commits)))
1872 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1873 return 1
1874
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001875 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001876 svn_head = None
1877 if cmd == 'dcommit' or base_has_submodules:
1878 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1879 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001880
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001882 # If the base_head is a submodule merge commit, the first parent of the
1883 # base_head should be a git-svn commit, which is what we're interested in.
1884 base_svn_head = base_branch
1885 if base_has_submodules:
1886 base_svn_head += '^1'
1887
1888 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001889 if extra_commits:
1890 print ('This branch has %d additional commits not upstreamed yet.'
1891 % len(extra_commits.splitlines()))
1892 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1893 'before attempting to %s.' % (base_branch, cmd))
1894 return 1
1895
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001896 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001897 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001898 author = None
1899 if options.contributor:
1900 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001901 hook_results = cl.RunHook(
1902 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001903 may_prompt=not options.force,
1904 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001905 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001906 if not hook_results.should_continue():
1907 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001908
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001909 # Check the tree status if the tree status URL is set.
1910 status = GetTreeStatus()
1911 if 'closed' == status:
1912 print('The tree is closed. Please wait for it to reopen. Use '
1913 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1914 return 1
1915 elif 'unknown' == status:
1916 print('Unable to determine tree status. Please verify manually and '
1917 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1918 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00001919 else:
1920 breakpad.SendStack(
1921 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001922 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1923 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001924 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001925
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001926 change_desc = ChangeDescription(options.message)
1927 if not change_desc.description and cl.GetIssue():
1928 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001930 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001931 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001932 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001933 else:
1934 print 'No description set.'
1935 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1936 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001937
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001938 # Keep a separate copy for the commit message, because the commit message
1939 # contains the link to the Rietveld issue, while the Rietveld message contains
1940 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001941 # Keep a separate copy for the commit message.
1942 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001943 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001944
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001945 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001946 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001947 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001948 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001949 commit_desc.append_footer('Patch from %s.' % options.contributor)
1950
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001951 print('Description:')
1952 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001953
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001954 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001955 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001956 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001957
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001958 # We want to squash all this branch's commits into one commit with the proper
1959 # description. We do this by doing a "reset --soft" to the base branch (which
1960 # keeps the working copy the same), then dcommitting that. If origin/master
1961 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1962 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001964 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1965 # Delete the branches if they exist.
1966 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1967 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1968 result = RunGitWithCode(showref_cmd)
1969 if result[0] == 0:
1970 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001971
1972 # We might be in a directory that's present in this branch but not in the
1973 # trunk. Move up to the top of the tree so that git commands that expect a
1974 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001975 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001976 if rel_base_path:
1977 os.chdir(rel_base_path)
1978
1979 # Stuff our change into the merge branch.
1980 # We wrap in a try...finally block so if anything goes wrong,
1981 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001982 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001983 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001984 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001985 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001986 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001987 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001988 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001990 RunGit(
1991 [
1992 'commit', '--author', options.contributor,
1993 '-m', commit_desc.description,
1994 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001995 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001996 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001997 if base_has_submodules:
1998 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1999 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2000 RunGit(['checkout', CHERRY_PICK_BRANCH])
2001 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002002 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002003 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002004 pending_prefix = settings.GetPendingRefPrefix()
2005 if not pending_prefix or branch.startswith(pending_prefix):
2006 # If not using refs/pending/heads/* at all, or target ref is already set
2007 # to pending, then push to the target ref directly.
2008 retcode, output = RunGitWithCode(
2009 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002010 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002011 else:
2012 # Cherry-pick the change on top of pending ref and then push it.
2013 assert branch.startswith('refs/'), branch
2014 assert pending_prefix[-1] == '/', pending_prefix
2015 pending_ref = pending_prefix + branch[len('refs/'):]
2016 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002017 revision = RunGit(['rev-parse', 'HEAD']).strip()
2018 pushed_to_pending = (retcode == 0)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002019 logging.debug(output)
2020 else:
2021 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002022 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00002023 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00002024 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002025 finally:
2026 # And then swap back to the original branch and clean up.
2027 RunGit(['checkout', '-q', cl.GetBranch()])
2028 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002029 if base_has_submodules:
2030 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002031
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002032 if retcode == 0 and pushed_to_pending:
2033 try:
2034 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2035 # We set pushed_to_pending to False, since it made it all the way to the
2036 # real ref.
2037 pushed_to_pending = False
2038 except KeyboardInterrupt:
2039 pass
2040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002042 if not revision:
2043 if cmd == 'dcommit' and 'Committed r' in output:
2044 revision = re.match(
2045 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2046 elif cmd == 'land' and retcode == 0:
2047 match = (re.match(r'.*?([a-f0-9]{7,})\.\.([a-f0-9]{7,})$', l)
2048 for l in output.splitlines(False))
2049 match = filter(None, match)
2050 if len(match) != 1:
2051 DieWithError(
2052 "Couldn't parse ouput to extract the committed hash:\n%s" % output)
2053 revision = match[0].group(2)
2054 else:
2055 return 1
2056
2057 revision = revision[:7]
2058
2059 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002060 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002061 if not to_pending:
2062 if viewvc_url and revision:
2063 change_desc.append_footer(
2064 'Committed: %s%s' % (viewvc_url, revision))
2065 elif revision:
2066 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002067 print ('Closing issue '
2068 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002069 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002070 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002071 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002072 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002073 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
2074 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002075 if options.bypass_hooks:
2076 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2077 else:
2078 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002079 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002080 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002081
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002082 if pushed_to_pending and retcode == 0:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002083 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2084 print 'The commit is in the pending queue (%s).' % pending_ref
2085 print (
2086 'It will show up on %s in ~1 min, once it gets Cr-Commit-Position '
2087 'footer.' % branch)
2088
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002089 if retcode == 0:
2090 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2091 if os.path.isfile(hook):
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002092 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002093
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002094 return 0
2095
2096
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002097def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2098 print
2099 print 'Waiting for commit to be landed on %s...' % real_ref
2100 print '(If you are impatient, you may Ctrl-C once without harm)'
2101 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2102 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2103
2104 loop = 0
2105 while True:
2106 sys.stdout.write('fetching (%d)... \r' % loop)
2107 sys.stdout.flush()
2108 loop += 1
2109
2110 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2111 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2112 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2113 for commit in commits.splitlines():
2114 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2115 print 'Found commit on %s' % real_ref
2116 return commit
2117
2118 current_rev = to_rev
2119
2120
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002121def PushToGitPending(remote, pending_ref, upstream_ref):
2122 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2123
2124 Returns:
2125 (retcode of last operation, output log of last operation).
2126 """
2127 assert pending_ref.startswith('refs/'), pending_ref
2128 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2129 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2130 code = 0
2131 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002132 max_attempts = 3
2133 attempts_left = max_attempts
2134 while attempts_left:
2135 if attempts_left != max_attempts:
2136 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2137 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002138
2139 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002140 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002141 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002142 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002143 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002144 print 'Fetch failed with exit code %d.' % code
2145 if out.strip():
2146 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002147 continue
2148
2149 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002150 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002151 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002152 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002153 if code:
2154 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002155 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2156 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002157 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2158 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002159 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002160 return code, out
2161
2162 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002163 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002164 code, out = RunGitWithCode(
2165 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2166 if code == 0:
2167 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002168 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002169 return code, out
2170
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002171 print 'Push failed with exit code %d.' % code
2172 if out.strip():
2173 print out.strip()
2174 if IsFatalPushFailure(out):
2175 print (
2176 'Fatal push error. Make sure your .netrc credentials and git '
2177 'user.email are correct and you have push access to the repo.')
2178 return code, out
2179
2180 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002181 return code, out
2182
2183
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002184def IsFatalPushFailure(push_stdout):
2185 """True if retrying push won't help."""
2186 return '(prohibited by Gerrit)' in push_stdout
2187
2188
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002189@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002190def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002191 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002193 message = """This doesn't appear to be an SVN repository.
2194If your project has a git mirror with an upstream SVN master, you probably need
2195to run 'git svn init', see your project's git mirror documentation.
2196If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002197to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002198Choose wisely, if you get this wrong, your commit might appear to succeed but
2199will instead be silently ignored."""
2200 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002201 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002202 return SendUpstream(parser, args, 'dcommit')
2203
2204
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002205@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002206def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002207 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002208 if settings.GetIsGitSvn():
2209 print('This appears to be an SVN repository.')
2210 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002211 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002212 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002213
2214
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002215@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002216def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002217 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002218 parser.add_option('-b', dest='newbranch',
2219 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002220 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002221 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002222 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2223 help='Change to the directory DIR immediately, '
2224 'before doing anything else.')
2225 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002226 help='failed patches spew .rej files rather than '
2227 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002228 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2229 help="don't commit after patch applies")
2230 (options, args) = parser.parse_args(args)
2231 if len(args) != 1:
2232 parser.print_help()
2233 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002234 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002235
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002236 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002237 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002238
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002239 if options.newbranch:
2240 if options.force:
2241 RunGit(['branch', '-D', options.newbranch],
2242 stderr=subprocess2.PIPE, error_ok=True)
2243 RunGit(['checkout', '-b', options.newbranch,
2244 Changelist().GetUpstreamBranch()])
2245
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002246 return PatchIssue(issue_arg, options.reject, options.nocommit,
2247 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002248
2249
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002250def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002251 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002252 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002253 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002254 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002255 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002256 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002257 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002258 # Assume it's a URL to the patch. Default to https.
2259 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002260 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002261 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002262 DieWithError('Must pass an issue ID or full URL for '
2263 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002264 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002265 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002266 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002267
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002268 # Switch up to the top-level directory, if necessary, in preparation for
2269 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002270 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002271 if top:
2272 os.chdir(top)
2273
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002274 # Git patches have a/ at the beginning of source paths. We strip that out
2275 # with a sed script rather than the -p flag to patch so we can feed either
2276 # Git or svn-style patches into the same apply command.
2277 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002278 try:
2279 patch_data = subprocess2.check_output(
2280 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2281 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002282 DieWithError('Git patch mungling failed.')
2283 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002284
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002285 # We use "git apply" to apply the patch instead of "patch" so that we can
2286 # pick up file adds.
2287 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002288 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002289 if directory:
2290 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002291 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002292 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002293 elif IsGitVersionAtLeast('1.7.12'):
2294 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002295 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002296 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002297 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002298 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002299 DieWithError('Failed to apply the patch')
2300
2301 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002302 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002303 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2304 cl = Changelist()
2305 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002306 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002307 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002308 else:
2309 print "Patch applied to index."
2310 return 0
2311
2312
2313def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002314 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002315 # Provide a wrapper for git svn rebase to help avoid accidental
2316 # git svn dcommit.
2317 # It's the only command that doesn't use parser at all since we just defer
2318 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002319
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002320 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002321
2322
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002323def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002324 """Fetches the tree status and returns either 'open', 'closed',
2325 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002326 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002327 if url:
2328 status = urllib2.urlopen(url).read().lower()
2329 if status.find('closed') != -1 or status == '0':
2330 return 'closed'
2331 elif status.find('open') != -1 or status == '1':
2332 return 'open'
2333 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002334 return 'unset'
2335
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002336
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002337def GetTreeStatusReason():
2338 """Fetches the tree status from a json url and returns the message
2339 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002340 url = settings.GetTreeStatusUrl()
2341 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002342 connection = urllib2.urlopen(json_url)
2343 status = json.loads(connection.read())
2344 connection.close()
2345 return status['message']
2346
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002347
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002348def GetBuilderMaster(bot_list):
2349 """For a given builder, fetch the master from AE if available."""
2350 map_url = 'https://builders-map.appspot.com/'
2351 try:
2352 master_map = json.load(urllib2.urlopen(map_url))
2353 except urllib2.URLError as e:
2354 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2355 (map_url, e))
2356 except ValueError as e:
2357 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2358 if not master_map:
2359 return None, 'Failed to build master map.'
2360
2361 result_master = ''
2362 for bot in bot_list:
2363 builder = bot.split(':', 1)[0]
2364 master_list = master_map.get(builder, [])
2365 if not master_list:
2366 return None, ('No matching master for builder %s.' % builder)
2367 elif len(master_list) > 1:
2368 return None, ('The builder name %s exists in multiple masters %s.' %
2369 (builder, master_list))
2370 else:
2371 cur_master = master_list[0]
2372 if not result_master:
2373 result_master = cur_master
2374 elif result_master != cur_master:
2375 return None, 'The builders do not belong to the same master.'
2376 return result_master, None
2377
2378
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002379def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002380 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002381 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002382 status = GetTreeStatus()
2383 if 'unset' == status:
2384 print 'You must configure your tree status URL by running "git cl config".'
2385 return 2
2386
2387 print "The tree is %s" % status
2388 print
2389 print GetTreeStatusReason()
2390 if status != 'open':
2391 return 1
2392 return 0
2393
2394
maruel@chromium.org15192402012-09-06 12:38:29 +00002395def CMDtry(parser, args):
2396 """Triggers a try job through Rietveld."""
2397 group = optparse.OptionGroup(parser, "Try job options")
2398 group.add_option(
2399 "-b", "--bot", action="append",
2400 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2401 "times to specify multiple builders. ex: "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002402 "'-b win_rel:ui_tests,webkit_unit_tests -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002403 "the try server waterfall for the builders name and the tests "
2404 "available. Can also be used to specify gtest_filter, e.g. "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002405 "-b win_rel:base_unittests:ValuesTest.*Value"))
maruel@chromium.org15192402012-09-06 12:38:29 +00002406 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002407 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002408 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002409 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002410 "-r", "--revision",
2411 help="Revision to use for the try job; default: the "
2412 "revision will be determined by the try server; see "
2413 "its waterfall for more info")
2414 group.add_option(
2415 "-c", "--clobber", action="store_true", default=False,
2416 help="Force a clobber before building; e.g. don't do an "
2417 "incremental build")
2418 group.add_option(
2419 "--project",
2420 help="Override which project to use. Projects are defined "
2421 "server-side to define what default bot set to use")
2422 group.add_option(
2423 "-t", "--testfilter", action="append", default=[],
2424 help=("Apply a testfilter to all the selected builders. Unless the "
2425 "builders configurations are similar, use multiple "
2426 "--bot <builder>:<test> arguments."))
2427 group.add_option(
2428 "-n", "--name", help="Try job name; default to current branch name")
2429 parser.add_option_group(group)
2430 options, args = parser.parse_args(args)
2431
2432 if args:
2433 parser.error('Unknown arguments: %s' % args)
2434
2435 cl = Changelist()
2436 if not cl.GetIssue():
2437 parser.error('Need to upload first')
2438
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002439 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002440 if props.get('closed'):
2441 parser.error('Cannot send tryjobs for a closed CL')
2442
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002443 if props.get('private'):
2444 parser.error('Cannot use trybots with private issue')
2445
maruel@chromium.org15192402012-09-06 12:38:29 +00002446 if not options.name:
2447 options.name = cl.GetBranch()
2448
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002449 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002450 options.master, err_msg = GetBuilderMaster(options.bot)
2451 if err_msg:
2452 parser.error('Tryserver master cannot be found because: %s\n'
2453 'Please manually specify the tryserver master'
2454 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002455
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002456 def GetMasterMap():
2457 # Process --bot and --testfilter.
2458 if not options.bot:
2459 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002460
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002461 # Get try masters from PRESUBMIT.py files.
2462 masters = presubmit_support.DoGetTryMasters(
2463 change,
2464 change.LocalPaths(),
2465 settings.GetRoot(),
2466 None,
2467 None,
2468 options.verbose,
2469 sys.stdout)
2470 if masters:
2471 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002472
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002473 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2474 options.bot = presubmit_support.DoGetTrySlaves(
2475 change,
2476 change.LocalPaths(),
2477 settings.GetRoot(),
2478 None,
2479 None,
2480 options.verbose,
2481 sys.stdout)
2482 if not options.bot:
2483 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002484
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002485 builders_and_tests = {}
2486 # TODO(machenbach): The old style command-line options don't support
2487 # multiple try masters yet.
2488 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2489 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2490
2491 for bot in old_style:
2492 if ':' in bot:
2493 builder, tests = bot.split(':', 1)
2494 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2495 elif ',' in bot:
2496 parser.error('Specify one bot per --bot flag')
2497 else:
2498 builders_and_tests.setdefault(bot, []).append('defaulttests')
2499
2500 for bot, tests in new_style:
2501 builders_and_tests.setdefault(bot, []).extend(tests)
2502
2503 # Return a master map with one master to be backwards compatible. The
2504 # master name defaults to an empty string, which will cause the master
2505 # not to be set on rietveld (deprecated).
2506 return {options.master: builders_and_tests}
2507
2508 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002509
maruel@chromium.org15192402012-09-06 12:38:29 +00002510 if options.testfilter:
2511 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002512 masters = dict((master, dict(
2513 (b, forced_tests) for b, t in slaves.iteritems()
2514 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002515
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002516 for builders in masters.itervalues():
2517 if any('triggered' in b for b in builders):
2518 print >> sys.stderr, (
2519 'ERROR You are trying to send a job to a triggered bot. This type of'
2520 ' bot requires an\ninitial job from a parent (usually a builder). '
2521 'Instead send your job to the parent.\n'
2522 'Bot list: %s' % builders)
2523 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002524
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002525 patchset = cl.GetMostRecentPatchset()
2526 if patchset and patchset != cl.GetPatchset():
2527 print(
2528 '\nWARNING Mismatch between local config and server. Did a previous '
2529 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2530 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002531 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002532 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002533 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002534 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002535 except urllib2.HTTPError, e:
2536 if e.code == 404:
2537 print('404 from rietveld; '
2538 'did you mean to use "git try" instead of "git cl try"?')
2539 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002540 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002541
2542 for (master, builders) in masters.iteritems():
2543 if master:
2544 print 'Master: %s' % master
2545 length = max(len(builder) for builder in builders)
2546 for builder in sorted(builders):
2547 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002548 return 0
2549
2550
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002551@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002552def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002553 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002554 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002555 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002556 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002557
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002558 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002559 if args:
2560 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002561 branch = cl.GetBranch()
2562 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002563 cl = Changelist()
2564 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002565
2566 # Clear configured merge-base, if there is one.
2567 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002568 else:
2569 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002570 return 0
2571
2572
thestig@chromium.org00858c82013-12-02 23:08:03 +00002573def CMDweb(parser, args):
2574 """Opens the current CL in the web browser."""
2575 _, args = parser.parse_args(args)
2576 if args:
2577 parser.error('Unrecognized args: %s' % ' '.join(args))
2578
2579 issue_url = Changelist().GetIssueURL()
2580 if not issue_url:
2581 print >> sys.stderr, 'ERROR No issue to open'
2582 return 1
2583
2584 webbrowser.open(issue_url)
2585 return 0
2586
2587
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002588def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002589 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002590 _, args = parser.parse_args(args)
2591 if args:
2592 parser.error('Unrecognized args: %s' % ' '.join(args))
2593 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002594 props = cl.GetIssueProperties()
2595 if props.get('private'):
2596 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002597 cl.SetFlag('commit', '1')
2598 return 0
2599
2600
groby@chromium.org411034a2013-02-26 15:12:01 +00002601def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002602 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002603 _, args = parser.parse_args(args)
2604 if args:
2605 parser.error('Unrecognized args: %s' % ' '.join(args))
2606 cl = Changelist()
2607 # Ensure there actually is an issue to close.
2608 cl.GetDescription()
2609 cl.CloseIssue()
2610 return 0
2611
2612
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002613def CMDdiff(parser, args):
2614 """shows differences between local tree and last upload."""
2615 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002616 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002617 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002618 if not issue:
2619 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002620 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002621 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002622
2623 # Create a new branch based on the merge-base
2624 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2625 try:
2626 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002627 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002628 if rtn != 0:
2629 return rtn
2630
2631 # Switch back to starting brand and diff against the temporary
2632 # branch containing the latest rietveld patch.
2633 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2634 finally:
2635 RunGit(['checkout', '-q', branch])
2636 RunGit(['branch', '-D', TMP_BRANCH])
2637
2638 return 0
2639
2640
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002641def CMDowners(parser, args):
2642 """interactively find the owners for reviewing"""
2643 parser.add_option(
2644 '--no-color',
2645 action='store_true',
2646 help='Use this option to disable color output')
2647 options, args = parser.parse_args(args)
2648
2649 author = RunGit(['config', 'user.email']).strip() or None
2650
2651 cl = Changelist()
2652
2653 if args:
2654 if len(args) > 1:
2655 parser.error('Unknown args')
2656 base_branch = args[0]
2657 else:
2658 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002659 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002660
2661 change = cl.GetChange(base_branch, None)
2662 return owners_finder.OwnersFinder(
2663 [f.LocalPath() for f in
2664 cl.GetChange(base_branch, None).AffectedFiles()],
2665 change.RepositoryRoot(), author,
2666 fopen=file, os_path=os.path, glob=glob.glob,
2667 disable_color=options.no_color).run()
2668
2669
enne@chromium.org555cfe42014-01-29 18:21:39 +00002670@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002671def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002672 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002673 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002674 parser.add_option('--full', action='store_true',
2675 help='Reformat the full content of all touched files')
2676 parser.add_option('--dry-run', action='store_true',
2677 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002678 parser.add_option('--diff', action='store_true',
2679 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002680 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002681
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002682 # git diff generates paths against the root of the repository. Change
2683 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002684 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002685 if rel_base_path:
2686 os.chdir(rel_base_path)
2687
digit@chromium.org29e47272013-05-17 17:01:46 +00002688 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002689 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002690 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002691 # Only list the names of modified files.
2692 diff_cmd.append('--name-only')
2693 else:
2694 # Only generate context-less patches.
2695 diff_cmd.append('-U0')
2696
2697 # Grab the merge-base commit, i.e. the upstream commit of the current
2698 # branch when it was created or the last time it was rebased. This is
2699 # to cover the case where the user may have called "git fetch origin",
2700 # moving the origin branch to a newer commit, but hasn't rebased yet.
2701 upstream_commit = None
2702 cl = Changelist()
2703 upstream_branch = cl.GetUpstreamBranch()
2704 if upstream_branch:
2705 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2706 upstream_commit = upstream_commit.strip()
2707
2708 if not upstream_commit:
2709 DieWithError('Could not find base commit for this branch. '
2710 'Are you in detached state?')
2711
2712 diff_cmd.append(upstream_commit)
2713
2714 # Handle source file filtering.
2715 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002716 if args:
2717 for arg in args:
2718 if os.path.isdir(arg):
2719 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2720 elif os.path.isfile(arg):
2721 diff_cmd.append(arg)
2722 else:
2723 DieWithError('Argument "%s" is not a file or a directory' % arg)
2724 else:
2725 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002726 diff_output = RunGit(diff_cmd)
2727
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002728 top_dir = os.path.normpath(
2729 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2730
2731 # Locate the clang-format binary in the checkout
2732 try:
2733 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2734 except clang_format.NotFoundError, e:
2735 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002736
digit@chromium.org29e47272013-05-17 17:01:46 +00002737 if opts.full:
2738 # diff_output is a list of files to send to clang-format.
2739 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002740 if not files:
2741 print "Nothing to format."
2742 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002743 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002744 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002745 cmd.append('-i')
2746 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002747 if opts.diff:
2748 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002749 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002750 env = os.environ.copy()
2751 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002752 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002753 try:
2754 script = clang_format.FindClangFormatScriptInChromiumTree(
2755 'clang-format-diff.py')
2756 except clang_format.NotFoundError, e:
2757 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002758
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002759 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002760 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002761 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002762
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002763 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002764 if opts.diff:
2765 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002766 if opts.dry_run and len(stdout) > 0:
2767 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002768
2769 return 0
2770
2771
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002772class OptionParser(optparse.OptionParser):
2773 """Creates the option parse and add --verbose support."""
2774 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002775 optparse.OptionParser.__init__(
2776 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002777 self.add_option(
2778 '-v', '--verbose', action='count', default=0,
2779 help='Use 2 times for more debugging info')
2780
2781 def parse_args(self, args=None, values=None):
2782 options, args = optparse.OptionParser.parse_args(self, args, values)
2783 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2784 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2785 return options, args
2786
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002787
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002788def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002789 if sys.hexversion < 0x02060000:
2790 print >> sys.stderr, (
2791 '\nYour python version %s is unsupported, please upgrade.\n' %
2792 sys.version.split(' ', 1)[0])
2793 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002794
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002795 # Reload settings.
2796 global settings
2797 settings = Settings()
2798
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002799 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002800 dispatcher = subcommand.CommandDispatcher(__name__)
2801 try:
2802 return dispatcher.execute(OptionParser(), argv)
2803 except urllib2.HTTPError, e:
2804 if e.code != 500:
2805 raise
2806 DieWithError(
2807 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2808 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002809
2810
2811if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002812 # These affect sys.stdout so do it outside of main() to simplify mocks in
2813 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002814 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002815 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002816 sys.exit(main(sys.argv[1:]))