blob: a2d9c628e6968dae956d98dd74f08ef7d82b53ba [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +000010from distutils.version import LooseVersion
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000011import glob
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000012import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000013import logging
14import optparse
15import os
maruel@chromium.org1033efd2013-07-23 23:25:09 +000016import Queue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000020import textwrap
maruel@chromium.org1033efd2013-07-23 23:25:09 +000021import threading
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000022import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000023import urlparse
thestig@chromium.org00858c82013-12-02 23:08:03 +000024import webbrowser
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000025
26try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000027 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000028except ImportError:
29 pass
30
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000032from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033from third_party import upload
34import breakpad # pylint: disable=W0611
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +000035import clang_format
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000036import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000037import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000038import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000039import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000040import scm
maruel@chromium.org0633fb42013-08-16 20:06:14 +000041import subcommand
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +000044import owners_finder
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045
maruel@chromium.org0633fb42013-08-16 20:06:14 +000046__version__ = '1.0'
maruel@chromium.org2a74d372011-03-29 19:05:50 +000047
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000048DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000049POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +000051GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000052CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000054# Shortcut since it quickly becomes redundant.
55Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000056
maruel@chromium.orgddd59412011-11-30 14:20:38 +000057# Initialized in main()
58settings = None
59
60
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000062 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063 sys.exit(1)
64
65
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000068 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000069 except subprocess2.CalledProcessError as e:
70 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000072 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000073 'Command "%s" failed.\n%s' % (
74 ' '.join(args), error_message or e.stdout or ''))
75 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
77
78def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000080 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000081
82
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000083def RunGitWithCode(args, suppress_stderr=False):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000084 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000085 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000086 env = os.environ.copy()
87 # 'cat' is a magical git string that disables pagers on all platforms.
88 env['GIT_PAGER'] = 'cat'
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000089 if suppress_stderr:
90 stderr = subprocess2.VOID
91 else:
92 stderr = sys.stderr
bratell@opera.com82b91cd2013-07-09 06:33:41 +000093 out, code = subprocess2.communicate(['git'] + args,
94 env=env,
enne@chromium.org3b7e15c2014-01-21 17:44:47 +000095 stdout=subprocess2.PIPE,
96 stderr=stderr)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000097 return code, out[0]
98 except ValueError:
99 # When the subprocess fails, it returns None. That triggers a ValueError
100 # when trying to unpack the return value into (out, code).
101 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000102
103
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000104def IsGitVersionAtLeast(min_version):
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000105 prefix = 'git version '
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000106 version = RunGit(['--version']).strip()
ilevy@chromium.orgcc56ee42013-07-10 22:16:29 +0000107 return (version.startswith(prefix) and
108 LooseVersion(version[len(prefix):]) >= LooseVersion(min_version))
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +0000109
110
maruel@chromium.org90541732011-04-01 17:54:18 +0000111def ask_for_data(prompt):
112 try:
113 return raw_input(prompt)
114 except KeyboardInterrupt:
115 # Hide the exception.
116 sys.exit(1)
117
118
iannucci@chromium.org79540052012-10-19 23:15:26 +0000119def git_set_branch_value(key, value):
120 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000121 if not branch:
122 return
123
124 cmd = ['config']
125 if isinstance(value, int):
126 cmd.append('--int')
127 git_key = 'branch.%s.%s' % (branch, key)
128 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000129
130
131def git_get_branch_default(key, default):
132 branch = Changelist().GetBranch()
133 if branch:
134 git_key = 'branch.%s.%s' % (branch, key)
135 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
136 try:
137 return int(stdout.strip())
138 except ValueError:
139 pass
140 return default
141
142
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000143def add_git_similarity(parser):
144 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000145 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000146 help='Sets the percentage that a pair of files need to match in order to'
147 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000148 parser.add_option(
149 '--find-copies', action='store_true',
150 help='Allows git to look for copies.')
151 parser.add_option(
152 '--no-find-copies', action='store_false', dest='find_copies',
153 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000154
155 old_parser_args = parser.parse_args
156 def Parse(args):
157 options, args = old_parser_args(args)
158
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000159 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000160 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000161 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000162 print('Note: Saving similarity of %d%% in git config.'
163 % options.similarity)
164 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000165
iannucci@chromium.org79540052012-10-19 23:15:26 +0000166 options.similarity = max(0, min(options.similarity, 100))
167
168 if options.find_copies is None:
169 options.find_copies = bool(
170 git_get_branch_default('git-find-copies', True))
171 else:
172 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000173
174 print('Using %d%% similarity for rename/copy detection. '
175 'Override with --similarity.' % options.similarity)
176
177 return options, args
178 parser.parse_args = Parse
179
180
ukai@chromium.org259e4682012-10-25 07:36:33 +0000181def is_dirty_git_tree(cmd):
182 # Make sure index is up-to-date before running diff-index.
183 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
184 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
185 if dirty:
186 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
187 print 'Uncommitted files: (git diff-index --name-status HEAD)'
188 print dirty[:4096]
189 if len(dirty) > 4096:
190 print '... (run "git diff-index --name-status HEAD" to see full output).'
191 return True
192 return False
193
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000194
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000195def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
196 """Return the corresponding git ref if |base_url| together with |glob_spec|
197 matches the full |url|.
198
199 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
200 """
201 fetch_suburl, as_ref = glob_spec.split(':')
202 if allow_wildcards:
203 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
204 if glob_match:
205 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
206 # "branches/{472,597,648}/src:refs/remotes/svn/*".
207 branch_re = re.escape(base_url)
208 if glob_match.group(1):
209 branch_re += '/' + re.escape(glob_match.group(1))
210 wildcard = glob_match.group(2)
211 if wildcard == '*':
212 branch_re += '([^/]*)'
213 else:
214 # Escape and replace surrounding braces with parentheses and commas
215 # with pipe symbols.
216 wildcard = re.escape(wildcard)
217 wildcard = re.sub('^\\\\{', '(', wildcard)
218 wildcard = re.sub('\\\\,', '|', wildcard)
219 wildcard = re.sub('\\\\}$', ')', wildcard)
220 branch_re += wildcard
221 if glob_match.group(3):
222 branch_re += re.escape(glob_match.group(3))
223 match = re.match(branch_re, url)
224 if match:
225 return re.sub('\*$', match.group(1), as_ref)
226
227 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
228 if fetch_suburl:
229 full_url = base_url + '/' + fetch_suburl
230 else:
231 full_url = base_url
232 if full_url == url:
233 return as_ref
234 return None
235
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000236
iannucci@chromium.org79540052012-10-19 23:15:26 +0000237def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000238 """Prints statistics about the change to the user."""
239 # --no-ext-diff is broken in some versions of Git, so try to work around
240 # this by overriding the environment (but there is still a problem if the
241 # git config key "diff.external" is used).
242 env = os.environ.copy()
243 if 'GIT_EXTERNAL_DIFF' in env:
244 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000245 # 'cat' is a magical git string that disables pagers on all platforms.
246 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000247
248 if find_copies:
249 similarity_options = ['--find-copies-harder', '-l100000',
250 '-C%s' % similarity]
251 else:
252 similarity_options = ['-M%s' % similarity]
253
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000254 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000255 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000256 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000257 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000258
259
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000260class Settings(object):
261 def __init__(self):
262 self.default_server = None
263 self.cc = None
264 self.root = None
265 self.is_git_svn = None
266 self.svn_branch = None
267 self.tree_status_url = None
268 self.viewvc_url = None
269 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000270 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000271 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272
273 def LazyUpdateIfNeeded(self):
274 """Updates the settings from a codereview.settings file, if available."""
275 if not self.updated:
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000276 # The only value that actually changes the behavior is
277 # autoupdate = "false". Everything else means "true".
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +0000278 autoupdate = RunGit(['config', 'rietveld.autoupdate'],
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000279 error_ok=True
280 ).strip().lower()
281
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000282 cr_settings_file = FindCodereviewSettingsFile()
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000283 if autoupdate != 'false' and cr_settings_file:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000284 LoadCodereviewSettingsFromFile(cr_settings_file)
pgervais@chromium.org87884cc2014-01-03 22:23:41 +0000285 # set updated to True to avoid infinite calling loop
286 # through DownloadHooks
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000287 self.updated = True
288 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000289 self.updated = True
290
291 def GetDefaultServerUrl(self, error_ok=False):
292 if not self.default_server:
293 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000294 self.default_server = gclient_utils.UpgradeToHttps(
295 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000296 if error_ok:
297 return self.default_server
298 if not self.default_server:
299 error_message = ('Could not find settings file. You must configure '
300 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000301 self.default_server = gclient_utils.UpgradeToHttps(
302 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000303 return self.default_server
304
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000305 def GetRoot(self):
306 if not self.root:
307 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
308 return self.root
309
310 def GetIsGitSvn(self):
311 """Return true if this repo looks like it's using git-svn."""
312 if self.is_git_svn is None:
313 # If you have any "svn-remote.*" config keys, we think you're using svn.
314 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000315 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000316 return self.is_git_svn
317
318 def GetSVNBranch(self):
319 if self.svn_branch is None:
320 if not self.GetIsGitSvn():
321 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
322
323 # Try to figure out which remote branch we're based on.
324 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000325 # 1) iterate through our branch history and find the svn URL.
326 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000327
328 # regexp matching the git-svn line that contains the URL.
329 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
330
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000331 env = os.environ.copy()
332 # 'cat' is a magical git string that disables pagers on all platforms.
333 env['GIT_PAGER'] = 'cat'
334
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000335 # We don't want to go through all of history, so read a line from the
336 # pipe at a time.
337 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000338 cmd = ['git', 'log', '-100', '--pretty=medium']
339 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000340 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000341 for line in proc.stdout:
342 match = git_svn_re.match(line)
343 if match:
344 url = match.group(1)
345 proc.stdout.close() # Cut pipe.
346 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000347
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000348 if url:
349 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
350 remotes = RunGit(['config', '--get-regexp',
351 r'^svn-remote\..*\.url']).splitlines()
352 for remote in remotes:
353 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000354 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000355 remote = match.group(1)
356 base_url = match.group(2)
szager@chromium.org4ac25532013-12-16 22:07:02 +0000357 rewrite_root = RunGit(
358 ['config', 'svn-remote.%s.rewriteRoot' % remote],
359 error_ok=True).strip()
360 if rewrite_root:
361 base_url = rewrite_root
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000362 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000363 ['config', 'svn-remote.%s.fetch' % remote],
364 error_ok=True).strip()
365 if fetch_spec:
366 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
367 if self.svn_branch:
368 break
369 branch_spec = RunGit(
370 ['config', 'svn-remote.%s.branches' % remote],
371 error_ok=True).strip()
372 if branch_spec:
373 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
374 if self.svn_branch:
375 break
376 tag_spec = RunGit(
377 ['config', 'svn-remote.%s.tags' % remote],
378 error_ok=True).strip()
379 if tag_spec:
380 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
381 if self.svn_branch:
382 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000383
384 if not self.svn_branch:
385 DieWithError('Can\'t guess svn branch -- try specifying it on the '
386 'command line')
387
388 return self.svn_branch
389
390 def GetTreeStatusUrl(self, error_ok=False):
391 if not self.tree_status_url:
392 error_message = ('You must configure your tree status URL by running '
393 '"git cl config".')
394 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
395 error_ok=error_ok,
396 error_message=error_message)
397 return self.tree_status_url
398
399 def GetViewVCUrl(self):
400 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000401 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402 return self.viewvc_url
403
rmistry@google.com90752582014-01-14 21:04:50 +0000404 def GetBugPrefix(self):
405 return self._GetConfig('rietveld.bug-prefix', error_ok=True)
406
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000407 def GetDefaultCCList(self):
408 return self._GetConfig('rietveld.cc', error_ok=True)
409
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000410 def GetDefaultPrivateFlag(self):
411 return self._GetConfig('rietveld.private', error_ok=True)
412
ukai@chromium.orge8077812012-02-03 03:41:46 +0000413 def GetIsGerrit(self):
414 """Return true if this repo is assosiated with gerrit code review system."""
415 if self.is_gerrit is None:
416 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
417 return self.is_gerrit
418
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000419 def GetGitEditor(self):
420 """Return the editor specified in the git config, or None if none is."""
421 if self.git_editor is None:
422 self.git_editor = self._GetConfig('core.editor', error_ok=True)
423 return self.git_editor or None
424
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000425 def _GetConfig(self, param, **kwargs):
426 self.LazyUpdateIfNeeded()
427 return RunGit(['config', param], **kwargs).strip()
428
429
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430def ShortBranchName(branch):
431 """Convert a name like 'refs/heads/foo' to just 'foo'."""
432 return branch.replace('refs/heads/', '')
433
434
435class Changelist(object):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000436 def __init__(self, branchref=None, issue=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000438 global settings
439 if not settings:
440 # Happens when git_cl.py is used as a utility library.
441 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000442 settings.GetDefaultServerUrl()
443 self.branchref = branchref
444 if self.branchref:
445 self.branch = ShortBranchName(self.branchref)
446 else:
447 self.branch = None
448 self.rietveld_server = None
449 self.upstream_branch = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000450 self.lookedup_issue = False
451 self.issue = issue or None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000452 self.has_description = False
453 self.description = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000454 self.lookedup_patchset = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000455 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000456 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000457 self.cc = None
458 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000459 self._remote = None
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000460 self._props = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000461
462 def GetCCList(self):
463 """Return the users cc'd on this CL.
464
465 Return is a string suitable for passing to gcl with the --cc flag.
466 """
467 if self.cc is None:
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000468 base_cc = settings.GetDefaultCCList()
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000469 more_cc = ','.join(self.watchers)
470 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
471 return self.cc
472
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +0000473 def GetCCListWithoutDefault(self):
474 """Return the users cc'd on this CL excluding default ones."""
475 if self.cc is None:
476 self.cc = ','.join(self.watchers)
477 return self.cc
478
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000479 def SetWatchers(self, watchers):
480 """Set the list of email addresses that should be cc'd based on the changed
481 files in this CL.
482 """
483 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000484
485 def GetBranch(self):
486 """Returns the short branch name, e.g. 'master'."""
487 if not self.branch:
488 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
489 self.branch = ShortBranchName(self.branchref)
490 return self.branch
491
492 def GetBranchRef(self):
493 """Returns the full branch name, e.g. 'refs/heads/master'."""
494 self.GetBranch() # Poke the lazy loader.
495 return self.branchref
496
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000497 @staticmethod
498 def FetchUpstreamTuple(branch):
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000499 """Returns a tuple containing remote and remote ref,
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000500 e.g. 'origin', 'refs/heads/master'
501 """
502 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000503 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
504 error_ok=True).strip()
505 if upstream_branch:
506 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
507 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000508 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
509 error_ok=True).strip()
510 if upstream_branch:
511 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000512 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000513 # Fall back on trying a git-svn upstream branch.
514 if settings.GetIsGitSvn():
515 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000516 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000517 # Else, try to guess the origin remote.
518 remote_branches = RunGit(['branch', '-r']).split()
519 if 'origin/master' in remote_branches:
520 # Fall back on origin/master if it exits.
521 remote = 'origin'
522 upstream_branch = 'refs/heads/master'
523 elif 'origin/trunk' in remote_branches:
524 # Fall back on origin/trunk if it exists. Generally a shared
525 # git-svn clone
526 remote = 'origin'
527 upstream_branch = 'refs/heads/trunk'
528 else:
529 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000530Either pass complete "git diff"-style arguments, like
531 git cl upload origin/master
532or verify this branch is set up to track another (via the --track argument to
533"git checkout -b ...").""")
534
535 return remote, upstream_branch
536
537 def GetUpstreamBranch(self):
538 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000539 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000540 if remote is not '.':
541 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
542 self.upstream_branch = upstream_branch
543 return self.upstream_branch
544
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000545 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000546 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000547 remote, branch = None, self.GetBranch()
548 seen_branches = set()
549 while branch not in seen_branches:
550 seen_branches.add(branch)
551 remote, branch = self.FetchUpstreamTuple(branch)
552 branch = ShortBranchName(branch)
553 if remote != '.' or branch.startswith('refs/remotes'):
554 break
555 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000556 remotes = RunGit(['remote'], error_ok=True).split()
557 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000558 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000559 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000560 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000561 logging.warning('Could not determine which remote this change is '
562 'associated with, so defaulting to "%s". This may '
563 'not be what you want. You may prevent this message '
564 'by running "git svn info" as documented here: %s',
565 self._remote,
566 GIT_INSTRUCTIONS_URL)
567 else:
568 logging.warn('Could not determine which remote this change is '
569 'associated with. You may prevent this message by '
570 'running "git svn info" as documented here: %s',
571 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000572 branch = 'HEAD'
573 if branch.startswith('refs/remotes'):
574 self._remote = (remote, branch)
575 else:
576 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000577 return self._remote
578
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000579 def GitSanityChecks(self, upstream_git_obj):
580 """Checks git repo status and ensures diff is from local commits."""
581
582 # Verify the commit we're diffing against is in our current branch.
583 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
584 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
585 if upstream_sha != common_ancestor:
586 print >> sys.stderr, (
587 'ERROR: %s is not in the current branch. You may need to rebase '
588 'your tracking branch' % upstream_sha)
589 return False
590
591 # List the commits inside the diff, and verify they are all local.
592 commits_in_diff = RunGit(
593 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
594 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
595 remote_branch = remote_branch.strip()
596 if code != 0:
597 _, remote_branch = self.GetRemoteBranch()
598
599 commits_in_remote = RunGit(
600 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
601
602 common_commits = set(commits_in_diff) & set(commits_in_remote)
603 if common_commits:
604 print >> sys.stderr, (
605 'ERROR: Your diff contains %d commits already in %s.\n'
606 'Run "git log --oneline %s..HEAD" to get a list of commits in '
607 'the diff. If you are using a custom git flow, you can override'
608 ' the reference used for this check with "git config '
609 'gitcl.remotebranch <git-ref>".' % (
610 len(common_commits), remote_branch, upstream_git_obj))
611 return False
612 return True
613
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000614 def GetGitBaseUrlFromConfig(self):
615 """Return the configured base URL from branch.<branchname>.baseurl.
616
617 Returns None if it is not set.
618 """
619 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
620 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000621
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622 def GetRemoteUrl(self):
623 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
624
625 Returns None if there is no remote.
626 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000627 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000628 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
629
630 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000631 """Returns the issue number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000632 if self.issue is None and not self.lookedup_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000633 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000634 self.issue = int(issue) or None if issue else None
635 self.lookedup_issue = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000636 return self.issue
637
638 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000639 if not self.rietveld_server:
640 # If we're on a branch then get the server potentially associated
641 # with that branch.
642 if self.GetIssue():
643 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
644 ['config', self._RietveldServer()], error_ok=True).strip())
645 if not self.rietveld_server:
646 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000647 return self.rietveld_server
648
649 def GetIssueURL(self):
650 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000651 if not self.GetIssue():
652 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000653 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
654
655 def GetDescription(self, pretty=False):
656 if not self.has_description:
657 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000658 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000659 try:
660 self.description = self.RpcServer().get_description(issue).strip()
661 except urllib2.HTTPError, e:
662 if e.code == 404:
663 DieWithError(
664 ('\nWhile fetching the description for issue %d, received a '
665 '404 (not found)\n'
666 'error. It is likely that you deleted this '
667 'issue on the server. If this is the\n'
668 'case, please run\n\n'
669 ' git cl issue 0\n\n'
670 'to clear the association with the deleted issue. Then run '
671 'this command again.') % issue)
672 else:
673 DieWithError(
yujie.mao@intel.comdaee1d32013-12-18 11:55:03 +0000674 '\nFailed to fetch issue description. HTTP error %d' % e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675 self.has_description = True
676 if pretty:
677 wrapper = textwrap.TextWrapper()
678 wrapper.initial_indent = wrapper.subsequent_indent = ' '
679 return wrapper.fill(self.description)
680 return self.description
681
682 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000683 """Returns the patchset number as a int or None if not set."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000684 if self.patchset is None and not self.lookedup_patchset:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685 patchset = RunGit(['config', self._PatchsetSetting()],
686 error_ok=True).strip()
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000687 self.patchset = int(patchset) or None if patchset else None
688 self.lookedup_patchset = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000689 return self.patchset
690
691 def SetPatchset(self, patchset):
692 """Set this branch's patchset. If patchset=0, clears the patchset."""
693 if patchset:
694 RunGit(['config', self._PatchsetSetting(), str(patchset)])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000695 self.patchset = patchset
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000696 else:
697 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000698 stderr=subprocess2.PIPE, error_ok=True)
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000699 self.patchset = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000700
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000701 def GetMostRecentPatchset(self):
702 return self.GetIssueProperties()['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000703
704 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000705 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000706 '/download/issue%s_%s.diff' % (issue, patchset))
707
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000708 def GetIssueProperties(self):
709 if self._props is None:
710 issue = self.GetIssue()
711 if not issue:
712 self._props = {}
713 else:
714 self._props = self.RpcServer().get_issue_properties(issue, True)
715 return self._props
716
maruel@chromium.orgcf087782013-07-23 13:08:48 +0000717 def GetApprovingReviewers(self):
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000718 return get_approving_reviewers(self.GetIssueProperties())
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000719
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720 def SetIssue(self, issue):
721 """Set this branch's issue. If issue=0, clears the issue."""
722 if issue:
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000723 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724 RunGit(['config', self._IssueSetting(), str(issue)])
725 if self.rietveld_server:
726 RunGit(['config', self._RietveldServer(), self.rietveld_server])
727 else:
teravest@chromium.orgd79d4b82013-10-23 20:09:08 +0000728 current_issue = self.GetIssue()
729 if current_issue:
730 RunGit(['config', '--unset', self._IssueSetting()])
maruel@chromium.org1033efd2013-07-23 23:25:09 +0000731 self.issue = None
732 self.SetPatchset(None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000734 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000735 if not self.GitSanityChecks(upstream_branch):
736 DieWithError('\nGit sanity check failure')
737
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000738 env = os.environ.copy()
739 # 'cat' is a magical git string that disables pagers on all platforms.
740 env['GIT_PAGER'] = 'cat'
741
742 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000743 if not root:
744 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000745 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000746
747 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000748 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000749 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000750 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000751 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000752 except subprocess2.CalledProcessError:
753 DieWithError(
pgervais@chromium.orgd6617f32013-11-19 00:34:54 +0000754 ('\nFailed to diff against upstream branch %s\n\n'
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000755 'This branch probably doesn\'t exist anymore. To reset the\n'
756 'tracking branch, please run\n'
757 ' git branch --set-upstream %s trunk\n'
758 'replacing trunk with origin/master or the relevant branch') %
759 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000760
maruel@chromium.org52424302012-08-29 15:14:30 +0000761 issue = self.GetIssue()
762 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000763 if issue:
764 description = self.GetDescription()
765 else:
766 # If the change was never uploaded, use the log messages of all commits
767 # up to the branch point, as git cl upload will prefill the description
768 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000769 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000770 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000771 '%s...' % (upstream_branch)],
772 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000773
774 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000775 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000776 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000777 name,
778 description,
779 absroot,
780 files,
781 issue,
782 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000783 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000784
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000785 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000786 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000787
788 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000789 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000790 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000791 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000792 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000793 except presubmit_support.PresubmitFailure, e:
794 DieWithError(
795 ('%s\nMaybe your depot_tools is out of date?\n'
796 'If all fails, contact maruel@') % e)
797
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000798 def UpdateDescription(self, description):
799 self.description = description
800 return self.RpcServer().update_description(
801 self.GetIssue(), self.description)
802
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000803 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000804 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000805 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000806
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000807 def SetFlag(self, flag, value):
808 """Patchset must match."""
809 if not self.GetPatchset():
810 DieWithError('The patchset needs to match. Send another patchset.')
811 try:
812 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000813 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000814 except urllib2.HTTPError, e:
815 if e.code == 404:
816 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
817 if e.code == 403:
818 DieWithError(
819 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
820 'match?') % (self.GetIssue(), self.GetPatchset()))
821 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000823 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000824 """Returns an upload.RpcServer() to access this review's rietveld instance.
825 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000826 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000827 self._rpc_server = rietveld.CachingRietveld(
828 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000829 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000830
831 def _IssueSetting(self):
832 """Return the git setting that stores this change's issue."""
833 return 'branch.%s.rietveldissue' % self.GetBranch()
834
835 def _PatchsetSetting(self):
836 """Return the git setting that stores this change's most recent patchset."""
837 return 'branch.%s.rietveldpatchset' % self.GetBranch()
838
839 def _RietveldServer(self):
840 """Returns the git setting that stores this change's rietveld server."""
841 return 'branch.%s.rietveldserver' % self.GetBranch()
842
843
844def GetCodereviewSettingsInteractively():
845 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000846 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847 server = settings.GetDefaultServerUrl(error_ok=True)
848 prompt = 'Rietveld server (host[:port])'
849 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000850 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851 if not server and not newserver:
852 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000853 if newserver:
854 newserver = gclient_utils.UpgradeToHttps(newserver)
855 if newserver != server:
856 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000857
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000858 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859 prompt = caption
860 if initial:
861 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000862 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863 if new_val == 'x':
864 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000865 elif new_val:
866 if is_url:
867 new_val = gclient_utils.UpgradeToHttps(new_val)
868 if new_val != initial:
869 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000870
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000871 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000872 SetProperty(settings.GetDefaultPrivateFlag(),
873 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000874 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000875 'tree-status-url', False)
876 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
rmistry@google.com90752582014-01-14 21:04:50 +0000877 SetProperty(settings.GetBugPrefix(), 'Bug Prefix', 'bug-prefix', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878
879 # TODO: configure a default branch to diff against, rather than this
880 # svn-based hackery.
881
882
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000883class ChangeDescription(object):
884 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000885 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
agable@chromium.org42c20792013-09-12 17:34:49 +0000886 BUG_LINE = r'^[ \t]*(BUG)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000887
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000888 def __init__(self, description):
agable@chromium.org42c20792013-09-12 17:34:49 +0000889 self._description_lines = (description or '').strip().splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000890
agable@chromium.org42c20792013-09-12 17:34:49 +0000891 @property # www.logilab.org/ticket/89786
892 def description(self): # pylint: disable=E0202
893 return '\n'.join(self._description_lines)
894
895 def set_description(self, desc):
896 if isinstance(desc, basestring):
897 lines = desc.splitlines()
898 else:
899 lines = [line.rstrip() for line in desc]
900 while lines and not lines[0]:
901 lines.pop(0)
902 while lines and not lines[-1]:
903 lines.pop(-1)
904 self._description_lines = lines
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000905
906 def update_reviewers(self, reviewers):
agable@chromium.org42c20792013-09-12 17:34:49 +0000907 """Rewrites the R=/TBR= line(s) as a single line each."""
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000908 assert isinstance(reviewers, list), reviewers
909 if not reviewers:
910 return
agable@chromium.org42c20792013-09-12 17:34:49 +0000911 reviewers = reviewers[:]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000912
agable@chromium.org42c20792013-09-12 17:34:49 +0000913 # Get the set of R= and TBR= lines and remove them from the desciption.
914 regexp = re.compile(self.R_LINE)
915 matches = [regexp.match(line) for line in self._description_lines]
916 new_desc = [l for i, l in enumerate(self._description_lines)
917 if not matches[i]]
918 self.set_description(new_desc)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000919
agable@chromium.org42c20792013-09-12 17:34:49 +0000920 # Construct new unified R= and TBR= lines.
921 r_names = []
922 tbr_names = []
923 for match in matches:
924 if not match:
925 continue
926 people = cleanup_list([match.group(2).strip()])
927 if match.group(1) == 'TBR':
928 tbr_names.extend(people)
929 else:
930 r_names.extend(people)
931 for name in r_names:
932 if name not in reviewers:
933 reviewers.append(name)
934 new_r_line = 'R=' + ', '.join(reviewers) if reviewers else None
935 new_tbr_line = 'TBR=' + ', '.join(tbr_names) if tbr_names else None
936
937 # Put the new lines in the description where the old first R= line was.
938 line_loc = next((i for i, match in enumerate(matches) if match), -1)
939 if 0 <= line_loc < len(self._description_lines):
940 if new_tbr_line:
941 self._description_lines.insert(line_loc, new_tbr_line)
942 if new_r_line:
943 self._description_lines.insert(line_loc, new_r_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000944 else:
agable@chromium.org42c20792013-09-12 17:34:49 +0000945 if new_r_line:
946 self.append_footer(new_r_line)
947 if new_tbr_line:
948 self.append_footer(new_tbr_line)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000949
950 def prompt(self):
951 """Asks the user to update the description."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000952 self.set_description([
953 '# Enter a description of the change.',
954 '# This will be displayed on the codereview site.',
955 '# The first line will also be used as the subject of the review.',
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000956 '#--------------------This line is 72 characters long'
agable@chromium.org42c20792013-09-12 17:34:49 +0000957 '--------------------',
958 ] + self._description_lines)
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000959
agable@chromium.org42c20792013-09-12 17:34:49 +0000960 regexp = re.compile(self.BUG_LINE)
961 if not any((regexp.match(line) for line in self._description_lines)):
rmistry@google.com90752582014-01-14 21:04:50 +0000962 self.append_footer('BUG=%s' % settings.GetBugPrefix())
agable@chromium.org42c20792013-09-12 17:34:49 +0000963 content = gclient_utils.RunEditor(self.description, True,
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000964 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000965 if not content:
966 DieWithError('Running editor failed')
agable@chromium.org42c20792013-09-12 17:34:49 +0000967 lines = content.splitlines()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000968
969 # Strip off comments.
agable@chromium.org42c20792013-09-12 17:34:49 +0000970 clean_lines = [line.rstrip() for line in lines if not line.startswith('#')]
971 if not clean_lines:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000972 DieWithError('No CL description, aborting')
agable@chromium.org42c20792013-09-12 17:34:49 +0000973 self.set_description(clean_lines)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000974
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000975 def append_footer(self, line):
agable@chromium.org42c20792013-09-12 17:34:49 +0000976 if self._description_lines:
977 # Add an empty line if either the last line or the new line isn't a tag.
978 last_line = self._description_lines[-1]
979 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
980 not presubmit_support.Change.TAG_LINE_RE.match(line)):
981 self._description_lines.append('')
982 self._description_lines.append(line)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000983
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000984 def get_reviewers(self):
985 """Retrieves the list of reviewers."""
agable@chromium.org42c20792013-09-12 17:34:49 +0000986 matches = [re.match(self.R_LINE, line) for line in self._description_lines]
987 reviewers = [match.group(2).strip() for match in matches if match]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000988 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000989
990
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000991def get_approving_reviewers(props):
992 """Retrieves the reviewers that approved a CL from the issue properties with
993 messages.
994
995 Note that the list may contain reviewers that are not committer, thus are not
996 considered by the CQ.
997 """
998 return sorted(
999 set(
1000 message['sender']
1001 for message in props['messages']
1002 if message['approval'] and message['sender'] in props['reviewers']
1003 )
1004 )
1005
1006
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007def FindCodereviewSettingsFile(filename='codereview.settings'):
1008 """Finds the given file starting in the cwd and going up.
1009
1010 Only looks up to the top of the repository unless an
1011 'inherit-review-settings-ok' file exists in the root of the repository.
1012 """
1013 inherit_ok_file = 'inherit-review-settings-ok'
1014 cwd = os.getcwd()
1015 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
1016 if os.path.isfile(os.path.join(root, inherit_ok_file)):
1017 root = '/'
1018 while True:
1019 if filename in os.listdir(cwd):
1020 if os.path.isfile(os.path.join(cwd, filename)):
1021 return open(os.path.join(cwd, filename))
1022 if cwd == root:
1023 break
1024 cwd = os.path.dirname(cwd)
1025
1026
1027def LoadCodereviewSettingsFromFile(fileobj):
1028 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +00001029 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001031 def SetProperty(name, setting, unset_error_ok=False):
1032 fullname = 'rietveld.' + name
1033 if setting in keyvals:
1034 RunGit(['config', fullname, keyvals[setting]])
1035 else:
1036 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
1037
1038 SetProperty('server', 'CODE_REVIEW_SERVER')
1039 # Only server setting is required. Other settings can be absent.
1040 # In that case, we ignore errors raised during option deletion attempt.
1041 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001042 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
1044 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
rmistry@google.com90752582014-01-14 21:04:50 +00001045 SetProperty('bug-prefix', 'BUG_PREFIX', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046
ukai@chromium.org7044efc2013-11-28 01:51:21 +00001047 if 'GERRIT_HOST' in keyvals:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001048 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
ukai@chromium.orge8077812012-02-03 03:41:46 +00001049
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001050 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
1051 #should be of the form
1052 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
1053 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
1054 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
1055 keyvals['ORIGIN_URL_CONFIG']])
1056
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001057
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001058def urlretrieve(source, destination):
1059 """urllib is broken for SSL connections via a proxy therefore we
1060 can't use urllib.urlretrieve()."""
1061 with open(destination, 'w') as f:
1062 f.write(urllib2.urlopen(source).read())
1063
1064
ukai@chromium.org712d6102013-11-27 00:52:58 +00001065def hasSheBang(fname):
1066 """Checks fname is a #! script."""
1067 with open(fname) as f:
1068 return f.read(2).startswith('#!')
1069
1070
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001071def DownloadHooks(force):
1072 """downloads hooks
1073
1074 Args:
1075 force: True to update hooks. False to install hooks if not present.
1076 """
1077 if not settings.GetIsGerrit():
1078 return
ukai@chromium.org712d6102013-11-27 00:52:58 +00001079 src = 'https://gerrit-review.googlesource.com/tools/hooks/commit-msg'
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001080 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1081 if not os.access(dst, os.X_OK):
1082 if os.path.exists(dst):
1083 if not force:
1084 return
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001085 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001086 urlretrieve(src, dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001087 if not hasSheBang(dst):
1088 DieWithError('Not a script: %s\n'
1089 'You need to download from\n%s\n'
1090 'into .git/hooks/commit-msg and '
1091 'chmod +x .git/hooks/commit-msg' % (dst, src))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001092 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1093 except Exception:
1094 if os.path.exists(dst):
1095 os.remove(dst)
ukai@chromium.org712d6102013-11-27 00:52:58 +00001096 DieWithError('\nFailed to download hooks.\n'
1097 'You need to download from\n%s\n'
1098 'into .git/hooks/commit-msg and '
1099 'chmod +x .git/hooks/commit-msg' % src)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001100
1101
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001102@subcommand.usage('[repo root containing codereview.settings]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103def CMDconfig(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001104 """Edits configuration for this tree."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105
pgervais@chromium.org87884cc2014-01-03 22:23:41 +00001106 parser.add_option('--activate-update', action='store_true',
1107 help='activate auto-updating [rietveld] section in '
1108 '.git/config')
1109 parser.add_option('--deactivate-update', action='store_true',
1110 help='deactivate auto-updating [rietveld] section in '
1111 '.git/config')
1112 options, args = parser.parse_args(args)
1113
1114 if options.deactivate_update:
1115 RunGit(['config', 'rietveld.autoupdate', 'false'])
1116 return
1117
1118 if options.activate_update:
1119 RunGit(['config', '--unset', 'rietveld.autoupdate'])
1120 return
1121
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001122 if len(args) == 0:
1123 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001124 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 return 0
1126
1127 url = args[0]
1128 if not url.endswith('codereview.settings'):
1129 url = os.path.join(url, 'codereview.settings')
1130
1131 # Load code review settings and download hooks (if available).
1132 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001133 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 return 0
1135
1136
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001137def CMDbaseurl(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001138 """Gets or sets base-url for this branch."""
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001139 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1140 branch = ShortBranchName(branchref)
1141 _, args = parser.parse_args(args)
1142 if not args:
1143 print("Current base-url:")
1144 return RunGit(['config', 'branch.%s.base-url' % branch],
1145 error_ok=False).strip()
1146 else:
1147 print("Setting base-url to %s" % args[0])
1148 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1149 error_ok=False).strip()
1150
1151
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152def CMDstatus(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001153 """Show status of changelists.
1154
1155 Colors are used to tell the state of the CL unless --fast is used:
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001156 - Red not sent for review or broken
1157 - Blue waiting for review
1158 - Yellow waiting for you to reply to review
1159 - Green LGTM'ed
1160 - Magenta in the commit queue
1161 - Cyan was committed, branch can be deleted
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001162
1163 Also see 'git cl comments'.
1164 """
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 parser.add_option('--field',
1166 help='print only specific field (desc|id|patch|url)')
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001167 parser.add_option('-f', '--fast', action='store_true',
1168 help='Do not retrieve review status')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 (options, args) = parser.parse_args(args)
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001170 if args:
1171 parser.error('Unsupported args: %s' % args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173 if options.field:
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001174 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175 if options.field.startswith('desc'):
1176 print cl.GetDescription()
1177 elif options.field == 'id':
1178 issueid = cl.GetIssue()
1179 if issueid:
1180 print issueid
1181 elif options.field == 'patch':
1182 patchset = cl.GetPatchset()
1183 if patchset:
1184 print patchset
1185 elif options.field == 'url':
1186 url = cl.GetIssueURL()
1187 if url:
1188 print url
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001189 return 0
1190
1191 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1192 if not branches:
1193 print('No local branch found.')
1194 return 0
1195
1196 changes = (Changelist(branchref=b) for b in branches.splitlines())
1197 branches = dict((c.GetBranch(), c.GetIssueURL()) for c in changes)
1198 alignment = max(5, max(len(b) for b in branches))
1199 print 'Branches associated with reviews:'
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001200 # Adhoc thread pool to request data concurrently.
1201 output = Queue.Queue()
1202
1203 # Silence upload.py otherwise it becomes unweldly.
1204 upload.verbosity = 0
1205
1206 if not options.fast:
1207 def fetch(b):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001208 """Fetches information for an issue and returns (branch, issue, color)."""
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001209 c = Changelist(branchref=b)
1210 i = c.GetIssueURL()
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001211 props = {}
1212 r = None
1213 if i:
1214 try:
1215 props = c.GetIssueProperties()
1216 r = c.GetApprovingReviewers() if i else None
1217 except urllib2.HTTPError:
1218 # The issue probably doesn't exist anymore.
1219 i += ' (broken)'
1220
1221 msgs = props.get('messages') or []
1222
1223 if not i:
1224 color = Fore.WHITE
1225 elif props.get('closed'):
1226 # Issue is closed.
1227 color = Fore.CYAN
jsbell@chromium.orgaeab41a2013-12-10 20:01:22 +00001228 elif props.get('commit'):
1229 # Issue is in the commit queue.
1230 color = Fore.MAGENTA
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001231 elif r:
1232 # Was LGTM'ed.
1233 color = Fore.GREEN
1234 elif not msgs:
1235 # No message was sent.
1236 color = Fore.RED
1237 elif msgs[-1]['sender'] != props.get('owner_email'):
1238 color = Fore.YELLOW
1239 else:
1240 color = Fore.BLUE
1241 output.put((b, i, color))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001242
1243 threads = [threading.Thread(target=fetch, args=(b,)) for b in branches]
1244 for t in threads:
1245 t.daemon = True
1246 t.start()
1247 else:
1248 # Do not use GetApprovingReviewers(), since it requires an HTTP request.
1249 for b in branches:
1250 c = Changelist(branchref=b)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001251 url = c.GetIssueURL()
1252 output.put((b, url, Fore.BLUE if url else Fore.WHITE))
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001253
1254 tmp = {}
1255 alignment = max(5, max(len(ShortBranchName(b)) for b in branches))
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001256 for branch in sorted(branches):
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001257 while branch not in tmp:
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001258 b, i, color = output.get()
1259 tmp[b] = (i, color)
1260 issue, color = tmp.pop(branch)
maruel@chromium.org885f6512013-07-27 02:17:26 +00001261 reset = Fore.RESET
1262 if not sys.stdout.isatty():
1263 color = ''
1264 reset = ''
binji@chromium.orgc3d17dd2013-12-19 00:55:31 +00001265 print ' %*s : %s%s%s' % (
maruel@chromium.org885f6512013-07-27 02:17:26 +00001266 alignment, ShortBranchName(branch), color, issue, reset)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001267
maruel@chromium.orge25c75b2013-07-23 18:30:56 +00001268 cl = Changelist()
1269 print
1270 print 'Current branch:',
1271 if not cl.GetIssue():
1272 print 'no issue assigned.'
1273 return 0
1274 print cl.GetBranch()
1275 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
1276 print 'Issue description:'
1277 print cl.GetDescription(pretty=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 return 0
1279
1280
maruel@chromium.org39c0b222013-08-17 16:57:01 +00001281def colorize_CMDstatus_doc():
1282 """To be called once in main() to add colors to git cl status help."""
1283 colors = [i for i in dir(Fore) if i[0].isupper()]
1284
1285 def colorize_line(line):
1286 for color in colors:
1287 if color in line.upper():
1288 # Extract whitespaces first and the leading '-'.
1289 indent = len(line) - len(line.lstrip(' ')) + 1
1290 return line[:indent] + getattr(Fore, color) + line[indent:] + Fore.RESET
1291 return line
1292
1293 lines = CMDstatus.__doc__.splitlines()
1294 CMDstatus.__doc__ = '\n'.join(colorize_line(l) for l in lines)
1295
1296
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001297@subcommand.usage('[issue_number]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298def CMDissue(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001299 """Sets or displays the current code review issue number.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300
1301 Pass issue number 0 to clear the current issue.
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001302 """
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001303 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304
1305 cl = Changelist()
1306 if len(args) > 0:
1307 try:
1308 issue = int(args[0])
1309 except ValueError:
1310 DieWithError('Pass a number to set the issue or none to list it.\n'
1311 'Maybe you want to run git cl status?')
1312 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001313 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001314 return 0
1315
1316
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001317def CMDcomments(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001318 """Shows review comments of the current changelist."""
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001319 (_, args) = parser.parse_args(args)
1320 if args:
1321 parser.error('Unsupported argument: %s' % args)
1322
1323 cl = Changelist()
1324 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001325 data = cl.GetIssueProperties()
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001326 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001327 if message['disapproval']:
1328 color = Fore.RED
1329 elif message['approval']:
1330 color = Fore.GREEN
1331 elif message['sender'] == data['owner_email']:
1332 color = Fore.MAGENTA
1333 else:
1334 color = Fore.BLUE
1335 print '\n%s%s %s%s' % (
1336 color, message['date'].split('.', 1)[0], message['sender'],
1337 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001338 if message['text'].strip():
1339 print '\n'.join(' ' + l for l in message['text'].splitlines())
1340 return 0
1341
1342
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001343def CMDdescription(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001344 """Brings up the editor for the current CL's description."""
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001345 cl = Changelist()
1346 if not cl.GetIssue():
1347 DieWithError('This branch has no associated changelist.')
1348 description = ChangeDescription(cl.GetDescription())
1349 description.prompt()
1350 cl.UpdateDescription(description.description)
1351 return 0
1352
1353
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354def CreateDescriptionFromLog(args):
1355 """Pulls out the commit log to use as a base for the CL description."""
1356 log_args = []
1357 if len(args) == 1 and not args[0].endswith('.'):
1358 log_args = [args[0] + '..']
1359 elif len(args) == 1 and args[0].endswith('...'):
1360 log_args = [args[0][:-1]]
1361 elif len(args) == 2:
1362 log_args = [args[0] + '..' + args[1]]
1363 else:
1364 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001365 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
1367
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368def CMDpresubmit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001369 """Runs presubmit tests on the current changelist."""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001370 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001372 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001373 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 (options, args) = parser.parse_args(args)
1375
ukai@chromium.org259e4682012-10-25 07:36:33 +00001376 if not options.force and is_dirty_git_tree('presubmit'):
1377 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001378 return 1
1379
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001380 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001381 if args:
1382 base_branch = args[0]
1383 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001384 # Default to diffing against the common ancestor of the upstream branch.
1385 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001387 cl.RunHook(
1388 committing=not options.upload,
1389 may_prompt=False,
1390 verbose=options.verbose,
1391 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001392 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001393
1394
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001395def AddChangeIdToCommitMessage(options, args):
1396 """Re-commits using the current message, assumes the commit hook is in
1397 place.
1398 """
1399 log_desc = options.message or CreateDescriptionFromLog(args)
1400 git_command = ['commit', '--amend', '-m', log_desc]
1401 RunGit(git_command)
1402 new_log_desc = CreateDescriptionFromLog(args)
1403 if CHANGE_ID in new_log_desc:
1404 print 'git-cl: Added Change-Id to commit message.'
1405 else:
1406 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1407
1408
ukai@chromium.orge8077812012-02-03 03:41:46 +00001409def GerritUpload(options, args, cl):
1410 """upload the current branch to gerrit."""
1411 # We assume the remote called "origin" is the one we want.
1412 # It is probably not worthwhile to support different workflows.
1413 remote = 'origin'
1414 branch = 'master'
1415 if options.target_branch:
1416 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001417
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001418 change_desc = ChangeDescription(
1419 options.message or CreateDescriptionFromLog(args))
1420 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001421 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001422 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001423 if CHANGE_ID not in change_desc.description:
1424 AddChangeIdToCommitMessage(options, args)
1425 if options.reviewers:
1426 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001427
ukai@chromium.orge8077812012-02-03 03:41:46 +00001428 receive_options = []
1429 cc = cl.GetCCList().split(',')
1430 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001431 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001432 cc = filter(None, cc)
1433 if cc:
1434 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001435 if change_desc.get_reviewers():
1436 receive_options.extend(
1437 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438
ukai@chromium.orge8077812012-02-03 03:41:46 +00001439 git_command = ['push']
1440 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001441 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001442 ' '.join(receive_options))
1443 git_command += [remote, 'HEAD:refs/for/' + branch]
1444 RunGit(git_command)
1445 # TODO(ukai): parse Change-Id: and set issue number?
1446 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001447
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001448
ukai@chromium.orge8077812012-02-03 03:41:46 +00001449def RietveldUpload(options, args, cl):
1450 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001451 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1452 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 if options.emulate_svn_auto_props:
1454 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001455
1456 change_desc = None
1457
pgervais@chromium.org91141372014-01-09 23:27:20 +00001458 if options.email is not None:
1459 upload_args.extend(['--email', options.email])
1460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001462 if options.title:
1463 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001464 if options.message:
1465 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001466 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001467 print ("This branch is associated with issue %s. "
1468 "Adding patch to that issue." % cl.GetIssue())
1469 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001470 if options.title:
1471 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001472 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001473 change_desc = ChangeDescription(message)
1474 if options.reviewers:
1475 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001476 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001477 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001478
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001479 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480 print "Description is empty; aborting."
1481 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001482
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001483 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001484 if change_desc.get_reviewers():
1485 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001486 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001487 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001488 DieWithError("Must specify reviewers to send email.")
1489 upload_args.append('--send_mail')
tyoshino@chromium.org99918ab2013-09-30 06:17:28 +00001490
1491 # We check this before applying rietveld.private assuming that in
1492 # rietveld.cc only addresses which we can send private CLs to are listed
1493 # if rietveld.private is set, and so we should ignore rietveld.cc only when
1494 # --private is specified explicitly on the command line.
1495 if options.private:
1496 logging.warn('rietveld.cc is ignored since private flag is specified. '
1497 'You need to review and add them manually if necessary.')
1498 cc = cl.GetCCListWithoutDefault()
1499 else:
1500 cc = cl.GetCCList()
1501 cc = ','.join(filter(None, (cc, ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001502 if cc:
1503 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001505 if options.private or settings.GetDefaultPrivateFlag() == "True":
1506 upload_args.append('--private')
1507
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001508 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001509 if not options.find_copies:
1510 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001511
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001512 # Include the upstream repo's URL in the change -- this is useful for
1513 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001514 remote_url = cl.GetGitBaseUrlFromConfig()
1515 if not remote_url:
1516 if settings.GetIsGitSvn():
1517 # URL is dependent on the current directory.
1518 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1519 if data:
1520 keys = dict(line.split(': ', 1) for line in data.splitlines()
1521 if ': ' in line)
1522 remote_url = keys.get('URL', None)
1523 else:
1524 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1525 remote_url = (cl.GetRemoteUrl() + '@'
1526 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001527 if remote_url:
1528 upload_args.extend(['--base_url', remote_url])
1529
1530 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001531 upload_args = ['upload'] + upload_args + args
1532 logging.info('upload.RealMain(%s)', upload_args)
1533 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org911fce12013-07-29 23:01:13 +00001534 issue = int(issue)
1535 patchset = int(patchset)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001536 except KeyboardInterrupt:
1537 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001538 except:
1539 # If we got an exception after the user typed a description for their
1540 # change, back up the description before re-raising.
1541 if change_desc:
1542 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1543 print '\nGot exception while uploading -- saving description to %s\n' \
1544 % backup_path
1545 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001546 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001547 backup_file.close()
1548 raise
1549
1550 if not cl.GetIssue():
1551 cl.SetIssue(issue)
1552 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001553
1554 if options.use_commit_queue:
1555 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556 return 0
1557
1558
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001559def cleanup_list(l):
1560 """Fixes a list so that comma separated items are put as individual items.
1561
1562 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1563 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1564 """
1565 items = sum((i.split(',') for i in l), [])
1566 stripped_items = (i.strip() for i in items)
1567 return sorted(filter(None, stripped_items))
1568
1569
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001570@subcommand.usage('[args to "git diff"]')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001571def CMDupload(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001572 """Uploads the current changelist to codereview."""
ukai@chromium.orge8077812012-02-03 03:41:46 +00001573 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1574 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001575 parser.add_option('--bypass-watchlists', action='store_true',
1576 dest='bypass_watchlists',
1577 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001578 parser.add_option('-f', action='store_true', dest='force',
1579 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001580 parser.add_option('-m', dest='message', help='message for patchset')
1581 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001582 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001583 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001584 help='reviewer email addresses')
1585 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001586 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001587 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001588 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001589 help='send email to reviewer immediately')
1590 parser.add_option("--emulate_svn_auto_props", action="store_true",
1591 dest="emulate_svn_auto_props",
1592 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001593 parser.add_option('-c', '--use-commit-queue', action='store_true',
1594 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001595 parser.add_option('--private', action='store_true',
1596 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001597 parser.add_option('--target_branch',
1598 help='When uploading to gerrit, remote branch to '
1599 'use for CL. Default: master')
pgervais@chromium.org91141372014-01-09 23:27:20 +00001600 parser.add_option('--email', default=None,
1601 help='email address to use to connect to Rietveld')
1602
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001603 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001604 (options, args) = parser.parse_args(args)
1605
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001606 if options.target_branch and not settings.GetIsGerrit():
1607 parser.error('Use --target_branch for non gerrit repository.')
1608
ukai@chromium.org259e4682012-10-25 07:36:33 +00001609 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001610 return 1
1611
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001612 options.reviewers = cleanup_list(options.reviewers)
1613 options.cc = cleanup_list(options.cc)
1614
ukai@chromium.orge8077812012-02-03 03:41:46 +00001615 cl = Changelist()
1616 if args:
1617 # TODO(ukai): is it ok for gerrit case?
1618 base_branch = args[0]
1619 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001620 # Default to diffing against common ancestor of upstream branch
1621 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001622 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001623
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001624 # Apply watchlists on upload.
1625 change = cl.GetChange(base_branch, None)
1626 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1627 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001628 if not options.bypass_watchlists:
1629 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001630
ukai@chromium.orge8077812012-02-03 03:41:46 +00001631 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001632 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001633 may_prompt=not options.force,
1634 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001635 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001636 if not hook_results.should_continue():
1637 return 1
1638 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001639 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001640
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001641 if cl.GetIssue():
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001642 latest_patchset = cl.GetMostRecentPatchset()
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001643 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001644 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001645 print ('The last upload made from this repository was patchset #%d but '
1646 'the most recent patchset on the server is #%d.'
1647 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001648 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1649 'from another machine or branch the patch you\'re uploading now '
1650 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001651 ask_for_data('About to upload; enter to confirm.')
1652
iannucci@chromium.org79540052012-10-19 23:15:26 +00001653 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001654 if settings.GetIsGerrit():
1655 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001656 ret = RietveldUpload(options, args, cl)
1657 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001658 git_set_branch_value('last-upload-hash',
1659 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001660
1661 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001662
1663
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001664def IsSubmoduleMergeCommit(ref):
1665 # When submodules are added to the repo, we expect there to be a single
1666 # non-git-svn merge commit at remote HEAD with a signature comment.
1667 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001668 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001669 return RunGit(cmd) != ''
1670
1671
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001672def SendUpstream(parser, args, cmd):
1673 """Common code for CmdPush and CmdDCommit
1674
1675 Squashed commit into a single.
1676 Updates changelog with metadata (e.g. pointer to review).
1677 Pushes/dcommits the code upstream.
1678 Updates review and closes.
1679 """
1680 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1681 help='bypass upload presubmit hook')
1682 parser.add_option('-m', dest='message',
1683 help="override review description")
1684 parser.add_option('-f', action='store_true', dest='force',
1685 help="force yes to questions (don't prompt)")
1686 parser.add_option('-c', dest='contributor',
1687 help="external contributor for patch (appended to " +
1688 "description and used as author for git). Should be " +
1689 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001690 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001691 (options, args) = parser.parse_args(args)
1692 cl = Changelist()
1693
1694 if not args or cmd == 'push':
1695 # Default to merging against our best guess of the upstream branch.
1696 args = [cl.GetUpstreamBranch()]
1697
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001698 if options.contributor:
1699 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1700 print "Please provide contibutor as 'First Last <email@example.com>'"
1701 return 1
1702
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001703 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001704 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001705
ukai@chromium.org259e4682012-10-25 07:36:33 +00001706 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707 return 1
1708
1709 # This rev-list syntax means "show all commits not in my branch that
1710 # are in base_branch".
1711 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1712 base_branch]).splitlines()
1713 if upstream_commits:
1714 print ('Base branch "%s" has %d commits '
1715 'not in this branch.' % (base_branch, len(upstream_commits)))
1716 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1717 return 1
1718
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001719 # This is the revision `svn dcommit` will commit on top of.
1720 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1721 '--pretty=format:%H'])
1722
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001723 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001724 # If the base_head is a submodule merge commit, the first parent of the
1725 # base_head should be a git-svn commit, which is what we're interested in.
1726 base_svn_head = base_branch
1727 if base_has_submodules:
1728 base_svn_head += '^1'
1729
1730 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001731 if extra_commits:
1732 print ('This branch has %d additional commits not upstreamed yet.'
1733 % len(extra_commits.splitlines()))
1734 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1735 'before attempting to %s.' % (base_branch, cmd))
1736 return 1
1737
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001738 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001739 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001740 author = None
1741 if options.contributor:
1742 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001743 hook_results = cl.RunHook(
1744 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001745 may_prompt=not options.force,
1746 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001747 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001748 if not hook_results.should_continue():
1749 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750
1751 if cmd == 'dcommit':
1752 # Check the tree status if the tree status URL is set.
1753 status = GetTreeStatus()
1754 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001755 print('The tree is closed. Please wait for it to reopen. Use '
1756 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001757 return 1
1758 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001759 print('Unable to determine tree status. Please verify manually and '
1760 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001761 else:
1762 breakpad.SendStack(
1763 'GitClHooksBypassedCommit',
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001764 'Issue %s/%s bypassed hook when committing (tree status was "%s")' %
1765 (cl.GetRietveldServer(), cl.GetIssue(), GetTreeStatus()),
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001766 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001767
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001768 change_desc = ChangeDescription(options.message)
1769 if not change_desc.description and cl.GetIssue():
1770 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001771
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001772 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001773 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001774 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001775 else:
1776 print 'No description set.'
1777 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1778 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001779
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001780 # Keep a separate copy for the commit message, because the commit message
1781 # contains the link to the Rietveld issue, while the Rietveld message contains
1782 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001783 # Keep a separate copy for the commit message.
1784 if cl.GetIssue():
maruel@chromium.orgcf087782013-07-23 13:08:48 +00001785 change_desc.update_reviewers(cl.GetApprovingReviewers())
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001786
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001787 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001788 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001789 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001790 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001791 commit_desc.append_footer('Patch from %s.' % options.contributor)
1792
agable@chromium.orgeec3ea32013-08-15 20:31:39 +00001793 print('Description:')
1794 print(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001795
1796 branches = [base_branch, cl.GetBranchRef()]
1797 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001798 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001799 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001800
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001801 # We want to squash all this branch's commits into one commit with the proper
1802 # description. We do this by doing a "reset --soft" to the base branch (which
1803 # keeps the working copy the same), then dcommitting that. If origin/master
1804 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1805 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001806 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001807 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1808 # Delete the branches if they exist.
1809 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1810 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1811 result = RunGitWithCode(showref_cmd)
1812 if result[0] == 0:
1813 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001814
1815 # We might be in a directory that's present in this branch but not in the
1816 # trunk. Move up to the top of the tree so that git commands that expect a
1817 # valid CWD won't fail after we check out the merge branch.
1818 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1819 if rel_base_path:
1820 os.chdir(rel_base_path)
1821
1822 # Stuff our change into the merge branch.
1823 # We wrap in a try...finally block so if anything goes wrong,
1824 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001825 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001826 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001827 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1828 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001830 RunGit(
1831 [
1832 'commit', '--author', options.contributor,
1833 '-m', commit_desc.description,
1834 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001836 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001837 if base_has_submodules:
1838 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1839 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1840 RunGit(['checkout', CHERRY_PICK_BRANCH])
1841 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001842 if cmd == 'push':
1843 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001844 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001845 retcode, output = RunGitWithCode(
1846 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1847 logging.debug(output)
1848 else:
1849 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001850 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001851 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001852 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001853 finally:
1854 # And then swap back to the original branch and clean up.
1855 RunGit(['checkout', '-q', cl.GetBranch()])
1856 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001857 if base_has_submodules:
1858 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001859
1860 if cl.GetIssue():
1861 if cmd == 'dcommit' and 'Committed r' in output:
1862 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1863 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001864 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1865 for l in output.splitlines(False))
1866 match = filter(None, match)
1867 if len(match) != 1:
1868 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1869 output)
1870 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001871 else:
1872 return 1
1873 viewvc_url = settings.GetViewVCUrl()
1874 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001875 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001876 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001877 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001878 print ('Closing issue '
1879 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001880 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001881 cl.CloseIssue()
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001882 props = cl.GetIssueProperties()
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001883 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001884 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00001885 if options.bypass_hooks:
1886 comment += ' (tree was closed).' if GetTreeStatus() == 'closed' else '.'
1887 else:
1888 comment += ' (presubmit successful).'
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001889 cl.RpcServer().add_comment(cl.GetIssue(), comment)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001890 cl.SetIssue(None)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001891
1892 if retcode == 0:
1893 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1894 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001895 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001896
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001897 return 0
1898
1899
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001900@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001901def CMDdcommit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001902 """Commits the current changelist via git-svn."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001903 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001904 message = """This doesn't appear to be an SVN repository.
1905If your project has a git mirror with an upstream SVN master, you probably need
1906to run 'git svn init', see your project's git mirror documentation.
1907If your project has a true writeable upstream repository, you probably want
1908to run 'git cl push' instead.
1909Choose wisely, if you get this wrong, your commit might appear to succeed but
1910will instead be silently ignored."""
1911 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001912 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001913 return SendUpstream(parser, args, 'dcommit')
1914
1915
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001916@subcommand.usage('[upstream branch to apply against]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001917def CMDpush(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00001918 """Commits the current changelist via git."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001919 if settings.GetIsGitSvn():
1920 print('This appears to be an SVN repository.')
1921 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001922 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001923 return SendUpstream(parser, args, 'push')
1924
1925
maruel@chromium.org0633fb42013-08-16 20:06:14 +00001926@subcommand.usage('<patch url or issue id>')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001927def CMDpatch(parser, args):
marq@chromium.orge5e59002013-10-02 23:21:25 +00001928 """Patches in a code review."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001929 parser.add_option('-b', dest='newbranch',
1930 help='create a new branch off trunk for the patch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001931 parser.add_option('-f', '--force', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001932 help='with -b, clobber any existing branch')
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001933 parser.add_option('-d', '--directory', action='store', metavar='DIR',
1934 help='Change to the directory DIR immediately, '
1935 'before doing anything else.')
1936 parser.add_option('--reject', action='store_true',
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00001937 help='failed patches spew .rej files rather than '
1938 'attempting a 3-way merge')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001939 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1940 help="don't commit after patch applies")
1941 (options, args) = parser.parse_args(args)
1942 if len(args) != 1:
1943 parser.print_help()
1944 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001945 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001946
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001947 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001948 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001949
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001950 if options.newbranch:
1951 if options.force:
1952 RunGit(['branch', '-D', options.newbranch],
1953 stderr=subprocess2.PIPE, error_ok=True)
1954 RunGit(['checkout', '-b', options.newbranch,
1955 Changelist().GetUpstreamBranch()])
1956
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001957 return PatchIssue(issue_arg, options.reject, options.nocommit,
1958 options.directory)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001959
1960
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00001961def PatchIssue(issue_arg, reject, nocommit, directory):
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00001962 if type(issue_arg) is int or issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001963 # Input is an issue id. Figure out the URL.
maruel@chromium.org52424302012-08-29 15:14:30 +00001964 issue = int(issue_arg)
jochen@chromium.orga26e0472013-07-24 10:25:01 +00001965 cl = Changelist(issue=issue)
maruel@chromium.org1033efd2013-07-23 23:25:09 +00001966 patchset = cl.GetMostRecentPatchset()
binji@chromium.org0281f522012-09-14 13:37:59 +00001967 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001968 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001969 # Assume it's a URL to the patch. Default to https.
1970 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001971 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001972 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001973 DieWithError('Must pass an issue ID or full URL for '
1974 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001975 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001976 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001977 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001978
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001979 # Switch up to the top-level directory, if necessary, in preparation for
1980 # applying the patch.
1981 top = RunGit(['rev-parse', '--show-cdup']).strip()
1982 if top:
1983 os.chdir(top)
1984
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001985 # Git patches have a/ at the beginning of source paths. We strip that out
1986 # with a sed script rather than the -p flag to patch so we can feed either
1987 # Git or svn-style patches into the same apply command.
1988 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001989 try:
1990 patch_data = subprocess2.check_output(
1991 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1992 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001993 DieWithError('Git patch mungling failed.')
1994 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001995 env = os.environ.copy()
1996 # 'cat' is a magical git string that disables pagers on all platforms.
1997 env['GIT_PAGER'] = 'cat'
1998
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001999 # We use "git apply" to apply the patch instead of "patch" so that we can
2000 # pick up file adds.
2001 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002002 cmd = ['git', 'apply', '--index', '-p0']
qsr@chromium.org1ef44af2013-10-16 16:24:32 +00002003 if directory:
2004 cmd.extend(('--directory', directory))
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002005 if reject:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002006 cmd.append('--reject')
tapted@chromium.org6a0b07c2013-07-10 01:29:19 +00002007 elif IsGitVersionAtLeast('1.7.12'):
2008 cmd.append('--3way')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002009 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002010 subprocess2.check_call(cmd, env=env,
2011 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00002012 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002013 DieWithError('Failed to apply the patch')
2014
2015 # If we had an issue, commit the current state and register the issue.
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002016 if not nocommit:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002017 RunGit(['commit', '-m', 'patch from issue %s' % issue])
2018 cl = Changelist()
2019 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00002020 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00002021 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002022 else:
2023 print "Patch applied to index."
2024 return 0
2025
2026
2027def CMDrebase(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002028 """Rebases current branch on top of svn repo."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002029 # Provide a wrapper for git svn rebase to help avoid accidental
2030 # git svn dcommit.
2031 # It's the only command that doesn't use parser at all since we just defer
2032 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00002033 env = os.environ.copy()
2034 # 'cat' is a magical git string that disables pagers on all platforms.
2035 env['GIT_PAGER'] = 'cat'
2036
2037 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002038
2039
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002040def GetTreeStatus(url=None):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002041 """Fetches the tree status and returns either 'open', 'closed',
2042 'unknown' or 'unset'."""
jochen@chromium.org3ec0d542014-01-14 20:00:03 +00002043 url = url or settings.GetTreeStatusUrl(error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002044 if url:
2045 status = urllib2.urlopen(url).read().lower()
2046 if status.find('closed') != -1 or status == '0':
2047 return 'closed'
2048 elif status.find('open') != -1 or status == '1':
2049 return 'open'
2050 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002051 return 'unset'
2052
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002053
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002054def GetTreeStatusReason():
2055 """Fetches the tree status from a json url and returns the message
2056 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00002057 url = settings.GetTreeStatusUrl()
2058 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002059 connection = urllib2.urlopen(json_url)
2060 status = json.loads(connection.read())
2061 connection.close()
2062 return status['message']
2063
dpranke@chromium.org970c5222011-03-12 00:32:24 +00002064
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002065def CMDtree(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002066 """Shows the status of the tree."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002067 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002068 status = GetTreeStatus()
2069 if 'unset' == status:
2070 print 'You must configure your tree status URL by running "git cl config".'
2071 return 2
2072
2073 print "The tree is %s" % status
2074 print
2075 print GetTreeStatusReason()
2076 if status != 'open':
2077 return 1
2078 return 0
2079
2080
maruel@chromium.org15192402012-09-06 12:38:29 +00002081def CMDtry(parser, args):
2082 """Triggers a try job through Rietveld."""
2083 group = optparse.OptionGroup(parser, "Try job options")
2084 group.add_option(
2085 "-b", "--bot", action="append",
2086 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
2087 "times to specify multiple builders. ex: "
2088 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
2089 "the try server waterfall for the builders name and the tests "
2090 "available. Can also be used to specify gtest_filter, e.g. "
2091 "-bwin_rel:base_unittests:ValuesTest.*Value"))
2092 group.add_option(
2093 "-r", "--revision",
2094 help="Revision to use for the try job; default: the "
2095 "revision will be determined by the try server; see "
2096 "its waterfall for more info")
2097 group.add_option(
2098 "-c", "--clobber", action="store_true", default=False,
2099 help="Force a clobber before building; e.g. don't do an "
2100 "incremental build")
2101 group.add_option(
2102 "--project",
2103 help="Override which project to use. Projects are defined "
2104 "server-side to define what default bot set to use")
2105 group.add_option(
2106 "-t", "--testfilter", action="append", default=[],
2107 help=("Apply a testfilter to all the selected builders. Unless the "
2108 "builders configurations are similar, use multiple "
2109 "--bot <builder>:<test> arguments."))
2110 group.add_option(
2111 "-n", "--name", help="Try job name; default to current branch name")
2112 parser.add_option_group(group)
2113 options, args = parser.parse_args(args)
2114
2115 if args:
2116 parser.error('Unknown arguments: %s' % args)
2117
2118 cl = Changelist()
2119 if not cl.GetIssue():
2120 parser.error('Need to upload first')
2121
2122 if not options.name:
2123 options.name = cl.GetBranch()
2124
2125 # Process --bot and --testfilter.
2126 if not options.bot:
2127 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00002128 change = cl.GetChange(
2129 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
2130 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00002131 options.bot = presubmit_support.DoGetTrySlaves(
2132 change,
2133 change.LocalPaths(),
2134 settings.GetRoot(),
2135 None,
2136 None,
2137 options.verbose,
2138 sys.stdout)
2139 if not options.bot:
2140 parser.error('No default try builder to try, use --bot')
2141
2142 builders_and_tests = {}
stip@chromium.org43064fd2013-12-18 20:07:44 +00002143 old_style = filter(lambda x: isinstance(x, basestring), options.bot)
2144 new_style = filter(lambda x: isinstance(x, tuple), options.bot)
2145
2146 for bot in old_style:
maruel@chromium.org15192402012-09-06 12:38:29 +00002147 if ':' in bot:
2148 builder, tests = bot.split(':', 1)
2149 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
2150 elif ',' in bot:
2151 parser.error('Specify one bot per --bot flag')
2152 else:
2153 builders_and_tests.setdefault(bot, []).append('defaulttests')
2154
stip@chromium.org43064fd2013-12-18 20:07:44 +00002155 for bot, tests in new_style:
2156 builders_and_tests.setdefault(bot, []).extend(tests)
2157
maruel@chromium.org15192402012-09-06 12:38:29 +00002158 if options.testfilter:
2159 forced_tests = sum((t.split(',') for t in options.testfilter), [])
2160 builders_and_tests = dict(
2161 (b, forced_tests) for b, t in builders_and_tests.iteritems()
2162 if t != ['compile'])
2163
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00002164 if any('triggered' in b for b in builders_and_tests):
2165 print >> sys.stderr, (
2166 'ERROR You are trying to send a job to a triggered bot. This type of'
2167 ' bot requires an\ninitial job from a parent (usually a builder). '
2168 'Instead send your job to the parent.\n'
2169 'Bot list: %s' % builders_and_tests)
2170 return 1
2171
ilevy@chromium.org36e420b2013-08-06 23:21:12 +00002172 patchset = cl.GetMostRecentPatchset()
2173 if patchset and patchset != cl.GetPatchset():
2174 print(
2175 '\nWARNING Mismatch between local config and server. Did a previous '
2176 'upload fail?\ngit-cl try always uses latest patchset from rietveld. '
2177 'Continuing using\npatchset %s.\n' % patchset)
fischman@chromium.orgd246c972013-12-21 22:47:38 +00002178 try:
2179 cl.RpcServer().trigger_try_jobs(
2180 cl.GetIssue(), patchset, options.name, options.clobber,
2181 options.revision, builders_and_tests)
2182 except urllib2.HTTPError, e:
2183 if e.code == 404:
2184 print('404 from rietveld; '
2185 'did you mean to use "git try" instead of "git cl try"?')
2186 return 1
maruel@chromium.org072d94b2012-09-20 19:20:08 +00002187 print('Tried jobs on:')
2188 length = max(len(builder) for builder in builders_and_tests)
2189 for builder in sorted(builders_and_tests):
2190 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00002191 return 0
2192
2193
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002194@subcommand.usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002195def CMDupstream(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002196 """Prints or sets the name of the upstream branch, if any."""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002197 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002198 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002199 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002200
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002201 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00002202 if args:
2203 # One arg means set upstream branch.
2204 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
2205 cl = Changelist()
2206 print "Upstream branch set to " + cl.GetUpstreamBranch()
2207 else:
2208 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002209 return 0
2210
2211
thestig@chromium.org00858c82013-12-02 23:08:03 +00002212def CMDweb(parser, args):
2213 """Opens the current CL in the web browser."""
2214 _, args = parser.parse_args(args)
2215 if args:
2216 parser.error('Unrecognized args: %s' % ' '.join(args))
2217
2218 issue_url = Changelist().GetIssueURL()
2219 if not issue_url:
2220 print >> sys.stderr, 'ERROR No issue to open'
2221 return 1
2222
2223 webbrowser.open(issue_url)
2224 return 0
2225
2226
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002227def CMDset_commit(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002228 """Sets the commit bit to trigger the Commit Queue."""
maruel@chromium.org27bb3872011-05-30 20:33:19 +00002229 _, args = parser.parse_args(args)
2230 if args:
2231 parser.error('Unrecognized args: %s' % ' '.join(args))
2232 cl = Changelist()
2233 cl.SetFlag('commit', '1')
2234 return 0
2235
2236
groby@chromium.org411034a2013-02-26 15:12:01 +00002237def CMDset_close(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002238 """Closes the issue."""
groby@chromium.org411034a2013-02-26 15:12:01 +00002239 _, args = parser.parse_args(args)
2240 if args:
2241 parser.error('Unrecognized args: %s' % ' '.join(args))
2242 cl = Changelist()
2243 # Ensure there actually is an issue to close.
2244 cl.GetDescription()
2245 cl.CloseIssue()
2246 return 0
2247
2248
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002249def CMDdiff(parser, args):
2250 """shows differences between local tree and last upload."""
2251 cl = Changelist()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002252 issue = cl.GetIssue()
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002253 branch = cl.GetBranch()
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002254 if not issue:
2255 DieWithError('No issue found for current branch (%s)' % branch)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002256 TMP_BRANCH = 'git-cl-diff'
2257 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2258
2259 # Create a new branch based on the merge-base
2260 RunGit(['checkout', '-q', '-b', TMP_BRANCH, base_branch])
2261 try:
2262 # Patch in the latest changes from rietveld.
sbc@chromium.org78dc9842013-11-25 18:43:44 +00002263 rtn = PatchIssue(issue, False, False, None)
sbc@chromium.org87b9bf02013-09-26 20:35:15 +00002264 if rtn != 0:
2265 return rtn
2266
2267 # Switch back to starting brand and diff against the temporary
2268 # branch containing the latest rietveld patch.
2269 subprocess2.check_call(['git', 'diff', TMP_BRANCH, branch])
2270 finally:
2271 RunGit(['checkout', '-q', branch])
2272 RunGit(['branch', '-D', TMP_BRANCH])
2273
2274 return 0
2275
2276
ikarienator@chromium.orgfaf3fdf2013-09-20 02:11:48 +00002277def CMDowners(parser, args):
2278 """interactively find the owners for reviewing"""
2279 parser.add_option(
2280 '--no-color',
2281 action='store_true',
2282 help='Use this option to disable color output')
2283 options, args = parser.parse_args(args)
2284
2285 author = RunGit(['config', 'user.email']).strip() or None
2286
2287 cl = Changelist()
2288
2289 if args:
2290 if len(args) > 1:
2291 parser.error('Unknown args')
2292 base_branch = args[0]
2293 else:
2294 # Default to diffing against the common ancestor of the upstream branch.
2295 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
2296
2297 change = cl.GetChange(base_branch, None)
2298 return owners_finder.OwnersFinder(
2299 [f.LocalPath() for f in
2300 cl.GetChange(base_branch, None).AffectedFiles()],
2301 change.RepositoryRoot(), author,
2302 fopen=file, os_path=os.path, glob=glob.glob,
2303 disable_color=options.no_color).run()
2304
2305
enne@chromium.org555cfe42014-01-29 18:21:39 +00002306@subcommand.usage('[files or directories to diff]')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002307def CMDformat(parser, args):
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002308 """Runs clang-format on the diff."""
thakis@chromium.orge139d952014-02-02 19:38:08 +00002309 CLANG_EXTS = ['.cc', '.cpp', '.h', '.mm']
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002310 parser.add_option('--full', action='store_true',
2311 help='Reformat the full content of all touched files')
2312 parser.add_option('--dry-run', action='store_true',
2313 help='Don\'t modify any file on disk.')
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002314 opts, args = parser.parse_args(args)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002315
enne@chromium.orgff7a1fb2013-12-10 19:21:41 +00002316 # git diff generates paths against the root of the repository. Change
2317 # to that directory so clang-format can find files even within subdirs.
2318 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
2319 if rel_base_path:
2320 os.chdir(rel_base_path)
2321
digit@chromium.org29e47272013-05-17 17:01:46 +00002322 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002323 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002324 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002325 # Only list the names of modified files.
2326 diff_cmd.append('--name-only')
2327 else:
2328 # Only generate context-less patches.
2329 diff_cmd.append('-U0')
2330
2331 # Grab the merge-base commit, i.e. the upstream commit of the current
2332 # branch when it was created or the last time it was rebased. This is
2333 # to cover the case where the user may have called "git fetch origin",
2334 # moving the origin branch to a newer commit, but hasn't rebased yet.
2335 upstream_commit = None
2336 cl = Changelist()
2337 upstream_branch = cl.GetUpstreamBranch()
2338 if upstream_branch:
2339 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2340 upstream_commit = upstream_commit.strip()
2341
2342 if not upstream_commit:
2343 DieWithError('Could not find base commit for this branch. '
2344 'Are you in detached state?')
2345
2346 diff_cmd.append(upstream_commit)
2347
2348 # Handle source file filtering.
2349 diff_cmd.append('--')
enne@chromium.org555cfe42014-01-29 18:21:39 +00002350 if args:
2351 for arg in args:
2352 if os.path.isdir(arg):
2353 diff_cmd += [os.path.join(arg, '*' + ext) for ext in CLANG_EXTS]
2354 elif os.path.isfile(arg):
2355 diff_cmd.append(arg)
2356 else:
2357 DieWithError('Argument "%s" is not a file or a directory' % arg)
2358 else:
2359 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
digit@chromium.org29e47272013-05-17 17:01:46 +00002360 diff_output = RunGit(diff_cmd)
2361
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002362 top_dir = os.path.normpath(
2363 RunGit(["rev-parse", "--show-toplevel"]).rstrip('\n'))
2364
2365 # Locate the clang-format binary in the checkout
2366 try:
2367 clang_format_tool = clang_format.FindClangFormatToolInChromiumTree()
2368 except clang_format.NotFoundError, e:
2369 DieWithError(e)
mdempsky@google.comc3b3dc02013-08-05 23:09:49 +00002370
digit@chromium.org29e47272013-05-17 17:01:46 +00002371 if opts.full:
2372 # diff_output is a list of files to send to clang-format.
2373 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002374 if not files:
2375 print "Nothing to format."
2376 return 0
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002377 cmd = [clang_format_tool]
2378 if not opts.dry_run:
2379 cmd.append('-i')
2380 stdout = RunCommand(cmd + files, cwd=top_dir)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002381 else:
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002382 env = os.environ.copy()
2383 env['PATH'] = os.path.dirname(clang_format_tool)
digit@chromium.org29e47272013-05-17 17:01:46 +00002384 # diff_output is a patch to send to clang-format-diff.py
nick@chromium.org3ac1c4e2014-01-16 02:44:42 +00002385 try:
2386 script = clang_format.FindClangFormatScriptInChromiumTree(
2387 'clang-format-diff.py')
2388 except clang_format.NotFoundError, e:
2389 DieWithError(e)
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002390
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002391 cmd = [sys.executable, script, '-p0']
2392 if not opts.dry_run:
2393 cmd.append('-i')
digit@chromium.orgd6ddc1c2013-10-25 15:36:32 +00002394
enne@chromium.org3b7e15c2014-01-21 17:44:47 +00002395 stdout = RunCommand(cmd, stdin=diff_output, cwd=top_dir, env=env)
2396 if opts.dry_run and len(stdout) > 0:
2397 return 2
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002398
2399 return 0
2400
2401
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002402class OptionParser(optparse.OptionParser):
2403 """Creates the option parse and add --verbose support."""
2404 def __init__(self, *args, **kwargs):
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002405 optparse.OptionParser.__init__(
2406 self, *args, prog='git cl', version=__version__, **kwargs)
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002407 self.add_option(
2408 '-v', '--verbose', action='count', default=0,
2409 help='Use 2 times for more debugging info')
2410
2411 def parse_args(self, args=None, values=None):
2412 options, args = optparse.OptionParser.parse_args(self, args, values)
2413 levels = [logging.WARNING, logging.INFO, logging.DEBUG]
2414 logging.basicConfig(level=levels[min(options.verbose, len(levels) - 1)])
2415 return options, args
2416
iannucci@chromium.orgd9c1b202013-07-24 23:52:11 +00002417
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002418def main(argv):
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002419 if sys.hexversion < 0x02060000:
2420 print >> sys.stderr, (
2421 '\nYour python version %s is unsupported, please upgrade.\n' %
2422 sys.version.split(' ', 1)[0])
2423 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002424
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002425 # Reload settings.
2426 global settings
2427 settings = Settings()
2428
maruel@chromium.org39c0b222013-08-17 16:57:01 +00002429 colorize_CMDstatus_doc()
maruel@chromium.org0633fb42013-08-16 20:06:14 +00002430 dispatcher = subcommand.CommandDispatcher(__name__)
2431 try:
2432 return dispatcher.execute(OptionParser(), argv)
2433 except urllib2.HTTPError, e:
2434 if e.code != 500:
2435 raise
2436 DieWithError(
2437 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2438 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002439
2440
2441if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002442 # These affect sys.stdout so do it outside of main() to simplify mocks in
2443 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002444 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002445 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002446 sys.exit(main(sys.argv[1:]))