blob: bd65e12b02e4b30162e46c54f72bd25ecbb453b6 [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
piman@chromium.org336f9122014-09-04 02:16:55 +000039import owners
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000042import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000044import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000045import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000046import watchlists
47
maruel@chromium.org0633fb42013-08-16 20:06:14 +000048__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000049
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000050DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000051POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000052DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000053GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000054CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055
thestig@chromium.org44202a22014-03-11 19:22:18 +000056# Valid extensions for files we want to lint.
57DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
58DEFAULT_LINT_IGNORE_REGEX = r"$^"
59
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000060# Shortcut since it quickly becomes redundant.
61Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000062
maruel@chromium.orgddd59412011-11-30 14:20:38 +000063# Initialized in main()
64settings = None
65
66
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000068 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069 sys.exit(1)
70
71
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000072def GetNoGitPagerEnv():
73 env = os.environ.copy()
74 # 'cat' is a magical git string that disables pagers on all platforms.
75 env['GIT_PAGER'] = 'cat'
76 return env
77
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +000078
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000081 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000082 except subprocess2.CalledProcessError as e:
83 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000084 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000085 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000086 'Command "%s" failed.\n%s' % (
87 ' '.join(args), error_message or e.stdout or ''))
88 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089
90
91def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000092 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000093 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000094
95
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000096def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000097 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000098 try:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000099 if suppress_stderr:
100 stderr = subprocess2.VOID
101 else:
102 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000103 out, code = subprocess2.communicate(['git'] + args,
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000104 env=GetNoGitPagerEnv(),
enne@chromium.org3b7e15c2014-01-21 17:44:47 +0000105 stdout=subprocess2.PIPE,
106 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +0000107 return code, out[0]
108 except ValueError:
109 # When the subprocess fails, it returns None. That triggers a ValueError
110 # when trying to unpack the return value into (out, code).
111 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000112
113
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000114def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000115 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000116 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000117 return (version.startswith(prefix) and
118 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000119
120
maruel@chromium.org90541732011-04-01 17:54:18 +0000121def ask_for_data(prompt):
122 try:
123 return raw_input(prompt)
124 except KeyboardInterrupt:
125 # Hide the exception.
126 sys.exit(1)
127
128
iannucci@chromium.org79540052012-10-19 23:15:26 +0000129def git_set_branch_value(key, value):
130 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000131 if not branch:
132 return
133
134 cmd = ['config']
135 if isinstance(value, int):
136 cmd.append('--int')
137 git_key = 'branch.%s.%s' % (branch, key)
138 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000139
140
141def git_get_branch_default(key, default):
142 branch = Changelist().GetBranch()
143 if branch:
144 git_key = 'branch.%s.%s' % (branch, key)
145 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
146 try:
147 return int(stdout.strip())
148 except ValueError:
149 pass
150 return default
151
152
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000153def add_git_similarity(parser):
154 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000155 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000156 help='Sets the percentage that a pair of files need to match in order to'
157 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000158 parser.add_option(
159 '--find-copies', action='store_true',
160 help='Allows git to look for copies.')
161 parser.add_option(
162 '--no-find-copies', action='store_false', dest='find_copies',
163 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000164
165 old_parser_args = parser.parse_args
166 def Parse(args):
167 options, args = old_parser_args(args)
168
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000169 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000170 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000171 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000172 print('Note: Saving similarity of %d%% in git config.'
173 % options.similarity)
174 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000175
iannucci@chromium.org79540052012-10-19 23:15:26 +0000176 options.similarity = max(0, min(options.similarity, 100))
177
178 if options.find_copies is None:
179 options.find_copies = bool(
180 git_get_branch_default('git-find-copies', True))
181 else:
182 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000183
184 print('Using %d%% similarity for rename/copy detection. '
185 'Override with --similarity.' % options.similarity)
186
187 return options, args
188 parser.parse_args = Parse
189
190
ukai@chromium.org259e4682012-10-25 07:36:33 +0000191def is_dirty_git_tree(cmd):
192 # Make sure index is up-to-date before running diff-index.
193 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
194 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
195 if dirty:
196 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
197 print 'Uncommitted files: (git diff-index --name-status HEAD)'
198 print dirty[:4096]
199 if len(dirty) > 4096:
200 print '... (run "git diff-index --name-status HEAD" to see full output).'
201 return True
202 return False
203
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000204
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000205def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
206 """Return the corresponding git ref if |base_url| together with |glob_spec|
207 matches the full |url|.
208
209 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
210 """
211 fetch_suburl, as_ref = glob_spec.split(':')
212 if allow_wildcards:
213 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
214 if glob_match:
215 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
216 # "branches/{472,597,648}/src:refs/remotes/svn/*".
217 branch_re = re.escape(base_url)
218 if glob_match.group(1):
219 branch_re += '/' + re.escape(glob_match.group(1))
220 wildcard = glob_match.group(2)
221 if wildcard == '*':
222 branch_re += '([^/]*)'
223 else:
224 # Escape and replace surrounding braces with parentheses and commas
225 # with pipe symbols.
226 wildcard = re.escape(wildcard)
227 wildcard = re.sub('^\\\\{', '(', wildcard)
228 wildcard = re.sub('\\\\,', '|', wildcard)
229 wildcard = re.sub('\\\\}$', ')', wildcard)
230 branch_re += wildcard
231 if glob_match.group(3):
232 branch_re += re.escape(glob_match.group(3))
233 match = re.match(branch_re, url)
234 if match:
235 return re.sub('\*$', match.group(1), as_ref)
236
237 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
238 if fetch_suburl:
239 full_url = base_url + '/' + fetch_suburl
240 else:
241 full_url = base_url
242 if full_url == url:
243 return as_ref
244 return None
245
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000246
iannucci@chromium.org79540052012-10-19 23:15:26 +0000247def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000248 """Prints statistics about the change to the user."""
249 # --no-ext-diff is broken in some versions of Git, so try to work around
250 # this by overriding the environment (but there is still a problem if the
251 # git config key "diff.external" is used).
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000252 env = GetNoGitPagerEnv()
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000253 if 'GIT_EXTERNAL_DIFF' in env:
254 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000255
256 if find_copies:
257 similarity_options = ['--find-copies-harder', '-l100000',
258 '-C%s' % similarity]
259 else:
260 similarity_options = ['-M%s' % similarity]
261
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000262 try:
263 stdout = sys.stdout.fileno()
264 except AttributeError:
265 stdout = None
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000266 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000267 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000268 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
szager@chromium.orgd057f9a2014-05-29 21:09:36 +0000269 stdout=stdout, env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000270
271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272class Settings(object):
273 def __init__(self):
274 self.default_server = None
275 self.cc = None
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000276 self.root = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000277 self.is_git_svn = None
278 self.svn_branch = None
279 self.tree_status_url = None
280 self.viewvc_url = None
281 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000282 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000283 self.git_editor = None
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000284 self.project = None
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000285 self.pending_ref_prefix = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286
287 def LazyUpdateIfNeeded(self):
288 """Updates the settings from a codereview.settings file, if available."""
289 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000290 # The only value that actually changes the behavior is
291 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000292 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000293 error_ok=True
294 ).strip().lower()
295
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000296 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000297 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000298 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000299 # set updated to True to avoid infinite calling loop
300 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000301 self.updated = True
302 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000303 self.updated = True
304
305 def GetDefaultServerUrl(self, error_ok=False):
306 if not self.default_server:
307 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000308 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000309 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 if error_ok:
311 return self.default_server
312 if not self.default_server:
313 error_message = ('Could not find settings file. You must configure '
314 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000315 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000316 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000317 return self.default_server
318
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000319 @staticmethod
320 def GetRelativeRoot():
321 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000322
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000323 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000324 if self.root is None:
325 self.root = os.path.abspath(self.GetRelativeRoot())
326 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000327
328 def GetIsGitSvn(self):
329 """Return true if this repo looks like it's using git-svn."""
330 if self.is_git_svn is None:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000331 if self.GetPendingRefPrefix():
332 # If PENDING_REF_PREFIX is set then it's a pure git repo no matter what.
333 self.is_git_svn = False
334 else:
335 # If you have any "svn-remote.*" config keys, we think you're using svn.
336 self.is_git_svn = RunGitWithCode(
337 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000338 return self.is_git_svn
339
340 def GetSVNBranch(self):
341 if self.svn_branch is None:
342 if not self.GetIsGitSvn():
343 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
344
345 # Try to figure out which remote branch we're based on.
346 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000347 # 1) iterate through our branch history and find the svn URL.
348 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000349
350 # regexp matching the git-svn line that contains the URL.
351 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
352
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000353 # We don't want to go through all of history, so read a line from the
354 # pipe at a time.
355 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000356 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000357 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
358 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000359 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000360 for line in proc.stdout:
361 match = git_svn_re.match(line)
362 if match:
363 url = match.group(1)
364 proc.stdout.close() # Cut pipe.
365 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000366
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000367 if url:
368 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
369 remotes = RunGit(['config', '--get-regexp',
370 r'^svn-remote\..*\.url']).splitlines()
371 for remote in remotes:
372 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000373 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000374 remote = match.group(1)
375 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000376 rewrite_root = RunGit(
377 ['config', 'svn-remote.%s.rewriteRoot' % remote],
378 error_ok=True).strip()
379 if rewrite_root:
380 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000381 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000382 ['config', 'svn-remote.%s.fetch' % remote],
383 error_ok=True).strip()
384 if fetch_spec:
385 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
386 if self.svn_branch:
387 break
388 branch_spec = RunGit(
389 ['config', 'svn-remote.%s.branches' % remote],
390 error_ok=True).strip()
391 if branch_spec:
392 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
393 if self.svn_branch:
394 break
395 tag_spec = RunGit(
396 ['config', 'svn-remote.%s.tags' % remote],
397 error_ok=True).strip()
398 if tag_spec:
399 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
400 if self.svn_branch:
401 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402
403 if not self.svn_branch:
404 DieWithError('Can\'t guess svn branch -- try specifying it on the '
405 'command line')
406
407 return self.svn_branch
408
409 def GetTreeStatusUrl(self, error_ok=False):
410 if not self.tree_status_url:
411 error_message = ('You must configure your tree status URL by running '
412 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000413 self.tree_status_url = self._GetRietveldConfig(
414 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000415 return self.tree_status_url
416
417 def GetViewVCUrl(self):
418 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000419 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000420 return self.viewvc_url
421
rmistry@google.com90752582014-01-14 21:04:50 +0000422 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000423 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000424
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000425 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000426 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000427
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000428 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000429 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000430
ukai@chromium.orge8077812012-02-03 03:41:46 +0000431 def GetIsGerrit(self):
432 """Return true if this repo is assosiated with gerrit code review system."""
433 if self.is_gerrit is None:
434 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
435 return self.is_gerrit
436
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000437 def GetGitEditor(self):
438 """Return the editor specified in the git config, or None if none is."""
439 if self.git_editor is None:
440 self.git_editor = self._GetConfig('core.editor', error_ok=True)
441 return self.git_editor or None
442
thestig@chromium.org44202a22014-03-11 19:22:18 +0000443 def GetLintRegex(self):
444 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
445 DEFAULT_LINT_REGEX)
446
447 def GetLintIgnoreRegex(self):
448 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
449 DEFAULT_LINT_IGNORE_REGEX)
450
sheyang@chromium.org152cf832014-06-11 21:37:49 +0000451 def GetProject(self):
452 if not self.project:
453 self.project = self._GetRietveldConfig('project', error_ok=True)
454 return self.project
455
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +0000456 def GetPendingRefPrefix(self):
457 if not self.pending_ref_prefix:
458 self.pending_ref_prefix = self._GetRietveldConfig(
459 'pending-ref-prefix', error_ok=True)
460 return self.pending_ref_prefix
461
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000462 def _GetRietveldConfig(self, param, **kwargs):
463 return self._GetConfig('rietveld.' + param, **kwargs)
464
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000465 def _GetConfig(self, param, **kwargs):
466 self.LazyUpdateIfNeeded()
467 return RunGit(['config', param], **kwargs).strip()
468
469
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000470def ShortBranchName(branch):
471 """Convert a name like 'refs/heads/foo' to just 'foo'."""
472 return branch.replace('refs/heads/', '')
473
474
475class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000476 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000477 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000478 global settings
479 if not settings:
480 # Happens when git_cl.py is used as a utility library.
481 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000482 settings.GetDefaultServerUrl()
483 self.branchref = branchref
484 if self.branchref:
485 self.branch = ShortBranchName(self.branchref)
486 else:
487 self.branch = None
488 self.rietveld_server = None
489 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000490 self.lookedup_issue = False
491 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000492 self.has_description = False
493 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000494 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000495 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000496 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000497 self.cc = None
498 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000499 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000500 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000501
502 def GetCCList(self):
503 """Return the users cc'd on this CL.
504
505 Return is a string suitable for passing to gcl with the --cc flag.
506 """
507 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000508 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000509 more_cc = ','.join(self.watchers)
510 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
511 return self.cc
512
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000513 def GetCCListWithoutDefault(self):
514 """Return the users cc'd on this CL excluding default ones."""
515 if self.cc is None:
516 self.cc = ','.join(self.watchers)
517 return self.cc
518
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000519 def SetWatchers(self, watchers):
520 """Set the list of email addresses that should be cc'd based on the changed
521 files in this CL.
522 """
523 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000524
525 def GetBranch(self):
526 """Returns the short branch name, e.g. 'master'."""
527 if not self.branch:
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000528 branchref = RunGit(['symbolic-ref', 'HEAD'],
529 stderr=subprocess2.VOID, error_ok=True).strip()
530 if not branchref:
531 return None
532 self.branchref = branchref
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000533 self.branch = ShortBranchName(self.branchref)
534 return self.branch
535
536 def GetBranchRef(self):
537 """Returns the full branch name, e.g. 'refs/heads/master'."""
538 self.GetBranch() # Poke the lazy loader.
539 return self.branchref
540
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000541 @staticmethod
542 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000543 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000544 e.g. 'origin', 'refs/heads/master'
545 """
546 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000547 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
548 error_ok=True).strip()
549 if upstream_branch:
550 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
551 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000552 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
553 error_ok=True).strip()
554 if upstream_branch:
555 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000556 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000557 # Fall back on trying a git-svn upstream branch.
558 if settings.GetIsGitSvn():
559 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000560 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000561 # Else, try to guess the origin remote.
562 remote_branches = RunGit(['branch', '-r']).split()
563 if 'origin/master' in remote_branches:
564 # Fall back on origin/master if it exits.
565 remote = 'origin'
566 upstream_branch = 'refs/heads/master'
567 elif 'origin/trunk' in remote_branches:
568 # Fall back on origin/trunk if it exists. Generally a shared
569 # git-svn clone
570 remote = 'origin'
571 upstream_branch = 'refs/heads/trunk'
572 else:
573 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574Either pass complete "git diff"-style arguments, like
575 git cl upload origin/master
576or verify this branch is set up to track another (via the --track argument to
577"git checkout -b ...").""")
578
579 return remote, upstream_branch
580
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000581 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000582 return git_common.get_or_create_merge_base(self.GetBranch(),
583 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000584
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 def GetUpstreamBranch(self):
586 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000587 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000589 upstream_branch = upstream_branch.replace('refs/heads/',
590 'refs/remotes/%s/' % remote)
591 upstream_branch = upstream_branch.replace('refs/branch-heads/',
592 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000593 self.upstream_branch = upstream_branch
594 return self.upstream_branch
595
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000596 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000597 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000598 remote, branch = None, self.GetBranch()
599 seen_branches = set()
600 while branch not in seen_branches:
601 seen_branches.add(branch)
602 remote, branch = self.FetchUpstreamTuple(branch)
603 branch = ShortBranchName(branch)
604 if remote != '.' or branch.startswith('refs/remotes'):
605 break
606 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000607 remotes = RunGit(['remote'], error_ok=True).split()
608 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000609 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000610 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000611 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000612 logging.warning('Could not determine which remote this change is '
613 'associated with, so defaulting to "%s". This may '
614 'not be what you want. You may prevent this message '
615 'by running "git svn info" as documented here: %s',
616 self._remote,
617 GIT_INSTRUCTIONS_URL)
618 else:
619 logging.warn('Could not determine which remote this change is '
620 'associated with. You may prevent this message by '
621 'running "git svn info" as documented here: %s',
622 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000623 branch = 'HEAD'
624 if branch.startswith('refs/remotes'):
625 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000626 elif branch.startswith('refs/branch-heads/'):
627 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000628 else:
629 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000630 return self._remote
631
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000632 def GitSanityChecks(self, upstream_git_obj):
633 """Checks git repo status and ensures diff is from local commits."""
634
635 # Verify the commit we're diffing against is in our current branch.
636 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
637 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
638 if upstream_sha != common_ancestor:
639 print >> sys.stderr, (
640 'ERROR: %s is not in the current branch. You may need to rebase '
641 'your tracking branch' % upstream_sha)
642 return False
643
644 # List the commits inside the diff, and verify they are all local.
645 commits_in_diff = RunGit(
646 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
647 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
648 remote_branch = remote_branch.strip()
649 if code != 0:
650 _, remote_branch = self.GetRemoteBranch()
651
652 commits_in_remote = RunGit(
653 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
654
655 common_commits = set(commits_in_diff) & set(commits_in_remote)
656 if common_commits:
657 print >> sys.stderr, (
658 'ERROR: Your diff contains %d commits already in %s.\n'
659 'Run "git log --oneline %s..HEAD" to get a list of commits in '
660 'the diff. If you are using a custom git flow, you can override'
661 ' the reference used for this check with "git config '
662 'gitcl.remotebranch <git-ref>".' % (
663 len(common_commits), remote_branch, upstream_git_obj))
664 return False
665 return True
666
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000667 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000668 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000669
670 Returns None if it is not set.
671 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000672 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
673 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000674
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675 def GetRemoteUrl(self):
676 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
677
678 Returns None if there is no remote.
679 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000680 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000681 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
682
683 # If URL is pointing to a local directory, it is probably a git cache.
684 if os.path.isdir(url):
685 url = RunGit(['config', 'remote.%s.url' % remote],
686 error_ok=True,
687 cwd=url).strip()
688 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689
690 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000691 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000692 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000693 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000694 self.issue = int(issue) or None if issue else None
695 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 return self.issue
697
698 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000699 if not self.rietveld_server:
700 # If we're on a branch then get the server potentially associated
701 # with that branch.
702 if self.GetIssue():
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000703 rietveld_server_config = self._RietveldServer()
704 if rietveld_server_config:
705 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
706 ['config', rietveld_server_config], error_ok=True).strip())
evan@chromium.org0af9b702012-02-11 00:42:16 +0000707 if not self.rietveld_server:
708 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000709 return self.rietveld_server
710
711 def GetIssueURL(self):
712 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000713 if not self.GetIssue():
714 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
716
717 def GetDescription(self, pretty=False):
718 if not self.has_description:
719 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000720 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000721 try:
722 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000723 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000724 if e.code == 404:
725 DieWithError(
726 ('\nWhile fetching the description for issue %d, received a '
727 '404 (not found)\n'
728 'error. It is likely that you deleted this '
729 'issue on the server. If this is the\n'
730 'case, please run\n\n'
731 ' git cl issue 0\n\n'
732 'to clear the association with the deleted issue. Then run '
733 'this command again.') % issue)
734 else:
735 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000736 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000737 except urllib2.URLError as e:
738 print >> sys.stderr, (
739 'Warning: Failed to retrieve CL description due to network '
740 'failure.')
741 self.description = ''
742
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743 self.has_description = True
744 if pretty:
745 wrapper = textwrap.TextWrapper()
746 wrapper.initial_indent = wrapper.subsequent_indent = ' '
747 return wrapper.fill(self.description)
748 return self.description
749
750 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000751 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000752 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000753 patchset = RunGit(['config', self._PatchsetSetting()],
754 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000755 self.patchset = int(patchset) or None if patchset else None
756 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000757 return self.patchset
758
759 def SetPatchset(self, patchset):
760 """Set this branch's patchset. If patchset=0, clears the patchset."""
761 if patchset:
762 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000763 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000764 else:
765 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000766 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000767 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000768
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000769 def GetMostRecentPatchset(self):
770 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000771
772 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000773 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000774 '/download/issue%s_%s.diff' % (issue, patchset))
775
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000776 def GetIssueProperties(self):
777 if self._props is None:
778 issue = self.GetIssue()
779 if not issue:
780 self._props = {}
781 else:
782 self._props = self.RpcServer().get_issue_properties(issue, True)
783 return self._props
784
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000785 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000786 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000787
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000788 def SetIssue(self, issue):
789 """Set this branch's issue. If issue=0, clears the issue."""
790 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000791 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000792 RunGit(['config', self._IssueSetting(), str(issue)])
793 if self.rietveld_server:
794 RunGit(['config', self._RietveldServer(), self.rietveld_server])
795 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000796 current_issue = self.GetIssue()
797 if current_issue:
798 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000799 self.issue = None
800 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000802 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000803 if not self.GitSanityChecks(upstream_branch):
804 DieWithError('\nGit sanity check failure')
805
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000806 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000807 if not root:
808 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000809 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000810
811 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000812 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000813 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000814 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000815 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000816 except subprocess2.CalledProcessError:
817 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000818 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000819 'This branch probably doesn\'t exist anymore. To reset the\n'
820 'tracking branch, please run\n'
821 ' git branch --set-upstream %s trunk\n'
822 'replacing trunk with origin/master or the relevant branch') %
823 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000824
maruel@chromium.org52424302012-08-29 15:14:30 +0000825 issue = self.GetIssue()
826 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000827 if issue:
828 description = self.GetDescription()
829 else:
830 # If the change was never uploaded, use the log messages of all commits
831 # up to the branch point, as git cl upload will prefill the description
832 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000833 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
834 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000835
836 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000837 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000838 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000839 name,
840 description,
841 absroot,
842 files,
843 issue,
844 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000845 author,
846 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000847
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +0000848 def GetStatus(self):
849 """Apply a rough heuristic to give a simple summary of an issue's review
850 or CQ status, assuming adherence to a common workflow.
851
852 Returns None if no issue for this branch, or one of the following keywords:
853 * 'error' - error from review tool (including deleted issues)
854 * 'unsent' - not sent for review
855 * 'waiting' - waiting for review
856 * 'reply' - waiting for owner to reply to review
857 * 'lgtm' - LGTM from at least one approved reviewer
858 * 'commit' - in the commit queue
859 * 'closed' - closed
860 """
861 if not self.GetIssue():
862 return None
863
864 try:
865 props = self.GetIssueProperties()
866 except urllib2.HTTPError:
867 return 'error'
868
869 if props.get('closed'):
870 # Issue is closed.
871 return 'closed'
872 if props.get('commit'):
873 # Issue is in the commit queue.
874 return 'commit'
875
876 try:
877 reviewers = self.GetApprovingReviewers()
878 except urllib2.HTTPError:
879 return 'error'
880
881 if reviewers:
882 # Was LGTM'ed.
883 return 'lgtm'
884
885 messages = props.get('messages') or []
886
887 if not messages:
888 # No message was sent.
889 return 'unsent'
890 if messages[-1]['sender'] != props.get('owner_email'):
891 # Non-LGTM reply from non-owner
892 return 'reply'
893 return 'waiting'
894
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000895 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000896 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000897
898 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000899 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000900 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000901 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000902 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000903 except presubmit_support.PresubmitFailure, e:
904 DieWithError(
905 ('%s\nMaybe your depot_tools is out of date?\n'
906 'If all fails, contact maruel@') % e)
907
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000908 def UpdateDescription(self, description):
909 self.description = description
910 return self.RpcServer().update_description(
911 self.GetIssue(), self.description)
912
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000913 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000914 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000915 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000916
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000917 def SetFlag(self, flag, value):
918 """Patchset must match."""
919 if not self.GetPatchset():
920 DieWithError('The patchset needs to match. Send another patchset.')
921 try:
922 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000923 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000924 except urllib2.HTTPError, e:
925 if e.code == 404:
926 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
927 if e.code == 403:
928 DieWithError(
929 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
930 'match?') % (self.GetIssue(), self.GetPatchset()))
931 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000933 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934 """Returns an upload.RpcServer() to access this review's rietveld instance.
935 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000936 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000937 self._rpc_server = rietveld.CachingRietveld(
938 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000939 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940
941 def _IssueSetting(self):
942 """Return the git setting that stores this change's issue."""
943 return 'branch.%s.rietveldissue' % self.GetBranch()
944
945 def _PatchsetSetting(self):
946 """Return the git setting that stores this change's most recent patchset."""
947 return 'branch.%s.rietveldpatchset' % self.GetBranch()
948
949 def _RietveldServer(self):
950 """Returns the git setting that stores this change's rietveld server."""
szager@chromium.orgd62c61f2014-10-20 22:33:21 +0000951 branch = self.GetBranch()
952 if branch:
953 return 'branch.%s.rietveldserver' % branch
954 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000955
956
957def GetCodereviewSettingsInteractively():
958 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000959 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960 server = settings.GetDefaultServerUrl(error_ok=True)
961 prompt = 'Rietveld server (host[:port])'
962 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000963 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000964 if not server and not newserver:
965 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000966 if newserver:
967 newserver = gclient_utils.UpgradeToHttps(newserver)
968 if newserver != server:
969 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000971 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 prompt = caption
973 if initial:
974 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000975 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000976 if new_val == 'x':
977 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000978 elif new_val:
979 if is_url:
980 new_val = gclient_utils.UpgradeToHttps(new_val)
981 if new_val != initial:
982 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000983
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000984 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000985 SetProperty(settings.GetDefaultPrivateFlag(),
986 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000988 'tree-status-url', False)
989 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000990 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991
992 # TODO: configure a default branch to diff against, rather than this
993 # svn-based hackery.
994
995
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000996class ChangeDescription(object):
997 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000998 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000999 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001000
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001001 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +00001002 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001003
agable@chromium.org42c20792013-09-12 17:34:49 +00001004 @property # www.logilab.org/ticket/89786
1005 def description(self): # pylint: disable=E0202
1006 return '\n'.join(self._description_lines)
1007
1008 def set_description(self, desc):
1009 if isinstance(desc, basestring):
1010 lines = desc.splitlines()
1011 else:
1012 lines = [line.rstrip() for line in desc]
1013 while lines and not lines[0]:
1014 lines.pop(0)
1015 while lines and not lines[-1]:
1016 lines.pop(-1)
1017 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001018
piman@chromium.org336f9122014-09-04 02:16:55 +00001019 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001020 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001021 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001022 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001023 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001024 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001025
agable@chromium.org42c20792013-09-12 17:34:49 +00001026 # Get the set of R= and TBR= lines and remove them from the desciption.
1027 regexp = re.compile(self.R_LINE)
1028 matches = [regexp.match(line) for line in self._description_lines]
1029 new_desc = [l for i, l in enumerate(self._description_lines)
1030 if not matches[i]]
1031 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001032
agable@chromium.org42c20792013-09-12 17:34:49 +00001033 # Construct new unified R= and TBR= lines.
1034 r_names = []
1035 tbr_names = []
1036 for match in matches:
1037 if not match:
1038 continue
1039 people = cleanup_list([match.group(2).strip()])
1040 if match.group(1) == 'TBR':
1041 tbr_names.extend(people)
1042 else:
1043 r_names.extend(people)
1044 for name in r_names:
1045 if name not in reviewers:
1046 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001047 if add_owners_tbr:
1048 owners_db = owners.Database(change.RepositoryRoot(),
1049 fopen=file, os_path=os.path, glob=glob.glob)
1050 all_reviewers = set(tbr_names + reviewers)
1051 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1052 all_reviewers)
1053 tbr_names.extend(owners_db.reviewers_for(missing_files,
1054 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001055 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1056 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1057
1058 # Put the new lines in the description where the old first R= line was.
1059 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1060 if 0 <= line_loc < len(self._description_lines):
1061 if new_tbr_line:
1062 self._description_lines.insert(line_loc, new_tbr_line)
1063 if new_r_line:
1064 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001065 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001066 if new_r_line:
1067 self.append_footer(new_r_line)
1068 if new_tbr_line:
1069 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001070
1071 def prompt(self):
1072 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001073 self.set_description([
1074 '# Enter a description of the change.',
1075 '# This will be displayed on the codereview site.',
1076 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001077 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001078 '--------------------',
1079 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001080
agable@chromium.org42c20792013-09-12 17:34:49 +00001081 regexp = re.compile(self.BUG_LINE)
1082 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001083 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001084 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001085 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001086 if not content:
1087 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001088 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001089
1090 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001091 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1092 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001093 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001094 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001095
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001096 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001097 if self._description_lines:
1098 # Add an empty line if either the last line or the new line isn't a tag.
1099 last_line = self._description_lines[-1]
1100 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1101 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1102 self._description_lines.append('')
1103 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001104
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001105 def get_reviewers(self):
1106 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001107 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1108 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001109 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001110
1111
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001112def get_approving_reviewers(props):
1113 """Retrieves the reviewers that approved a CL from the issue properties with
1114 messages.
1115
1116 Note that the list may contain reviewers that are not committer, thus are not
1117 considered by the CQ.
1118 """
1119 return sorted(
1120 set(
1121 message['sender']
1122 for message in props['messages']
1123 if message['approval'] and message['sender'] in props['reviewers']
1124 )
1125 )
1126
1127
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128def FindCodereviewSettingsFile(filename='codereview.settings'):
1129 """Finds the given file starting in the cwd and going up.
1130
1131 Only looks up to the top of the repository unless an
1132 'inherit-review-settings-ok' file exists in the root of the repository.
1133 """
1134 inherit_ok_file = 'inherit-review-settings-ok'
1135 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001136 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1138 root = '/'
1139 while True:
1140 if filename in os.listdir(cwd):
1141 if os.path.isfile(os.path.join(cwd, filename)):
1142 return open(os.path.join(cwd, filename))
1143 if cwd == root:
1144 break
1145 cwd = os.path.dirname(cwd)
1146
1147
1148def LoadCodereviewSettingsFromFile(fileobj):
1149 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001150 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 def SetProperty(name, setting, unset_error_ok=False):
1153 fullname = 'rietveld.' + name
1154 if setting in keyvals:
1155 RunGit(['config', fullname, keyvals[setting]])
1156 else:
1157 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1158
1159 SetProperty('server', 'CODE_REVIEW_SERVER')
1160 # Only server setting is required. Other settings can be absent.
1161 # In that case, we ignore errors raised during option deletion attempt.
1162 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001163 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1165 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001166 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001167 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1168 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001169 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001170 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001172 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001173 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001174
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1176 #should be of the form
1177 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1178 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1179 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1180 keyvals['ORIGIN_URL_CONFIG']])
1181
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001182
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001183def urlretrieve(source, destination):
1184 """urllib is broken for SSL connections via a proxy therefore we
1185 can't use urllib.urlretrieve()."""
1186 with open(destination, 'w') as f:
1187 f.write(urllib2.urlopen(source).read())
1188
1189
ukai@chromium.org712d6102013-11-27 00:52:58 +00001190def hasSheBang(fname):
1191 """Checks fname is a #! script."""
1192 with open(fname) as f:
1193 return f.read(2).startswith('#!')
1194
1195
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001196def DownloadHooks(force):
1197 """downloads hooks
1198
1199 Args:
1200 force: True to update hooks. False to install hooks if not present.
1201 """
1202 if not settings.GetIsGerrit():
1203 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001204 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001205 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1206 if not os.access(dst, os.X_OK):
1207 if os.path.exists(dst):
1208 if not force:
1209 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001210 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001211 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001212 if not hasSheBang(dst):
1213 DieWithError('Not a script: %s\n'
1214 'You need to download from\n%s\n'
1215 'into .git/hooks/commit-msg and '
1216 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001217 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1218 except Exception:
1219 if os.path.exists(dst):
1220 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001221 DieWithError('\nFailed to download hooks.\n'
1222 'You need to download from\n%s\n'
1223 'into .git/hooks/commit-msg and '
1224 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001225
1226
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001227@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001229 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001231 parser.add_option('--activate-update', action='store_true',
1232 help='activate auto-updating [rietveld] section in '
1233 '.git/config')
1234 parser.add_option('--deactivate-update', action='store_true',
1235 help='deactivate auto-updating [rietveld] section in '
1236 '.git/config')
1237 options, args = parser.parse_args(args)
1238
1239 if options.deactivate_update:
1240 RunGit(['config', 'rietveld.autoupdate', 'false'])
1241 return
1242
1243 if options.activate_update:
1244 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1245 return
1246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247 if len(args) == 0:
1248 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001249 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 return 0
1251
1252 url = args[0]
1253 if not url.endswith('codereview.settings'):
1254 url = os.path.join(url, 'codereview.settings')
1255
1256 # Load code review settings and download hooks (if available).
1257 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001258 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 return 0
1260
1261
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001262def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001263 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001264 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1265 branch = ShortBranchName(branchref)
1266 _, args = parser.parse_args(args)
1267 if not args:
1268 print("Current base-url:")
1269 return RunGit(['config', 'branch.%s.base-url' % branch],
1270 error_ok=False).strip()
1271 else:
1272 print("Setting base-url to %s" % args[0])
1273 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1274 error_ok=False).strip()
1275
1276
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001277def color_for_status(status):
1278 """Maps a Changelist status to color, for CMDstatus and other tools."""
1279 return {
1280 'unsent': Fore.RED,
1281 'waiting': Fore.BLUE,
1282 'reply': Fore.YELLOW,
1283 'lgtm': Fore.GREEN,
1284 'commit': Fore.MAGENTA,
1285 'closed': Fore.CYAN,
1286 'error': Fore.WHITE,
1287 }.get(status, Fore.WHITE)
1288
1289
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001291 """Show status of changelists.
1292
1293 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001294 - Red not sent for review or broken
1295 - Blue waiting for review
1296 - Yellow waiting for you to reply to review
1297 - Green LGTM'ed
1298 - Magenta in the commit queue
1299 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001300
1301 Also see 'git cl comments'.
1302 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 parser.add_option('--field',
1304 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001305 parser.add_option('-f', '--fast', action='store_true',
1306 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001308 if args:
1309 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001312 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 if options.field.startswith('desc'):
1314 print cl.GetDescription()
1315 elif options.field == 'id':
1316 issueid = cl.GetIssue()
1317 if issueid:
1318 print issueid
1319 elif options.field == 'patch':
1320 patchset = cl.GetPatchset()
1321 if patchset:
1322 print patchset
1323 elif options.field == 'url':
1324 url = cl.GetIssueURL()
1325 if url:
1326 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001327 return 0
1328
1329 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1330 if not branches:
1331 print('No local branch found.')
1332 return 0
1333
1334 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001335 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001336 alignment = max(5, max(len(b) for b in branches))
1337 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001338 # Adhoc thread pool to request data concurrently.
1339 output = Queue.Queue()
1340
1341 # Silence upload.py otherwise it becomes unweldly.
1342 upload.verbosity = 0
1343
1344 if not options.fast:
1345 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001346 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001347 c = Changelist(branchref=b)
1348 i = c.GetIssueURL()
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001349 status = c.GetStatus()
1350 color = color_for_status(status)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001351
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001352 if i and (not status or status == 'error'):
1353 # The issue probably doesn't exist anymore.
1354 i += ' (broken)'
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001355
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001356 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001357
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001358 # Process one branch synchronously to work through authentication, then
1359 # spawn threads to process all the other branches in parallel.
1360 if branches:
1361 fetch(branches[0])
1362 threads = [
1363 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001364 for t in threads:
1365 t.daemon = True
1366 t.start()
1367 else:
1368 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1369 for b in branches:
1370 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001371 url = c.GetIssueURL()
1372 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001373
1374 tmp = {}
1375 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001376 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001377 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001378 b, i, color = output.get()
1379 tmp[b] = (i, color)
1380 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001381 reset = Fore.RESET
1382 if not sys.stdout.isatty():
1383 color = ''
1384 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001385 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001386 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001387
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001388 cl = Changelist()
1389 print
1390 print 'Current branch:',
1391 if not cl.GetIssue():
1392 print 'no issue assigned.'
1393 return 0
1394 print cl.GetBranch()
1395 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001396 if not options.fast:
1397 print 'Issue description:'
1398 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 return 0
1400
1401
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001402def colorize_CMDstatus_doc():
1403 """To be called once in main() to add colors to git cl status help."""
1404 colors = [i for i in dir(Fore) if i[0].isupper()]
1405
1406 def colorize_line(line):
1407 for color in colors:
1408 if color in line.upper():
1409 # Extract whitespaces first and the leading '-'.
1410 indent = len(line) - len(line.lstrip(' ')) + 1
1411 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1412 return line
1413
1414 lines = CMDstatus.__doc__.splitlines()
1415 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1416
1417
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001418@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001419def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001420 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
1422 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001423 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001424 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001425
1426 cl = Changelist()
1427 if len(args) > 0:
1428 try:
1429 issue = int(args[0])
1430 except ValueError:
1431 DieWithError('Pass a number to set the issue or none to list it.\n'
1432 'Maybe you want to run git cl status?')
1433 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001434 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001435 return 0
1436
1437
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001438def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001439 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001440 (_, args) = parser.parse_args(args)
1441 if args:
1442 parser.error('Unsupported argument: %s' % args)
1443
1444 cl = Changelist()
1445 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001446 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001447 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001448 if message['disapproval']:
1449 color = Fore.RED
1450 elif message['approval']:
1451 color = Fore.GREEN
1452 elif message['sender'] == data['owner_email']:
1453 color = Fore.MAGENTA
1454 else:
1455 color = Fore.BLUE
1456 print '\n%s%s %s%s' % (
1457 color, message['date'].split('.', 1)[0], message['sender'],
1458 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001459 if message['text'].strip():
1460 print '\n'.join(' ' + l for l in message['text'].splitlines())
1461 return 0
1462
1463
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001464def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001465 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001466 cl = Changelist()
1467 if not cl.GetIssue():
1468 DieWithError('This branch has no associated changelist.')
1469 description = ChangeDescription(cl.GetDescription())
1470 description.prompt()
1471 cl.UpdateDescription(description.description)
1472 return 0
1473
1474
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475def CreateDescriptionFromLog(args):
1476 """Pulls out the commit log to use as a base for the CL description."""
1477 log_args = []
1478 if len(args) == 1 and not args[0].endswith('.'):
1479 log_args = [args[0] + '..']
1480 elif len(args) == 1 and args[0].endswith('...'):
1481 log_args = [args[0][:-1]]
1482 elif len(args) == 2:
1483 log_args = [args[0] + '..' + args[1]]
1484 else:
1485 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001486 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001487
1488
thestig@chromium.org44202a22014-03-11 19:22:18 +00001489def CMDlint(parser, args):
1490 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001491 parser.add_option('--filter', action='append', metavar='-x,+y',
1492 help='Comma-separated list of cpplint\'s category-filters')
1493 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001494
1495 # Access to a protected member _XX of a client class
1496 # pylint: disable=W0212
1497 try:
1498 import cpplint
1499 import cpplint_chromium
1500 except ImportError:
1501 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1502 return 1
1503
1504 # Change the current working directory before calling lint so that it
1505 # shows the correct base.
1506 previous_cwd = os.getcwd()
1507 os.chdir(settings.GetRoot())
1508 try:
1509 cl = Changelist()
1510 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1511 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001512 if not files:
1513 print "Cannot lint an empty CL"
1514 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001515
1516 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001517 command = args + files
1518 if options.filter:
1519 command = ['--filter=' + ','.join(options.filter)] + command
1520 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001521
1522 white_regex = re.compile(settings.GetLintRegex())
1523 black_regex = re.compile(settings.GetLintIgnoreRegex())
1524 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1525 for filename in filenames:
1526 if white_regex.match(filename):
1527 if black_regex.match(filename):
1528 print "Ignoring file %s" % filename
1529 else:
1530 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1531 extra_check_functions)
1532 else:
1533 print "Skipping file %s" % filename
1534 finally:
1535 os.chdir(previous_cwd)
1536 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1537 if cpplint._cpplint_state.error_count != 0:
1538 return 1
1539 return 0
1540
1541
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001543 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001544 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001546 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001547 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001548 (options, args) = parser.parse_args(args)
1549
ukai@chromium.org259e4682012-10-25 07:36:33 +00001550 if not options.force and is_dirty_git_tree('presubmit'):
1551 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001552 return 1
1553
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001554 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001555 if args:
1556 base_branch = args[0]
1557 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001558 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001559 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001560
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001561 cl.RunHook(
1562 committing=not options.upload,
1563 may_prompt=False,
1564 verbose=options.verbose,
1565 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001566 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001567
1568
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001569def AddChangeIdToCommitMessage(options, args):
1570 """Re-commits using the current message, assumes the commit hook is in
1571 place.
1572 """
1573 log_desc = options.message or CreateDescriptionFromLog(args)
1574 git_command = ['commit', '--amend', '-m', log_desc]
1575 RunGit(git_command)
1576 new_log_desc = CreateDescriptionFromLog(args)
1577 if CHANGE_ID in new_log_desc:
1578 print 'git-cl: Added Change-Id to commit message.'
1579 else:
1580 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1581
1582
piman@chromium.org336f9122014-09-04 02:16:55 +00001583def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001584 """upload the current branch to gerrit."""
1585 # We assume the remote called "origin" is the one we want.
1586 # It is probably not worthwhile to support different workflows.
1587 remote = 'origin'
1588 branch = 'master'
1589 if options.target_branch:
1590 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001591
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001592 change_desc = ChangeDescription(
1593 options.message or CreateDescriptionFromLog(args))
1594 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001595 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001596 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001597 if CHANGE_ID not in change_desc.description:
1598 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001599
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001600 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001601 if len(commits) > 1:
1602 print('WARNING: This will upload %d commits. Run the following command '
1603 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001604 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001605 print('You can also use `git squash-branch` to squash these into a single'
1606 'commit.')
1607 ask_for_data('About to upload; enter to confirm.')
1608
piman@chromium.org336f9122014-09-04 02:16:55 +00001609 if options.reviewers or options.tbr_owners:
1610 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001611
ukai@chromium.orge8077812012-02-03 03:41:46 +00001612 receive_options = []
1613 cc = cl.GetCCList().split(',')
1614 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001615 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001616 cc = filter(None, cc)
1617 if cc:
1618 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001619 if change_desc.get_reviewers():
1620 receive_options.extend(
1621 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001622
ukai@chromium.orge8077812012-02-03 03:41:46 +00001623 git_command = ['push']
1624 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001625 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001626 ' '.join(receive_options))
1627 git_command += [remote, 'HEAD:refs/for/' + branch]
1628 RunGit(git_command)
1629 # TODO(ukai): parse Change-Id: and set issue number?
1630 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001631
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001632
piman@chromium.org336f9122014-09-04 02:16:55 +00001633def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001634 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001635 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1636 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001637 if options.emulate_svn_auto_props:
1638 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639
1640 change_desc = None
1641
pgervais@chromium.org91141372014-01-09 23:27:20 +00001642 if options.email is not None:
1643 upload_args.extend(['--email', options.email])
1644
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001646 if options.title:
1647 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001648 if options.message:
1649 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001650 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001651 print ("This branch is associated with issue %s. "
1652 "Adding patch to that issue." % cl.GetIssue())
1653 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001654 if options.title:
1655 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001656 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001657 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00001658 if options.reviewers or options.tbr_owners:
1659 change_desc.update_reviewers(options.reviewers,
1660 options.tbr_owners,
1661 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001662 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001663 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001664
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001665 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001666 print "Description is empty; aborting."
1667 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001668
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001669 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001670 if change_desc.get_reviewers():
1671 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001672 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001673 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001674 DieWithError("Must specify reviewers to send email.")
1675 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001676
1677 # We check this before applying rietveld.private assuming that in
1678 # rietveld.cc only addresses which we can send private CLs to are listed
1679 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1680 # --private is specified explicitly on the command line.
1681 if options.private:
1682 logging.warn('rietveld.cc is ignored since private flag is specified. '
1683 'You need to review and add them manually if necessary.')
1684 cc = cl.GetCCListWithoutDefault()
1685 else:
1686 cc = cl.GetCCList()
1687 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001688 if cc:
1689 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001690
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001691 if options.private or settings.GetDefaultPrivateFlag() == "True":
1692 upload_args.append('--private')
1693
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001694 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001695 if not options.find_copies:
1696 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001697
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001698 # Include the upstream repo's URL in the change -- this is useful for
1699 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001700 remote_url = cl.GetGitBaseUrlFromConfig()
1701 if not remote_url:
1702 if settings.GetIsGitSvn():
1703 # URL is dependent on the current directory.
1704 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1705 if data:
1706 keys = dict(line.split(': ', 1) for line in data.splitlines()
1707 if ': ' in line)
1708 remote_url = keys.get('URL', None)
1709 else:
jam@chromium.org80c51ae2014-10-17 18:43:02 +00001710 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1711 remote_url = (cl.GetRemoteUrl() + '@'
1712 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001713 if remote_url:
1714 upload_args.extend(['--base_url', remote_url])
1715
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001716 project = settings.GetProject()
1717 if project:
1718 upload_args.extend(['--project', project])
1719
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001720 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001721 upload_args = ['upload'] + upload_args + args
1722 logging.info('upload.RealMain(%s)', upload_args)
1723 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001724 issue = int(issue)
1725 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001726 except KeyboardInterrupt:
1727 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001728 except:
1729 # If we got an exception after the user typed a description for their
1730 # change, back up the description before re-raising.
1731 if change_desc:
1732 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1733 print '\nGot exception while uploading -- saving description to %s\n' \
1734 % backup_path
1735 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001736 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001737 backup_file.close()
1738 raise
1739
1740 if not cl.GetIssue():
1741 cl.SetIssue(issue)
1742 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001743
1744 if options.use_commit_queue:
1745 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001746 return 0
1747
1748
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001749def cleanup_list(l):
1750 """Fixes a list so that comma separated items are put as individual items.
1751
1752 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1753 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1754 """
1755 items = sum((i.split(',') for i in l), [])
1756 stripped_items = (i.strip() for i in items)
1757 return sorted(filter(None, stripped_items))
1758
1759
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001760@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001761def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001762 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001763 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1764 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001765 parser.add_option('--bypass-watchlists', action='store_true',
1766 dest='bypass_watchlists',
1767 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001768 parser.add_option('-f', action='store_true', dest='force',
1769 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001770 parser.add_option('-m', dest='message', help='message for patchset')
1771 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001772 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001773 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001774 help='reviewer email addresses')
1775 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001776 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001777 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001778 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001779 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001780 parser.add_option('--emulate_svn_auto_props',
1781 '--emulate-svn-auto-props',
1782 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001783 dest="emulate_svn_auto_props",
1784 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001785 parser.add_option('-c', '--use-commit-queue', action='store_true',
1786 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001787 parser.add_option('--private', action='store_true',
1788 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001789 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001790 '--target-branch',
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001791 help='When uploading to gerrit, remote branch to '
1792 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001793 parser.add_option('--email', default=None,
1794 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00001795 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
1796 help='add a set of OWNERS to TBR')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001797
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001798 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001799 (options, args) = parser.parse_args(args)
1800
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001801 if options.target_branch and not settings.GetIsGerrit():
1802 parser.error('Use --target_branch for non gerrit repository.')
1803
ukai@chromium.org259e4682012-10-25 07:36:33 +00001804 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001805 return 1
1806
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001807 options.reviewers = cleanup_list(options.reviewers)
1808 options.cc = cleanup_list(options.cc)
1809
ukai@chromium.orge8077812012-02-03 03:41:46 +00001810 cl = Changelist()
1811 if args:
1812 # TODO(ukai): is it ok for gerrit case?
1813 base_branch = args[0]
1814 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001815 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001816 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001817 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001818
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001819 # Apply watchlists on upload.
1820 change = cl.GetChange(base_branch, None)
1821 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1822 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001823 if not options.bypass_watchlists:
1824 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001825
ukai@chromium.orge8077812012-02-03 03:41:46 +00001826 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00001827 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001828 # Set the reviewer list now so that presubmit checks can access it.
1829 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00001830 change_description.update_reviewers(options.reviewers,
1831 options.tbr_owners,
1832 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001833 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001834 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001835 may_prompt=not options.force,
1836 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001837 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001838 if not hook_results.should_continue():
1839 return 1
1840 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001841 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001842
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001843 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001844 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001845 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001846 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001847 print ('The last upload made from this repository was patchset #%d but '
1848 'the most recent patchset on the server is #%d.'
1849 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001850 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1851 'from another machine or branch the patch you\'re uploading now '
1852 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001853 ask_for_data('About to upload; enter to confirm.')
1854
iannucci@chromium.org79540052012-10-19 23:15:26 +00001855 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001856 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00001857 return GerritUpload(options, args, cl, change)
1858 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001859 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001860 git_set_branch_value('last-upload-hash',
1861 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001862
1863 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001864
1865
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001866def IsSubmoduleMergeCommit(ref):
1867 # When submodules are added to the repo, we expect there to be a single
1868 # non-git-svn merge commit at remote HEAD with a signature comment.
1869 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001870 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001871 return RunGit(cmd) != ''
1872
1873
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001874def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001875 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001877 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001878 Updates changelog with metadata (e.g. pointer to review).
1879 Pushes/dcommits the code upstream.
1880 Updates review and closes.
1881 """
1882 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1883 help='bypass upload presubmit hook')
1884 parser.add_option('-m', dest='message',
1885 help="override review description")
1886 parser.add_option('-f', action='store_true', dest='force',
1887 help="force yes to questions (don't prompt)")
1888 parser.add_option('-c', dest='contributor',
1889 help="external contributor for patch (appended to " +
1890 "description and used as author for git). Should be " +
1891 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001892 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001893 (options, args) = parser.parse_args(args)
1894 cl = Changelist()
1895
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001896 current = cl.GetBranch()
1897 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1898 if not settings.GetIsGitSvn() and remote == '.':
1899 print
1900 print 'Attempting to push branch %r into another local branch!' % current
1901 print
1902 print 'Either reparent this branch on top of origin/master:'
1903 print ' git reparent-branch --root'
1904 print
1905 print 'OR run `git rebase-update` if you think the parent branch is already'
1906 print 'committed.'
1907 print
1908 print ' Current parent: %r' % upstream_branch
1909 return 1
1910
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001911 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001912 # Default to merging against our best guess of the upstream branch.
1913 args = [cl.GetUpstreamBranch()]
1914
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001915 if options.contributor:
1916 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1917 print "Please provide contibutor as 'First Last <email@example.com>'"
1918 return 1
1919
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001920 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001921 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001922
ukai@chromium.org259e4682012-10-25 07:36:33 +00001923 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001924 return 1
1925
1926 # This rev-list syntax means "show all commits not in my branch that
1927 # are in base_branch".
1928 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1929 base_branch]).splitlines()
1930 if upstream_commits:
1931 print ('Base branch "%s" has %d commits '
1932 'not in this branch.' % (base_branch, len(upstream_commits)))
1933 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1934 return 1
1935
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001936 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001937 svn_head = None
1938 if cmd == 'dcommit' or base_has_submodules:
1939 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1940 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001941
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001942 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001943 # If the base_head is a submodule merge commit, the first parent of the
1944 # base_head should be a git-svn commit, which is what we're interested in.
1945 base_svn_head = base_branch
1946 if base_has_submodules:
1947 base_svn_head += '^1'
1948
1949 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001950 if extra_commits:
1951 print ('This branch has %d additional commits not upstreamed yet.'
1952 % len(extra_commits.splitlines()))
1953 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1954 'before attempting to %s.' % (base_branch, cmd))
1955 return 1
1956
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001957 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001958 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001959 author = None
1960 if options.contributor:
1961 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001962 hook_results = cl.RunHook(
1963 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001964 may_prompt=not options.force,
1965 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001966 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001967 if not hook_results.should_continue():
1968 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001969
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001970 # Check the tree status if the tree status URL is set.
1971 status = GetTreeStatus()
1972 if 'closed' == status:
1973 print('The tree is closed. Please wait for it to reopen. Use '
1974 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1975 return 1
1976 elif 'unknown' == status:
1977 print('Unable to determine tree status. Please verify manually and '
1978 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1979 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00001980 else:
1981 breakpad.SendStack(
1982 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001983 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1984 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001985 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001986
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001987 change_desc = ChangeDescription(options.message)
1988 if not change_desc.description and cl.GetIssue():
1989 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001990
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001991 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001992 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001993 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001994 else:
1995 print 'No description set.'
1996 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1997 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001998
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001999 # Keep a separate copy for the commit message, because the commit message
2000 # contains the link to the Rietveld issue, while the Rietveld message contains
2001 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002002 # Keep a separate copy for the commit message.
2003 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00002004 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00002005
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002006 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00002007 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002008 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002010 commit_desc.append_footer('Patch from %s.' % options.contributor)
2011
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002012 print('Description:')
2013 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002014
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002015 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002016 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002017 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002018
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002019 # We want to squash all this branch's commits into one commit with the proper
2020 # description. We do this by doing a "reset --soft" to the base branch (which
2021 # keeps the working copy the same), then dcommitting that. If origin/master
2022 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2023 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002024 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002025 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2026 # Delete the branches if they exist.
2027 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2028 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2029 result = RunGitWithCode(showref_cmd)
2030 if result[0] == 0:
2031 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032
2033 # We might be in a directory that's present in this branch but not in the
2034 # trunk. Move up to the top of the tree so that git commands that expect a
2035 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002036 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002037 if rel_base_path:
2038 os.chdir(rel_base_path)
2039
2040 # Stuff our change into the merge branch.
2041 # We wrap in a try...finally block so if anything goes wrong,
2042 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002043 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002044 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002045 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002046 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002047 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002048 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002049 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002050 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002051 RunGit(
2052 [
2053 'commit', '--author', options.contributor,
2054 '-m', commit_desc.description,
2055 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002056 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002057 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002058 if base_has_submodules:
2059 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2060 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2061 RunGit(['checkout', CHERRY_PICK_BRANCH])
2062 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002063 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002064 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002065 pending_prefix = settings.GetPendingRefPrefix()
2066 if not pending_prefix or branch.startswith(pending_prefix):
2067 # If not using refs/pending/heads/* at all, or target ref is already set
2068 # to pending, then push to the target ref directly.
2069 retcode, output = RunGitWithCode(
2070 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002071 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002072 else:
2073 # Cherry-pick the change on top of pending ref and then push it.
2074 assert branch.startswith('refs/'), branch
2075 assert pending_prefix[-1] == '/', pending_prefix
2076 pending_ref = pending_prefix + branch[len('refs/'):]
2077 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002078 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002079 if retcode == 0:
2080 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002081 else:
2082 # dcommit the merge branch.
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002083 _, output = RunGitWithCode(['svn', 'dcommit',
2084 '-C%s' % options.similarity,
2085 '--no-rebase', '--rmdir'])
2086 if 'Committed r' in output:
2087 revision = re.match(
2088 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2089 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002090 finally:
2091 # And then swap back to the original branch and clean up.
2092 RunGit(['checkout', '-q', cl.GetBranch()])
2093 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002094 if base_has_submodules:
2095 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002096
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002097 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002098 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002099 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002100
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002101 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002102 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002103 try:
2104 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2105 # We set pushed_to_pending to False, since it made it all the way to the
2106 # real ref.
2107 pushed_to_pending = False
2108 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002109 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002110
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002112 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002113 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002114 if not to_pending:
2115 if viewvc_url and revision:
2116 change_desc.append_footer(
2117 'Committed: %s%s' % (viewvc_url, revision))
2118 elif revision:
2119 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002120 print ('Closing issue '
2121 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002122 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002124 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002125 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002126 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
mark@chromium.org782570c2014-09-26 21:48:02 +00002127 patch_num, props['patchsets'][-1], to_pending, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002128 if options.bypass_hooks:
2129 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2130 else:
2131 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002132 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002133 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002134
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002135 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002136 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2137 print 'The commit is in the pending queue (%s).' % pending_ref
2138 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002139 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002140 'footer.' % branch)
2141
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002142 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2143 if os.path.isfile(hook):
2144 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002145
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002146 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002147
2148
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002149def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2150 print
2151 print 'Waiting for commit to be landed on %s...' % real_ref
2152 print '(If you are impatient, you may Ctrl-C once without harm)'
2153 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2154 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2155
2156 loop = 0
2157 while True:
2158 sys.stdout.write('fetching (%d)... \r' % loop)
2159 sys.stdout.flush()
2160 loop += 1
2161
2162 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2163 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2164 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2165 for commit in commits.splitlines():
2166 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2167 print 'Found commit on %s' % real_ref
2168 return commit
2169
2170 current_rev = to_rev
2171
2172
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002173def PushToGitPending(remote, pending_ref, upstream_ref):
2174 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2175
2176 Returns:
2177 (retcode of last operation, output log of last operation).
2178 """
2179 assert pending_ref.startswith('refs/'), pending_ref
2180 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2181 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2182 code = 0
2183 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002184 max_attempts = 3
2185 attempts_left = max_attempts
2186 while attempts_left:
2187 if attempts_left != max_attempts:
2188 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2189 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002190
2191 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002192 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002193 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002194 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002195 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002196 print 'Fetch failed with exit code %d.' % code
2197 if out.strip():
2198 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002199 continue
2200
2201 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002202 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002203 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002204 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002205 if code:
2206 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002207 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2208 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002209 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2210 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002211 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002212 return code, out
2213
2214 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002215 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002216 code, out = RunGitWithCode(
2217 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2218 if code == 0:
2219 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002220 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002221 return code, out
2222
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002223 print 'Push failed with exit code %d.' % code
2224 if out.strip():
2225 print out.strip()
2226 if IsFatalPushFailure(out):
2227 print (
2228 'Fatal push error. Make sure your .netrc credentials and git '
2229 'user.email are correct and you have push access to the repo.')
2230 return code, out
2231
2232 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002233 return code, out
2234
2235
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002236def IsFatalPushFailure(push_stdout):
2237 """True if retrying push won't help."""
2238 return '(prohibited by Gerrit)' in push_stdout
2239
2240
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002241@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002242def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002243 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002244 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002245 message = """This doesn't appear to be an SVN repository.
2246If your project has a git mirror with an upstream SVN master, you probably need
2247to run 'git svn init', see your project's git mirror documentation.
2248If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002249to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002250Choose wisely, if you get this wrong, your commit might appear to succeed but
2251will instead be silently ignored."""
2252 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002253 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002254 return SendUpstream(parser, args, 'dcommit')
2255
2256
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002257@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002258def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002259 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002260 if settings.GetIsGitSvn():
2261 print('This appears to be an SVN repository.')
2262 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002263 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002264 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002265
2266
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002267@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002268def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002269 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002270 parser.add_option('-b', dest='newbranch',
2271 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002272 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002273 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002274 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2275 help='Change to the directory DIR immediately, '
2276 'before doing anything else.')
2277 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002278 help='failed patches spew .rej files rather than '
2279 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002280 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2281 help="don't commit after patch applies")
2282 (options, args) = parser.parse_args(args)
2283 if len(args) != 1:
2284 parser.print_help()
2285 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002286 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002287
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002288 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002289 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002290
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002291 if options.newbranch:
2292 if options.force:
2293 RunGit(['branch', '-D', options.newbranch],
2294 stderr=subprocess2.PIPE, error_ok=True)
2295 RunGit(['checkout', '-b', options.newbranch,
2296 Changelist().GetUpstreamBranch()])
2297
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002298 return PatchIssue(issue_arg, options.reject, options.nocommit,
2299 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002300
2301
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002302def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002303 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002304 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002305 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002306 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002307 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002308 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002309 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002310 # Assume it's a URL to the patch. Default to https.
2311 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002312 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002313 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002314 DieWithError('Must pass an issue ID or full URL for '
2315 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002316 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002317 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002318 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002319
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002320 # Switch up to the top-level directory, if necessary, in preparation for
2321 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002322 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002323 if top:
2324 os.chdir(top)
2325
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002326 # Git patches have a/ at the beginning of source paths. We strip that out
2327 # with a sed script rather than the -p flag to patch so we can feed either
2328 # Git or svn-style patches into the same apply command.
2329 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002330 try:
2331 patch_data = subprocess2.check_output(
2332 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2333 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002334 DieWithError('Git patch mungling failed.')
2335 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002336
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002337 # We use "git apply" to apply the patch instead of "patch" so that we can
2338 # pick up file adds.
2339 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002340 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002341 if directory:
2342 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002343 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002344 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002345 elif IsGitVersionAtLeast('1.7.12'):
2346 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002347 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002348 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002349 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002350 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002351 DieWithError('Failed to apply the patch')
2352
2353 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002354 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002355 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2356 cl = Changelist()
2357 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002358 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002359 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002360 else:
2361 print "Patch applied to index."
2362 return 0
2363
2364
2365def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002366 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002367 # Provide a wrapper for git svn rebase to help avoid accidental
2368 # git svn dcommit.
2369 # It's the only command that doesn't use parser at all since we just defer
2370 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002371
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002372 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002373
2374
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002375def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002376 """Fetches the tree status and returns either 'open', 'closed',
2377 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002378 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002379 if url:
2380 status = urllib2.urlopen(url).read().lower()
2381 if status.find('closed') != -1 or status == '0':
2382 return 'closed'
2383 elif status.find('open') != -1 or status == '1':
2384 return 'open'
2385 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002386 return 'unset'
2387
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002388
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002389def GetTreeStatusReason():
2390 """Fetches the tree status from a json url and returns the message
2391 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002392 url = settings.GetTreeStatusUrl()
2393 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002394 connection = urllib2.urlopen(json_url)
2395 status = json.loads(connection.read())
2396 connection.close()
2397 return status['message']
2398
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002399
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002400def GetBuilderMaster(bot_list):
2401 """For a given builder, fetch the master from AE if available."""
2402 map_url = 'https://builders-map.appspot.com/'
2403 try:
2404 master_map = json.load(urllib2.urlopen(map_url))
2405 except urllib2.URLError as e:
2406 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2407 (map_url, e))
2408 except ValueError as e:
2409 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2410 if not master_map:
2411 return None, 'Failed to build master map.'
2412
2413 result_master = ''
2414 for bot in bot_list:
2415 builder = bot.split(':', 1)[0]
2416 master_list = master_map.get(builder, [])
2417 if not master_list:
2418 return None, ('No matching master for builder %s.' % builder)
2419 elif len(master_list) > 1:
2420 return None, ('The builder name %s exists in multiple masters %s.' %
2421 (builder, master_list))
2422 else:
2423 cur_master = master_list[0]
2424 if not result_master:
2425 result_master = cur_master
2426 elif result_master != cur_master:
2427 return None, 'The builders do not belong to the same master.'
2428 return result_master, None
2429
2430
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002431def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002432 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002433 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002434 status = GetTreeStatus()
2435 if 'unset' == status:
2436 print 'You must configure your tree status URL by running "git cl config".'
2437 return 2
2438
2439 print "The tree is %s" % status
2440 print
2441 print GetTreeStatusReason()
2442 if status != 'open':
2443 return 1
2444 return 0
2445
2446
maruel@chromium.org15192402012-09-06 12:38:29 +00002447def CMDtry(parser, args):
2448 """Triggers a try job through Rietveld."""
2449 group = optparse.OptionGroup(parser, "Try job options")
2450 group.add_option(
2451 "-b", "--bot", action="append",
2452 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2453 "times to specify multiple builders. ex: "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002454 "'-b win_rel:ui_tests,webkit_unit_tests -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002455 "the try server waterfall for the builders name and the tests "
2456 "available. Can also be used to specify gtest_filter, e.g. "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002457 "-b win_rel:base_unittests:ValuesTest.*Value"))
maruel@chromium.org15192402012-09-06 12:38:29 +00002458 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002459 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002460 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002461 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002462 "-r", "--revision",
2463 help="Revision to use for the try job; default: the "
2464 "revision will be determined by the try server; see "
2465 "its waterfall for more info")
2466 group.add_option(
2467 "-c", "--clobber", action="store_true", default=False,
2468 help="Force a clobber before building; e.g. don't do an "
2469 "incremental build")
2470 group.add_option(
2471 "--project",
2472 help="Override which project to use. Projects are defined "
2473 "server-side to define what default bot set to use")
2474 group.add_option(
2475 "-t", "--testfilter", action="append", default=[],
2476 help=("Apply a testfilter to all the selected builders. Unless the "
2477 "builders configurations are similar, use multiple "
2478 "--bot <builder>:<test> arguments."))
2479 group.add_option(
2480 "-n", "--name", help="Try job name; default to current branch name")
2481 parser.add_option_group(group)
2482 options, args = parser.parse_args(args)
2483
2484 if args:
2485 parser.error('Unknown arguments: %s' % args)
2486
2487 cl = Changelist()
2488 if not cl.GetIssue():
2489 parser.error('Need to upload first')
2490
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002491 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002492 if props.get('closed'):
2493 parser.error('Cannot send tryjobs for a closed CL')
2494
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002495 if props.get('private'):
2496 parser.error('Cannot use trybots with private issue')
2497
maruel@chromium.org15192402012-09-06 12:38:29 +00002498 if not options.name:
2499 options.name = cl.GetBranch()
2500
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002501 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002502 options.master, err_msg = GetBuilderMaster(options.bot)
2503 if err_msg:
2504 parser.error('Tryserver master cannot be found because: %s\n'
2505 'Please manually specify the tryserver master'
2506 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002507
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002508 def GetMasterMap():
2509 # Process --bot and --testfilter.
2510 if not options.bot:
2511 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002512
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002513 # Get try masters from PRESUBMIT.py files.
2514 masters = presubmit_support.DoGetTryMasters(
2515 change,
2516 change.LocalPaths(),
2517 settings.GetRoot(),
2518 None,
2519 None,
2520 options.verbose,
2521 sys.stdout)
2522 if masters:
2523 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002524
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002525 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2526 options.bot = presubmit_support.DoGetTrySlaves(
2527 change,
2528 change.LocalPaths(),
2529 settings.GetRoot(),
2530 None,
2531 None,
2532 options.verbose,
2533 sys.stdout)
2534 if not options.bot:
2535 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002536
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002537 builders_and_tests = {}
2538 # TODO(machenbach): The old style command-line options don't support
2539 # multiple try masters yet.
2540 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2541 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2542
2543 for bot in old_style:
2544 if ':' in bot:
2545 builder, tests = bot.split(':', 1)
2546 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2547 elif ',' in bot:
2548 parser.error('Specify one bot per --bot flag')
2549 else:
2550 builders_and_tests.setdefault(bot, []).append('defaulttests')
2551
2552 for bot, tests in new_style:
2553 builders_and_tests.setdefault(bot, []).extend(tests)
2554
2555 # Return a master map with one master to be backwards compatible. The
2556 # master name defaults to an empty string, which will cause the master
2557 # not to be set on rietveld (deprecated).
2558 return {options.master: builders_and_tests}
2559
2560 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002561
maruel@chromium.org15192402012-09-06 12:38:29 +00002562 if options.testfilter:
2563 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002564 masters = dict((master, dict(
2565 (b, forced_tests) for b, t in slaves.iteritems()
2566 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002567
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002568 for builders in masters.itervalues():
2569 if any('triggered' in b for b in builders):
2570 print >> sys.stderr, (
2571 'ERROR You are trying to send a job to a triggered bot. This type of'
2572 ' bot requires an\ninitial job from a parent (usually a builder). '
2573 'Instead send your job to the parent.\n'
2574 'Bot list: %s' % builders)
2575 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002576
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002577 patchset = cl.GetMostRecentPatchset()
2578 if patchset and patchset != cl.GetPatchset():
2579 print(
2580 '\nWARNING Mismatch between local config and server. Did a previous '
2581 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2582 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002583 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002584 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002585 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002586 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002587 except urllib2.HTTPError, e:
2588 if e.code == 404:
2589 print('404 from rietveld; '
2590 'did you mean to use "git try" instead of "git cl try"?')
2591 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002592 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002593
2594 for (master, builders) in masters.iteritems():
2595 if master:
2596 print 'Master: %s' % master
2597 length = max(len(builder) for builder in builders)
2598 for builder in sorted(builders):
2599 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002600 return 0
2601
2602
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002603@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002604def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002605 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002606 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002607 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002608 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002609
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002610 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002611 if args:
2612 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002613 branch = cl.GetBranch()
2614 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002615 cl = Changelist()
2616 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002617
2618 # Clear configured merge-base, if there is one.
2619 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002620 else:
2621 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002622 return 0
2623
2624
thestig@chromium.org00858c82013-12-02 23:08:03 +00002625def CMDweb(parser, args):
2626 """Opens the current CL in the web browser."""
2627 _, args = parser.parse_args(args)
2628 if args:
2629 parser.error('Unrecognized args: %s' % ' '.join(args))
2630
2631 issue_url = Changelist().GetIssueURL()
2632 if not issue_url:
2633 print >> sys.stderr, 'ERROR No issue to open'
2634 return 1
2635
2636 webbrowser.open(issue_url)
2637 return 0
2638
2639
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002640def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002641 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002642 _, args = parser.parse_args(args)
2643 if args:
2644 parser.error('Unrecognized args: %s' % ' '.join(args))
2645 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002646 props = cl.GetIssueProperties()
2647 if props.get('private'):
2648 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002649 cl.SetFlag('commit', '1')
2650 return 0
2651
2652
groby@chromium.org411034a2013-02-26 15:12:01 +00002653def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002654 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002655 _, args = parser.parse_args(args)
2656 if args:
2657 parser.error('Unrecognized args: %s' % ' '.join(args))
2658 cl = Changelist()
2659 # Ensure there actually is an issue to close.
2660 cl.GetDescription()
2661 cl.CloseIssue()
2662 return 0
2663
2664
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002665def CMDdiff(parser, args):
2666 """shows differences between local tree and last upload."""
2667 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002668 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002669 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002670 if not issue:
2671 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002672 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002673 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002674
2675 # Create a new branch based on the merge-base
2676 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2677 try:
2678 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002679 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002680 if rtn != 0:
2681 return rtn
2682
2683 # Switch back to starting brand and diff against the temporary
2684 # branch containing the latest rietveld patch.
2685 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2686 finally:
2687 RunGit(['checkout', '-q', branch])
2688 RunGit(['branch', '-D', TMP_BRANCH])
2689
2690 return 0
2691
2692
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002693def CMDowners(parser, args):
2694 """interactively find the owners for reviewing"""
2695 parser.add_option(
2696 '--no-color',
2697 action='store_true',
2698 help='Use this option to disable color output')
2699 options, args = parser.parse_args(args)
2700
2701 author = RunGit(['config', 'user.email']).strip() or None
2702
2703 cl = Changelist()
2704
2705 if args:
2706 if len(args) > 1:
2707 parser.error('Unknown args')
2708 base_branch = args[0]
2709 else:
2710 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002711 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002712
2713 change = cl.GetChange(base_branch, None)
2714 return owners_finder.OwnersFinder(
2715 [f.LocalPath() for f in
2716 cl.GetChange(base_branch, None).AffectedFiles()],
2717 change.RepositoryRoot(), author,
2718 fopen=file, os_path=os.path, glob=glob.glob,
2719 disable_color=options.no_color).run()
2720
2721
enne@chromium.org555cfe42014-01-29 18:21:39 +00002722@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002723def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002724 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002725 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002726 parser.add_option('--full', action='store_true',
2727 help='Reformat the full content of all touched files')
2728 parser.add_option('--dry-run', action='store_true',
2729 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002730 parser.add_option('--diff', action='store_true',
2731 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002732 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002733
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002734 # git diff generates paths against the root of the repository. Change
2735 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002736 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002737 if rel_base_path:
2738 os.chdir(rel_base_path)
2739
digit@chromium.org29e47272013-05-17 17:01:46 +00002740 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002741 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002742 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002743 # Only list the names of modified files.
2744 diff_cmd.append('--name-only')
2745 else:
2746 # Only generate context-less patches.
2747 diff_cmd.append('-U0')
2748
2749 # Grab the merge-base commit, i.e. the upstream commit of the current
2750 # branch when it was created or the last time it was rebased. This is
2751 # to cover the case where the user may have called "git fetch origin",
2752 # moving the origin branch to a newer commit, but hasn't rebased yet.
2753 upstream_commit = None
2754 cl = Changelist()
2755 upstream_branch = cl.GetUpstreamBranch()
2756 if upstream_branch:
2757 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2758 upstream_commit = upstream_commit.strip()
2759
2760 if not upstream_commit:
2761 DieWithError('Could not find base commit for this branch. '
2762 'Are you in detached state?')
2763
2764 diff_cmd.append(upstream_commit)
2765
2766 # Handle source file filtering.
2767 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002768 if args:
2769 for arg in args:
2770 if os.path.isdir(arg):
2771 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2772 elif os.path.isfile(arg):
2773 diff_cmd.append(arg)
2774 else:
2775 DieWithError('Argument "%s" is not a file or a directory' % arg)
2776 else:
2777 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002778 diff_output = RunGit(diff_cmd)
2779
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002780 top_dir = os.path.normpath(
2781 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2782
2783 # Locate the clang-format binary in the checkout
2784 try:
2785 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2786 except clang_format.NotFoundError, e:
2787 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002788
digit@chromium.org29e47272013-05-17 17:01:46 +00002789 if opts.full:
2790 # diff_output is a list of files to send to clang-format.
2791 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002792 if not files:
2793 print "Nothing to format."
2794 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002795 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002796 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002797 cmd.append('-i')
2798 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002799 if opts.diff:
2800 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002801 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002802 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00002803 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00002804 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002805 try:
2806 script = clang_format.FindClangFormatScriptInChromiumTree(
2807 'clang-format-diff.py')
2808 except clang_format.NotFoundError, e:
2809 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002810
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002811 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002812 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002813 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002814
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002815 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002816 if opts.diff:
2817 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002818 if opts.dry_run and len(stdout) > 0:
2819 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002820
2821 return 0
2822
2823
maruel@chromium.org29404b52014-09-08 22:58:00 +00002824def CMDlol(parser, args):
2825 # This command is intentionally undocumented.
maruel@chromium.org552c31b2014-09-10 11:08:06 +00002826 print('\n'.join((
2827 ' / /',
2828 ' (\\/_//`)',
2829 ' / \'/',
2830 ' 0 0 \\',
2831 ' / \\',
2832 ' / __/ \\',
2833 ' /, _/ \\ \\_',
2834 ' `-./ ) | ~^~^~^~^~^~^~^~\\~.',
2835 ' ( / \\_}',
2836 ' | / |',
2837 ' ; | \\ /',
2838 ' \\/ ,/ \\ |',
2839 ' / /~~|~|~~~~~~|~|\\ |',
2840 ' / / | | | | `\\ \\',
2841 ' / / | | | | \\ \\',
2842 ' / ( | | | | \\ \\',
2843 ' jgs /,_) /__) /__) /,_/',
2844 ' \'\'\'\'\'"""""\'\'\'""""""\'\'\'""""""\'\'"""""\'\'\'\'\'')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00002845 return 0
2846
2847
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002848class OptionParser(optparse.OptionParser):
2849 """Creates the option parse and add --verbose support."""
2850 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002851 optparse.OptionParser.__init__(
2852 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002853 self.add_option(
2854 '-v', '--verbose', action='count', default=0,
2855 help='Use 2 times for more debugging info')
2856
2857 def parse_args(self, args=None, values=None):
2858 options, args = optparse.OptionParser.parse_args(self, args, values)
2859 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2860 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2861 return options, args
2862
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002863
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002864def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002865 if sys.hexversion < 0x02060000:
2866 print >> sys.stderr, (
2867 '\nYour python version %s is unsupported, please upgrade.\n' %
2868 sys.version.split(' ', 1)[0])
2869 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002870
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002871 # Reload settings.
2872 global settings
2873 settings = Settings()
2874
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002875 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002876 dispatcher = subcommand.CommandDispatcher(__name__)
2877 try:
2878 return dispatcher.execute(OptionParser(), argv)
2879 except urllib2.HTTPError, e:
2880 if e.code != 500:
2881 raise
2882 DieWithError(
2883 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2884 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002885
2886
2887if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002888 # These affect sys.stdout so do it outside of main() to simplify mocks in
2889 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002890 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002891 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002892 sys.exit(main(sys.argv[1:]))