blob: eeb7acfddf6cd64940fdd3ce6b38468036fd00c1 [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:
528 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
529 self.branch = ShortBranchName(self.branchref)
530 return self.branch
531
532 def GetBranchRef(self):
533 """Returns the full branch name, e.g. 'refs/heads/master'."""
534 self.GetBranch() # Poke the lazy loader.
535 return self.branchref
536
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000537 @staticmethod
538 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000539 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000540 e.g. 'origin', 'refs/heads/master'
541 """
542 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000543 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
544 error_ok=True).strip()
545 if upstream_branch:
546 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
547 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000548 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
549 error_ok=True).strip()
550 if upstream_branch:
551 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000552 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000553 # Fall back on trying a git-svn upstream branch.
554 if settings.GetIsGitSvn():
555 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000556 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000557 # Else, try to guess the origin remote.
558 remote_branches = RunGit(['branch', '-r']).split()
559 if 'origin/master' in remote_branches:
560 # Fall back on origin/master if it exits.
561 remote = 'origin'
562 upstream_branch = 'refs/heads/master'
563 elif 'origin/trunk' in remote_branches:
564 # Fall back on origin/trunk if it exists. Generally a shared
565 # git-svn clone
566 remote = 'origin'
567 upstream_branch = 'refs/heads/trunk'
568 else:
569 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570Either pass complete "git diff"-style arguments, like
571 git cl upload origin/master
572or verify this branch is set up to track another (via the --track argument to
573"git checkout -b ...").""")
574
575 return remote, upstream_branch
576
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000577 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000578 return git_common.get_or_create_merge_base(self.GetBranch(),
579 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000580
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000581 def GetUpstreamBranch(self):
582 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000583 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000584 if remote is not '.':
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000585 upstream_branch = upstream_branch.replace('refs/heads/',
586 'refs/remotes/%s/' % remote)
587 upstream_branch = upstream_branch.replace('refs/branch-heads/',
588 'refs/remotes/branch-heads/')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000589 self.upstream_branch = upstream_branch
590 return self.upstream_branch
591
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000592 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000593 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000594 remote, branch = None, self.GetBranch()
595 seen_branches = set()
596 while branch not in seen_branches:
597 seen_branches.add(branch)
598 remote, branch = self.FetchUpstreamTuple(branch)
599 branch = ShortBranchName(branch)
600 if remote != '.' or branch.startswith('refs/remotes'):
601 break
602 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000603 remotes = RunGit(['remote'], error_ok=True).split()
604 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000605 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000606 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000607 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000608 logging.warning('Could not determine which remote this change is '
609 'associated with, so defaulting to "%s". This may '
610 'not be what you want. You may prevent this message '
611 'by running "git svn info" as documented here: %s',
612 self._remote,
613 GIT_INSTRUCTIONS_URL)
614 else:
615 logging.warn('Could not determine which remote this change is '
616 'associated with. You may prevent this message by '
617 'running "git svn info" as documented here: %s',
618 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000619 branch = 'HEAD'
620 if branch.startswith('refs/remotes'):
621 self._remote = (remote, branch)
mmoss@chromium.orge7585452014-08-24 01:41:11 +0000622 elif branch.startswith('refs/branch-heads/'):
623 self._remote = (remote, branch.replace('refs/', 'refs/remotes/'))
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000624 else:
625 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000626 return self._remote
627
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000628 def GitSanityChecks(self, upstream_git_obj):
629 """Checks git repo status and ensures diff is from local commits."""
630
631 # Verify the commit we're diffing against is in our current branch.
632 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
633 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
634 if upstream_sha != common_ancestor:
635 print >> sys.stderr, (
636 'ERROR: %s is not in the current branch. You may need to rebase '
637 'your tracking branch' % upstream_sha)
638 return False
639
640 # List the commits inside the diff, and verify they are all local.
641 commits_in_diff = RunGit(
642 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
643 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
644 remote_branch = remote_branch.strip()
645 if code != 0:
646 _, remote_branch = self.GetRemoteBranch()
647
648 commits_in_remote = RunGit(
649 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
650
651 common_commits = set(commits_in_diff) & set(commits_in_remote)
652 if common_commits:
653 print >> sys.stderr, (
654 'ERROR: Your diff contains %d commits already in %s.\n'
655 'Run "git log --oneline %s..HEAD" to get a list of commits in '
656 'the diff. If you are using a custom git flow, you can override'
657 ' the reference used for this check with "git config '
658 'gitcl.remotebranch <git-ref>".' % (
659 len(common_commits), remote_branch, upstream_git_obj))
660 return False
661 return True
662
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000663 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000664 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000665
666 Returns None if it is not set.
667 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000668 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
669 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000670
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671 def GetRemoteUrl(self):
672 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
673
674 Returns None if there is no remote.
675 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000676 remote, _ = self.GetRemoteBranch()
dyen@chromium.org2a13d4f2014-06-13 00:06:37 +0000677 url = RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
678
679 # If URL is pointing to a local directory, it is probably a git cache.
680 if os.path.isdir(url):
681 url = RunGit(['config', 'remote.%s.url' % remote],
682 error_ok=True,
683 cwd=url).strip()
684 return url
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
686 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000687 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000688 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000690 self.issue = int(issue) or None if issue else None
691 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 return self.issue
693
694 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000695 if not self.rietveld_server:
696 # If we're on a branch then get the server potentially associated
697 # with that branch.
698 if self.GetIssue():
699 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
700 ['config', self._RietveldServer()], error_ok=True).strip())
701 if not self.rietveld_server:
702 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000703 return self.rietveld_server
704
705 def GetIssueURL(self):
706 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000707 if not self.GetIssue():
708 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000709 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
710
711 def GetDescription(self, pretty=False):
712 if not self.has_description:
713 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000714 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000715 try:
716 self.description = self.RpcServer().get_description(issue).strip()
maruel@chromium.org85616e02014-07-28 15:37:55 +0000717 except urllib2.HTTPError as e:
miket@chromium.org183df1a2012-01-04 19:44:55 +0000718 if e.code == 404:
719 DieWithError(
720 ('\nWhile fetching the description for issue %d, received a '
721 '404 (not found)\n'
722 'error. It is likely that you deleted this '
723 'issue on the server. If this is the\n'
724 'case, please run\n\n'
725 ' git cl issue 0\n\n'
726 'to clear the association with the deleted issue. Then run '
727 'this command again.') % issue)
728 else:
729 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000730 '\nFailed to fetch issue description. HTTP error %d' % e.code)
maruel@chromium.org85616e02014-07-28 15:37:55 +0000731 except urllib2.URLError as e:
732 print >> sys.stderr, (
733 'Warning: Failed to retrieve CL description due to network '
734 'failure.')
735 self.description = ''
736
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000737 self.has_description = True
738 if pretty:
739 wrapper = textwrap.TextWrapper()
740 wrapper.initial_indent = wrapper.subsequent_indent = ' '
741 return wrapper.fill(self.description)
742 return self.description
743
744 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000745 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000746 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 patchset = RunGit(['config', self._PatchsetSetting()],
748 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000749 self.patchset = int(patchset) or None if patchset else None
750 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751 return self.patchset
752
753 def SetPatchset(self, patchset):
754 """Set this branch's patchset. If patchset=0, clears the patchset."""
755 if patchset:
756 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000757 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758 else:
759 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000760 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000761 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000763 def GetMostRecentPatchset(self):
764 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000765
766 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000767 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000768 '/download/issue%s_%s.diff' % (issue, patchset))
769
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000770 def GetIssueProperties(self):
771 if self._props is None:
772 issue = self.GetIssue()
773 if not issue:
774 self._props = {}
775 else:
776 self._props = self.RpcServer().get_issue_properties(issue, True)
777 return self._props
778
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000779 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000780 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000781
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782 def SetIssue(self, issue):
783 """Set this branch's issue. If issue=0, clears the issue."""
784 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000785 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000786 RunGit(['config', self._IssueSetting(), str(issue)])
787 if self.rietveld_server:
788 RunGit(['config', self._RietveldServer(), self.rietveld_server])
789 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000790 current_issue = self.GetIssue()
791 if current_issue:
792 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000793 self.issue = None
794 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000795
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000796 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000797 if not self.GitSanityChecks(upstream_branch):
798 DieWithError('\nGit sanity check failure')
799
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000800 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000801 if not root:
802 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000803 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000804
805 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000806 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000807 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000808 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000809 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000810 except subprocess2.CalledProcessError:
811 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000812 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000813 'This branch probably doesn\'t exist anymore. To reset the\n'
814 'tracking branch, please run\n'
815 ' git branch --set-upstream %s trunk\n'
816 'replacing trunk with origin/master or the relevant branch') %
817 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000818
maruel@chromium.org52424302012-08-29 15:14:30 +0000819 issue = self.GetIssue()
820 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000821 if issue:
822 description = self.GetDescription()
823 else:
824 # If the change was never uploaded, use the log messages of all commits
825 # up to the branch point, as git cl upload will prefill the description
826 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000827 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
828 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000829
830 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000831 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000832 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000833 name,
834 description,
835 absroot,
836 files,
837 issue,
838 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000839 author,
840 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000841
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +0000842 def GetStatus(self):
843 """Apply a rough heuristic to give a simple summary of an issue's review
844 or CQ status, assuming adherence to a common workflow.
845
846 Returns None if no issue for this branch, or one of the following keywords:
847 * 'error' - error from review tool (including deleted issues)
848 * 'unsent' - not sent for review
849 * 'waiting' - waiting for review
850 * 'reply' - waiting for owner to reply to review
851 * 'lgtm' - LGTM from at least one approved reviewer
852 * 'commit' - in the commit queue
853 * 'closed' - closed
854 """
855 if not self.GetIssue():
856 return None
857
858 try:
859 props = self.GetIssueProperties()
860 except urllib2.HTTPError:
861 return 'error'
862
863 if props.get('closed'):
864 # Issue is closed.
865 return 'closed'
866 if props.get('commit'):
867 # Issue is in the commit queue.
868 return 'commit'
869
870 try:
871 reviewers = self.GetApprovingReviewers()
872 except urllib2.HTTPError:
873 return 'error'
874
875 if reviewers:
876 # Was LGTM'ed.
877 return 'lgtm'
878
879 messages = props.get('messages') or []
880
881 if not messages:
882 # No message was sent.
883 return 'unsent'
884 if messages[-1]['sender'] != props.get('owner_email'):
885 # Non-LGTM reply from non-owner
886 return 'reply'
887 return 'waiting'
888
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000889 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000890 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000891
892 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000893 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000894 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000895 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000896 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000897 except presubmit_support.PresubmitFailure, e:
898 DieWithError(
899 ('%s\nMaybe your depot_tools is out of date?\n'
900 'If all fails, contact maruel@') % e)
901
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000902 def UpdateDescription(self, description):
903 self.description = description
904 return self.RpcServer().update_description(
905 self.GetIssue(), self.description)
906
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000908 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000909 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000911 def SetFlag(self, flag, value):
912 """Patchset must match."""
913 if not self.GetPatchset():
914 DieWithError('The patchset needs to match. Send another patchset.')
915 try:
916 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000917 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000918 except urllib2.HTTPError, e:
919 if e.code == 404:
920 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
921 if e.code == 403:
922 DieWithError(
923 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
924 'match?') % (self.GetIssue(), self.GetPatchset()))
925 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000927 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000928 """Returns an upload.RpcServer() to access this review's rietveld instance.
929 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000930 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000931 self._rpc_server = rietveld.CachingRietveld(
932 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000933 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934
935 def _IssueSetting(self):
936 """Return the git setting that stores this change's issue."""
937 return 'branch.%s.rietveldissue' % self.GetBranch()
938
939 def _PatchsetSetting(self):
940 """Return the git setting that stores this change's most recent patchset."""
941 return 'branch.%s.rietveldpatchset' % self.GetBranch()
942
943 def _RietveldServer(self):
944 """Returns the git setting that stores this change's rietveld server."""
945 return 'branch.%s.rietveldserver' % self.GetBranch()
946
947
948def GetCodereviewSettingsInteractively():
949 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000950 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951 server = settings.GetDefaultServerUrl(error_ok=True)
952 prompt = 'Rietveld server (host[:port])'
953 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000954 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000955 if not server and not newserver:
956 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000957 if newserver:
958 newserver = gclient_utils.UpgradeToHttps(newserver)
959 if newserver != server:
960 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000962 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963 prompt = caption
964 if initial:
965 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000966 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967 if new_val == 'x':
968 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000969 elif new_val:
970 if is_url:
971 new_val = gclient_utils.UpgradeToHttps(new_val)
972 if new_val != initial:
973 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000974
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000975 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000976 SetProperty(settings.GetDefaultPrivateFlag(),
977 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000979 'tree-status-url', False)
980 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000981 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000982
983 # TODO: configure a default branch to diff against, rather than this
984 # svn-based hackery.
985
986
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000987class ChangeDescription(object):
988 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000989 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000990 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000991
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000992 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000993 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000994
agable@chromium.org42c20792013-09-12 17:34:49 +0000995 @property # www.logilab.org/ticket/89786
996 def description(self): # pylint: disable=E0202
997 return '\n'.join(self._description_lines)
998
999 def set_description(self, desc):
1000 if isinstance(desc, basestring):
1001 lines = desc.splitlines()
1002 else:
1003 lines = [line.rstrip() for line in desc]
1004 while lines and not lines[0]:
1005 lines.pop(0)
1006 while lines and not lines[-1]:
1007 lines.pop(-1)
1008 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001009
piman@chromium.org336f9122014-09-04 02:16:55 +00001010 def update_reviewers(self, reviewers, add_owners_tbr=False, change=None):
agable@chromium.org42c20792013-09-12 17:34:49 +00001011 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001012 assert isinstance(reviewers, list), reviewers
piman@chromium.org336f9122014-09-04 02:16:55 +00001013 if not reviewers and not add_owners_tbr:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001014 return
agable@chromium.org42c20792013-09-12 17:34:49 +00001015 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001016
agable@chromium.org42c20792013-09-12 17:34:49 +00001017 # Get the set of R= and TBR= lines and remove them from the desciption.
1018 regexp = re.compile(self.R_LINE)
1019 matches = [regexp.match(line) for line in self._description_lines]
1020 new_desc = [l for i, l in enumerate(self._description_lines)
1021 if not matches[i]]
1022 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001023
agable@chromium.org42c20792013-09-12 17:34:49 +00001024 # Construct new unified R= and TBR= lines.
1025 r_names = []
1026 tbr_names = []
1027 for match in matches:
1028 if not match:
1029 continue
1030 people = cleanup_list([match.group(2).strip()])
1031 if match.group(1) == 'TBR':
1032 tbr_names.extend(people)
1033 else:
1034 r_names.extend(people)
1035 for name in r_names:
1036 if name not in reviewers:
1037 reviewers.append(name)
piman@chromium.org336f9122014-09-04 02:16:55 +00001038 if add_owners_tbr:
1039 owners_db = owners.Database(change.RepositoryRoot(),
1040 fopen=file, os_path=os.path, glob=glob.glob)
1041 all_reviewers = set(tbr_names + reviewers)
1042 missing_files = owners_db.files_not_covered_by(change.LocalPaths(),
1043 all_reviewers)
1044 tbr_names.extend(owners_db.reviewers_for(missing_files,
1045 change.author_email))
agable@chromium.org42c20792013-09-12 17:34:49 +00001046 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
1047 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
1048
1049 # Put the new lines in the description where the old first R= line was.
1050 line_loc = next((i for i, match in enumerate(matches) if match), -1)
1051 if 0 <= line_loc < len(self._description_lines):
1052 if new_tbr_line:
1053 self._description_lines.insert(line_loc, new_tbr_line)
1054 if new_r_line:
1055 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001056 else:
agable@chromium.org42c20792013-09-12 17:34:49 +00001057 if new_r_line:
1058 self.append_footer(new_r_line)
1059 if new_tbr_line:
1060 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001061
1062 def prompt(self):
1063 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001064 self.set_description([
1065 '# Enter a description of the change.',
1066 '# This will be displayed on the codereview site.',
1067 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +00001068 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +00001069 '--------------------',
1070 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001071
agable@chromium.org42c20792013-09-12 17:34:49 +00001072 regexp = re.compile(self.BUG_LINE)
1073 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +00001074 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +00001075 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +00001076 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001077 if not content:
1078 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +00001079 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001080
1081 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +00001082 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
1083 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +00001084 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +00001085 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001086
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001087 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +00001088 if self._description_lines:
1089 # Add an empty line if either the last line or the new line isn't a tag.
1090 last_line = self._description_lines[-1]
1091 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1092 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1093 self._description_lines.append('')
1094 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001095
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001096 def get_reviewers(self):
1097 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001098 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1099 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001100 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001101
1102
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001103def get_approving_reviewers(props):
1104 """Retrieves the reviewers that approved a CL from the issue properties with
1105 messages.
1106
1107 Note that the list may contain reviewers that are not committer, thus are not
1108 considered by the CQ.
1109 """
1110 return sorted(
1111 set(
1112 message['sender']
1113 for message in props['messages']
1114 if message['approval'] and message['sender'] in props['reviewers']
1115 )
1116 )
1117
1118
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001119def FindCodereviewSettingsFile(filename='codereview.settings'):
1120 """Finds the given file starting in the cwd and going up.
1121
1122 Only looks up to the top of the repository unless an
1123 'inherit-review-settings-ok' file exists in the root of the repository.
1124 """
1125 inherit_ok_file = 'inherit-review-settings-ok'
1126 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001127 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1129 root = '/'
1130 while True:
1131 if filename in os.listdir(cwd):
1132 if os.path.isfile(os.path.join(cwd, filename)):
1133 return open(os.path.join(cwd, filename))
1134 if cwd == root:
1135 break
1136 cwd = os.path.dirname(cwd)
1137
1138
1139def LoadCodereviewSettingsFromFile(fileobj):
1140 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001141 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001143 def SetProperty(name, setting, unset_error_ok=False):
1144 fullname = 'rietveld.' + name
1145 if setting in keyvals:
1146 RunGit(['config', fullname, keyvals[setting]])
1147 else:
1148 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1149
1150 SetProperty('server', 'CODE_REVIEW_SERVER')
1151 # Only server setting is required. Other settings can be absent.
1152 # In that case, we ignore errors raised during option deletion attempt.
1153 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001154 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1156 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001157 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001158 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1159 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001160 SetProperty('project', 'PROJECT', unset_error_ok=True)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001161 SetProperty('pending-ref-prefix', 'PENDING_REF_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001162
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001163 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001164 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001165
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1167 #should be of the form
1168 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1169 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1170 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1171 keyvals['ORIGIN_URL_CONFIG']])
1172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001174def urlretrieve(source, destination):
1175 """urllib is broken for SSL connections via a proxy therefore we
1176 can't use urllib.urlretrieve()."""
1177 with open(destination, 'w') as f:
1178 f.write(urllib2.urlopen(source).read())
1179
1180
ukai@chromium.org712d6102013-11-27 00:52:58 +00001181def hasSheBang(fname):
1182 """Checks fname is a #! script."""
1183 with open(fname) as f:
1184 return f.read(2).startswith('#!')
1185
1186
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001187def DownloadHooks(force):
1188 """downloads hooks
1189
1190 Args:
1191 force: True to update hooks. False to install hooks if not present.
1192 """
1193 if not settings.GetIsGerrit():
1194 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001195 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001196 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1197 if not os.access(dst, os.X_OK):
1198 if os.path.exists(dst):
1199 if not force:
1200 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001201 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001202 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001203 if not hasSheBang(dst):
1204 DieWithError('Not a script: %s\n'
1205 'You need to download from\n%s\n'
1206 'into .git/hooks/commit-msg and '
1207 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001208 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1209 except Exception:
1210 if os.path.exists(dst):
1211 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001212 DieWithError('\nFailed to download hooks.\n'
1213 'You need to download from\n%s\n'
1214 'into .git/hooks/commit-msg and '
1215 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001216
1217
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001218@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001219def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001220 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001222 parser.add_option('--activate-update', action='store_true',
1223 help='activate auto-updating [rietveld] section in '
1224 '.git/config')
1225 parser.add_option('--deactivate-update', action='store_true',
1226 help='deactivate auto-updating [rietveld] section in '
1227 '.git/config')
1228 options, args = parser.parse_args(args)
1229
1230 if options.deactivate_update:
1231 RunGit(['config', 'rietveld.autoupdate', 'false'])
1232 return
1233
1234 if options.activate_update:
1235 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1236 return
1237
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 if len(args) == 0:
1239 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001240 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241 return 0
1242
1243 url = args[0]
1244 if not url.endswith('codereview.settings'):
1245 url = os.path.join(url, 'codereview.settings')
1246
1247 # Load code review settings and download hooks (if available).
1248 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001249 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001250 return 0
1251
1252
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001253def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001254 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001255 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1256 branch = ShortBranchName(branchref)
1257 _, args = parser.parse_args(args)
1258 if not args:
1259 print("Current base-url:")
1260 return RunGit(['config', 'branch.%s.base-url' % branch],
1261 error_ok=False).strip()
1262 else:
1263 print("Setting base-url to %s" % args[0])
1264 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1265 error_ok=False).strip()
1266
1267
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001268def color_for_status(status):
1269 """Maps a Changelist status to color, for CMDstatus and other tools."""
1270 return {
1271 'unsent': Fore.RED,
1272 'waiting': Fore.BLUE,
1273 'reply': Fore.YELLOW,
1274 'lgtm': Fore.GREEN,
1275 'commit': Fore.MAGENTA,
1276 'closed': Fore.CYAN,
1277 'error': Fore.WHITE,
1278 }.get(status, Fore.WHITE)
1279
1280
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001282 """Show status of changelists.
1283
1284 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001285 - Red not sent for review or broken
1286 - Blue waiting for review
1287 - Yellow waiting for you to reply to review
1288 - Green LGTM'ed
1289 - Magenta in the commit queue
1290 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001291
1292 Also see 'git cl comments'.
1293 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001294 parser.add_option('--field',
1295 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001296 parser.add_option('-f', '--fast', action='store_true',
1297 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001299 if args:
1300 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001301
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001303 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304 if options.field.startswith('desc'):
1305 print cl.GetDescription()
1306 elif options.field == 'id':
1307 issueid = cl.GetIssue()
1308 if issueid:
1309 print issueid
1310 elif options.field == 'patch':
1311 patchset = cl.GetPatchset()
1312 if patchset:
1313 print patchset
1314 elif options.field == 'url':
1315 url = cl.GetIssueURL()
1316 if url:
1317 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001318 return 0
1319
1320 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1321 if not branches:
1322 print('No local branch found.')
1323 return 0
1324
1325 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001326 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001327 alignment = max(5, max(len(b) for b in branches))
1328 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001329 # Adhoc thread pool to request data concurrently.
1330 output = Queue.Queue()
1331
1332 # Silence upload.py otherwise it becomes unweldly.
1333 upload.verbosity = 0
1334
1335 if not options.fast:
1336 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001337 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001338 c = Changelist(branchref=b)
1339 i = c.GetIssueURL()
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001340 status = c.GetStatus()
1341 color = color_for_status(status)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001342
jsbell@chromium.orgb99fbd92014-09-11 17:29:28 +00001343 if i and (not status or status == 'error'):
1344 # The issue probably doesn't exist anymore.
1345 i += ' (broken)'
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001346
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001347 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001348
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001349 # Process one branch synchronously to work through authentication, then
1350 # spawn threads to process all the other branches in parallel.
1351 if branches:
1352 fetch(branches[0])
1353 threads = [
1354 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001355 for t in threads:
1356 t.daemon = True
1357 t.start()
1358 else:
1359 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1360 for b in branches:
1361 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001362 url = c.GetIssueURL()
1363 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001364
1365 tmp = {}
1366 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001367 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001368 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001369 b, i, color = output.get()
1370 tmp[b] = (i, color)
1371 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001372 reset = Fore.RESET
1373 if not sys.stdout.isatty():
1374 color = ''
1375 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001376 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001377 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001378
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001379 cl = Changelist()
1380 print
1381 print 'Current branch:',
1382 if not cl.GetIssue():
1383 print 'no issue assigned.'
1384 return 0
1385 print cl.GetBranch()
1386 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
maruel@chromium.org85616e02014-07-28 15:37:55 +00001387 if not options.fast:
1388 print 'Issue description:'
1389 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001390 return 0
1391
1392
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001393def colorize_CMDstatus_doc():
1394 """To be called once in main() to add colors to git cl status help."""
1395 colors = [i for i in dir(Fore) if i[0].isupper()]
1396
1397 def colorize_line(line):
1398 for color in colors:
1399 if color in line.upper():
1400 # Extract whitespaces first and the leading '-'.
1401 indent = len(line) - len(line.lstrip(' ')) + 1
1402 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1403 return line
1404
1405 lines = CMDstatus.__doc__.splitlines()
1406 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1407
1408
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001409@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001410def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001411 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412
1413 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001414 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001415 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001416
1417 cl = Changelist()
1418 if len(args) > 0:
1419 try:
1420 issue = int(args[0])
1421 except ValueError:
1422 DieWithError('Pass a number to set the issue or none to list it.\n'
1423 'Maybe you want to run git cl status?')
1424 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001425 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001426 return 0
1427
1428
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001429def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001430 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001431 (_, args) = parser.parse_args(args)
1432 if args:
1433 parser.error('Unsupported argument: %s' % args)
1434
1435 cl = Changelist()
1436 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001437 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001438 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001439 if message['disapproval']:
1440 color = Fore.RED
1441 elif message['approval']:
1442 color = Fore.GREEN
1443 elif message['sender'] == data['owner_email']:
1444 color = Fore.MAGENTA
1445 else:
1446 color = Fore.BLUE
1447 print '\n%s%s %s%s' % (
1448 color, message['date'].split('.', 1)[0], message['sender'],
1449 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001450 if message['text'].strip():
1451 print '\n'.join(' ' + l for l in message['text'].splitlines())
1452 return 0
1453
1454
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001455def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001456 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001457 cl = Changelist()
1458 if not cl.GetIssue():
1459 DieWithError('This branch has no associated changelist.')
1460 description = ChangeDescription(cl.GetDescription())
1461 description.prompt()
1462 cl.UpdateDescription(description.description)
1463 return 0
1464
1465
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466def CreateDescriptionFromLog(args):
1467 """Pulls out the commit log to use as a base for the CL description."""
1468 log_args = []
1469 if len(args) == 1 and not args[0].endswith('.'):
1470 log_args = [args[0] + '..']
1471 elif len(args) == 1 and args[0].endswith('...'):
1472 log_args = [args[0][:-1]]
1473 elif len(args) == 2:
1474 log_args = [args[0] + '..' + args[1]]
1475 else:
1476 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001477 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478
1479
thestig@chromium.org44202a22014-03-11 19:22:18 +00001480def CMDlint(parser, args):
1481 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001482 parser.add_option('--filter', action='append', metavar='-x,+y',
1483 help='Comma-separated list of cpplint\'s category-filters')
1484 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001485
1486 # Access to a protected member _XX of a client class
1487 # pylint: disable=W0212
1488 try:
1489 import cpplint
1490 import cpplint_chromium
1491 except ImportError:
1492 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1493 return 1
1494
1495 # Change the current working directory before calling lint so that it
1496 # shows the correct base.
1497 previous_cwd = os.getcwd()
1498 os.chdir(settings.GetRoot())
1499 try:
1500 cl = Changelist()
1501 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1502 files = [f.LocalPath() for f in change.AffectedFiles()]
thestig@chromium.org5839eb52014-05-30 16:20:51 +00001503 if not files:
1504 print "Cannot lint an empty CL"
1505 return 1
thestig@chromium.org44202a22014-03-11 19:22:18 +00001506
1507 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001508 command = args + files
1509 if options.filter:
1510 command = ['--filter=' + ','.join(options.filter)] + command
1511 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001512
1513 white_regex = re.compile(settings.GetLintRegex())
1514 black_regex = re.compile(settings.GetLintIgnoreRegex())
1515 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1516 for filename in filenames:
1517 if white_regex.match(filename):
1518 if black_regex.match(filename):
1519 print "Ignoring file %s" % filename
1520 else:
1521 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1522 extra_check_functions)
1523 else:
1524 print "Skipping file %s" % filename
1525 finally:
1526 os.chdir(previous_cwd)
1527 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1528 if cpplint._cpplint_state.error_count != 0:
1529 return 1
1530 return 0
1531
1532
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001533def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001534 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001535 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001537 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001538 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001539 (options, args) = parser.parse_args(args)
1540
ukai@chromium.org259e4682012-10-25 07:36:33 +00001541 if not options.force and is_dirty_git_tree('presubmit'):
1542 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001543 return 1
1544
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001545 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001546 if args:
1547 base_branch = args[0]
1548 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001549 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001550 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001551
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001552 cl.RunHook(
1553 committing=not options.upload,
1554 may_prompt=False,
1555 verbose=options.verbose,
1556 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001557 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001558
1559
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001560def AddChangeIdToCommitMessage(options, args):
1561 """Re-commits using the current message, assumes the commit hook is in
1562 place.
1563 """
1564 log_desc = options.message or CreateDescriptionFromLog(args)
1565 git_command = ['commit', '--amend', '-m', log_desc]
1566 RunGit(git_command)
1567 new_log_desc = CreateDescriptionFromLog(args)
1568 if CHANGE_ID in new_log_desc:
1569 print 'git-cl: Added Change-Id to commit message.'
1570 else:
1571 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1572
1573
piman@chromium.org336f9122014-09-04 02:16:55 +00001574def GerritUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001575 """upload the current branch to gerrit."""
1576 # We assume the remote called "origin" is the one we want.
1577 # It is probably not worthwhile to support different workflows.
1578 remote = 'origin'
1579 branch = 'master'
1580 if options.target_branch:
1581 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001583 change_desc = ChangeDescription(
1584 options.message or CreateDescriptionFromLog(args))
1585 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001586 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001587 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001588 if CHANGE_ID not in change_desc.description:
1589 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001590
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001591 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001592 if len(commits) > 1:
1593 print('WARNING: This will upload %d commits. Run the following command '
1594 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001595 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001596 print('You can also use `git squash-branch` to squash these into a single'
1597 'commit.')
1598 ask_for_data('About to upload; enter to confirm.')
1599
piman@chromium.org336f9122014-09-04 02:16:55 +00001600 if options.reviewers or options.tbr_owners:
1601 change_desc.update_reviewers(options.reviewers, options.tbr_owners, change)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001602
ukai@chromium.orge8077812012-02-03 03:41:46 +00001603 receive_options = []
1604 cc = cl.GetCCList().split(',')
1605 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001606 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001607 cc = filter(None, cc)
1608 if cc:
1609 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001610 if change_desc.get_reviewers():
1611 receive_options.extend(
1612 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001613
ukai@chromium.orge8077812012-02-03 03:41:46 +00001614 git_command = ['push']
1615 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001616 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001617 ' '.join(receive_options))
1618 git_command += [remote, 'HEAD:refs/for/' + branch]
1619 RunGit(git_command)
1620 # TODO(ukai): parse Change-Id: and set issue number?
1621 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001622
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001623
piman@chromium.org336f9122014-09-04 02:16:55 +00001624def RietveldUpload(options, args, cl, change):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001625 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001626 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1627 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628 if options.emulate_svn_auto_props:
1629 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001630
1631 change_desc = None
1632
pgervais@chromium.org91141372014-01-09 23:27:20 +00001633 if options.email is not None:
1634 upload_args.extend(['--email', options.email])
1635
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001636 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001637 if options.title:
1638 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001639 if options.message:
1640 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001641 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001642 print ("This branch is associated with issue %s. "
1643 "Adding patch to that issue." % cl.GetIssue())
1644 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001645 if options.title:
1646 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001647 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001648 change_desc = ChangeDescription(message)
piman@chromium.org336f9122014-09-04 02:16:55 +00001649 if options.reviewers or options.tbr_owners:
1650 change_desc.update_reviewers(options.reviewers,
1651 options.tbr_owners,
1652 change)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001653 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001654 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001655
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001656 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001657 print "Description is empty; aborting."
1658 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001659
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001660 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001661 if change_desc.get_reviewers():
1662 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001663 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001664 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001665 DieWithError("Must specify reviewers to send email.")
1666 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001667
1668 # We check this before applying rietveld.private assuming that in
1669 # rietveld.cc only addresses which we can send private CLs to are listed
1670 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1671 # --private is specified explicitly on the command line.
1672 if options.private:
1673 logging.warn('rietveld.cc is ignored since private flag is specified. '
1674 'You need to review and add them manually if necessary.')
1675 cc = cl.GetCCListWithoutDefault()
1676 else:
1677 cc = cl.GetCCList()
1678 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001679 if cc:
1680 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001681
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001682 if options.private or settings.GetDefaultPrivateFlag() == "True":
1683 upload_args.append('--private')
1684
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001685 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001686 if not options.find_copies:
1687 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001688
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001689 # Include the upstream repo's URL in the change -- this is useful for
1690 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001691 remote_url = cl.GetGitBaseUrlFromConfig()
1692 if not remote_url:
1693 if settings.GetIsGitSvn():
1694 # URL is dependent on the current directory.
1695 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1696 if data:
1697 keys = dict(line.split(': ', 1) for line in data.splitlines()
1698 if ': ' in line)
1699 remote_url = keys.get('URL', None)
1700 else:
1701 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1702 remote_url = (cl.GetRemoteUrl() + '@'
1703 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001704 if remote_url:
1705 upload_args.extend(['--base_url', remote_url])
1706
sheyang@chromium.org152cf832014-06-11 21:37:49 +00001707 project = settings.GetProject()
1708 if project:
1709 upload_args.extend(['--project', project])
1710
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001711 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001712 upload_args = ['upload'] + upload_args + args
1713 logging.info('upload.RealMain(%s)', upload_args)
1714 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001715 issue = int(issue)
1716 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001717 except KeyboardInterrupt:
1718 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001719 except:
1720 # If we got an exception after the user typed a description for their
1721 # change, back up the description before re-raising.
1722 if change_desc:
1723 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1724 print '\nGot exception while uploading -- saving description to %s\n' \
1725 % backup_path
1726 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001727 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001728 backup_file.close()
1729 raise
1730
1731 if not cl.GetIssue():
1732 cl.SetIssue(issue)
1733 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001734
1735 if options.use_commit_queue:
1736 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001737 return 0
1738
1739
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001740def cleanup_list(l):
1741 """Fixes a list so that comma separated items are put as individual items.
1742
1743 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1744 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1745 """
1746 items = sum((i.split(',') for i in l), [])
1747 stripped_items = (i.strip() for i in items)
1748 return sorted(filter(None, stripped_items))
1749
1750
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001751@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001752def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001753 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001754 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1755 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001756 parser.add_option('--bypass-watchlists', action='store_true',
1757 dest='bypass_watchlists',
1758 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001759 parser.add_option('-f', action='store_true', dest='force',
1760 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001761 parser.add_option('-m', dest='message', help='message for patchset')
1762 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001763 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001764 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001765 help='reviewer email addresses')
1766 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001767 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001768 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001769 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001770 help='send email to reviewer immediately')
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001771 parser.add_option('--emulate_svn_auto_props',
1772 '--emulate-svn-auto-props',
1773 action="store_true",
ukai@chromium.orge8077812012-02-03 03:41:46 +00001774 dest="emulate_svn_auto_props",
1775 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001776 parser.add_option('-c', '--use-commit-queue', action='store_true',
1777 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001778 parser.add_option('--private', action='store_true',
1779 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001780 parser.add_option('--target_branch',
pgervais@chromium.orgb9f27512014-08-08 15:52:33 +00001781 '--target-branch',
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001782 help='When uploading to gerrit, remote branch to '
1783 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001784 parser.add_option('--email', default=None,
1785 help='email address to use to connect to Rietveld')
piman@chromium.org336f9122014-09-04 02:16:55 +00001786 parser.add_option('--tbr-owners', dest='tbr_owners', action='store_true',
1787 help='add a set of OWNERS to TBR')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001788
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001789 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001790 (options, args) = parser.parse_args(args)
1791
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001792 if options.target_branch and not settings.GetIsGerrit():
1793 parser.error('Use --target_branch for non gerrit repository.')
1794
ukai@chromium.org259e4682012-10-25 07:36:33 +00001795 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001796 return 1
1797
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001798 options.reviewers = cleanup_list(options.reviewers)
1799 options.cc = cleanup_list(options.cc)
1800
ukai@chromium.orge8077812012-02-03 03:41:46 +00001801 cl = Changelist()
1802 if args:
1803 # TODO(ukai): is it ok for gerrit case?
1804 base_branch = args[0]
1805 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001806 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001807 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001808 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001809
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001810 # Apply watchlists on upload.
1811 change = cl.GetChange(base_branch, None)
1812 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1813 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001814 if not options.bypass_watchlists:
1815 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001816
ukai@chromium.orge8077812012-02-03 03:41:46 +00001817 if not options.bypass_hooks:
piman@chromium.org336f9122014-09-04 02:16:55 +00001818 if options.reviewers or options.tbr_owners:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001819 # Set the reviewer list now so that presubmit checks can access it.
1820 change_description = ChangeDescription(change.FullDescriptionText())
piman@chromium.org336f9122014-09-04 02:16:55 +00001821 change_description.update_reviewers(options.reviewers,
1822 options.tbr_owners,
1823 change)
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001824 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001825 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001826 may_prompt=not options.force,
1827 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001828 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001829 if not hook_results.should_continue():
1830 return 1
1831 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001832 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001833
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001834 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001835 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001836 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001837 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001838 print ('The last upload made from this repository was patchset #%d but '
1839 'the most recent patchset on the server is #%d.'
1840 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001841 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1842 'from another machine or branch the patch you\'re uploading now '
1843 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001844 ask_for_data('About to upload; enter to confirm.')
1845
iannucci@chromium.org79540052012-10-19 23:15:26 +00001846 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001847 if settings.GetIsGerrit():
piman@chromium.org336f9122014-09-04 02:16:55 +00001848 return GerritUpload(options, args, cl, change)
1849 ret = RietveldUpload(options, args, cl, change)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001850 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001851 git_set_branch_value('last-upload-hash',
1852 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001853
1854 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001855
1856
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001857def IsSubmoduleMergeCommit(ref):
1858 # When submodules are added to the repo, we expect there to be a single
1859 # non-git-svn merge commit at remote HEAD with a signature comment.
1860 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001861 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001862 return RunGit(cmd) != ''
1863
1864
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001865def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001866 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001867
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001868 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001869 Updates changelog with metadata (e.g. pointer to review).
1870 Pushes/dcommits the code upstream.
1871 Updates review and closes.
1872 """
1873 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1874 help='bypass upload presubmit hook')
1875 parser.add_option('-m', dest='message',
1876 help="override review description")
1877 parser.add_option('-f', action='store_true', dest='force',
1878 help="force yes to questions (don't prompt)")
1879 parser.add_option('-c', dest='contributor',
1880 help="external contributor for patch (appended to " +
1881 "description and used as author for git). Should be " +
1882 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001883 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001884 (options, args) = parser.parse_args(args)
1885 cl = Changelist()
1886
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001887 current = cl.GetBranch()
1888 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1889 if not settings.GetIsGitSvn() and remote == '.':
1890 print
1891 print 'Attempting to push branch %r into another local branch!' % current
1892 print
1893 print 'Either reparent this branch on top of origin/master:'
1894 print ' git reparent-branch --root'
1895 print
1896 print 'OR run `git rebase-update` if you think the parent branch is already'
1897 print 'committed.'
1898 print
1899 print ' Current parent: %r' % upstream_branch
1900 return 1
1901
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001902 if not args or cmd == 'land':
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 # Default to merging against our best guess of the upstream branch.
1904 args = [cl.GetUpstreamBranch()]
1905
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001906 if options.contributor:
1907 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1908 print "Please provide contibutor as 'First Last <email@example.com>'"
1909 return 1
1910
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001911 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001912 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913
ukai@chromium.org259e4682012-10-25 07:36:33 +00001914 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915 return 1
1916
1917 # This rev-list syntax means "show all commits not in my branch that
1918 # are in base_branch".
1919 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1920 base_branch]).splitlines()
1921 if upstream_commits:
1922 print ('Base branch "%s" has %d commits '
1923 'not in this branch.' % (base_branch, len(upstream_commits)))
1924 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1925 return 1
1926
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001927 # This is the revision `svn dcommit` will commit on top of.
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001928 svn_head = None
1929 if cmd == 'dcommit' or base_has_submodules:
1930 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1931 '--pretty=format:%H'])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001932
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001933 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001934 # If the base_head is a submodule merge commit, the first parent of the
1935 # base_head should be a git-svn commit, which is what we're interested in.
1936 base_svn_head = base_branch
1937 if base_has_submodules:
1938 base_svn_head += '^1'
1939
1940 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001941 if extra_commits:
1942 print ('This branch has %d additional commits not upstreamed yet.'
1943 % len(extra_commits.splitlines()))
1944 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1945 'before attempting to %s.' % (base_branch, cmd))
1946 return 1
1947
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001948 merge_base = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001949 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001950 author = None
1951 if options.contributor:
1952 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001953 hook_results = cl.RunHook(
1954 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001955 may_prompt=not options.force,
1956 verbose=options.verbose,
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001957 change=cl.GetChange(merge_base, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001958 if not hook_results.should_continue():
1959 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001960
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00001961 # Check the tree status if the tree status URL is set.
1962 status = GetTreeStatus()
1963 if 'closed' == status:
1964 print('The tree is closed. Please wait for it to reopen. Use '
1965 '"git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1966 return 1
1967 elif 'unknown' == status:
1968 print('Unable to determine tree status. Please verify manually and '
1969 'use "git cl %s --bypass-hooks" to commit on a closed tree.' % cmd)
1970 return 1
maruel@chromium.orgac637152012-01-16 14:19:54 +00001971 else:
1972 breakpad.SendStack(
1973 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001974 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1975 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001976 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001977
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001978 change_desc = ChangeDescription(options.message)
1979 if not change_desc.description and cl.GetIssue():
1980 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001981
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001982 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001983 if not cl.GetIssue() and options.bypass_hooks:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00001984 change_desc = ChangeDescription(CreateDescriptionFromLog([merge_base]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001985 else:
1986 print 'No description set.'
1987 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1988 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001989
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001990 # Keep a separate copy for the commit message, because the commit message
1991 # contains the link to the Rietveld issue, while the Rietveld message contains
1992 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001993 # Keep a separate copy for the commit message.
1994 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001995 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001996
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001997 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001998 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001999 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002000 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002001 commit_desc.append_footer('Patch from %s.' % options.contributor)
2002
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00002003 print('Description:')
2004 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002005
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002006 branches = [merge_base, cl.GetBranchRef()]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002007 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00002008 print_stats(options.similarity, options.find_copies, branches)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002009
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002010 # We want to squash all this branch's commits into one commit with the proper
2011 # description. We do this by doing a "reset --soft" to the base branch (which
2012 # keeps the working copy the same), then dcommitting that. If origin/master
2013 # has a submodule merge commit, we'll also need to cherry-pick the squashed
2014 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002015 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002016 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
2017 # Delete the branches if they exist.
2018 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
2019 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
2020 result = RunGitWithCode(showref_cmd)
2021 if result[0] == 0:
2022 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002023
2024 # We might be in a directory that's present in this branch but not in the
2025 # trunk. Move up to the top of the tree so that git commands that expect a
2026 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002027 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028 if rel_base_path:
2029 os.chdir(rel_base_path)
2030
2031 # Stuff our change into the merge branch.
2032 # We wrap in a try...finally block so if anything goes wrong,
2033 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002034 retcode = -1
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002035 pushed_to_pending = False
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002036 pending_ref = None
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002037 revision = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00002039 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002040 RunGit(['reset', '--soft', merge_base])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002042 RunGit(
2043 [
2044 'commit', '--author', options.contributor,
2045 '-m', commit_desc.description,
2046 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002047 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002048 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002049 if base_has_submodules:
2050 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
2051 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
2052 RunGit(['checkout', CHERRY_PICK_BRANCH])
2053 RunGit(['cherry-pick', cherry_pick_commit])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002054 if cmd == 'land':
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002055 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002056 pending_prefix = settings.GetPendingRefPrefix()
2057 if not pending_prefix or branch.startswith(pending_prefix):
2058 # If not using refs/pending/heads/* at all, or target ref is already set
2059 # to pending, then push to the target ref directly.
2060 retcode, output = RunGitWithCode(
2061 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002062 pushed_to_pending = pending_prefix and branch.startswith(pending_prefix)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002063 else:
2064 # Cherry-pick the change on top of pending ref and then push it.
2065 assert branch.startswith('refs/'), branch
2066 assert pending_prefix[-1] == '/', pending_prefix
2067 pending_ref = pending_prefix + branch[len('refs/'):]
2068 retcode, output = PushToGitPending(remote, pending_ref, branch)
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002069 pushed_to_pending = (retcode == 0)
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002070 if retcode == 0:
2071 revision = RunGit(['rev-parse', 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002072 else:
2073 # dcommit the merge branch.
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002074 _, output = RunGitWithCode(['svn', 'dcommit',
2075 '-C%s' % options.similarity,
2076 '--no-rebase', '--rmdir'])
2077 if 'Committed r' in output:
2078 revision = re.match(
2079 '.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
2080 logging.debug(output)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002081 finally:
2082 # And then swap back to the original branch and clean up.
2083 RunGit(['checkout', '-q', cl.GetBranch()])
2084 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00002085 if base_has_submodules:
2086 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002087
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002088 if not revision:
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002089 print 'Failed to push. If this persists, please file a bug.'
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002090 return 1
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002091
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002092 killed = False
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002093 if pushed_to_pending:
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002094 try:
2095 revision = WaitForRealCommit(remote, revision, base_branch, branch)
2096 # We set pushed_to_pending to False, since it made it all the way to the
2097 # real ref.
2098 pushed_to_pending = False
2099 except KeyboardInterrupt:
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002100 killed = True
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002101
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002102 if cl.GetIssue():
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002103 to_pending = ' to pending queue' if pushed_to_pending else ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002104 viewvc_url = settings.GetViewVCUrl()
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002105 if not to_pending:
2106 if viewvc_url and revision:
2107 change_desc.append_footer(
2108 'Committed: %s%s' % (viewvc_url, revision))
2109 elif revision:
2110 change_desc.append_footer('Committed: %s' % (revision,))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002111 print ('Closing issue '
2112 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00002113 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002114 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002115 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00002116 patch_num = len(props['patchsets'])
rmistry@google.com52d224a2014-08-27 14:44:41 +00002117 comment = "Committed patchset #%d (id:%d)%s manually as %s" % (
iannucci@chromium.org34504a12014-08-29 23:51:37 +00002118 patch_num, props['patchsets'][-1], to_pending, revision[:7])
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002119 if options.bypass_hooks:
2120 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
2121 else:
2122 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00002123 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002124 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002125
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002126 if pushed_to_pending:
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002127 _, branch = cl.FetchUpstreamTuple(cl.GetBranch())
2128 print 'The commit is in the pending queue (%s).' % pending_ref
2129 print (
thakis@chromium.org5f32a962014-09-05 21:33:23 +00002130 'It will show up on %s in ~1 min, once it gets a Cr-Commit-Position '
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002131 'footer.' % branch)
2132
iannucci@chromium.org6c217b12014-08-29 22:10:59 +00002133 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2134 if os.path.isfile(hook):
2135 RunCommand([hook, merge_base], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002136
iannucci@chromium.orgbbe9cc52014-09-05 18:25:51 +00002137 return 1 if killed else 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002138
2139
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002140def WaitForRealCommit(remote, pushed_commit, local_base_ref, real_ref):
2141 print
2142 print 'Waiting for commit to be landed on %s...' % real_ref
2143 print '(If you are impatient, you may Ctrl-C once without harm)'
2144 target_tree = RunGit(['rev-parse', '%s:' % pushed_commit]).strip()
2145 current_rev = RunGit(['rev-parse', local_base_ref]).strip()
2146
2147 loop = 0
2148 while True:
2149 sys.stdout.write('fetching (%d)... \r' % loop)
2150 sys.stdout.flush()
2151 loop += 1
2152
2153 RunGit(['retry', 'fetch', remote, real_ref], stderr=subprocess2.VOID)
2154 to_rev = RunGit(['rev-parse', 'FETCH_HEAD']).strip()
2155 commits = RunGit(['rev-list', '%s..%s' % (current_rev, to_rev)])
2156 for commit in commits.splitlines():
2157 if RunGit(['rev-parse', '%s:' % commit]).strip() == target_tree:
2158 print 'Found commit on %s' % real_ref
2159 return commit
2160
2161 current_rev = to_rev
2162
2163
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002164def PushToGitPending(remote, pending_ref, upstream_ref):
2165 """Fetches pending_ref, cherry-picks current HEAD on top of it, pushes.
2166
2167 Returns:
2168 (retcode of last operation, output log of last operation).
2169 """
2170 assert pending_ref.startswith('refs/'), pending_ref
2171 local_pending_ref = 'refs/git-cl/' + pending_ref[len('refs/'):]
2172 cherry = RunGit(['rev-parse', 'HEAD']).strip()
2173 code = 0
2174 out = ''
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002175 max_attempts = 3
2176 attempts_left = max_attempts
2177 while attempts_left:
2178 if attempts_left != max_attempts:
2179 print 'Retrying, %d attempts left...' % (attempts_left - 1,)
2180 attempts_left -= 1
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002181
2182 # Fetch. Retry fetch errors.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002183 print 'Fetching pending ref %s...' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002184 code, out = RunGitWithCode(
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002185 ['retry', 'fetch', remote, '+%s:%s' % (pending_ref, local_pending_ref)])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002186 if code:
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002187 print 'Fetch failed with exit code %d.' % code
2188 if out.strip():
2189 print out.strip()
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002190 continue
2191
2192 # Try to cherry pick. Abort on merge conflicts.
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002193 print 'Cherry-picking commit on top of pending ref...'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002194 RunGitWithCode(['checkout', local_pending_ref], suppress_stderr=True)
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002195 code, out = RunGitWithCode(['cherry-pick', cherry])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002196 if code:
2197 print (
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002198 'Your patch doesn\'t apply cleanly to ref \'%s\', '
2199 'the following files have merge conflicts:' % pending_ref)
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002200 print RunGit(['diff', '--name-status', '--diff-filter=U']).strip()
2201 print 'Please rebase your patch and try again.'
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002202 RunGitWithCode(['cherry-pick', '--abort'])
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002203 return code, out
2204
2205 # Applied cleanly, try to push now. Retry on error (flake or non-ff push).
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002206 print 'Pushing commit to %s... It can take a while.' % pending_ref
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002207 code, out = RunGitWithCode(
2208 ['retry', 'push', '--porcelain', remote, 'HEAD:%s' % pending_ref])
2209 if code == 0:
2210 # Success.
iannucci@chromium.orge6896b52014-08-29 01:38:03 +00002211 print 'Commit pushed to pending ref successfully!'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002212 return code, out
2213
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002214 print 'Push failed with exit code %d.' % code
2215 if out.strip():
2216 print out.strip()
2217 if IsFatalPushFailure(out):
2218 print (
2219 'Fatal push error. Make sure your .netrc credentials and git '
2220 'user.email are correct and you have push access to the repo.')
2221 return code, out
2222
2223 print 'All attempts to push to pending ref failed.'
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002224 return code, out
2225
2226
vadimsh@chromium.org749fbd92014-08-26 21:57:53 +00002227def IsFatalPushFailure(push_stdout):
2228 """True if retrying push won't help."""
2229 return '(prohibited by Gerrit)' in push_stdout
2230
2231
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002232@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002233def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002234 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002235 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002236 message = """This doesn't appear to be an SVN repository.
2237If your project has a git mirror with an upstream SVN master, you probably need
2238to run 'git svn init', see your project's git mirror documentation.
2239If your project has a true writeable upstream repository, you probably want
mark@chromium.org671c7a32014-07-31 17:09:36 +00002240to run 'git cl land' instead.
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002241Choose wisely, if you get this wrong, your commit might appear to succeed but
2242will instead be silently ignored."""
2243 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002244 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002245 return SendUpstream(parser, args, 'dcommit')
2246
2247
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002248@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002249def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002250 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002251 if settings.GetIsGitSvn():
2252 print('This appears to be an SVN repository.')
2253 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002254 ask_for_data('[Press enter to push or ctrl-C to quit]')
vadimsh@chromium.org566a02a2014-08-22 01:34:13 +00002255 return SendUpstream(parser, args, 'land')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002256
2257
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002258@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002259def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002260 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002261 parser.add_option('-b', dest='newbranch',
2262 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002263 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002264 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002265 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2266 help='Change to the directory DIR immediately, '
2267 'before doing anything else.')
2268 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002269 help='failed patches spew .rej files rather than '
2270 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002271 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2272 help="don't commit after patch applies")
2273 (options, args) = parser.parse_args(args)
2274 if len(args) != 1:
2275 parser.print_help()
2276 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002277 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002278
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002279 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002280 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002281
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002282 if options.newbranch:
2283 if options.force:
2284 RunGit(['branch', '-D', options.newbranch],
2285 stderr=subprocess2.PIPE, error_ok=True)
2286 RunGit(['checkout', '-b', options.newbranch,
2287 Changelist().GetUpstreamBranch()])
2288
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002289 return PatchIssue(issue_arg, options.reject, options.nocommit,
2290 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002291
2292
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002293def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002294 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002295 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002296 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002297 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002298 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002299 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002300 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002301 # Assume it's a URL to the patch. Default to https.
2302 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002303 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002304 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002305 DieWithError('Must pass an issue ID or full URL for '
2306 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002307 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002308 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002309 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002310
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002311 # Switch up to the top-level directory, if necessary, in preparation for
2312 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002313 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002314 if top:
2315 os.chdir(top)
2316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002317 # Git patches have a/ at the beginning of source paths. We strip that out
2318 # with a sed script rather than the -p flag to patch so we can feed either
2319 # Git or svn-style patches into the same apply command.
2320 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002321 try:
2322 patch_data = subprocess2.check_output(
2323 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2324 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002325 DieWithError('Git patch mungling failed.')
2326 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002327
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002328 # We use "git apply" to apply the patch instead of "patch" so that we can
2329 # pick up file adds.
2330 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002331 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002332 if directory:
2333 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002334 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002335 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002336 elif IsGitVersionAtLeast('1.7.12'):
2337 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002338 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002339 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002340 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002341 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002342 DieWithError('Failed to apply the patch')
2343
2344 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002345 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002346 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2347 cl = Changelist()
2348 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002349 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002350 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002351 else:
2352 print "Patch applied to index."
2353 return 0
2354
2355
2356def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002357 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002358 # Provide a wrapper for git svn rebase to help avoid accidental
2359 # git svn dcommit.
2360 # It's the only command that doesn't use parser at all since we just defer
2361 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002362
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002363 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002364
2365
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002366def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002367 """Fetches the tree status and returns either 'open', 'closed',
2368 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002369 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002370 if url:
2371 status = urllib2.urlopen(url).read().lower()
2372 if status.find('closed') != -1 or status == '0':
2373 return 'closed'
2374 elif status.find('open') != -1 or status == '1':
2375 return 'open'
2376 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002377 return 'unset'
2378
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002379
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002380def GetTreeStatusReason():
2381 """Fetches the tree status from a json url and returns the message
2382 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002383 url = settings.GetTreeStatusUrl()
2384 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002385 connection = urllib2.urlopen(json_url)
2386 status = json.loads(connection.read())
2387 connection.close()
2388 return status['message']
2389
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002390
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002391def GetBuilderMaster(bot_list):
2392 """For a given builder, fetch the master from AE if available."""
2393 map_url = 'https://builders-map.appspot.com/'
2394 try:
2395 master_map = json.load(urllib2.urlopen(map_url))
2396 except urllib2.URLError as e:
2397 return None, ('Failed to fetch builder-to-master map from %s. Error: %s.' %
2398 (map_url, e))
2399 except ValueError as e:
2400 return None, ('Invalid json string from %s. Error: %s.' % (map_url, e))
2401 if not master_map:
2402 return None, 'Failed to build master map.'
2403
2404 result_master = ''
2405 for bot in bot_list:
2406 builder = bot.split(':', 1)[0]
2407 master_list = master_map.get(builder, [])
2408 if not master_list:
2409 return None, ('No matching master for builder %s.' % builder)
2410 elif len(master_list) > 1:
2411 return None, ('The builder name %s exists in multiple masters %s.' %
2412 (builder, master_list))
2413 else:
2414 cur_master = master_list[0]
2415 if not result_master:
2416 result_master = cur_master
2417 elif result_master != cur_master:
2418 return None, 'The builders do not belong to the same master.'
2419 return result_master, None
2420
2421
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002422def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002423 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002424 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002425 status = GetTreeStatus()
2426 if 'unset' == status:
2427 print 'You must configure your tree status URL by running "git cl config".'
2428 return 2
2429
2430 print "The tree is %s" % status
2431 print
2432 print GetTreeStatusReason()
2433 if status != 'open':
2434 return 1
2435 return 0
2436
2437
maruel@chromium.org15192402012-09-06 12:38:29 +00002438def CMDtry(parser, args):
2439 """Triggers a try job through Rietveld."""
2440 group = optparse.OptionGroup(parser, "Try job options")
2441 group.add_option(
2442 "-b", "--bot", action="append",
2443 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2444 "times to specify multiple builders. ex: "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002445 "'-b win_rel:ui_tests,webkit_unit_tests -b win_layout'. See "
maruel@chromium.org15192402012-09-06 12:38:29 +00002446 "the try server waterfall for the builders name and the tests "
2447 "available. Can also be used to specify gtest_filter, e.g. "
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002448 "-b win_rel:base_unittests:ValuesTest.*Value"))
maruel@chromium.org15192402012-09-06 12:38:29 +00002449 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002450 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002451 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002452 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002453 "-r", "--revision",
2454 help="Revision to use for the try job; default: the "
2455 "revision will be determined by the try server; see "
2456 "its waterfall for more info")
2457 group.add_option(
2458 "-c", "--clobber", action="store_true", default=False,
2459 help="Force a clobber before building; e.g. don't do an "
2460 "incremental build")
2461 group.add_option(
2462 "--project",
2463 help="Override which project to use. Projects are defined "
2464 "server-side to define what default bot set to use")
2465 group.add_option(
2466 "-t", "--testfilter", action="append", default=[],
2467 help=("Apply a testfilter to all the selected builders. Unless the "
2468 "builders configurations are similar, use multiple "
2469 "--bot <builder>:<test> arguments."))
2470 group.add_option(
2471 "-n", "--name", help="Try job name; default to current branch name")
2472 parser.add_option_group(group)
2473 options, args = parser.parse_args(args)
2474
2475 if args:
2476 parser.error('Unknown arguments: %s' % args)
2477
2478 cl = Changelist()
2479 if not cl.GetIssue():
2480 parser.error('Need to upload first')
2481
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002482 props = cl.GetIssueProperties()
agable@chromium.org787e3062014-08-20 16:31:19 +00002483 if props.get('closed'):
2484 parser.error('Cannot send tryjobs for a closed CL')
2485
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002486 if props.get('private'):
2487 parser.error('Cannot use trybots with private issue')
2488
maruel@chromium.org15192402012-09-06 12:38:29 +00002489 if not options.name:
2490 options.name = cl.GetBranch()
2491
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002492 if options.bot and not options.master:
sheyang@chromium.org2b34d552014-08-14 22:18:42 +00002493 options.master, err_msg = GetBuilderMaster(options.bot)
2494 if err_msg:
2495 parser.error('Tryserver master cannot be found because: %s\n'
2496 'Please manually specify the tryserver master'
2497 ', e.g. "-m tryserver.chromium.linux".' % err_msg)
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002498
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002499 def GetMasterMap():
2500 # Process --bot and --testfilter.
2501 if not options.bot:
2502 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002503
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002504 # Get try masters from PRESUBMIT.py files.
2505 masters = presubmit_support.DoGetTryMasters(
2506 change,
2507 change.LocalPaths(),
2508 settings.GetRoot(),
2509 None,
2510 None,
2511 options.verbose,
2512 sys.stdout)
2513 if masters:
2514 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002515
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002516 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2517 options.bot = presubmit_support.DoGetTrySlaves(
2518 change,
2519 change.LocalPaths(),
2520 settings.GetRoot(),
2521 None,
2522 None,
2523 options.verbose,
2524 sys.stdout)
2525 if not options.bot:
2526 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002527
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002528 builders_and_tests = {}
2529 # TODO(machenbach): The old style command-line options don't support
2530 # multiple try masters yet.
2531 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2532 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2533
2534 for bot in old_style:
2535 if ':' in bot:
2536 builder, tests = bot.split(':', 1)
2537 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2538 elif ',' in bot:
2539 parser.error('Specify one bot per --bot flag')
2540 else:
2541 builders_and_tests.setdefault(bot, []).append('defaulttests')
2542
2543 for bot, tests in new_style:
2544 builders_and_tests.setdefault(bot, []).extend(tests)
2545
2546 # Return a master map with one master to be backwards compatible. The
2547 # master name defaults to an empty string, which will cause the master
2548 # not to be set on rietveld (deprecated).
2549 return {options.master: builders_and_tests}
2550
2551 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002552
maruel@chromium.org15192402012-09-06 12:38:29 +00002553 if options.testfilter:
2554 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002555 masters = dict((master, dict(
2556 (b, forced_tests) for b, t in slaves.iteritems()
2557 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002558
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002559 for builders in masters.itervalues():
2560 if any('triggered' in b for b in builders):
2561 print >> sys.stderr, (
2562 'ERROR You are trying to send a job to a triggered bot. This type of'
2563 ' bot requires an\ninitial job from a parent (usually a builder). '
2564 'Instead send your job to the parent.\n'
2565 'Bot list: %s' % builders)
2566 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002567
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002568 patchset = cl.GetMostRecentPatchset()
2569 if patchset and patchset != cl.GetPatchset():
2570 print(
2571 '\nWARNING Mismatch between local config and server. Did a previous '
2572 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2573 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002574 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002575 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002576 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002577 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002578 except urllib2.HTTPError, e:
2579 if e.code == 404:
2580 print('404 from rietveld; '
2581 'did you mean to use "git try" instead of "git cl try"?')
2582 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002583 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002584
2585 for (master, builders) in masters.iteritems():
2586 if master:
2587 print 'Master: %s' % master
2588 length = max(len(builder) for builder in builders)
2589 for builder in sorted(builders):
2590 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002591 return 0
2592
2593
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002594@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002595def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002596 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002597 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002598 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002599 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002601 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002602 if args:
2603 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002604 branch = cl.GetBranch()
2605 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002606 cl = Changelist()
2607 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002608
2609 # Clear configured merge-base, if there is one.
2610 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002611 else:
2612 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002613 return 0
2614
2615
thestig@chromium.org00858c82013-12-02 23:08:03 +00002616def CMDweb(parser, args):
2617 """Opens the current CL in the web browser."""
2618 _, args = parser.parse_args(args)
2619 if args:
2620 parser.error('Unrecognized args: %s' % ' '.join(args))
2621
2622 issue_url = Changelist().GetIssueURL()
2623 if not issue_url:
2624 print >> sys.stderr, 'ERROR No issue to open'
2625 return 1
2626
2627 webbrowser.open(issue_url)
2628 return 0
2629
2630
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002631def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002632 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002633 _, args = parser.parse_args(args)
2634 if args:
2635 parser.error('Unrecognized args: %s' % ' '.join(args))
2636 cl = Changelist()
jrobbins@chromium.org16f10f72014-06-24 22:14:36 +00002637 props = cl.GetIssueProperties()
2638 if props.get('private'):
2639 parser.error('Cannot set commit on private issue')
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002640 cl.SetFlag('commit', '1')
2641 return 0
2642
2643
groby@chromium.org411034a2013-02-26 15:12:01 +00002644def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002645 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002646 _, args = parser.parse_args(args)
2647 if args:
2648 parser.error('Unrecognized args: %s' % ' '.join(args))
2649 cl = Changelist()
2650 # Ensure there actually is an issue to close.
2651 cl.GetDescription()
2652 cl.CloseIssue()
2653 return 0
2654
2655
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002656def CMDdiff(parser, args):
2657 """shows differences between local tree and last upload."""
2658 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002659 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002660 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002661 if not issue:
2662 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002663 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002664 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002665
2666 # Create a new branch based on the merge-base
2667 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2668 try:
2669 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002670 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002671 if rtn != 0:
2672 return rtn
2673
2674 # Switch back to starting brand and diff against the temporary
2675 # branch containing the latest rietveld patch.
2676 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2677 finally:
2678 RunGit(['checkout', '-q', branch])
2679 RunGit(['branch', '-D', TMP_BRANCH])
2680
2681 return 0
2682
2683
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002684def CMDowners(parser, args):
2685 """interactively find the owners for reviewing"""
2686 parser.add_option(
2687 '--no-color',
2688 action='store_true',
2689 help='Use this option to disable color output')
2690 options, args = parser.parse_args(args)
2691
2692 author = RunGit(['config', 'user.email']).strip() or None
2693
2694 cl = Changelist()
2695
2696 if args:
2697 if len(args) > 1:
2698 parser.error('Unknown args')
2699 base_branch = args[0]
2700 else:
2701 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002702 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002703
2704 change = cl.GetChange(base_branch, None)
2705 return owners_finder.OwnersFinder(
2706 [f.LocalPath() for f in
2707 cl.GetChange(base_branch, None).AffectedFiles()],
2708 change.RepositoryRoot(), author,
2709 fopen=file, os_path=os.path, glob=glob.glob,
2710 disable_color=options.no_color).run()
2711
2712
enne@chromium.org555cfe42014-01-29 18:21:39 +00002713@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002714def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002715 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002716 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002717 parser.add_option('--full', action='store_true',
2718 help='Reformat the full content of all touched files')
2719 parser.add_option('--dry-run', action='store_true',
2720 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002721 parser.add_option('--diff', action='store_true',
2722 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002723 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002724
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002725 # git diff generates paths against the root of the repository. Change
2726 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002727 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002728 if rel_base_path:
2729 os.chdir(rel_base_path)
2730
digit@chromium.org29e47272013-05-17 17:01:46 +00002731 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002732 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002733 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002734 # Only list the names of modified files.
2735 diff_cmd.append('--name-only')
2736 else:
2737 # Only generate context-less patches.
2738 diff_cmd.append('-U0')
2739
2740 # Grab the merge-base commit, i.e. the upstream commit of the current
2741 # branch when it was created or the last time it was rebased. This is
2742 # to cover the case where the user may have called "git fetch origin",
2743 # moving the origin branch to a newer commit, but hasn't rebased yet.
2744 upstream_commit = None
2745 cl = Changelist()
2746 upstream_branch = cl.GetUpstreamBranch()
2747 if upstream_branch:
2748 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2749 upstream_commit = upstream_commit.strip()
2750
2751 if not upstream_commit:
2752 DieWithError('Could not find base commit for this branch. '
2753 'Are you in detached state?')
2754
2755 diff_cmd.append(upstream_commit)
2756
2757 # Handle source file filtering.
2758 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002759 if args:
2760 for arg in args:
2761 if os.path.isdir(arg):
2762 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2763 elif os.path.isfile(arg):
2764 diff_cmd.append(arg)
2765 else:
2766 DieWithError('Argument "%s" is not a file or a directory' % arg)
2767 else:
2768 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002769 diff_output = RunGit(diff_cmd)
2770
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002771 top_dir = os.path.normpath(
2772 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2773
2774 # Locate the clang-format binary in the checkout
2775 try:
2776 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2777 except clang_format.NotFoundError, e:
2778 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002779
digit@chromium.org29e47272013-05-17 17:01:46 +00002780 if opts.full:
2781 # diff_output is a list of files to send to clang-format.
2782 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002783 if not files:
2784 print "Nothing to format."
2785 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002786 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002787 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002788 cmd.append('-i')
2789 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002790 if opts.diff:
2791 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002792 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002793 env = os.environ.copy()
thakis@chromium.orgbd2a9b92014-09-05 18:54:22 +00002794 env['PATH'] = str(os.path.dirname(clang_format_tool))
digit@chromium.org29e47272013-05-17 17:01:46 +00002795 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002796 try:
2797 script = clang_format.FindClangFormatScriptInChromiumTree(
2798 'clang-format-diff.py')
2799 except clang_format.NotFoundError, e:
2800 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002801
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002802 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002803 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002804 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002805
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002806 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002807 if opts.diff:
2808 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002809 if opts.dry_run and len(stdout) > 0:
2810 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002811
2812 return 0
2813
2814
maruel@chromium.org29404b52014-09-08 22:58:00 +00002815def CMDlol(parser, args):
2816 # This command is intentionally undocumented.
maruel@chromium.org552c31b2014-09-10 11:08:06 +00002817 print('\n'.join((
2818 ' / /',
2819 ' (\\/_//`)',
2820 ' / \'/',
2821 ' 0 0 \\',
2822 ' / \\',
2823 ' / __/ \\',
2824 ' /, _/ \\ \\_',
2825 ' `-./ ) | ~^~^~^~^~^~^~^~\\~.',
2826 ' ( / \\_}',
2827 ' | / |',
2828 ' ; | \\ /',
2829 ' \\/ ,/ \\ |',
2830 ' / /~~|~|~~~~~~|~|\\ |',
2831 ' / / | | | | `\\ \\',
2832 ' / / | | | | \\ \\',
2833 ' / ( | | | | \\ \\',
2834 ' jgs /,_) /__) /__) /,_/',
2835 ' \'\'\'\'\'"""""\'\'\'""""""\'\'\'""""""\'\'"""""\'\'\'\'\'')))
maruel@chromium.org29404b52014-09-08 22:58:00 +00002836 return 0
2837
2838
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002839class OptionParser(optparse.OptionParser):
2840 """Creates the option parse and add --verbose support."""
2841 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002842 optparse.OptionParser.__init__(
2843 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002844 self.add_option(
2845 '-v', '--verbose', action='count', default=0,
2846 help='Use 2 times for more debugging info')
2847
2848 def parse_args(self, args=None, values=None):
2849 options, args = optparse.OptionParser.parse_args(self, args, values)
2850 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2851 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2852 return options, args
2853
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002854
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002855def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002856 if sys.hexversion < 0x02060000:
2857 print >> sys.stderr, (
2858 '\nYour python version %s is unsupported, please upgrade.\n' %
2859 sys.version.split(' ', 1)[0])
2860 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002861
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002862 # Reload settings.
2863 global settings
2864 settings = Settings()
2865
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002866 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002867 dispatcher = subcommand.CommandDispatcher(__name__)
2868 try:
2869 return dispatcher.execute(OptionParser(), argv)
2870 except urllib2.HTTPError, e:
2871 if e.code != 500:
2872 raise
2873 DieWithError(
2874 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2875 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002876
2877
2878if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002879 # These affect sys.stdout so do it outside of main() to simplify mocks in
2880 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002881 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002882 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002883 sys.exit(main(sys.argv[1:]))