blob: f8668c1a8b9fcaf514ab7a620981c486fb671af3 [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
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000010import datetime
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000011from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000012import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000013import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import logging
15import optparse
16import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000017import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000019import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021import textwrap
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +000022import time
maruel@chromium.org1033efd2013-07-23 23:25:09 +000023import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000025import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000026import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000027
28try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000029 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000030except ImportError:
31 pass
32
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000034from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035from third_party import upload
36import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000037import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000038import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000039import gclient_utils
iannucci@chromium.org9e849272014-04-04 00:31:55 +000040import git_common
41import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000042import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000043import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000044import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000045import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000046import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047import watchlists
48
maruel@chromium.org0633fb42013-08-16 20:06:14 +000049__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000050
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000051DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000052POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000054GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000055CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000056
thestig@chromium.org44202a22014-03-11 19:22:18 +000057# Valid extensions for files we want to lint.
58DEFAULT_LINT_REGEX = r"(.*\.cpp|.*\.cc|.*\.h)"
59DEFAULT_LINT_IGNORE_REGEX = r"$^"
60
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000061# Shortcut since it quickly becomes redundant.
62Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000063
maruel@chromium.orgddd59412011-11-30 14:20:38 +000064# Initialized in main()
65settings = None
66
67
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000069 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000070 sys.exit(1)
71
72
thestig@chromium.org8b0553c2014-02-11 00:33:37 +000073def GetNoGitPagerEnv():
74 env = os.environ.copy()
75 # 'cat' is a magical git string that disables pagers on all platforms.
76 env['GIT_PAGER'] = 'cat'
77 return env
78
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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000284
285 def LazyUpdateIfNeeded(self):
286 """Updates the settings from a codereview.settings file, if available."""
287 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000288 # The only value that actually changes the behavior is
289 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000290 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000291 error_ok=True
292 ).strip().lower()
293
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000294 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000295 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000296 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000297 # set updated to True to avoid infinite calling loop
298 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000299 self.updated = True
300 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000301 self.updated = True
302
303 def GetDefaultServerUrl(self, error_ok=False):
304 if not self.default_server:
305 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000306 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000307 self._GetRietveldConfig('server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000308 if error_ok:
309 return self.default_server
310 if not self.default_server:
311 error_message = ('Could not find settings file. You must configure '
312 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000313 self.default_server = gclient_utils.UpgradeToHttps(
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000314 self._GetRietveldConfig('server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000315 return self.default_server
316
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000317 @staticmethod
318 def GetRelativeRoot():
319 return RunGit(['rev-parse', '--show-cdup']).strip()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000320
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000321 def GetRoot(self):
thestig@chromium.org7a54e812014-02-11 19:57:22 +0000322 if self.root is None:
323 self.root = os.path.abspath(self.GetRelativeRoot())
324 return self.root
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000325
326 def GetIsGitSvn(self):
327 """Return true if this repo looks like it's using git-svn."""
328 if self.is_git_svn is None:
329 # If you have any "svn-remote.*" config keys, we think you're using svn.
330 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000331 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000332 return self.is_git_svn
333
334 def GetSVNBranch(self):
335 if self.svn_branch is None:
336 if not self.GetIsGitSvn():
337 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
338
339 # Try to figure out which remote branch we're based on.
340 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000341 # 1) iterate through our branch history and find the svn URL.
342 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000343
344 # regexp matching the git-svn line that contains the URL.
345 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
346
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000347 # We don't want to go through all of history, so read a line from the
348 # pipe at a time.
349 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000350 cmd = ['git', 'log', '-100', '--pretty=medium']
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000351 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE,
352 env=GetNoGitPagerEnv())
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000353 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000354 for line in proc.stdout:
355 match = git_svn_re.match(line)
356 if match:
357 url = match.group(1)
358 proc.stdout.close() # Cut pipe.
359 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000360
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000361 if url:
362 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
363 remotes = RunGit(['config', '--get-regexp',
364 r'^svn-remote\..*\.url']).splitlines()
365 for remote in remotes:
366 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000367 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000368 remote = match.group(1)
369 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000370 rewrite_root = RunGit(
371 ['config', 'svn-remote.%s.rewriteRoot' % remote],
372 error_ok=True).strip()
373 if rewrite_root:
374 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000375 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000376 ['config', 'svn-remote.%s.fetch' % remote],
377 error_ok=True).strip()
378 if fetch_spec:
379 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
380 if self.svn_branch:
381 break
382 branch_spec = RunGit(
383 ['config', 'svn-remote.%s.branches' % remote],
384 error_ok=True).strip()
385 if branch_spec:
386 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
387 if self.svn_branch:
388 break
389 tag_spec = RunGit(
390 ['config', 'svn-remote.%s.tags' % remote],
391 error_ok=True).strip()
392 if tag_spec:
393 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
394 if self.svn_branch:
395 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000396
397 if not self.svn_branch:
398 DieWithError('Can\'t guess svn branch -- try specifying it on the '
399 'command line')
400
401 return self.svn_branch
402
403 def GetTreeStatusUrl(self, error_ok=False):
404 if not self.tree_status_url:
405 error_message = ('You must configure your tree status URL by running '
406 '"git cl config".')
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000407 self.tree_status_url = self._GetRietveldConfig(
408 'tree-status-url', error_ok=error_ok, error_message=error_message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000409 return self.tree_status_url
410
411 def GetViewVCUrl(self):
412 if not self.viewvc_url:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000413 self.viewvc_url = self._GetRietveldConfig('viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000414 return self.viewvc_url
415
rmistry@google.com90752582014-01-14 21:04:50 +0000416 def GetBugPrefix(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000417 return self._GetRietveldConfig('bug-prefix', error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +0000418
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000419 def GetDefaultCCList(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000420 return self._GetRietveldConfig('cc', error_ok=True)
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000421
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000422 def GetDefaultPrivateFlag(self):
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000423 return self._GetRietveldConfig('private', error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000424
ukai@chromium.orge8077812012-02-03 03:41:46 +0000425 def GetIsGerrit(self):
426 """Return true if this repo is assosiated with gerrit code review system."""
427 if self.is_gerrit is None:
428 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
429 return self.is_gerrit
430
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000431 def GetGitEditor(self):
432 """Return the editor specified in the git config, or None if none is."""
433 if self.git_editor is None:
434 self.git_editor = self._GetConfig('core.editor', error_ok=True)
435 return self.git_editor or None
436
thestig@chromium.org44202a22014-03-11 19:22:18 +0000437 def GetLintRegex(self):
438 return (self._GetRietveldConfig('cpplint-regex', error_ok=True) or
439 DEFAULT_LINT_REGEX)
440
441 def GetLintIgnoreRegex(self):
442 return (self._GetRietveldConfig('cpplint-ignore-regex', error_ok=True) or
443 DEFAULT_LINT_IGNORE_REGEX)
444
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000445 def _GetRietveldConfig(self, param, **kwargs):
446 return self._GetConfig('rietveld.' + param, **kwargs)
447
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000448 def _GetConfig(self, param, **kwargs):
449 self.LazyUpdateIfNeeded()
450 return RunGit(['config', param], **kwargs).strip()
451
452
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000453def ShortBranchName(branch):
454 """Convert a name like 'refs/heads/foo' to just 'foo'."""
455 return branch.replace('refs/heads/', '')
456
457
458class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000459 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000460 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000461 global settings
462 if not settings:
463 # Happens when git_cl.py is used as a utility library.
464 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000465 settings.GetDefaultServerUrl()
466 self.branchref = branchref
467 if self.branchref:
468 self.branch = ShortBranchName(self.branchref)
469 else:
470 self.branch = None
471 self.rietveld_server = None
472 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000473 self.lookedup_issue = False
474 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000475 self.has_description = False
476 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000477 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000478 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000479 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000480 self.cc = None
481 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000482 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000483 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000484
485 def GetCCList(self):
486 """Return the users cc'd on this CL.
487
488 Return is a string suitable for passing to gcl with the --cc flag.
489 """
490 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000491 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000492 more_cc = ','.join(self.watchers)
493 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
494 return self.cc
495
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000496 def GetCCListWithoutDefault(self):
497 """Return the users cc'd on this CL excluding default ones."""
498 if self.cc is None:
499 self.cc = ','.join(self.watchers)
500 return self.cc
501
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000502 def SetWatchers(self, watchers):
503 """Set the list of email addresses that should be cc'd based on the changed
504 files in this CL.
505 """
506 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000507
508 def GetBranch(self):
509 """Returns the short branch name, e.g. 'master'."""
510 if not self.branch:
511 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
512 self.branch = ShortBranchName(self.branchref)
513 return self.branch
514
515 def GetBranchRef(self):
516 """Returns the full branch name, e.g. 'refs/heads/master'."""
517 self.GetBranch() # Poke the lazy loader.
518 return self.branchref
519
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000520 @staticmethod
521 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000522 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000523 e.g. 'origin', 'refs/heads/master'
524 """
525 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000526 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
527 error_ok=True).strip()
528 if upstream_branch:
529 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
530 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000531 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
532 error_ok=True).strip()
533 if upstream_branch:
534 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000535 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000536 # Fall back on trying a git-svn upstream branch.
537 if settings.GetIsGitSvn():
538 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000539 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000540 # Else, try to guess the origin remote.
541 remote_branches = RunGit(['branch', '-r']).split()
542 if 'origin/master' in remote_branches:
543 # Fall back on origin/master if it exits.
544 remote = 'origin'
545 upstream_branch = 'refs/heads/master'
546 elif 'origin/trunk' in remote_branches:
547 # Fall back on origin/trunk if it exists. Generally a shared
548 # git-svn clone
549 remote = 'origin'
550 upstream_branch = 'refs/heads/trunk'
551 else:
552 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000553Either pass complete "git diff"-style arguments, like
554 git cl upload origin/master
555or verify this branch is set up to track another (via the --track argument to
556"git checkout -b ...").""")
557
558 return remote, upstream_branch
559
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000560 def GetCommonAncestorWithUpstream(self):
iannucci@chromium.org9e849272014-04-04 00:31:55 +0000561 return git_common.get_or_create_merge_base(self.GetBranch(),
562 self.GetUpstreamBranch())
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000563
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000564 def GetUpstreamBranch(self):
565 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000566 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000567 if remote is not '.':
568 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
569 self.upstream_branch = upstream_branch
570 return self.upstream_branch
571
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000572 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000573 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000574 remote, branch = None, self.GetBranch()
575 seen_branches = set()
576 while branch not in seen_branches:
577 seen_branches.add(branch)
578 remote, branch = self.FetchUpstreamTuple(branch)
579 branch = ShortBranchName(branch)
580 if remote != '.' or branch.startswith('refs/remotes'):
581 break
582 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000583 remotes = RunGit(['remote'], error_ok=True).split()
584 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000585 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000586 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000587 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000588 logging.warning('Could not determine which remote this change is '
589 'associated with, so defaulting to "%s". This may '
590 'not be what you want. You may prevent this message '
591 'by running "git svn info" as documented here: %s',
592 self._remote,
593 GIT_INSTRUCTIONS_URL)
594 else:
595 logging.warn('Could not determine which remote this change is '
596 'associated with. You may prevent this message by '
597 'running "git svn info" as documented here: %s',
598 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000599 branch = 'HEAD'
600 if branch.startswith('refs/remotes'):
601 self._remote = (remote, branch)
602 else:
603 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000604 return self._remote
605
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000606 def GitSanityChecks(self, upstream_git_obj):
607 """Checks git repo status and ensures diff is from local commits."""
608
609 # Verify the commit we're diffing against is in our current branch.
610 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
611 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
612 if upstream_sha != common_ancestor:
613 print >> sys.stderr, (
614 'ERROR: %s is not in the current branch. You may need to rebase '
615 'your tracking branch' % upstream_sha)
616 return False
617
618 # List the commits inside the diff, and verify they are all local.
619 commits_in_diff = RunGit(
620 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
621 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
622 remote_branch = remote_branch.strip()
623 if code != 0:
624 _, remote_branch = self.GetRemoteBranch()
625
626 commits_in_remote = RunGit(
627 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
628
629 common_commits = set(commits_in_diff) & set(commits_in_remote)
630 if common_commits:
631 print >> sys.stderr, (
632 'ERROR: Your diff contains %d commits already in %s.\n'
633 'Run "git log --oneline %s..HEAD" to get a list of commits in '
634 'the diff. If you are using a custom git flow, you can override'
635 ' the reference used for this check with "git config '
636 'gitcl.remotebranch <git-ref>".' % (
637 len(common_commits), remote_branch, upstream_git_obj))
638 return False
639 return True
640
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000641 def GetGitBaseUrlFromConfig(self):
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000642 """Return the configured base URL from branch.<branchname>.baseurl.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000643
644 Returns None if it is not set.
645 """
sheyang@chromium.orga656e702014-05-15 20:43:05 +0000646 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
647 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000648
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000649 def GetRemoteUrl(self):
650 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
651
652 Returns None if there is no remote.
653 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000654 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
656
657 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000658 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000659 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000661 self.issue = int(issue) or None if issue else None
662 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000663 return self.issue
664
665 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000666 if not self.rietveld_server:
667 # If we're on a branch then get the server potentially associated
668 # with that branch.
669 if self.GetIssue():
670 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
671 ['config', self._RietveldServer()], error_ok=True).strip())
672 if not self.rietveld_server:
673 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000674 return self.rietveld_server
675
676 def GetIssueURL(self):
677 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000678 if not self.GetIssue():
679 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000680 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
681
682 def GetDescription(self, pretty=False):
683 if not self.has_description:
684 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000685 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000686 try:
687 self.description = self.RpcServer().get_description(issue).strip()
688 except urllib2.HTTPError, e:
689 if e.code == 404:
690 DieWithError(
691 ('\nWhile fetching the description for issue %d, received a '
692 '404 (not found)\n'
693 'error. It is likely that you deleted this '
694 'issue on the server. If this is the\n'
695 'case, please run\n\n'
696 ' git cl issue 0\n\n'
697 'to clear the association with the deleted issue. Then run '
698 'this command again.') % issue)
699 else:
700 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000701 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702 self.has_description = True
703 if pretty:
704 wrapper = textwrap.TextWrapper()
705 wrapper.initial_indent = wrapper.subsequent_indent = ' '
706 return wrapper.fill(self.description)
707 return self.description
708
709 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000710 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000711 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712 patchset = RunGit(['config', self._PatchsetSetting()],
713 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000714 self.patchset = int(patchset) or None if patchset else None
715 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716 return self.patchset
717
718 def SetPatchset(self, patchset):
719 """Set this branch's patchset. If patchset=0, clears the patchset."""
720 if patchset:
721 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000722 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000723 else:
724 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000725 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000726 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000728 def GetMostRecentPatchset(self):
729 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000730
731 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000732 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000733 '/download/issue%s_%s.diff' % (issue, patchset))
734
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000735 def GetIssueProperties(self):
736 if self._props is None:
737 issue = self.GetIssue()
738 if not issue:
739 self._props = {}
740 else:
741 self._props = self.RpcServer().get_issue_properties(issue, True)
742 return self._props
743
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000744 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000745 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000746
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000747 def SetIssue(self, issue):
748 """Set this branch's issue. If issue=0, clears the issue."""
749 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000750 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751 RunGit(['config', self._IssueSetting(), str(issue)])
752 if self.rietveld_server:
753 RunGit(['config', self._RietveldServer(), self.rietveld_server])
754 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000755 current_issue = self.GetIssue()
756 if current_issue:
757 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000758 self.issue = None
759 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000760
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000761 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000762 if not self.GitSanityChecks(upstream_branch):
763 DieWithError('\nGit sanity check failure')
764
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000765 root = settings.GetRelativeRoot()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000766 if not root:
767 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000768 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000769
770 # We use the sha1 of HEAD as a name of this change.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000771 name = RunGitWithCode(['rev-parse', 'HEAD'])[1].strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000772 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000773 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000774 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000775 except subprocess2.CalledProcessError:
776 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000777 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000778 'This branch probably doesn\'t exist anymore. To reset the\n'
779 'tracking branch, please run\n'
780 ' git branch --set-upstream %s trunk\n'
781 'replacing trunk with origin/master or the relevant branch') %
782 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000783
maruel@chromium.org52424302012-08-29 15:14:30 +0000784 issue = self.GetIssue()
785 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000786 if issue:
787 description = self.GetDescription()
788 else:
789 # If the change was never uploaded, use the log messages of all commits
790 # up to the branch point, as git cl upload will prefill the description
791 # with these log messages.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +0000792 args = ['log', '--pretty=format:%s%n%n%b', '%s...' % (upstream_branch)]
793 description = RunGitWithCode(args)[1].strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000794
795 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000796 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000797 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000798 name,
799 description,
800 absroot,
801 files,
802 issue,
803 patchset,
agable@chromium.orgea84ef12014-04-30 19:55:12 +0000804 author,
805 upstream=upstream_branch)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000806
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000807 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000808 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000809
810 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000811 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000812 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000813 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000814 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000815 except presubmit_support.PresubmitFailure, e:
816 DieWithError(
817 ('%s\nMaybe your depot_tools is out of date?\n'
818 'If all fails, contact maruel@') % e)
819
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000820 def UpdateDescription(self, description):
821 self.description = description
822 return self.RpcServer().update_description(
823 self.GetIssue(), self.description)
824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000826 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000827 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000828
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000829 def SetFlag(self, flag, value):
830 """Patchset must match."""
831 if not self.GetPatchset():
832 DieWithError('The patchset needs to match. Send another patchset.')
833 try:
834 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000835 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000836 except urllib2.HTTPError, e:
837 if e.code == 404:
838 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
839 if e.code == 403:
840 DieWithError(
841 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
842 'match?') % (self.GetIssue(), self.GetPatchset()))
843 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000844
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000845 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 """Returns an upload.RpcServer() to access this review's rietveld instance.
847 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000848 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000849 self._rpc_server = rietveld.CachingRietveld(
850 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000851 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000852
853 def _IssueSetting(self):
854 """Return the git setting that stores this change's issue."""
855 return 'branch.%s.rietveldissue' % self.GetBranch()
856
857 def _PatchsetSetting(self):
858 """Return the git setting that stores this change's most recent patchset."""
859 return 'branch.%s.rietveldpatchset' % self.GetBranch()
860
861 def _RietveldServer(self):
862 """Returns the git setting that stores this change's rietveld server."""
863 return 'branch.%s.rietveldserver' % self.GetBranch()
864
865
866def GetCodereviewSettingsInteractively():
867 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000868 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869 server = settings.GetDefaultServerUrl(error_ok=True)
870 prompt = 'Rietveld server (host[:port])'
871 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000872 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873 if not server and not newserver:
874 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000875 if newserver:
876 newserver = gclient_utils.UpgradeToHttps(newserver)
877 if newserver != server:
878 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000879
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000880 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000881 prompt = caption
882 if initial:
883 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000884 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000885 if new_val == 'x':
886 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000887 elif new_val:
888 if is_url:
889 new_val = gclient_utils.UpgradeToHttps(new_val)
890 if new_val != initial:
891 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000893 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000894 SetProperty(settings.GetDefaultPrivateFlag(),
895 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000897 'tree-status-url', False)
898 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000899 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900
901 # TODO: configure a default branch to diff against, rather than this
902 # svn-based hackery.
903
904
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000905class ChangeDescription(object):
906 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000907 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000908 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000909
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000910 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000911 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000912
agable@chromium.org42c20792013-09-12 17:34:49 +0000913 @property # www.logilab.org/ticket/89786
914 def description(self): # pylint: disable=E0202
915 return '\n'.join(self._description_lines)
916
917 def set_description(self, desc):
918 if isinstance(desc, basestring):
919 lines = desc.splitlines()
920 else:
921 lines = [line.rstrip() for line in desc]
922 while lines and not lines[0]:
923 lines.pop(0)
924 while lines and not lines[-1]:
925 lines.pop(-1)
926 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000927
928 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000929 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000930 assert isinstance(reviewers, list), reviewers
931 if not reviewers:
932 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000933 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000934
agable@chromium.org42c20792013-09-12 17:34:49 +0000935 # Get the set of R= and TBR= lines and remove them from the desciption.
936 regexp = re.compile(self.R_LINE)
937 matches = [regexp.match(line) for line in self._description_lines]
938 new_desc = [l for i, l in enumerate(self._description_lines)
939 if not matches[i]]
940 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000941
agable@chromium.org42c20792013-09-12 17:34:49 +0000942 # Construct new unified R= and TBR= lines.
943 r_names = []
944 tbr_names = []
945 for match in matches:
946 if not match:
947 continue
948 people = cleanup_list([match.group(2).strip()])
949 if match.group(1) == 'TBR':
950 tbr_names.extend(people)
951 else:
952 r_names.extend(people)
953 for name in r_names:
954 if name not in reviewers:
955 reviewers.append(name)
956 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
957 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
958
959 # Put the new lines in the description where the old first R= line was.
960 line_loc = next((i for i, match in enumerate(matches) if match), -1)
961 if 0 <= line_loc < len(self._description_lines):
962 if new_tbr_line:
963 self._description_lines.insert(line_loc, new_tbr_line)
964 if new_r_line:
965 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000966 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000967 if new_r_line:
968 self.append_footer(new_r_line)
969 if new_tbr_line:
970 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000971
972 def prompt(self):
973 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000974 self.set_description([
975 '# Enter a description of the change.',
976 '# This will be displayed on the codereview site.',
977 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000978 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000979 '--------------------',
980 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000981
agable@chromium.org42c20792013-09-12 17:34:49 +0000982 regexp = re.compile(self.BUG_LINE)
983 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000984 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000985 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000986 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000987 if not content:
988 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000989 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000990
991 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000992 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
993 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000994 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000995 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000996
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000997 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000998 if self._description_lines:
999 # Add an empty line if either the last line or the new line isn't a tag.
1000 last_line = self._description_lines[-1]
1001 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
1002 not presubmit_support.Change.TAG_LINE_RE.match(line)):
1003 self._description_lines.append('')
1004 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001005
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001006 def get_reviewers(self):
1007 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +00001008 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
1009 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001010 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +00001011
1012
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001013def get_approving_reviewers(props):
1014 """Retrieves the reviewers that approved a CL from the issue properties with
1015 messages.
1016
1017 Note that the list may contain reviewers that are not committer, thus are not
1018 considered by the CQ.
1019 """
1020 return sorted(
1021 set(
1022 message['sender']
1023 for message in props['messages']
1024 if message['approval'] and message['sender'] in props['reviewers']
1025 )
1026 )
1027
1028
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029def FindCodereviewSettingsFile(filename='codereview.settings'):
1030 """Finds the given file starting in the cwd and going up.
1031
1032 Only looks up to the top of the repository unless an
1033 'inherit-review-settings-ok' file exists in the root of the repository.
1034 """
1035 inherit_ok_file = 'inherit-review-settings-ok'
1036 cwd = os.getcwd()
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001037 root = settings.GetRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001038 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1039 root = '/'
1040 while True:
1041 if filename in os.listdir(cwd):
1042 if os.path.isfile(os.path.join(cwd, filename)):
1043 return open(os.path.join(cwd, filename))
1044 if cwd == root:
1045 break
1046 cwd = os.path.dirname(cwd)
1047
1048
1049def LoadCodereviewSettingsFromFile(fileobj):
1050 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001051 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053 def SetProperty(name, setting, unset_error_ok=False):
1054 fullname = 'rietveld.' + name
1055 if setting in keyvals:
1056 RunGit(['config', fullname, keyvals[setting]])
1057 else:
1058 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1059
1060 SetProperty('server', 'CODE_REVIEW_SERVER')
1061 # Only server setting is required. Other settings can be absent.
1062 # In that case, we ignore errors raised during option deletion attempt.
1063 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001064 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001065 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1066 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001067 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001068 SetProperty('cpplint-regex', 'LINT_REGEX', unset_error_ok=True)
1069 SetProperty('cpplint-ignore-regex', 'LINT_IGNORE_REGEX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001070
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001071 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001072 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001073
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001074 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1075 #should be of the form
1076 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1077 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1078 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1079 keyvals['ORIGIN_URL_CONFIG']])
1080
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001082def urlretrieve(source, destination):
1083 """urllib is broken for SSL connections via a proxy therefore we
1084 can't use urllib.urlretrieve()."""
1085 with open(destination, 'w') as f:
1086 f.write(urllib2.urlopen(source).read())
1087
1088
ukai@chromium.org712d6102013-11-27 00:52:58 +00001089def hasSheBang(fname):
1090 """Checks fname is a #! script."""
1091 with open(fname) as f:
1092 return f.read(2).startswith('#!')
1093
1094
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001095def DownloadHooks(force):
1096 """downloads hooks
1097
1098 Args:
1099 force: True to update hooks. False to install hooks if not present.
1100 """
1101 if not settings.GetIsGerrit():
1102 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001103 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001104 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1105 if not os.access(dst, os.X_OK):
1106 if os.path.exists(dst):
1107 if not force:
1108 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001109 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001110 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001111 if not hasSheBang(dst):
1112 DieWithError('Not a script: %s\n'
1113 'You need to download from\n%s\n'
1114 'into .git/hooks/commit-msg and '
1115 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001116 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1117 except Exception:
1118 if os.path.exists(dst):
1119 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001120 DieWithError('\nFailed to download hooks.\n'
1121 'You need to download from\n%s\n'
1122 'into .git/hooks/commit-msg and '
1123 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001124
1125
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001126@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001127def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001128 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001130 parser.add_option('--activate-update', action='store_true',
1131 help='activate auto-updating [rietveld] section in '
1132 '.git/config')
1133 parser.add_option('--deactivate-update', action='store_true',
1134 help='deactivate auto-updating [rietveld] section in '
1135 '.git/config')
1136 options, args = parser.parse_args(args)
1137
1138 if options.deactivate_update:
1139 RunGit(['config', 'rietveld.autoupdate', 'false'])
1140 return
1141
1142 if options.activate_update:
1143 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1144 return
1145
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 if len(args) == 0:
1147 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001148 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 return 0
1150
1151 url = args[0]
1152 if not url.endswith('codereview.settings'):
1153 url = os.path.join(url, 'codereview.settings')
1154
1155 # Load code review settings and download hooks (if available).
1156 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001157 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001158 return 0
1159
1160
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001161def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001162 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001163 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1164 branch = ShortBranchName(branchref)
1165 _, args = parser.parse_args(args)
1166 if not args:
1167 print("Current base-url:")
1168 return RunGit(['config', 'branch.%s.base-url' % branch],
1169 error_ok=False).strip()
1170 else:
1171 print("Setting base-url to %s" % args[0])
1172 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1173 error_ok=False).strip()
1174
1175
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001177 """Show status of changelists.
1178
1179 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001180 - Red not sent for review or broken
1181 - Blue waiting for review
1182 - Yellow waiting for you to reply to review
1183 - Green LGTM'ed
1184 - Magenta in the commit queue
1185 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001186
1187 Also see 'git cl comments'.
1188 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001189 parser.add_option('--field',
1190 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001191 parser.add_option('-f', '--fast', action='store_true',
1192 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001193 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001194 if args:
1195 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001196
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001197 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001198 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001199 if options.field.startswith('desc'):
1200 print cl.GetDescription()
1201 elif options.field == 'id':
1202 issueid = cl.GetIssue()
1203 if issueid:
1204 print issueid
1205 elif options.field == 'patch':
1206 patchset = cl.GetPatchset()
1207 if patchset:
1208 print patchset
1209 elif options.field == 'url':
1210 url = cl.GetIssueURL()
1211 if url:
1212 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001213 return 0
1214
1215 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1216 if not branches:
1217 print('No local branch found.')
1218 return 0
1219
1220 changes = (Changelist(branchref=b) for b in branches.splitlines())
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001221 branches = [c.GetBranch() for c in changes]
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001222 alignment = max(5, max(len(b) for b in branches))
1223 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001224 # Adhoc thread pool to request data concurrently.
1225 output = Queue.Queue()
1226
1227 # Silence upload.py otherwise it becomes unweldly.
1228 upload.verbosity = 0
1229
1230 if not options.fast:
1231 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001232 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001233 c = Changelist(branchref=b)
1234 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001235 props = {}
1236 r = None
1237 if i:
1238 try:
1239 props = c.GetIssueProperties()
1240 r = c.GetApprovingReviewers() if i else None
1241 except urllib2.HTTPError:
1242 # The issue probably doesn't exist anymore.
1243 i += ' (broken)'
1244
1245 msgs = props.get('messages') or []
1246
1247 if not i:
1248 color = Fore.WHITE
1249 elif props.get('closed'):
1250 # Issue is closed.
1251 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001252 elif props.get('commit'):
1253 # Issue is in the commit queue.
1254 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001255 elif r:
1256 # Was LGTM'ed.
1257 color = Fore.GREEN
1258 elif not msgs:
1259 # No message was sent.
1260 color = Fore.RED
1261 elif msgs[-1]['sender'] != props.get('owner_email'):
1262 color = Fore.YELLOW
1263 else:
1264 color = Fore.BLUE
1265 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001266
jrobbins@chromium.orga4c03052014-04-25 19:06:36 +00001267 # Process one branch synchronously to work through authentication, then
1268 # spawn threads to process all the other branches in parallel.
1269 if branches:
1270 fetch(branches[0])
1271 threads = [
1272 threading.Thread(target=fetch, args=(b,)) for b in branches[1:]]
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001273 for t in threads:
1274 t.daemon = True
1275 t.start()
1276 else:
1277 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1278 for b in branches:
1279 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001280 url = c.GetIssueURL()
1281 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001282
1283 tmp = {}
1284 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001285 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001286 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001287 b, i, color = output.get()
1288 tmp[b] = (i, color)
1289 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001290 reset = Fore.RESET
1291 if not sys.stdout.isatty():
1292 color = ''
1293 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001294 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001295 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001296
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001297 cl = Changelist()
1298 print
1299 print 'Current branch:',
1300 if not cl.GetIssue():
1301 print 'no issue assigned.'
1302 return 0
1303 print cl.GetBranch()
1304 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1305 print 'Issue description:'
1306 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001307 return 0
1308
1309
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001310def colorize_CMDstatus_doc():
1311 """To be called once in main() to add colors to git cl status help."""
1312 colors = [i for i in dir(Fore) if i[0].isupper()]
1313
1314 def colorize_line(line):
1315 for color in colors:
1316 if color in line.upper():
1317 # Extract whitespaces first and the leading '-'.
1318 indent = len(line) - len(line.lstrip(' ')) + 1
1319 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1320 return line
1321
1322 lines = CMDstatus.__doc__.splitlines()
1323 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1324
1325
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001326@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001328 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329
1330 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001331 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001332 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001333
1334 cl = Changelist()
1335 if len(args) > 0:
1336 try:
1337 issue = int(args[0])
1338 except ValueError:
1339 DieWithError('Pass a number to set the issue or none to list it.\n'
1340 'Maybe you want to run git cl status?')
1341 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001342 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343 return 0
1344
1345
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001346def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001347 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001348 (_, args) = parser.parse_args(args)
1349 if args:
1350 parser.error('Unsupported argument: %s' % args)
1351
1352 cl = Changelist()
1353 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001354 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001355 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001356 if message['disapproval']:
1357 color = Fore.RED
1358 elif message['approval']:
1359 color = Fore.GREEN
1360 elif message['sender'] == data['owner_email']:
1361 color = Fore.MAGENTA
1362 else:
1363 color = Fore.BLUE
1364 print '\n%s%s %s%s' % (
1365 color, message['date'].split('.', 1)[0], message['sender'],
1366 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001367 if message['text'].strip():
1368 print '\n'.join(' ' + l for l in message['text'].splitlines())
1369 return 0
1370
1371
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001372def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001373 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001374 cl = Changelist()
1375 if not cl.GetIssue():
1376 DieWithError('This branch has no associated changelist.')
1377 description = ChangeDescription(cl.GetDescription())
1378 description.prompt()
1379 cl.UpdateDescription(description.description)
1380 return 0
1381
1382
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383def CreateDescriptionFromLog(args):
1384 """Pulls out the commit log to use as a base for the CL description."""
1385 log_args = []
1386 if len(args) == 1 and not args[0].endswith('.'):
1387 log_args = [args[0] + '..']
1388 elif len(args) == 1 and args[0].endswith('...'):
1389 log_args = [args[0][:-1]]
1390 elif len(args) == 2:
1391 log_args = [args[0] + '..' + args[1]]
1392 else:
1393 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001394 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395
1396
thestig@chromium.org44202a22014-03-11 19:22:18 +00001397def CMDlint(parser, args):
1398 """Runs cpplint on the current changelist."""
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001399 parser.add_option('--filter', action='append', metavar='-x,+y',
1400 help='Comma-separated list of cpplint\'s category-filters')
1401 (options, args) = parser.parse_args(args)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001402
1403 # Access to a protected member _XX of a client class
1404 # pylint: disable=W0212
1405 try:
1406 import cpplint
1407 import cpplint_chromium
1408 except ImportError:
1409 print "Your depot_tools is missing cpplint.py and/or cpplint_chromium.py."
1410 return 1
1411
1412 # Change the current working directory before calling lint so that it
1413 # shows the correct base.
1414 previous_cwd = os.getcwd()
1415 os.chdir(settings.GetRoot())
1416 try:
1417 cl = Changelist()
1418 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
1419 files = [f.LocalPath() for f in change.AffectedFiles()]
1420
1421 # Process cpplints arguments if any.
tzik@chromium.orgf204d4b2014-03-13 07:40:55 +00001422 command = args + files
1423 if options.filter:
1424 command = ['--filter=' + ','.join(options.filter)] + command
1425 filenames = cpplint.ParseArguments(command)
thestig@chromium.org44202a22014-03-11 19:22:18 +00001426
1427 white_regex = re.compile(settings.GetLintRegex())
1428 black_regex = re.compile(settings.GetLintIgnoreRegex())
1429 extra_check_functions = [cpplint_chromium.CheckPointerDeclarationWhitespace]
1430 for filename in filenames:
1431 if white_regex.match(filename):
1432 if black_regex.match(filename):
1433 print "Ignoring file %s" % filename
1434 else:
1435 cpplint.ProcessFile(filename, cpplint._cpplint_state.verbose_level,
1436 extra_check_functions)
1437 else:
1438 print "Skipping file %s" % filename
1439 finally:
1440 os.chdir(previous_cwd)
1441 print "Total errors found: %d\n" % cpplint._cpplint_state.error_count
1442 if cpplint._cpplint_state.error_count != 0:
1443 return 1
1444 return 0
1445
1446
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001448 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001449 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001451 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001452 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 (options, args) = parser.parse_args(args)
1454
ukai@chromium.org259e4682012-10-25 07:36:33 +00001455 if not options.force and is_dirty_git_tree('presubmit'):
1456 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001457 return 1
1458
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001459 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001460 if args:
1461 base_branch = args[0]
1462 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001463 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001464 base_branch = cl.GetCommonAncestorWithUpstream()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001466 cl.RunHook(
1467 committing=not options.upload,
1468 may_prompt=False,
1469 verbose=options.verbose,
1470 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001471 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001472
1473
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001474def AddChangeIdToCommitMessage(options, args):
1475 """Re-commits using the current message, assumes the commit hook is in
1476 place.
1477 """
1478 log_desc = options.message or CreateDescriptionFromLog(args)
1479 git_command = ['commit', '--amend', '-m', log_desc]
1480 RunGit(git_command)
1481 new_log_desc = CreateDescriptionFromLog(args)
1482 if CHANGE_ID in new_log_desc:
1483 print 'git-cl: Added Change-Id to commit message.'
1484 else:
1485 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1486
1487
ukai@chromium.orge8077812012-02-03 03:41:46 +00001488def GerritUpload(options, args, cl):
1489 """upload the current branch to gerrit."""
1490 # We assume the remote called "origin" is the one we want.
1491 # It is probably not worthwhile to support different workflows.
1492 remote = 'origin'
1493 branch = 'master'
1494 if options.target_branch:
1495 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001496
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001497 change_desc = ChangeDescription(
1498 options.message or CreateDescriptionFromLog(args))
1499 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001500 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001501 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001502 if CHANGE_ID not in change_desc.description:
1503 AddChangeIdToCommitMessage(options, args)
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001504
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001505 commits = RunGit(['rev-list', '%s/%s..' % (remote, branch)]).splitlines()
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001506 if len(commits) > 1:
1507 print('WARNING: This will upload %d commits. Run the following command '
1508 'to see which commits will be uploaded: ' % len(commits))
bauerb@chromium.org279c2182014-05-16 09:22:09 +00001509 print('git log %s/%s..' % (remote, branch))
bauerb@chromium.orgf75c2302014-05-01 08:19:30 +00001510 print('You can also use `git squash-branch` to squash these into a single'
1511 'commit.')
1512 ask_for_data('About to upload; enter to confirm.')
1513
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001514 if options.reviewers:
1515 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001516
ukai@chromium.orge8077812012-02-03 03:41:46 +00001517 receive_options = []
1518 cc = cl.GetCCList().split(',')
1519 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001520 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001521 cc = filter(None, cc)
1522 if cc:
1523 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001524 if change_desc.get_reviewers():
1525 receive_options.extend(
1526 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527
ukai@chromium.orge8077812012-02-03 03:41:46 +00001528 git_command = ['push']
1529 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001530 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001531 ' '.join(receive_options))
1532 git_command += [remote, 'HEAD:refs/for/' + branch]
1533 RunGit(git_command)
1534 # TODO(ukai): parse Change-Id: and set issue number?
1535 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001536
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537
ukai@chromium.orge8077812012-02-03 03:41:46 +00001538def RietveldUpload(options, args, cl):
1539 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001540 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1541 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001542 if options.emulate_svn_auto_props:
1543 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544
1545 change_desc = None
1546
pgervais@chromium.org91141372014-01-09 23:27:20 +00001547 if options.email is not None:
1548 upload_args.extend(['--email', options.email])
1549
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001550 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001551 if options.title:
1552 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001553 if options.message:
1554 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001555 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556 print ("This branch is associated with issue %s. "
1557 "Adding patch to that issue." % cl.GetIssue())
1558 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001559 if options.title:
1560 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001561 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001562 change_desc = ChangeDescription(message)
1563 if options.reviewers:
1564 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001565 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001566 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001567
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001568 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001569 print "Description is empty; aborting."
1570 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001571
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001572 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001573 if change_desc.get_reviewers():
1574 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001575 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001576 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001577 DieWithError("Must specify reviewers to send email.")
1578 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001579
1580 # We check this before applying rietveld.private assuming that in
1581 # rietveld.cc only addresses which we can send private CLs to are listed
1582 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1583 # --private is specified explicitly on the command line.
1584 if options.private:
1585 logging.warn('rietveld.cc is ignored since private flag is specified. '
1586 'You need to review and add them manually if necessary.')
1587 cc = cl.GetCCListWithoutDefault()
1588 else:
1589 cc = cl.GetCCList()
1590 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001591 if cc:
1592 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001593
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001594 if options.private or settings.GetDefaultPrivateFlag() == "True":
1595 upload_args.append('--private')
1596
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001597 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001598 if not options.find_copies:
1599 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001600
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001601 # Include the upstream repo's URL in the change -- this is useful for
1602 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001603 remote_url = cl.GetGitBaseUrlFromConfig()
1604 if not remote_url:
1605 if settings.GetIsGitSvn():
1606 # URL is dependent on the current directory.
1607 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1608 if data:
1609 keys = dict(line.split(': ', 1) for line in data.splitlines()
1610 if ': ' in line)
1611 remote_url = keys.get('URL', None)
1612 else:
1613 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1614 remote_url = (cl.GetRemoteUrl() + '@'
1615 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001616 if remote_url:
1617 upload_args.extend(['--base_url', remote_url])
1618
1619 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001620 upload_args = ['upload'] + upload_args + args
1621 logging.info('upload.RealMain(%s)', upload_args)
1622 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001623 issue = int(issue)
1624 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001625 except KeyboardInterrupt:
1626 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001627 except:
1628 # If we got an exception after the user typed a description for their
1629 # change, back up the description before re-raising.
1630 if change_desc:
1631 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1632 print '\nGot exception while uploading -- saving description to %s\n' \
1633 % backup_path
1634 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001635 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001636 backup_file.close()
1637 raise
1638
1639 if not cl.GetIssue():
1640 cl.SetIssue(issue)
1641 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001642
1643 if options.use_commit_queue:
1644 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645 return 0
1646
1647
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001648def cleanup_list(l):
1649 """Fixes a list so that comma separated items are put as individual items.
1650
1651 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1652 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1653 """
1654 items = sum((i.split(',') for i in l), [])
1655 stripped_items = (i.strip() for i in items)
1656 return sorted(filter(None, stripped_items))
1657
1658
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001659@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001660def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001661 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001662 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1663 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001664 parser.add_option('--bypass-watchlists', action='store_true',
1665 dest='bypass_watchlists',
1666 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001667 parser.add_option('-f', action='store_true', dest='force',
1668 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001669 parser.add_option('-m', dest='message', help='message for patchset')
1670 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001671 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001672 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001673 help='reviewer email addresses')
1674 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001675 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001676 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001677 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001678 help='send email to reviewer immediately')
1679 parser.add_option("--emulate_svn_auto_props", action="store_true",
1680 dest="emulate_svn_auto_props",
1681 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001682 parser.add_option('-c', '--use-commit-queue', action='store_true',
1683 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001684 parser.add_option('--private', action='store_true',
1685 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001686 parser.add_option('--target_branch',
1687 help='When uploading to gerrit, remote branch to '
1688 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001689 parser.add_option('--email', default=None,
1690 help='email address to use to connect to Rietveld')
1691
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001692 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001693 (options, args) = parser.parse_args(args)
1694
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001695 if options.target_branch and not settings.GetIsGerrit():
1696 parser.error('Use --target_branch for non gerrit repository.')
1697
ukai@chromium.org259e4682012-10-25 07:36:33 +00001698 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001699 return 1
1700
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001701 options.reviewers = cleanup_list(options.reviewers)
1702 options.cc = cleanup_list(options.cc)
1703
ukai@chromium.orge8077812012-02-03 03:41:46 +00001704 cl = Changelist()
1705 if args:
1706 # TODO(ukai): is it ok for gerrit case?
1707 base_branch = args[0]
1708 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001709 # Default to diffing against common ancestor of upstream branch
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001710 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001711 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001712
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001713 # Apply watchlists on upload.
1714 change = cl.GetChange(base_branch, None)
1715 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1716 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001717 if not options.bypass_watchlists:
1718 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001719
ukai@chromium.orge8077812012-02-03 03:41:46 +00001720 if not options.bypass_hooks:
isherman@chromium.orgb5cded62014-03-25 17:47:57 +00001721 if options.reviewers:
1722 # Set the reviewer list now so that presubmit checks can access it.
1723 change_description = ChangeDescription(change.FullDescriptionText())
1724 change_description.update_reviewers(options.reviewers)
1725 change.SetDescriptionText(change_description.description)
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001726 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001727 may_prompt=not options.force,
1728 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001729 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001730 if not hook_results.should_continue():
1731 return 1
1732 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001733 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001734
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001735 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001736 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001737 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001738 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001739 print ('The last upload made from this repository was patchset #%d but '
1740 'the most recent patchset on the server is #%d.'
1741 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001742 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1743 'from another machine or branch the patch you\'re uploading now '
1744 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001745 ask_for_data('About to upload; enter to confirm.')
1746
iannucci@chromium.org79540052012-10-19 23:15:26 +00001747 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001748 if settings.GetIsGerrit():
1749 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001750 ret = RietveldUpload(options, args, cl)
1751 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001752 git_set_branch_value('last-upload-hash',
1753 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001754
1755 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001756
1757
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001758def IsSubmoduleMergeCommit(ref):
1759 # When submodules are added to the repo, we expect there to be a single
1760 # non-git-svn merge commit at remote HEAD with a signature comment.
1761 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001762 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001763 return RunGit(cmd) != ''
1764
1765
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001766def SendUpstream(parser, args, cmd):
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00001767 """Common code for CMDland and CmdDCommit
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001768
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001769 Squashes branch into a single commit.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001770 Updates changelog with metadata (e.g. pointer to review).
1771 Pushes/dcommits the code upstream.
1772 Updates review and closes.
1773 """
1774 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1775 help='bypass upload presubmit hook')
1776 parser.add_option('-m', dest='message',
1777 help="override review description")
1778 parser.add_option('-f', action='store_true', dest='force',
1779 help="force yes to questions (don't prompt)")
1780 parser.add_option('-c', dest='contributor',
1781 help="external contributor for patch (appended to " +
1782 "description and used as author for git). Should be " +
1783 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001784 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001785 (options, args) = parser.parse_args(args)
1786 cl = Changelist()
1787
iannucci@chromium.org5724c962014-04-11 09:32:56 +00001788 current = cl.GetBranch()
1789 remote, upstream_branch = cl.FetchUpstreamTuple(cl.GetBranch())
1790 if not settings.GetIsGitSvn() and remote == '.':
1791 print
1792 print 'Attempting to push branch %r into another local branch!' % current
1793 print
1794 print 'Either reparent this branch on top of origin/master:'
1795 print ' git reparent-branch --root'
1796 print
1797 print 'OR run `git rebase-update` if you think the parent branch is already'
1798 print 'committed.'
1799 print
1800 print ' Current parent: %r' % upstream_branch
1801 return 1
1802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001803 if not args or cmd == 'push':
1804 # Default to merging against our best guess of the upstream branch.
1805 args = [cl.GetUpstreamBranch()]
1806
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001807 if options.contributor:
1808 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1809 print "Please provide contibutor as 'First Last <email@example.com>'"
1810 return 1
1811
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001812 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001813 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814
ukai@chromium.org259e4682012-10-25 07:36:33 +00001815 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001816 return 1
1817
1818 # This rev-list syntax means "show all commits not in my branch that
1819 # are in base_branch".
1820 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1821 base_branch]).splitlines()
1822 if upstream_commits:
1823 print ('Base branch "%s" has %d commits '
1824 'not in this branch.' % (base_branch, len(upstream_commits)))
1825 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1826 return 1
1827
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001828 # This is the revision `svn dcommit` will commit on top of.
1829 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1830 '--pretty=format:%H'])
1831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001832 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001833 # If the base_head is a submodule merge commit, the first parent of the
1834 # base_head should be a git-svn commit, which is what we're interested in.
1835 base_svn_head = base_branch
1836 if base_has_submodules:
1837 base_svn_head += '^1'
1838
1839 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001840 if extra_commits:
1841 print ('This branch has %d additional commits not upstreamed yet.'
1842 % len(extra_commits.splitlines()))
1843 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1844 'before attempting to %s.' % (base_branch, cmd))
1845 return 1
1846
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001847 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001848 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001849 author = None
1850 if options.contributor:
1851 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001852 hook_results = cl.RunHook(
1853 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001854 may_prompt=not options.force,
1855 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001856 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001857 if not hook_results.should_continue():
1858 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859
1860 if cmd == 'dcommit':
1861 # Check the tree status if the tree status URL is set.
1862 status = GetTreeStatus()
1863 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001864 print('The tree is closed. Please wait for it to reopen. Use '
1865 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001866 return 1
1867 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001868 print('Unable to determine tree status. Please verify manually and '
1869 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001870 else:
1871 breakpad.SendStack(
1872 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001873 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1874 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001875 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001876
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001877 change_desc = ChangeDescription(options.message)
1878 if not change_desc.description and cl.GetIssue():
1879 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001880
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001881 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001882 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001883 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001884 else:
1885 print 'No description set.'
1886 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1887 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001888
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001889 # Keep a separate copy for the commit message, because the commit message
1890 # contains the link to the Rietveld issue, while the Rietveld message contains
1891 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001892 # Keep a separate copy for the commit message.
1893 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001894 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001895
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001896 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001897 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001898 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001899 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001900 commit_desc.append_footer('Patch from %s.' % options.contributor)
1901
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001902 print('Description:')
1903 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001904
1905 branches = [base_branch, cl.GetBranchRef()]
1906 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001907 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001908 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001909
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001910 # We want to squash all this branch's commits into one commit with the proper
1911 # description. We do this by doing a "reset --soft" to the base branch (which
1912 # keeps the working copy the same), then dcommitting that. If origin/master
1913 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1914 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001915 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001916 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1917 # Delete the branches if they exist.
1918 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1919 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1920 result = RunGitWithCode(showref_cmd)
1921 if result[0] == 0:
1922 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923
1924 # We might be in a directory that's present in this branch but not in the
1925 # trunk. Move up to the top of the tree so that git commands that expect a
1926 # valid CWD won't fail after we check out the merge branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00001927 rel_base_path = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001928 if rel_base_path:
1929 os.chdir(rel_base_path)
1930
1931 # Stuff our change into the merge branch.
1932 # We wrap in a try...finally block so if anything goes wrong,
1933 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001934 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001935 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001936 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1937 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001938 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001939 RunGit(
1940 [
1941 'commit', '--author', options.contributor,
1942 '-m', commit_desc.description,
1943 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001944 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001945 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001946 if base_has_submodules:
1947 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1948 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1949 RunGit(['checkout', CHERRY_PICK_BRANCH])
1950 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951 if cmd == 'push':
1952 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001953 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001954 retcode, output = RunGitWithCode(
1955 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1956 logging.debug(output)
1957 else:
1958 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001959 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001960 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001961 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001962 finally:
1963 # And then swap back to the original branch and clean up.
1964 RunGit(['checkout', '-q', cl.GetBranch()])
1965 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001966 if base_has_submodules:
1967 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968
1969 if cl.GetIssue():
1970 if cmd == 'dcommit' and 'Committed r' in output:
1971 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1972 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001973 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1974 for l in output.splitlines(False))
1975 match = filter(None, match)
1976 if len(match) != 1:
1977 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1978 output)
1979 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001980 else:
1981 return 1
1982 viewvc_url = settings.GetViewVCUrl()
1983 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001984 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001985 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001986 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001987 print ('Closing issue '
1988 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001989 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001990 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001991 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001992 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001993 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001994 if options.bypass_hooks:
1995 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1996 else:
1997 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001998 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001999 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002000
2001 if retcode == 0:
2002 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
2003 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002004 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00002005
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006 return 0
2007
2008
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002009@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002010def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002011 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002012 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00002013 message = """This doesn't appear to be an SVN repository.
2014If your project has a git mirror with an upstream SVN master, you probably need
2015to run 'git svn init', see your project's git mirror documentation.
2016If your project has a true writeable upstream repository, you probably want
2017to run 'git cl push' instead.
2018Choose wisely, if you get this wrong, your commit might appear to succeed but
2019will instead be silently ignored."""
2020 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00002021 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002022 return SendUpstream(parser, args, 'dcommit')
2023
2024
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002025@subcommand.usage('[upstream branch to apply against]')
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002026def CMDland(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002027 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002028 if settings.GetIsGitSvn():
2029 print('This appears to be an SVN repository.')
2030 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00002031 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002032 return SendUpstream(parser, args, 'push')
2033
2034
pgervais@chromium.orgcee6dc42014-05-07 17:04:03 +00002035@subcommand.usage('[upstream branch to apply against]')
2036def CMDpush(parser, args):
2037 """Temporary alias for 'land'.
2038 """
2039 print(
2040 "\n=======\n"
2041 "'git cl push' has been renamed to 'git cl land'.\n"
2042 "Currently they are treated as synonyms, but 'git cl push' will stop\n"
2043 "working after 2014/07/01.\n"
2044 "=======\n")
2045 now = datetime.datetime.utcfromtimestamp(time.time())
2046 if now > datetime.datetime(2014, 7, 1):
2047 print('******\nReally, you should not use this command anymore... \n'
2048 'Pausing 10 sec to help you remember :-)')
2049 for n in xrange(10):
2050 time.sleep(1)
2051 print('%s seconds...' % (n+1))
2052 print('******')
2053 return CMDland(parser, args)
2054
2055
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002056@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002057def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00002058 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002059 parser.add_option('-b', dest='newbranch',
2060 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002061 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002062 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002063 parser.add_option('-d', '--directory', action='store', metavar='DIR',
2064 help='Change to the directory DIR immediately, '
2065 'before doing anything else.')
2066 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002067 help='failed patches spew .rej files rather than '
2068 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002069 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
2070 help="don't commit after patch applies")
2071 (options, args) = parser.parse_args(args)
2072 if len(args) != 1:
2073 parser.print_help()
2074 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002075 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002076
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002077 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00002078 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002079
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002080 if options.newbranch:
2081 if options.force:
2082 RunGit(['branch', '-D', options.newbranch],
2083 stderr=subprocess2.PIPE, error_ok=True)
2084 RunGit(['checkout', '-b', options.newbranch,
2085 Changelist().GetUpstreamBranch()])
2086
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002087 return PatchIssue(issue_arg, options.reject, options.nocommit,
2088 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002089
2090
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002091def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002092 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002093 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00002094 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00002095 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00002096 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00002097 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002098 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00002099 # Assume it's a URL to the patch. Default to https.
2100 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00002101 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002102 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002103 DieWithError('Must pass an issue ID or full URL for '
2104 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00002105 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00002106 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00002107 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002108
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002109 # Switch up to the top-level directory, if necessary, in preparation for
2110 # applying the patch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002111 top = settings.GetRelativeRoot()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002112 if top:
2113 os.chdir(top)
2114
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002115 # Git patches have a/ at the beginning of source paths. We strip that out
2116 # with a sed script rather than the -p flag to patch so we can feed either
2117 # Git or svn-style patches into the same apply command.
2118 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002119 try:
2120 patch_data = subprocess2.check_output(
2121 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
2122 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002123 DieWithError('Git patch mungling failed.')
2124 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002125
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002126 # We use "git apply" to apply the patch instead of "patch" so that we can
2127 # pick up file adds.
2128 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002129 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002130 if directory:
2131 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002132 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002134 elif IsGitVersionAtLeast('1.7.12'):
2135 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002136 try:
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002137 subprocess2.check_call(cmd, env=GetNoGitPagerEnv(),
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002138 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002139 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002140 DieWithError('Failed to apply the patch')
2141
2142 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002143 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002144 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2145 cl = Changelist()
2146 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002147 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002148 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002149 else:
2150 print "Patch applied to index."
2151 return 0
2152
2153
2154def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002155 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002156 # Provide a wrapper for git svn rebase to help avoid accidental
2157 # git svn dcommit.
2158 # It's the only command that doesn't use parser at all since we just defer
2159 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002160
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002161 return RunGitWithCode(['svn', 'rebase'] + args)[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002162
2163
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002164def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002165 """Fetches the tree status and returns either 'open', 'closed',
2166 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002167 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002168 if url:
2169 status = urllib2.urlopen(url).read().lower()
2170 if status.find('closed') != -1 or status == '0':
2171 return 'closed'
2172 elif status.find('open') != -1 or status == '1':
2173 return 'open'
2174 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002175 return 'unset'
2176
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002177
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002178def GetTreeStatusReason():
2179 """Fetches the tree status from a json url and returns the message
2180 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002181 url = settings.GetTreeStatusUrl()
2182 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002183 connection = urllib2.urlopen(json_url)
2184 status = json.loads(connection.read())
2185 connection.close()
2186 return status['message']
2187
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002188
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002189def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002190 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002191 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002192 status = GetTreeStatus()
2193 if 'unset' == status:
2194 print 'You must configure your tree status URL by running "git cl config".'
2195 return 2
2196
2197 print "The tree is %s" % status
2198 print
2199 print GetTreeStatusReason()
2200 if status != 'open':
2201 return 1
2202 return 0
2203
2204
maruel@chromium.org15192402012-09-06 12:38:29 +00002205def CMDtry(parser, args):
2206 """Triggers a try job through Rietveld."""
2207 group = optparse.OptionGroup(parser, "Try job options")
2208 group.add_option(
2209 "-b", "--bot", action="append",
2210 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2211 "times to specify multiple builders. ex: "
2212 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2213 "the try server waterfall for the builders name and the tests "
2214 "available. Can also be used to specify gtest_filter, e.g. "
2215 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2216 group.add_option(
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002217 "-m", "--master", default='',
iannucci@chromium.org9e849272014-04-04 00:31:55 +00002218 help=("Specify a try master where to run the tries."))
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002219 group.add_option(
maruel@chromium.org15192402012-09-06 12:38:29 +00002220 "-r", "--revision",
2221 help="Revision to use for the try job; default: the "
2222 "revision will be determined by the try server; see "
2223 "its waterfall for more info")
2224 group.add_option(
2225 "-c", "--clobber", action="store_true", default=False,
2226 help="Force a clobber before building; e.g. don't do an "
2227 "incremental build")
2228 group.add_option(
2229 "--project",
2230 help="Override which project to use. Projects are defined "
2231 "server-side to define what default bot set to use")
2232 group.add_option(
2233 "-t", "--testfilter", action="append", default=[],
2234 help=("Apply a testfilter to all the selected builders. Unless the "
2235 "builders configurations are similar, use multiple "
2236 "--bot <builder>:<test> arguments."))
2237 group.add_option(
2238 "-n", "--name", help="Try job name; default to current branch name")
2239 parser.add_option_group(group)
2240 options, args = parser.parse_args(args)
2241
2242 if args:
2243 parser.error('Unknown arguments: %s' % args)
2244
2245 cl = Changelist()
2246 if not cl.GetIssue():
2247 parser.error('Need to upload first')
2248
2249 if not options.name:
2250 options.name = cl.GetBranch()
2251
phajdan.jr@chromium.org8da7f272014-03-14 01:28:39 +00002252 if options.bot and not options.master:
2253 parser.error('For manually specified bots please also specify the '
2254 'tryserver master, e.g. "-m tryserver.chromium".')
2255
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002256 def GetMasterMap():
2257 # Process --bot and --testfilter.
2258 if not options.bot:
2259 change = cl.GetChange(cl.GetCommonAncestorWithUpstream(), None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002260
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002261 # Get try masters from PRESUBMIT.py files.
2262 masters = presubmit_support.DoGetTryMasters(
2263 change,
2264 change.LocalPaths(),
2265 settings.GetRoot(),
2266 None,
2267 None,
2268 options.verbose,
2269 sys.stdout)
2270 if masters:
2271 return masters
stip@chromium.org43064fd2013-12-18 20:07:44 +00002272
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002273 # Fall back to deprecated method: get try slaves from PRESUBMIT.py files.
2274 options.bot = presubmit_support.DoGetTrySlaves(
2275 change,
2276 change.LocalPaths(),
2277 settings.GetRoot(),
2278 None,
2279 None,
2280 options.verbose,
2281 sys.stdout)
2282 if not options.bot:
2283 parser.error('No default try builder to try, use --bot')
maruel@chromium.org15192402012-09-06 12:38:29 +00002284
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002285 builders_and_tests = {}
2286 # TODO(machenbach): The old style command-line options don't support
2287 # multiple try masters yet.
2288 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2289 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2290
2291 for bot in old_style:
2292 if ':' in bot:
2293 builder, tests = bot.split(':', 1)
2294 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2295 elif ',' in bot:
2296 parser.error('Specify one bot per --bot flag')
2297 else:
2298 builders_and_tests.setdefault(bot, []).append('defaulttests')
2299
2300 for bot, tests in new_style:
2301 builders_and_tests.setdefault(bot, []).extend(tests)
2302
2303 # Return a master map with one master to be backwards compatible. The
2304 # master name defaults to an empty string, which will cause the master
2305 # not to be set on rietveld (deprecated).
2306 return {options.master: builders_and_tests}
2307
2308 masters = GetMasterMap()
stip@chromium.org43064fd2013-12-18 20:07:44 +00002309
maruel@chromium.org15192402012-09-06 12:38:29 +00002310 if options.testfilter:
2311 forced_tests = sum((t.split(',') for t in options.testfilter), [])
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002312 masters = dict((master, dict(
2313 (b, forced_tests) for b, t in slaves.iteritems()
2314 if t != ['compile'])) for master, slaves in masters.iteritems())
maruel@chromium.org15192402012-09-06 12:38:29 +00002315
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002316 for builders in masters.itervalues():
2317 if any('triggered' in b for b in builders):
2318 print >> sys.stderr, (
2319 'ERROR You are trying to send a job to a triggered bot. This type of'
2320 ' bot requires an\ninitial job from a parent (usually a builder). '
2321 'Instead send your job to the parent.\n'
2322 'Bot list: %s' % builders)
2323 return 1
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002324
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002325 patchset = cl.GetMostRecentPatchset()
2326 if patchset and patchset != cl.GetPatchset():
2327 print(
2328 '\nWARNING Mismatch between local config and server. Did a previous '
2329 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2330 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002331 try:
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002332 cl.RpcServer().trigger_distributed_try_jobs(
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002333 cl.GetIssue(), patchset, options.name, options.clobber,
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002334 options.revision, masters)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002335 except urllib2.HTTPError, e:
2336 if e.code == 404:
2337 print('404 from rietveld; '
2338 'did you mean to use "git try" instead of "git cl try"?')
2339 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002340 print('Tried jobs on:')
machenbach@chromium.org58a69cb2014-03-01 02:08:29 +00002341
2342 for (master, builders) in masters.iteritems():
2343 if master:
2344 print 'Master: %s' % master
2345 length = max(len(builder) for builder in builders)
2346 for builder in sorted(builders):
2347 print ' %*s: %s' % (length, builder, ','.join(builders[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002348 return 0
2349
2350
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002351@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002352def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002353 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002354 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002355 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002356 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002357
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002358 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002359 if args:
2360 # One arg means set upstream branch.
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002361 branch = cl.GetBranch()
2362 RunGit(['branch', '--set-upstream', branch, args[0]])
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002363 cl = Changelist()
2364 print "Upstream branch set to " + cl.GetUpstreamBranch()
bauerb@chromium.orgc9cf90a2014-04-28 20:32:31 +00002365
2366 # Clear configured merge-base, if there is one.
2367 git_common.remove_merge_base(branch)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002368 else:
2369 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002370 return 0
2371
2372
thestig@chromium.org00858c82013-12-02 23:08:03 +00002373def CMDweb(parser, args):
2374 """Opens the current CL in the web browser."""
2375 _, args = parser.parse_args(args)
2376 if args:
2377 parser.error('Unrecognized args: %s' % ' '.join(args))
2378
2379 issue_url = Changelist().GetIssueURL()
2380 if not issue_url:
2381 print >> sys.stderr, 'ERROR No issue to open'
2382 return 1
2383
2384 webbrowser.open(issue_url)
2385 return 0
2386
2387
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002388def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002389 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002390 _, args = parser.parse_args(args)
2391 if args:
2392 parser.error('Unrecognized args: %s' % ' '.join(args))
2393 cl = Changelist()
2394 cl.SetFlag('commit', '1')
2395 return 0
2396
2397
groby@chromium.org411034a2013-02-26 15:12:01 +00002398def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002399 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002400 _, args = parser.parse_args(args)
2401 if args:
2402 parser.error('Unrecognized args: %s' % ' '.join(args))
2403 cl = Changelist()
2404 # Ensure there actually is an issue to close.
2405 cl.GetDescription()
2406 cl.CloseIssue()
2407 return 0
2408
2409
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002410def CMDdiff(parser, args):
2411 """shows differences between local tree and last upload."""
2412 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002413 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002414 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002415 if not issue:
2416 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002417 TMP_BRANCH = 'git-cl-diff'
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002418 base_branch = cl.GetCommonAncestorWithUpstream()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002419
2420 # Create a new branch based on the merge-base
2421 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2422 try:
2423 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002424 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002425 if rtn != 0:
2426 return rtn
2427
2428 # Switch back to starting brand and diff against the temporary
2429 # branch containing the latest rietveld patch.
2430 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2431 finally:
2432 RunGit(['checkout', '-q', branch])
2433 RunGit(['branch', '-D', TMP_BRANCH])
2434
2435 return 0
2436
2437
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002438def CMDowners(parser, args):
2439 """interactively find the owners for reviewing"""
2440 parser.add_option(
2441 '--no-color',
2442 action='store_true',
2443 help='Use this option to disable color output')
2444 options, args = parser.parse_args(args)
2445
2446 author = RunGit(['config', 'user.email']).strip() or None
2447
2448 cl = Changelist()
2449
2450 if args:
2451 if len(args) > 1:
2452 parser.error('Unknown args')
2453 base_branch = args[0]
2454 else:
2455 # Default to diffing against the common ancestor of the upstream branch.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002456 base_branch = cl.GetCommonAncestorWithUpstream()
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002457
2458 change = cl.GetChange(base_branch, None)
2459 return owners_finder.OwnersFinder(
2460 [f.LocalPath() for f in
2461 cl.GetChange(base_branch, None).AffectedFiles()],
2462 change.RepositoryRoot(), author,
2463 fopen=file, os_path=os.path, glob=glob.glob,
2464 disable_color=options.no_color).run()
2465
2466
enne@chromium.org555cfe42014-01-29 18:21:39 +00002467@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002468def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002469 """Runs clang-format on the diff."""
nick@chromium.org8ca1aa32014-02-25 23:57:03 +00002470 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm', '.proto']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002471 parser.add_option('--full', action='store_true',
2472 help='Reformat the full content of all touched files')
2473 parser.add_option('--dry-run', action='store_true',
2474 help='Don\'t modify any file on disk.')
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002475 parser.add_option('--diff', action='store_true',
2476 help='Print diff to stdout rather than modifying files.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002477 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002478
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002479 # git diff generates paths against the root of the repository. Change
2480 # to that directory so clang-format can find files even within subdirs.
thestig@chromium.org8b0553c2014-02-11 00:33:37 +00002481 rel_base_path = settings.GetRelativeRoot()
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002482 if rel_base_path:
2483 os.chdir(rel_base_path)
2484
digit@chromium.org29e47272013-05-17 17:01:46 +00002485 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002486 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002487 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002488 # Only list the names of modified files.
2489 diff_cmd.append('--name-only')
2490 else:
2491 # Only generate context-less patches.
2492 diff_cmd.append('-U0')
2493
2494 # Grab the merge-base commit, i.e. the upstream commit of the current
2495 # branch when it was created or the last time it was rebased. This is
2496 # to cover the case where the user may have called "git fetch origin",
2497 # moving the origin branch to a newer commit, but hasn't rebased yet.
2498 upstream_commit = None
2499 cl = Changelist()
2500 upstream_branch = cl.GetUpstreamBranch()
2501 if upstream_branch:
2502 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2503 upstream_commit = upstream_commit.strip()
2504
2505 if not upstream_commit:
2506 DieWithError('Could not find base commit for this branch. '
2507 'Are you in detached state?')
2508
2509 diff_cmd.append(upstream_commit)
2510
2511 # Handle source file filtering.
2512 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002513 if args:
2514 for arg in args:
2515 if os.path.isdir(arg):
2516 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2517 elif os.path.isfile(arg):
2518 diff_cmd.append(arg)
2519 else:
2520 DieWithError('Argument "%s" is not a file or a directory' % arg)
2521 else:
2522 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002523 diff_output = RunGit(diff_cmd)
2524
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002525 top_dir = os.path.normpath(
2526 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2527
2528 # Locate the clang-format binary in the checkout
2529 try:
2530 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2531 except clang_format.NotFoundError, e:
2532 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002533
digit@chromium.org29e47272013-05-17 17:01:46 +00002534 if opts.full:
2535 # diff_output is a list of files to send to clang-format.
2536 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002537 if not files:
2538 print "Nothing to format."
2539 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002540 cmd = [clang_format_tool]
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002541 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002542 cmd.append('-i')
2543 stdout = RunCommand(cmd + files, cwd=top_dir)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002544 if opts.diff:
2545 sys.stdout.write(stdout)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002546 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002547 env = os.environ.copy()
2548 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002549 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002550 try:
2551 script = clang_format.FindClangFormatScriptInChromiumTree(
2552 'clang-format-diff.py')
2553 except clang_format.NotFoundError, e:
2554 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002555
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002556 cmd = [sys.executable, script, '-p0']
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002557 if not opts.dry_run and not opts.diff:
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002558 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002559
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002560 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
wittman@chromium.org04d5a222014-03-07 18:30:42 +00002561 if opts.diff:
2562 sys.stdout.write(stdout)
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002563 if opts.dry_run and len(stdout) > 0:
2564 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002565
2566 return 0
2567
2568
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002569class OptionParser(optparse.OptionParser):
2570 """Creates the option parse and add --verbose support."""
2571 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002572 optparse.OptionParser.__init__(
2573 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002574 self.add_option(
2575 '-v', '--verbose', action='count', default=0,
2576 help='Use 2 times for more debugging info')
2577
2578 def parse_args(self, args=None, values=None):
2579 options, args = optparse.OptionParser.parse_args(self, args, values)
2580 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2581 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2582 return options, args
2583
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002584
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002585def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002586 if sys.hexversion < 0x02060000:
2587 print >> sys.stderr, (
2588 '\nYour python version %s is unsupported, please upgrade.\n' %
2589 sys.version.split(' ', 1)[0])
2590 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002591
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002592 # Reload settings.
2593 global settings
2594 settings = Settings()
2595
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002596 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002597 dispatcher = subcommand.CommandDispatcher(__name__)
2598 try:
2599 return dispatcher.execute(OptionParser(), argv)
2600 except urllib2.HTTPError, e:
2601 if e.code != 500:
2602 raise
2603 DieWithError(
2604 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2605 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002606
2607
2608if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002609 # These affect sys.stdout so do it outside of main() to simplify mocks in
2610 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002611 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002612 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002613 sys.exit(main(sys.argv[1:]))