blob: 2ed721e83f4e26ac8f1819504032edbd58b73e15 [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
maruel@chromium.org967c0a82013-06-17 22:52:24 +000010import difflib
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000011import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000012import logging
13import optparse
14import os
15import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000016import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000018import textwrap
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
maruel@chromium.org967c0a82013-06-17 22:52:24 +000020import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021
22try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000023 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000024except ImportError:
25 pass
26
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000028from third_party import colorama
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029from third_party import upload
30import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000031import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000032import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000034import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000036import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000037import watchlists
38
39
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000040DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000041POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000043GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000044CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000045
maruel@chromium.org2e23ce32013-05-07 12:42:28 +000046# Shortcut since it quickly becomes redundant.
47Fore = colorama.Fore
maruel@chromium.org90541732011-04-01 17:54:18 +000048
maruel@chromium.orgddd59412011-11-30 14:20:38 +000049# Initialized in main()
50settings = None
51
52
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000053def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000054 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055 sys.exit(1)
56
57
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000058def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000060 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org78936cb2013-04-11 00:17:52 +000061 except subprocess2.CalledProcessError as e:
62 logging.debug('Failed running %s', args)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000063 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000064 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065 'Command "%s" failed.\n%s' % (
66 ' '.join(args), error_message or e.stdout or ''))
67 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068
69
70def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 """Returns stdout."""
bratell@opera.com82b91cd2013-07-09 06:33:41 +000072 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000073
74
75def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000076 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000077 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +000078 env = os.environ.copy()
79 # 'cat' is a magical git string that disables pagers on all platforms.
80 env['GIT_PAGER'] = 'cat'
81 out, code = subprocess2.communicate(['git'] + args,
82 env=env,
bratell@opera.comf267b0e2013-05-02 09:11:43 +000083 stdout=subprocess2.PIPE)
szager@chromium.org9bb85e22012-06-13 20:28:23 +000084 return code, out[0]
85 except ValueError:
86 # When the subprocess fails, it returns None. That triggers a ValueError
87 # when trying to unpack the return value into (out, code).
88 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000089
90
91def usage(more):
92 def hook(fn):
93 fn.usage_more = more
94 return fn
95 return hook
96
97
maruel@chromium.org90541732011-04-01 17:54:18 +000098def ask_for_data(prompt):
99 try:
100 return raw_input(prompt)
101 except KeyboardInterrupt:
102 # Hide the exception.
103 sys.exit(1)
104
105
iannucci@chromium.org79540052012-10-19 23:15:26 +0000106def git_set_branch_value(key, value):
107 branch = Changelist().GetBranch()
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +0000108 if not branch:
109 return
110
111 cmd = ['config']
112 if isinstance(value, int):
113 cmd.append('--int')
114 git_key = 'branch.%s.%s' % (branch, key)
115 RunGit(cmd + [git_key, str(value)])
iannucci@chromium.org79540052012-10-19 23:15:26 +0000116
117
118def git_get_branch_default(key, default):
119 branch = Changelist().GetBranch()
120 if branch:
121 git_key = 'branch.%s.%s' % (branch, key)
122 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
123 try:
124 return int(stdout.strip())
125 except ValueError:
126 pass
127 return default
128
129
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000130def add_git_similarity(parser):
131 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000132 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000133 help='Sets the percentage that a pair of files need to match in order to'
134 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000135 parser.add_option(
136 '--find-copies', action='store_true',
137 help='Allows git to look for copies.')
138 parser.add_option(
139 '--no-find-copies', action='store_false', dest='find_copies',
140 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000141
142 old_parser_args = parser.parse_args
143 def Parse(args):
144 options, args = old_parser_args(args)
145
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000146 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000147 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000148 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000149 print('Note: Saving similarity of %d%% in git config.'
150 % options.similarity)
151 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000152
iannucci@chromium.org79540052012-10-19 23:15:26 +0000153 options.similarity = max(0, min(options.similarity, 100))
154
155 if options.find_copies is None:
156 options.find_copies = bool(
157 git_get_branch_default('git-find-copies', True))
158 else:
159 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000160
161 print('Using %d%% similarity for rename/copy detection. '
162 'Override with --similarity.' % options.similarity)
163
164 return options, args
165 parser.parse_args = Parse
166
167
ukai@chromium.org259e4682012-10-25 07:36:33 +0000168def is_dirty_git_tree(cmd):
169 # Make sure index is up-to-date before running diff-index.
170 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
171 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
172 if dirty:
173 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
174 print 'Uncommitted files: (git diff-index --name-status HEAD)'
175 print dirty[:4096]
176 if len(dirty) > 4096:
177 print '... (run "git diff-index --name-status HEAD" to see full output).'
178 return True
179 return False
180
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000181
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000182def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
183 """Return the corresponding git ref if |base_url| together with |glob_spec|
184 matches the full |url|.
185
186 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
187 """
188 fetch_suburl, as_ref = glob_spec.split(':')
189 if allow_wildcards:
190 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
191 if glob_match:
192 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
193 # "branches/{472,597,648}/src:refs/remotes/svn/*".
194 branch_re = re.escape(base_url)
195 if glob_match.group(1):
196 branch_re += '/' + re.escape(glob_match.group(1))
197 wildcard = glob_match.group(2)
198 if wildcard == '*':
199 branch_re += '([^/]*)'
200 else:
201 # Escape and replace surrounding braces with parentheses and commas
202 # with pipe symbols.
203 wildcard = re.escape(wildcard)
204 wildcard = re.sub('^\\\\{', '(', wildcard)
205 wildcard = re.sub('\\\\,', '|', wildcard)
206 wildcard = re.sub('\\\\}$', ')', wildcard)
207 branch_re += wildcard
208 if glob_match.group(3):
209 branch_re += re.escape(glob_match.group(3))
210 match = re.match(branch_re, url)
211 if match:
212 return re.sub('\*$', match.group(1), as_ref)
213
214 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
215 if fetch_suburl:
216 full_url = base_url + '/' + fetch_suburl
217 else:
218 full_url = base_url
219 if full_url == url:
220 return as_ref
221 return None
222
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000223
iannucci@chromium.org79540052012-10-19 23:15:26 +0000224def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000225 """Prints statistics about the change to the user."""
226 # --no-ext-diff is broken in some versions of Git, so try to work around
227 # this by overriding the environment (but there is still a problem if the
228 # git config key "diff.external" is used).
229 env = os.environ.copy()
230 if 'GIT_EXTERNAL_DIFF' in env:
231 del env['GIT_EXTERNAL_DIFF']
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000232 # 'cat' is a magical git string that disables pagers on all platforms.
233 env['GIT_PAGER'] = 'cat'
iannucci@chromium.org79540052012-10-19 23:15:26 +0000234
235 if find_copies:
236 similarity_options = ['--find-copies-harder', '-l100000',
237 '-C%s' % similarity]
238 else:
239 similarity_options = ['-M%s' % similarity]
240
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000241 return subprocess2.call(
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000242 ['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000243 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
iannucci@chromium.org79540052012-10-19 23:15:26 +0000244 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000245
246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000247class Settings(object):
248 def __init__(self):
249 self.default_server = None
250 self.cc = None
251 self.root = None
252 self.is_git_svn = None
253 self.svn_branch = None
254 self.tree_status_url = None
255 self.viewvc_url = None
256 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000257 self.is_gerrit = None
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000258 self.git_editor = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000259
260 def LazyUpdateIfNeeded(self):
261 """Updates the settings from a codereview.settings file, if available."""
262 if not self.updated:
263 cr_settings_file = FindCodereviewSettingsFile()
264 if cr_settings_file:
265 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000266 self.updated = True
267 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000268 self.updated = True
269
270 def GetDefaultServerUrl(self, error_ok=False):
271 if not self.default_server:
272 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000273 self.default_server = gclient_utils.UpgradeToHttps(
274 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000275 if error_ok:
276 return self.default_server
277 if not self.default_server:
278 error_message = ('Could not find settings file. You must configure '
279 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000280 self.default_server = gclient_utils.UpgradeToHttps(
281 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000282 return self.default_server
283
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000284 def GetRoot(self):
285 if not self.root:
286 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
287 return self.root
288
289 def GetIsGitSvn(self):
290 """Return true if this repo looks like it's using git-svn."""
291 if self.is_git_svn is None:
292 # If you have any "svn-remote.*" config keys, we think you're using svn.
293 self.is_git_svn = RunGitWithCode(
zimmerle@gmail.com3cdcf562013-04-12 19:39:38 +0000294 ['config', '--local', '--get-regexp', r'^svn-remote\.'])[0] == 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 return self.is_git_svn
296
297 def GetSVNBranch(self):
298 if self.svn_branch is None:
299 if not self.GetIsGitSvn():
300 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
301
302 # Try to figure out which remote branch we're based on.
303 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000304 # 1) iterate through our branch history and find the svn URL.
305 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000306
307 # regexp matching the git-svn line that contains the URL.
308 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
309
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000310 env = os.environ.copy()
311 # 'cat' is a magical git string that disables pagers on all platforms.
312 env['GIT_PAGER'] = 'cat'
313
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000314 # We don't want to go through all of history, so read a line from the
315 # pipe at a time.
316 # The -100 is an arbitrary limit so we don't search forever.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000317 cmd = ['git', 'log', '-100', '--pretty=medium']
318 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE, env=env)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000319 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000320 for line in proc.stdout:
321 match = git_svn_re.match(line)
322 if match:
323 url = match.group(1)
324 proc.stdout.close() # Cut pipe.
325 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000326
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000327 if url:
328 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
329 remotes = RunGit(['config', '--get-regexp',
330 r'^svn-remote\..*\.url']).splitlines()
331 for remote in remotes:
332 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000333 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000334 remote = match.group(1)
335 base_url = match.group(2)
336 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000337 ['config', 'svn-remote.%s.fetch' % remote],
338 error_ok=True).strip()
339 if fetch_spec:
340 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
341 if self.svn_branch:
342 break
343 branch_spec = RunGit(
344 ['config', 'svn-remote.%s.branches' % remote],
345 error_ok=True).strip()
346 if branch_spec:
347 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
348 if self.svn_branch:
349 break
350 tag_spec = RunGit(
351 ['config', 'svn-remote.%s.tags' % remote],
352 error_ok=True).strip()
353 if tag_spec:
354 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
355 if self.svn_branch:
356 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000357
358 if not self.svn_branch:
359 DieWithError('Can\'t guess svn branch -- try specifying it on the '
360 'command line')
361
362 return self.svn_branch
363
364 def GetTreeStatusUrl(self, error_ok=False):
365 if not self.tree_status_url:
366 error_message = ('You must configure your tree status URL by running '
367 '"git cl config".')
368 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
369 error_ok=error_ok,
370 error_message=error_message)
371 return self.tree_status_url
372
373 def GetViewVCUrl(self):
374 if not self.viewvc_url:
ilevy@chromium.orga78f7c02012-11-28 02:06:45 +0000375 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000376 return self.viewvc_url
377
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000378 def GetDefaultCCList(self):
379 return self._GetConfig('rietveld.cc', error_ok=True)
380
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000381 def GetDefaultPrivateFlag(self):
382 return self._GetConfig('rietveld.private', error_ok=True)
383
ukai@chromium.orge8077812012-02-03 03:41:46 +0000384 def GetIsGerrit(self):
385 """Return true if this repo is assosiated with gerrit code review system."""
386 if self.is_gerrit is None:
387 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
388 return self.is_gerrit
389
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000390 def GetGitEditor(self):
391 """Return the editor specified in the git config, or None if none is."""
392 if self.git_editor is None:
393 self.git_editor = self._GetConfig('core.editor', error_ok=True)
394 return self.git_editor or None
395
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000396 def _GetConfig(self, param, **kwargs):
397 self.LazyUpdateIfNeeded()
398 return RunGit(['config', param], **kwargs).strip()
399
400
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000401def ShortBranchName(branch):
402 """Convert a name like 'refs/heads/foo' to just 'foo'."""
403 return branch.replace('refs/heads/', '')
404
405
406class Changelist(object):
407 def __init__(self, branchref=None):
408 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000409 global settings
410 if not settings:
411 # Happens when git_cl.py is used as a utility library.
412 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000413 settings.GetDefaultServerUrl()
414 self.branchref = branchref
415 if self.branchref:
416 self.branch = ShortBranchName(self.branchref)
417 else:
418 self.branch = None
419 self.rietveld_server = None
420 self.upstream_branch = None
421 self.has_issue = False
422 self.issue = None
423 self.has_description = False
424 self.description = None
425 self.has_patchset = False
426 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000427 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000428 self.cc = None
429 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000430 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000431
432 def GetCCList(self):
433 """Return the users cc'd on this CL.
434
435 Return is a string suitable for passing to gcl with the --cc flag.
436 """
437 if self.cc is None:
438 base_cc = settings .GetDefaultCCList()
439 more_cc = ','.join(self.watchers)
440 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
441 return self.cc
442
443 def SetWatchers(self, watchers):
444 """Set the list of email addresses that should be cc'd based on the changed
445 files in this CL.
446 """
447 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000448
449 def GetBranch(self):
450 """Returns the short branch name, e.g. 'master'."""
451 if not self.branch:
452 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
453 self.branch = ShortBranchName(self.branchref)
454 return self.branch
455
456 def GetBranchRef(self):
457 """Returns the full branch name, e.g. 'refs/heads/master'."""
458 self.GetBranch() # Poke the lazy loader.
459 return self.branchref
460
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000461 @staticmethod
462 def FetchUpstreamTuple(branch):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000463 """Returns a tuple containg remote and remote ref,
464 e.g. 'origin', 'refs/heads/master'
465 """
466 remote = '.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000467 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
468 error_ok=True).strip()
469 if upstream_branch:
470 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
471 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000472 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
473 error_ok=True).strip()
474 if upstream_branch:
475 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000476 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000477 # Fall back on trying a git-svn upstream branch.
478 if settings.GetIsGitSvn():
479 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000480 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000481 # Else, try to guess the origin remote.
482 remote_branches = RunGit(['branch', '-r']).split()
483 if 'origin/master' in remote_branches:
484 # Fall back on origin/master if it exits.
485 remote = 'origin'
486 upstream_branch = 'refs/heads/master'
487 elif 'origin/trunk' in remote_branches:
488 # Fall back on origin/trunk if it exists. Generally a shared
489 # git-svn clone
490 remote = 'origin'
491 upstream_branch = 'refs/heads/trunk'
492 else:
493 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494Either pass complete "git diff"-style arguments, like
495 git cl upload origin/master
496or verify this branch is set up to track another (via the --track argument to
497"git checkout -b ...").""")
498
499 return remote, upstream_branch
500
501 def GetUpstreamBranch(self):
502 if self.upstream_branch is None:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000503 remote, upstream_branch = self.FetchUpstreamTuple(self.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000504 if remote is not '.':
505 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
506 self.upstream_branch = upstream_branch
507 return self.upstream_branch
508
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000509 def GetRemoteBranch(self):
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000510 if not self._remote:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000511 remote, branch = None, self.GetBranch()
512 seen_branches = set()
513 while branch not in seen_branches:
514 seen_branches.add(branch)
515 remote, branch = self.FetchUpstreamTuple(branch)
516 branch = ShortBranchName(branch)
517 if remote != '.' or branch.startswith('refs/remotes'):
518 break
519 else:
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000520 remotes = RunGit(['remote'], error_ok=True).split()
521 if len(remotes) == 1:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000522 remote, = remotes
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000523 elif 'origin' in remotes:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000524 remote = 'origin'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000525 logging.warning('Could not determine which remote this change is '
526 'associated with, so defaulting to "%s". This may '
527 'not be what you want. You may prevent this message '
528 'by running "git svn info" as documented here: %s',
529 self._remote,
530 GIT_INSTRUCTIONS_URL)
531 else:
532 logging.warn('Could not determine which remote this change is '
533 'associated with. You may prevent this message by '
534 'running "git svn info" as documented here: %s',
535 GIT_INSTRUCTIONS_URL)
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000536 branch = 'HEAD'
537 if branch.startswith('refs/remotes'):
538 self._remote = (remote, branch)
539 else:
540 self._remote = (remote, 'refs/remotes/%s/%s' % (remote, branch))
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000541 return self._remote
542
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000543 def GitSanityChecks(self, upstream_git_obj):
544 """Checks git repo status and ensures diff is from local commits."""
545
546 # Verify the commit we're diffing against is in our current branch.
547 upstream_sha = RunGit(['rev-parse', '--verify', upstream_git_obj]).strip()
548 common_ancestor = RunGit(['merge-base', upstream_sha, 'HEAD']).strip()
549 if upstream_sha != common_ancestor:
550 print >> sys.stderr, (
551 'ERROR: %s is not in the current branch. You may need to rebase '
552 'your tracking branch' % upstream_sha)
553 return False
554
555 # List the commits inside the diff, and verify they are all local.
556 commits_in_diff = RunGit(
557 ['rev-list', '^%s' % upstream_sha, 'HEAD']).splitlines()
558 code, remote_branch = RunGitWithCode(['config', 'gitcl.remotebranch'])
559 remote_branch = remote_branch.strip()
560 if code != 0:
561 _, remote_branch = self.GetRemoteBranch()
562
563 commits_in_remote = RunGit(
564 ['rev-list', '^%s' % upstream_sha, remote_branch]).splitlines()
565
566 common_commits = set(commits_in_diff) & set(commits_in_remote)
567 if common_commits:
568 print >> sys.stderr, (
569 'ERROR: Your diff contains %d commits already in %s.\n'
570 'Run "git log --oneline %s..HEAD" to get a list of commits in '
571 'the diff. If you are using a custom git flow, you can override'
572 ' the reference used for this check with "git config '
573 'gitcl.remotebranch <git-ref>".' % (
574 len(common_commits), remote_branch, upstream_git_obj))
575 return False
576 return True
577
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000578 def GetGitBaseUrlFromConfig(self):
579 """Return the configured base URL from branch.<branchname>.baseurl.
580
581 Returns None if it is not set.
582 """
583 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
584 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000585
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000586 def GetRemoteUrl(self):
587 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
588
589 Returns None if there is no remote.
590 """
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000591 remote, _ = self.GetRemoteBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000592 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
593
594 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000595 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000596 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000597 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
598 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000599 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600 else:
601 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000602 self.has_issue = True
603 return self.issue
604
605 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000606 if not self.rietveld_server:
607 # If we're on a branch then get the server potentially associated
608 # with that branch.
609 if self.GetIssue():
610 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
611 ['config', self._RietveldServer()], error_ok=True).strip())
612 if not self.rietveld_server:
613 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000614 return self.rietveld_server
615
616 def GetIssueURL(self):
617 """Get the URL for a particular issue."""
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +0000618 if not self.GetIssue():
619 return None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000620 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
621
622 def GetDescription(self, pretty=False):
623 if not self.has_description:
624 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000625 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000626 try:
627 self.description = self.RpcServer().get_description(issue).strip()
628 except urllib2.HTTPError, e:
629 if e.code == 404:
630 DieWithError(
631 ('\nWhile fetching the description for issue %d, received a '
632 '404 (not found)\n'
633 'error. It is likely that you deleted this '
634 'issue on the server. If this is the\n'
635 'case, please run\n\n'
636 ' git cl issue 0\n\n'
637 'to clear the association with the deleted issue. Then run '
638 'this command again.') % issue)
639 else:
640 DieWithError(
641 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000642 self.has_description = True
643 if pretty:
644 wrapper = textwrap.TextWrapper()
645 wrapper.initial_indent = wrapper.subsequent_indent = ' '
646 return wrapper.fill(self.description)
647 return self.description
648
649 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000650 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000651 if not self.has_patchset:
652 patchset = RunGit(['config', self._PatchsetSetting()],
653 error_ok=True).strip()
654 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000655 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 else:
657 self.patchset = None
658 self.has_patchset = True
659 return self.patchset
660
661 def SetPatchset(self, patchset):
662 """Set this branch's patchset. If patchset=0, clears the patchset."""
663 if patchset:
664 RunGit(['config', self._PatchsetSetting(), str(patchset)])
665 else:
666 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000667 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668 self.has_patchset = False
669
binji@chromium.org0281f522012-09-14 13:37:59 +0000670 def GetMostRecentPatchset(self, issue):
671 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000672 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000673
674 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000675 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000676 '/download/issue%s_%s.diff' % (issue, patchset))
677
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000678 def GetApprovingReviewers(self, issue):
679 return get_approving_reviewers(
680 self.RpcServer().get_issue_properties(int(issue), True))
681
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000682 def SetIssue(self, issue):
683 """Set this branch's issue. If issue=0, clears the issue."""
684 if issue:
685 RunGit(['config', self._IssueSetting(), str(issue)])
686 if self.rietveld_server:
687 RunGit(['config', self._RietveldServer(), self.rietveld_server])
688 else:
689 RunGit(['config', '--unset', self._IssueSetting()])
690 self.SetPatchset(0)
691 self.has_issue = False
692
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000693 def GetChange(self, upstream_branch, author):
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +0000694 if not self.GitSanityChecks(upstream_branch):
695 DieWithError('\nGit sanity check failure')
696
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000697 env = os.environ.copy()
698 # 'cat' is a magical git string that disables pagers on all platforms.
699 env['GIT_PAGER'] = 'cat'
700
701 root = RunCommand(['git', 'rev-parse', '--show-cdup'], env=env).strip()
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000702 if not root:
703 root = '.'
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000704 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000705
706 # We use the sha1 of HEAD as a name of this change.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000707 name = RunCommand(['git', 'rev-parse', 'HEAD'], env=env).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000708 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000709 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000710 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000711 except subprocess2.CalledProcessError:
712 DieWithError(
713 ('\nFailed to diff against upstream branch %s!\n\n'
714 'This branch probably doesn\'t exist anymore. To reset the\n'
715 'tracking branch, please run\n'
716 ' git branch --set-upstream %s trunk\n'
717 'replacing trunk with origin/master or the relevant branch') %
718 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000719
maruel@chromium.org52424302012-08-29 15:14:30 +0000720 issue = self.GetIssue()
721 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000722 if issue:
723 description = self.GetDescription()
724 else:
725 # If the change was never uploaded, use the log messages of all commits
726 # up to the branch point, as git cl upload will prefill the description
727 # with these log messages.
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000728 description = RunCommand(['git',
bratell@opera.comf267b0e2013-05-02 09:11:43 +0000729 'log', '--pretty=format:%s%n%n%b',
bratell@opera.com82b91cd2013-07-09 06:33:41 +0000730 '%s...' % (upstream_branch)],
731 env=env).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000732
733 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000734 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000735 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000736 name,
737 description,
738 absroot,
739 files,
740 issue,
741 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000742 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000743
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +0000744 def RunHook(self, committing, may_prompt, verbose, change):
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000745 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000746
747 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000748 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000749 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000750 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000751 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000752 except presubmit_support.PresubmitFailure, e:
753 DieWithError(
754 ('%s\nMaybe your depot_tools is out of date?\n'
755 'If all fails, contact maruel@') % e)
756
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000757 def UpdateDescription(self, description):
758 self.description = description
759 return self.RpcServer().update_description(
760 self.GetIssue(), self.description)
761
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000762 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000763 """Updates the description and closes the issue."""
maruel@chromium.orgb021b322013-04-08 17:57:29 +0000764 return self.RpcServer().close_issue(self.GetIssue())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000765
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000766 def SetFlag(self, flag, value):
767 """Patchset must match."""
768 if not self.GetPatchset():
769 DieWithError('The patchset needs to match. Send another patchset.')
770 try:
771 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000772 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000773 except urllib2.HTTPError, e:
774 if e.code == 404:
775 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
776 if e.code == 403:
777 DieWithError(
778 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
779 'match?') % (self.GetIssue(), self.GetPatchset()))
780 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000781
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000782 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000783 """Returns an upload.RpcServer() to access this review's rietveld instance.
784 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000785 if not self._rpc_server:
maruel@chromium.org4bac4b52012-11-27 20:33:52 +0000786 self._rpc_server = rietveld.CachingRietveld(
787 self.GetRietveldServer(), None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000788 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000789
790 def _IssueSetting(self):
791 """Return the git setting that stores this change's issue."""
792 return 'branch.%s.rietveldissue' % self.GetBranch()
793
794 def _PatchsetSetting(self):
795 """Return the git setting that stores this change's most recent patchset."""
796 return 'branch.%s.rietveldpatchset' % self.GetBranch()
797
798 def _RietveldServer(self):
799 """Returns the git setting that stores this change's rietveld server."""
800 return 'branch.%s.rietveldserver' % self.GetBranch()
801
802
803def GetCodereviewSettingsInteractively():
804 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000805 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000806 server = settings.GetDefaultServerUrl(error_ok=True)
807 prompt = 'Rietveld server (host[:port])'
808 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000809 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000810 if not server and not newserver:
811 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000812 if newserver:
813 newserver = gclient_utils.UpgradeToHttps(newserver)
814 if newserver != server:
815 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000816
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000817 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 prompt = caption
819 if initial:
820 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000821 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000822 if new_val == 'x':
823 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000824 elif new_val:
825 if is_url:
826 new_val = gclient_utils.UpgradeToHttps(new_val)
827 if new_val != initial:
828 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000830 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000831 SetProperty(settings.GetDefaultPrivateFlag(),
832 'Private flag (rietveld only)', 'private', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000833 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000834 'tree-status-url', False)
835 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000836
837 # TODO: configure a default branch to diff against, rather than this
838 # svn-based hackery.
839
840
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000841class ChangeDescription(object):
842 """Contains a parsed form of the change description."""
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000843 R_LINE = r'^[ \t]*(TBR|R)[ \t]*=[ \t]*(.*?)[ \t]*$'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000844
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000845 def __init__(self, description):
846 self._description = (description or '').strip()
847
848 @property
849 def description(self):
850 return self._description
851
852 def update_reviewers(self, reviewers):
853 """Rewrites the R=/TBR= line(s) as a single line."""
854 assert isinstance(reviewers, list), reviewers
855 if not reviewers:
856 return
857 regexp = re.compile(self.R_LINE, re.MULTILINE)
858 matches = list(regexp.finditer(self._description))
859 is_tbr = any(m.group(1) == 'TBR' for m in matches)
860 if len(matches) > 1:
861 # Erase all except the first one.
862 for i in xrange(len(matches) - 1, 0, -1):
863 self._description = (
864 self._description[:matches[i].start()] +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000865 self._description[matches[i].end():])
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000866
867 if is_tbr:
868 new_r_line = 'TBR=' + ', '.join(reviewers)
869 else:
870 new_r_line = 'R=' + ', '.join(reviewers)
871
872 if matches:
873 self._description = (
874 self._description[:matches[0].start()] + new_r_line +
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000875 self._description[matches[0].end():]).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000876 else:
877 self.append_footer(new_r_line)
878
879 def prompt(self):
880 """Asks the user to update the description."""
881 self._description = (
882 '# Enter a description of the change.\n'
janx@chromium.org104b2db2013-04-18 12:58:40 +0000883 '# This will be displayed on the codereview site.\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000884 '# The first line will also be used as the subject of the review.\n'
alancutter@chromium.orgbd1073e2013-06-01 00:34:38 +0000885 '#--------------------This line is 72 characters long'
886 '--------------------\n'
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000887 ) + self._description
888
889 if '\nBUG=' not in self._description:
890 self.append_footer('BUG=')
jbroman@chromium.org615a2622013-05-03 13:20:14 +0000891 content = gclient_utils.RunEditor(self._description, True,
892 git_editor=settings.GetGitEditor())
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000893 if not content:
894 DieWithError('Running editor failed')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000895
896 # Strip off comments.
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000897 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000898 if not content:
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000899 DieWithError('No CL description, aborting')
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000900 self._description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000901
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000902 def append_footer(self, line):
903 # Adds a LF if the last line did not have 'FOO=BAR' or if the new one isn't.
904 if self._description:
905 if '\n' not in self._description:
906 self._description += '\n'
907 else:
908 last_line = self._description.rsplit('\n', 1)[1]
909 if (not presubmit_support.Change.TAG_LINE_RE.match(last_line) or
910 not presubmit_support.Change.TAG_LINE_RE.match(line)):
911 self._description += '\n'
912 self._description += '\n' + line
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000913
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000914 def get_reviewers(self):
915 """Retrieves the list of reviewers."""
916 regexp = re.compile(self.R_LINE, re.MULTILINE)
maruel@chromium.orgc6f60e82013-04-19 17:01:57 +0000917 reviewers = [i.group(2).strip() for i in regexp.finditer(self._description)]
maruel@chromium.org78936cb2013-04-11 00:17:52 +0000918 return cleanup_list(reviewers)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000919
920
maruel@chromium.orge52678e2013-04-26 18:34:44 +0000921def get_approving_reviewers(props):
922 """Retrieves the reviewers that approved a CL from the issue properties with
923 messages.
924
925 Note that the list may contain reviewers that are not committer, thus are not
926 considered by the CQ.
927 """
928 return sorted(
929 set(
930 message['sender']
931 for message in props['messages']
932 if message['approval'] and message['sender'] in props['reviewers']
933 )
934 )
935
936
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937def FindCodereviewSettingsFile(filename='codereview.settings'):
938 """Finds the given file starting in the cwd and going up.
939
940 Only looks up to the top of the repository unless an
941 'inherit-review-settings-ok' file exists in the root of the repository.
942 """
943 inherit_ok_file = 'inherit-review-settings-ok'
944 cwd = os.getcwd()
945 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
946 if os.path.isfile(os.path.join(root, inherit_ok_file)):
947 root = '/'
948 while True:
949 if filename in os.listdir(cwd):
950 if os.path.isfile(os.path.join(cwd, filename)):
951 return open(os.path.join(cwd, filename))
952 if cwd == root:
953 break
954 cwd = os.path.dirname(cwd)
955
956
957def LoadCodereviewSettingsFromFile(fileobj):
958 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000959 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961 def SetProperty(name, setting, unset_error_ok=False):
962 fullname = 'rietveld.' + name
963 if setting in keyvals:
964 RunGit(['config', fullname, keyvals[setting]])
965 else:
966 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
967
968 SetProperty('server', 'CODE_REVIEW_SERVER')
969 # Only server setting is required. Other settings can be absent.
970 # In that case, we ignore errors raised during option deletion attempt.
971 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +0000972 SetProperty('private', 'PRIVATE', unset_error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000973 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
974 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
975
ukai@chromium.orge8077812012-02-03 03:41:46 +0000976 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
977 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
978 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000979
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000980 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
981 #should be of the form
982 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
983 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
984 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
985 keyvals['ORIGIN_URL_CONFIG']])
986
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000988def urlretrieve(source, destination):
989 """urllib is broken for SSL connections via a proxy therefore we
990 can't use urllib.urlretrieve()."""
991 with open(destination, 'w') as f:
992 f.write(urllib2.urlopen(source).read())
993
994
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000995def DownloadHooks(force):
996 """downloads hooks
997
998 Args:
999 force: True to update hooks. False to install hooks if not present.
1000 """
1001 if not settings.GetIsGerrit():
1002 return
1003 server_url = settings.GetDefaultServerUrl()
1004 src = '%s/tools/hooks/commit-msg' % server_url
1005 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
1006 if not os.access(dst, os.X_OK):
1007 if os.path.exists(dst):
1008 if not force:
1009 return
1010 os.remove(dst)
1011 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +00001012 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001013 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1014 except Exception:
1015 if os.path.exists(dst):
1016 os.remove(dst)
1017 DieWithError('\nFailed to download hooks from %s' % src)
1018
1019
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001020@usage('[repo root containing codereview.settings]')
1021def CMDconfig(parser, args):
1022 """edit configuration for this tree"""
1023
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001024 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001025 if len(args) == 0:
1026 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001027 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028 return 0
1029
1030 url = args[0]
1031 if not url.endswith('codereview.settings'):
1032 url = os.path.join(url, 'codereview.settings')
1033
1034 # Load code review settings and download hooks (if available).
1035 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +00001036 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001037 return 0
1038
1039
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001040def CMDbaseurl(parser, args):
1041 """get or set base-url for this branch"""
1042 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
1043 branch = ShortBranchName(branchref)
1044 _, args = parser.parse_args(args)
1045 if not args:
1046 print("Current base-url:")
1047 return RunGit(['config', 'branch.%s.base-url' % branch],
1048 error_ok=False).strip()
1049 else:
1050 print("Setting base-url to %s" % args[0])
1051 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
1052 error_ok=False).strip()
1053
1054
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001055def CMDstatus(parser, args):
1056 """show status of changelists"""
1057 parser.add_option('--field',
1058 help='print only specific field (desc|id|patch|url)')
1059 (options, args) = parser.parse_args(args)
1060
1061 # TODO: maybe make show_branches a flag if necessary.
1062 show_branches = not options.field
1063
1064 if show_branches:
1065 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
1066 if branches:
rch@chromium.org92d67162012-04-02 20:10:35 +00001067 changes = (Changelist(branchref=b) for b in branches.splitlines())
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001068 branches = dict((cl.GetBranch(), cl.GetIssueURL()) for cl in changes)
rch@chromium.org92d67162012-04-02 20:10:35 +00001069 alignment = max(5, max(len(b) for b in branches))
dbeam@chromium.org015fd3d2013-06-18 19:02:50 +00001070 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +00001071 for branch in sorted(branches):
hinoka@google.com55c05c92013-06-26 22:13:31 +00001072 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001073
1074 cl = Changelist()
1075 if options.field:
1076 if options.field.startswith('desc'):
1077 print cl.GetDescription()
1078 elif options.field == 'id':
1079 issueid = cl.GetIssue()
1080 if issueid:
1081 print issueid
1082 elif options.field == 'patch':
1083 patchset = cl.GetPatchset()
1084 if patchset:
1085 print patchset
1086 elif options.field == 'url':
1087 url = cl.GetIssueURL()
1088 if url:
1089 print url
1090 else:
1091 print
1092 print 'Current branch:',
1093 if not cl.GetIssue():
1094 print 'no issue assigned.'
1095 return 0
1096 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +00001097 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098 print 'Issue description:'
1099 print cl.GetDescription(pretty=True)
1100 return 0
1101
1102
1103@usage('[issue_number]')
1104def CMDissue(parser, args):
1105 """Set or display the current code review issue number.
1106
1107 Pass issue number 0 to clear the current issue.
1108"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001109 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001110
1111 cl = Changelist()
1112 if len(args) > 0:
1113 try:
1114 issue = int(args[0])
1115 except ValueError:
1116 DieWithError('Pass a number to set the issue or none to list it.\n'
1117 'Maybe you want to run git cl status?')
1118 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +00001119 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 return 0
1121
1122
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001123def CMDcomments(parser, args):
1124 """show review comments of the current changelist"""
1125 (_, args) = parser.parse_args(args)
1126 if args:
1127 parser.error('Unsupported argument: %s' % args)
1128
1129 cl = Changelist()
1130 if cl.GetIssue():
1131 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
1132 for message in sorted(data['messages'], key=lambda x: x['date']):
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00001133 if message['disapproval']:
1134 color = Fore.RED
1135 elif message['approval']:
1136 color = Fore.GREEN
1137 elif message['sender'] == data['owner_email']:
1138 color = Fore.MAGENTA
1139 else:
1140 color = Fore.BLUE
1141 print '\n%s%s %s%s' % (
1142 color, message['date'].split('.', 1)[0], message['sender'],
1143 Fore.RESET)
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001144 if message['text'].strip():
1145 print '\n'.join(' ' + l for l in message['text'].splitlines())
1146 return 0
1147
1148
rsesek@chromium.orgeec76592013-05-20 16:27:57 +00001149def CMDdescription(parser, args):
1150 """brings up the editor for the current CL's description."""
1151 cl = Changelist()
1152 if not cl.GetIssue():
1153 DieWithError('This branch has no associated changelist.')
1154 description = ChangeDescription(cl.GetDescription())
1155 description.prompt()
1156 cl.UpdateDescription(description.description)
1157 return 0
1158
1159
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001160def CreateDescriptionFromLog(args):
1161 """Pulls out the commit log to use as a base for the CL description."""
1162 log_args = []
1163 if len(args) == 1 and not args[0].endswith('.'):
1164 log_args = [args[0] + '..']
1165 elif len(args) == 1 and args[0].endswith('...'):
1166 log_args = [args[0][:-1]]
1167 elif len(args) == 2:
1168 log_args = [args[0] + '..' + args[1]]
1169 else:
1170 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001171 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001172
1173
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174def CMDpresubmit(parser, args):
1175 """run presubmit tests on the current changelist"""
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001176 parser.add_option('-u', '--upload', action='store_true',
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001177 help='Run upload hook instead of the push/dcommit hook')
ilevy@chromium.org375a9022013-01-07 01:12:05 +00001178 parser.add_option('-f', '--force', action='store_true',
sbc@chromium.org495ad152012-09-04 23:07:42 +00001179 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001180 (options, args) = parser.parse_args(args)
1181
ukai@chromium.org259e4682012-10-25 07:36:33 +00001182 if not options.force and is_dirty_git_tree('presubmit'):
1183 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 return 1
1185
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001186 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001187 if args:
1188 base_branch = args[0]
1189 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001190 # Default to diffing against the common ancestor of the upstream branch.
1191 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001193 cl.RunHook(
1194 committing=not options.upload,
1195 may_prompt=False,
1196 verbose=options.verbose,
1197 change=cl.GetChange(base_branch, None))
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001198 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001199
1200
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001201def AddChangeIdToCommitMessage(options, args):
1202 """Re-commits using the current message, assumes the commit hook is in
1203 place.
1204 """
1205 log_desc = options.message or CreateDescriptionFromLog(args)
1206 git_command = ['commit', '--amend', '-m', log_desc]
1207 RunGit(git_command)
1208 new_log_desc = CreateDescriptionFromLog(args)
1209 if CHANGE_ID in new_log_desc:
1210 print 'git-cl: Added Change-Id to commit message.'
1211 else:
1212 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1213
1214
ukai@chromium.orge8077812012-02-03 03:41:46 +00001215def GerritUpload(options, args, cl):
1216 """upload the current branch to gerrit."""
1217 # We assume the remote called "origin" is the one we want.
1218 # It is probably not worthwhile to support different workflows.
1219 remote = 'origin'
1220 branch = 'master'
1221 if options.target_branch:
1222 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001223
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001224 change_desc = ChangeDescription(
1225 options.message or CreateDescriptionFromLog(args))
1226 if not change_desc.description:
ukai@chromium.orge8077812012-02-03 03:41:46 +00001227 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228 return 1
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001229 if CHANGE_ID not in change_desc.description:
1230 AddChangeIdToCommitMessage(options, args)
1231 if options.reviewers:
1232 change_desc.update_reviewers(options.reviewers)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233
ukai@chromium.orge8077812012-02-03 03:41:46 +00001234 receive_options = []
1235 cc = cl.GetCCList().split(',')
1236 if options.cc:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001237 cc.extend(options.cc)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001238 cc = filter(None, cc)
1239 if cc:
1240 receive_options += ['--cc=' + email for email in cc]
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001241 if change_desc.get_reviewers():
1242 receive_options.extend(
1243 '--reviewer=' + email for email in change_desc.get_reviewers())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001244
ukai@chromium.orge8077812012-02-03 03:41:46 +00001245 git_command = ['push']
1246 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001247 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001248 ' '.join(receive_options))
1249 git_command += [remote, 'HEAD:refs/for/' + branch]
1250 RunGit(git_command)
1251 # TODO(ukai): parse Change-Id: and set issue number?
1252 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001253
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001254
ukai@chromium.orge8077812012-02-03 03:41:46 +00001255def RietveldUpload(options, args, cl):
1256 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1258 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 if options.emulate_svn_auto_props:
1260 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261
1262 change_desc = None
1263
1264 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001265 if options.title:
1266 upload_args.extend(['--title', options.title])
rogerta@chromium.orgafadfca2013-05-29 14:15:53 +00001267 if options.message:
1268 upload_args.extend(['--message', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001269 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 print ("This branch is associated with issue %s. "
1271 "Adding patch to that issue." % cl.GetIssue())
1272 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001273 if options.title:
1274 upload_args.extend(['--title', options.title])
rogerta@chromium.org43e34f02013-03-25 14:52:48 +00001275 message = options.title or options.message or CreateDescriptionFromLog(args)
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001276 change_desc = ChangeDescription(message)
1277 if options.reviewers:
1278 change_desc.update_reviewers(options.reviewers)
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001279 if not options.force:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001280 change_desc.prompt()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001281
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001282 if not change_desc.description:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001283 print "Description is empty; aborting."
1284 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001285
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001286 upload_args.extend(['--message', change_desc.description])
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001287 if change_desc.get_reviewers():
1288 upload_args.append('--reviewers=' + ','.join(change_desc.get_reviewers()))
maruel@chromium.orga3353652011-11-30 14:26:57 +00001289 if options.send_mail:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001290 if not change_desc.get_reviewers():
maruel@chromium.orga3353652011-11-30 14:26:57 +00001291 DieWithError("Must specify reviewers to send email.")
1292 upload_args.append('--send_mail')
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001293 cc = ','.join(filter(None, (cl.GetCCList(), ','.join(options.cc))))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001294 if cc:
1295 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001297 if options.private or settings.GetDefaultPrivateFlag() == "True":
1298 upload_args.append('--private')
1299
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001300 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001301 if not options.find_copies:
1302 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001303
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304 # Include the upstream repo's URL in the change -- this is useful for
1305 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001306 remote_url = cl.GetGitBaseUrlFromConfig()
1307 if not remote_url:
1308 if settings.GetIsGitSvn():
1309 # URL is dependent on the current directory.
1310 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1311 if data:
1312 keys = dict(line.split(': ', 1) for line in data.splitlines()
1313 if ': ' in line)
1314 remote_url = keys.get('URL', None)
1315 else:
1316 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1317 remote_url = (cl.GetRemoteUrl() + '@'
1318 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001319 if remote_url:
1320 upload_args.extend(['--base_url', remote_url])
1321
1322 try:
ilevy@chromium.org82880192012-11-26 15:41:57 +00001323 upload_args = ['upload'] + upload_args + args
1324 logging.info('upload.RealMain(%s)', upload_args)
1325 issue, patchset = upload.RealMain(upload_args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001326 except KeyboardInterrupt:
1327 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001328 except:
1329 # If we got an exception after the user typed a description for their
1330 # change, back up the description before re-raising.
1331 if change_desc:
1332 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1333 print '\nGot exception while uploading -- saving description to %s\n' \
1334 % backup_path
1335 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001336 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337 backup_file.close()
1338 raise
1339
1340 if not cl.GetIssue():
1341 cl.SetIssue(issue)
1342 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001343
1344 if options.use_commit_queue:
1345 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346 return 0
1347
1348
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001349def cleanup_list(l):
1350 """Fixes a list so that comma separated items are put as individual items.
1351
1352 So that "--reviewers joe@c,john@c --reviewers joa@c" results in
1353 options.reviewers == sorted(['joe@c', 'john@c', 'joa@c']).
1354 """
1355 items = sum((i.split(',') for i in l), [])
1356 stripped_items = (i.strip() for i in items)
1357 return sorted(filter(None, stripped_items))
1358
1359
ukai@chromium.orge8077812012-02-03 03:41:46 +00001360@usage('[args to "git diff"]')
1361def CMDupload(parser, args):
1362 """upload the current changelist to codereview"""
1363 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1364 help='bypass upload presubmit hook')
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001365 parser.add_option('--bypass-watchlists', action='store_true',
1366 dest='bypass_watchlists',
1367 help='bypass watchlists auto CC-ing reviewers')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001368 parser.add_option('-f', action='store_true', dest='force',
1369 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001370 parser.add_option('-m', dest='message', help='message for patchset')
1371 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001372 parser.add_option('-r', '--reviewers',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001373 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001374 help='reviewer email addresses')
1375 parser.add_option('--cc',
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001376 action='append', default=[],
ukai@chromium.orge8077812012-02-03 03:41:46 +00001377 help='cc email addresses')
adamk@chromium.org36f47302013-04-05 01:08:31 +00001378 parser.add_option('-s', '--send-mail', action='store_true',
ukai@chromium.orge8077812012-02-03 03:41:46 +00001379 help='send email to reviewer immediately')
1380 parser.add_option("--emulate_svn_auto_props", action="store_true",
1381 dest="emulate_svn_auto_props",
1382 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001383 parser.add_option('-c', '--use-commit-queue', action='store_true',
1384 help='tell the commit queue to commit this patchset')
tyoshino@chromium.orgc1737d02013-05-29 14:17:28 +00001385 parser.add_option('--private', action='store_true',
1386 help='set the review private (rietveld only)')
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001387 parser.add_option('--target_branch',
1388 help='When uploading to gerrit, remote branch to '
1389 'use for CL. Default: master')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001390 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001391 (options, args) = parser.parse_args(args)
1392
ukai@chromium.org8ef7ab22012-11-28 04:24:52 +00001393 if options.target_branch and not settings.GetIsGerrit():
1394 parser.error('Use --target_branch for non gerrit repository.')
1395
ukai@chromium.org259e4682012-10-25 07:36:33 +00001396 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001397 return 1
1398
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001399 options.reviewers = cleanup_list(options.reviewers)
1400 options.cc = cleanup_list(options.cc)
1401
ukai@chromium.orge8077812012-02-03 03:41:46 +00001402 cl = Changelist()
1403 if args:
1404 # TODO(ukai): is it ok for gerrit case?
1405 base_branch = args[0]
1406 else:
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001407 # Default to diffing against common ancestor of upstream branch
1408 base_branch = RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip()
sbc@chromium.org5e07e062013-02-28 23:55:44 +00001409 args = [base_branch, 'HEAD']
ukai@chromium.orge8077812012-02-03 03:41:46 +00001410
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001411 # Apply watchlists on upload.
1412 change = cl.GetChange(base_branch, None)
1413 watchlist = watchlists.Watchlists(change.RepositoryRoot())
1414 files = [f.LocalPath() for f in change.AffectedFiles()]
brettw@chromium.orgb65c43c2013-06-10 22:04:49 +00001415 if not options.bypass_watchlists:
1416 cl.SetWatchers(watchlist.GetWatchersForPaths(files))
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001417
ukai@chromium.orge8077812012-02-03 03:41:46 +00001418 if not options.bypass_hooks:
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001419 hook_results = cl.RunHook(committing=False,
ukai@chromium.orge8077812012-02-03 03:41:46 +00001420 may_prompt=not options.force,
1421 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001422 change=change)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001423 if not hook_results.should_continue():
1424 return 1
1425 if not options.reviewers and hook_results.reviewers:
maruel@chromium.orgeb52a5c2013-04-10 23:17:09 +00001426 options.reviewers = hook_results.reviewers.split(',')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001427
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001428 if cl.GetIssue():
1429 latest_patchset = cl.GetMostRecentPatchset(cl.GetIssue())
1430 local_patchset = cl.GetPatchset()
dmikurube@chromium.org07d149f2013-04-03 11:40:23 +00001431 if latest_patchset and local_patchset and local_patchset != latest_patchset:
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001432 print ('The last upload made from this repository was patchset #%d but '
1433 'the most recent patchset on the server is #%d.'
1434 % (local_patchset, latest_patchset))
koz@chromium.orgc7192782013-04-09 23:28:46 +00001435 print ('Uploading will still work, but if you\'ve uploaded to this issue '
1436 'from another machine or branch the patch you\'re uploading now '
1437 'might not include those changes.')
koz@chromium.org5974d7a2013-04-02 20:50:37 +00001438 ask_for_data('About to upload; enter to confirm.')
1439
iannucci@chromium.org79540052012-10-19 23:15:26 +00001440 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001441 if settings.GetIsGerrit():
1442 return GerritUpload(options, args, cl)
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001443 ret = RietveldUpload(options, args, cl)
1444 if not ret:
rogerta@chromium.org4a6cd042013-04-12 15:40:42 +00001445 git_set_branch_value('last-upload-hash',
1446 RunGit(['rev-parse', 'HEAD']).strip())
rogerta@chromium.orgcaa16552013-03-18 20:45:05 +00001447
1448 return ret
ukai@chromium.orge8077812012-02-03 03:41:46 +00001449
1450
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001451def IsSubmoduleMergeCommit(ref):
1452 # When submodules are added to the repo, we expect there to be a single
1453 # non-git-svn merge commit at remote HEAD with a signature comment.
1454 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001455 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001456 return RunGit(cmd) != ''
1457
1458
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001459def SendUpstream(parser, args, cmd):
1460 """Common code for CmdPush and CmdDCommit
1461
1462 Squashed commit into a single.
1463 Updates changelog with metadata (e.g. pointer to review).
1464 Pushes/dcommits the code upstream.
1465 Updates review and closes.
1466 """
1467 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1468 help='bypass upload presubmit hook')
1469 parser.add_option('-m', dest='message',
1470 help="override review description")
1471 parser.add_option('-f', action='store_true', dest='force',
1472 help="force yes to questions (don't prompt)")
1473 parser.add_option('-c', dest='contributor',
1474 help="external contributor for patch (appended to " +
1475 "description and used as author for git). Should be " +
1476 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001477 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478 (options, args) = parser.parse_args(args)
1479 cl = Changelist()
1480
1481 if not args or cmd == 'push':
1482 # Default to merging against our best guess of the upstream branch.
1483 args = [cl.GetUpstreamBranch()]
1484
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001485 if options.contributor:
1486 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1487 print "Please provide contibutor as 'First Last <email@example.com>'"
1488 return 1
1489
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001491 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492
ukai@chromium.org259e4682012-10-25 07:36:33 +00001493 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001494 return 1
1495
1496 # This rev-list syntax means "show all commits not in my branch that
1497 # are in base_branch".
1498 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1499 base_branch]).splitlines()
1500 if upstream_commits:
1501 print ('Base branch "%s" has %d commits '
1502 'not in this branch.' % (base_branch, len(upstream_commits)))
1503 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1504 return 1
1505
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001506 # This is the revision `svn dcommit` will commit on top of.
1507 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1508 '--pretty=format:%H'])
1509
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001511 # If the base_head is a submodule merge commit, the first parent of the
1512 # base_head should be a git-svn commit, which is what we're interested in.
1513 base_svn_head = base_branch
1514 if base_has_submodules:
1515 base_svn_head += '^1'
1516
1517 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001518 if extra_commits:
1519 print ('This branch has %d additional commits not upstreamed yet.'
1520 % len(extra_commits.splitlines()))
1521 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1522 'before attempting to %s.' % (base_branch, cmd))
1523 return 1
1524
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001525 base_branch = RunGit(['merge-base', base_branch, 'HEAD']).strip()
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001526 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001527 author = None
1528 if options.contributor:
1529 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001530 hook_results = cl.RunHook(
1531 committing=True,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001532 may_prompt=not options.force,
1533 verbose=options.verbose,
ilevy@chromium.org051ad0e2013-03-04 21:57:34 +00001534 change=cl.GetChange(base_branch, author))
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001535 if not hook_results.should_continue():
1536 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537
1538 if cmd == 'dcommit':
1539 # Check the tree status if the tree status URL is set.
1540 status = GetTreeStatus()
1541 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001542 print('The tree is closed. Please wait for it to reopen. Use '
1543 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001544 return 1
1545 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001546 print('Unable to determine tree status. Please verify manually and '
1547 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001548 else:
1549 breakpad.SendStack(
1550 'GitClHooksBypassedCommit',
1551 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001552 (cl.GetRietveldServer(), cl.GetIssue()),
1553 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001554
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001555 change_desc = ChangeDescription(options.message)
1556 if not change_desc.description and cl.GetIssue():
1557 change_desc = ChangeDescription(cl.GetDescription())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001558
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001559 if not change_desc.description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001560 if not cl.GetIssue() and options.bypass_hooks:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001561 change_desc = ChangeDescription(CreateDescriptionFromLog([base_branch]))
erg@chromium.org1a173982012-08-29 20:43:05 +00001562 else:
1563 print 'No description set.'
1564 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1565 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001566
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001567 # Keep a separate copy for the commit message, because the commit message
1568 # contains the link to the Rietveld issue, while the Rietveld message contains
1569 # the commit viewvc url.
maruel@chromium.orge52678e2013-04-26 18:34:44 +00001570 # Keep a separate copy for the commit message.
1571 if cl.GetIssue():
1572 change_desc.update_reviewers(cl.GetApprovingReviewers(cl.GetIssue()))
1573
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001574 commit_desc = ChangeDescription(change_desc.description)
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001575 if cl.GetIssue():
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001576 commit_desc.append_footer('Review URL: %s' % cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001577 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001578 commit_desc.append_footer('Patch from %s.' % options.contributor)
1579
1580 print 'Description:', repr(commit_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001581
1582 branches = [base_branch, cl.GetBranchRef()]
1583 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001584 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001585 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001586
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001587 # We want to squash all this branch's commits into one commit with the proper
1588 # description. We do this by doing a "reset --soft" to the base branch (which
1589 # keeps the working copy the same), then dcommitting that. If origin/master
1590 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1591 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001592 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001593 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1594 # Delete the branches if they exist.
1595 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1596 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1597 result = RunGitWithCode(showref_cmd)
1598 if result[0] == 0:
1599 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001600
1601 # We might be in a directory that's present in this branch but not in the
1602 # trunk. Move up to the top of the tree so that git commands that expect a
1603 # valid CWD won't fail after we check out the merge branch.
1604 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1605 if rel_base_path:
1606 os.chdir(rel_base_path)
1607
1608 # Stuff our change into the merge branch.
1609 # We wrap in a try...finally block so if anything goes wrong,
1610 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001611 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001612 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001613 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1614 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001615 if options.contributor:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001616 RunGit(
1617 [
1618 'commit', '--author', options.contributor,
1619 '-m', commit_desc.description,
1620 ])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001621 else:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001622 RunGit(['commit', '-m', commit_desc.description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001623 if base_has_submodules:
1624 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1625 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1626 RunGit(['checkout', CHERRY_PICK_BRANCH])
1627 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001628 if cmd == 'push':
1629 # push the merge branch.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001630 remote, branch = cl.FetchUpstreamTuple(cl.GetBranch())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001631 retcode, output = RunGitWithCode(
1632 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1633 logging.debug(output)
1634 else:
1635 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001636 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001637 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001638 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001639 finally:
1640 # And then swap back to the original branch and clean up.
1641 RunGit(['checkout', '-q', cl.GetBranch()])
1642 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001643 if base_has_submodules:
1644 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001645
1646 if cl.GetIssue():
1647 if cmd == 'dcommit' and 'Committed r' in output:
1648 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1649 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001650 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1651 for l in output.splitlines(False))
1652 match = filter(None, match)
1653 if len(match) != 1:
1654 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1655 output)
1656 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001657 else:
1658 return 1
1659 viewvc_url = settings.GetViewVCUrl()
1660 if viewvc_url and revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001661 change_desc.append_footer('Committed: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001662 elif revision:
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001663 change_desc.append_footer('Committed: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001664 print ('Closing issue '
1665 '(you may be prompted for your codereview password)...')
maruel@chromium.org78936cb2013-04-11 00:17:52 +00001666 cl.UpdateDescription(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001667 cl.CloseIssue()
iannucci@chromium.org16b51402013-02-17 05:33:36 +00001668 props = cl.RpcServer().get_issue_properties(cl.GetIssue(), False)
sadrul@chromium.org34b5d822013-02-18 01:39:24 +00001669 patch_num = len(props['patchsets'])
iannucci@chromium.org25a4ab42013-02-15 23:22:05 +00001670 comment = "Committed patchset #%d manually as r%s" % (patch_num, revision)
iannucci@chromium.orgb85a3162013-01-26 01:11:13 +00001671 comment += ' (presubmit successful).' if not options.bypass_hooks else '.'
1672 cl.RpcServer().add_comment(cl.GetIssue(), comment)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001673 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001674
1675 if retcode == 0:
1676 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1677 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001678 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001679
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001680 return 0
1681
1682
1683@usage('[upstream branch to apply against]')
1684def CMDdcommit(parser, args):
1685 """commit the current changelist via git-svn"""
1686 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001687 message = """This doesn't appear to be an SVN repository.
1688If your project has a git mirror with an upstream SVN master, you probably need
1689to run 'git svn init', see your project's git mirror documentation.
1690If your project has a true writeable upstream repository, you probably want
1691to run 'git cl push' instead.
1692Choose wisely, if you get this wrong, your commit might appear to succeed but
1693will instead be silently ignored."""
1694 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001695 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001696 return SendUpstream(parser, args, 'dcommit')
1697
1698
1699@usage('[upstream branch to apply against]')
1700def CMDpush(parser, args):
1701 """commit the current changelist via git"""
1702 if settings.GetIsGitSvn():
1703 print('This appears to be an SVN repository.')
1704 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001705 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001706 return SendUpstream(parser, args, 'push')
1707
1708
1709@usage('<patch url or issue id>')
1710def CMDpatch(parser, args):
1711 """patch in a code review"""
1712 parser.add_option('-b', dest='newbranch',
1713 help='create a new branch off trunk for the patch')
1714 parser.add_option('-f', action='store_true', dest='force',
1715 help='with -b, clobber any existing branch')
1716 parser.add_option('--reject', action='store_true', dest='reject',
tapted@chromium.org0bdc2652013-06-07 23:47:05 +00001717 help='allow failed patches and spew .rej files')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1719 help="don't commit after patch applies")
1720 (options, args) = parser.parse_args(args)
1721 if len(args) != 1:
1722 parser.print_help()
1723 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001724 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001726 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001727 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001728
maruel@chromium.org52424302012-08-29 15:14:30 +00001729 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001730 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001731 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001732 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001733 patchset = cl.GetMostRecentPatchset(issue)
1734 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001736 # Assume it's a URL to the patch. Default to https.
1737 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001738 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001739 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001740 DieWithError('Must pass an issue ID or full URL for '
1741 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001742 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001743 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001744 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001745
1746 if options.newbranch:
1747 if options.force:
1748 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001749 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001750 RunGit(['checkout', '-b', options.newbranch,
1751 Changelist().GetUpstreamBranch()])
1752
1753 # Switch up to the top-level directory, if necessary, in preparation for
1754 # applying the patch.
1755 top = RunGit(['rev-parse', '--show-cdup']).strip()
1756 if top:
1757 os.chdir(top)
1758
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001759 # Git patches have a/ at the beginning of source paths. We strip that out
1760 # with a sed script rather than the -p flag to patch so we can feed either
1761 # Git or svn-style patches into the same apply command.
1762 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001763 try:
1764 patch_data = subprocess2.check_output(
1765 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1766 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001767 DieWithError('Git patch mungling failed.')
1768 logging.info(patch_data)
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001769 env = os.environ.copy()
1770 # 'cat' is a magical git string that disables pagers on all platforms.
1771 env['GIT_PAGER'] = 'cat'
1772
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001773 # We use "git apply" to apply the patch instead of "patch" so that we can
1774 # pick up file adds.
1775 # The --index flag means: also insert into the index (so we catch adds).
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001776 cmd = ['git', 'apply', '--index', '-p0']
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001777 if options.reject:
1778 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001779 try:
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001780 subprocess2.check_call(cmd, env=env,
1781 stdin=patch_data, stdout=subprocess2.VOID)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001782 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001783 DieWithError('Failed to apply the patch')
1784
1785 # If we had an issue, commit the current state and register the issue.
1786 if not options.nocommit:
1787 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1788 cl = Changelist()
1789 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001790 cl.SetPatchset(patchset)
pdr@chromium.org98ca6622013-04-09 20:58:40 +00001791 print "Committed patch locally."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001792 else:
1793 print "Patch applied to index."
1794 return 0
1795
1796
1797def CMDrebase(parser, args):
1798 """rebase current branch on top of svn repo"""
1799 # Provide a wrapper for git svn rebase to help avoid accidental
1800 # git svn dcommit.
1801 # It's the only command that doesn't use parser at all since we just defer
1802 # execution to git-svn.
bratell@opera.com82b91cd2013-07-09 06:33:41 +00001803 env = os.environ.copy()
1804 # 'cat' is a magical git string that disables pagers on all platforms.
1805 env['GIT_PAGER'] = 'cat'
1806
1807 return subprocess2.call(['git', 'svn', 'rebase'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001808
1809
1810def GetTreeStatus():
1811 """Fetches the tree status and returns either 'open', 'closed',
1812 'unknown' or 'unset'."""
1813 url = settings.GetTreeStatusUrl(error_ok=True)
1814 if url:
1815 status = urllib2.urlopen(url).read().lower()
1816 if status.find('closed') != -1 or status == '0':
1817 return 'closed'
1818 elif status.find('open') != -1 or status == '1':
1819 return 'open'
1820 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001821 return 'unset'
1822
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001823
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001824def GetTreeStatusReason():
1825 """Fetches the tree status from a json url and returns the message
1826 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001827 url = settings.GetTreeStatusUrl()
1828 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001829 connection = urllib2.urlopen(json_url)
1830 status = json.loads(connection.read())
1831 connection.close()
1832 return status['message']
1833
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001834
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001835def CMDtree(parser, args):
1836 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001837 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001838 status = GetTreeStatus()
1839 if 'unset' == status:
1840 print 'You must configure your tree status URL by running "git cl config".'
1841 return 2
1842
1843 print "The tree is %s" % status
1844 print
1845 print GetTreeStatusReason()
1846 if status != 'open':
1847 return 1
1848 return 0
1849
1850
maruel@chromium.org15192402012-09-06 12:38:29 +00001851def CMDtry(parser, args):
1852 """Triggers a try job through Rietveld."""
1853 group = optparse.OptionGroup(parser, "Try job options")
1854 group.add_option(
1855 "-b", "--bot", action="append",
1856 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1857 "times to specify multiple builders. ex: "
1858 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1859 "the try server waterfall for the builders name and the tests "
1860 "available. Can also be used to specify gtest_filter, e.g. "
1861 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1862 group.add_option(
1863 "-r", "--revision",
1864 help="Revision to use for the try job; default: the "
1865 "revision will be determined by the try server; see "
1866 "its waterfall for more info")
1867 group.add_option(
1868 "-c", "--clobber", action="store_true", default=False,
1869 help="Force a clobber before building; e.g. don't do an "
1870 "incremental build")
1871 group.add_option(
1872 "--project",
1873 help="Override which project to use. Projects are defined "
1874 "server-side to define what default bot set to use")
1875 group.add_option(
1876 "-t", "--testfilter", action="append", default=[],
1877 help=("Apply a testfilter to all the selected builders. Unless the "
1878 "builders configurations are similar, use multiple "
1879 "--bot <builder>:<test> arguments."))
1880 group.add_option(
1881 "-n", "--name", help="Try job name; default to current branch name")
1882 parser.add_option_group(group)
1883 options, args = parser.parse_args(args)
1884
1885 if args:
1886 parser.error('Unknown arguments: %s' % args)
1887
1888 cl = Changelist()
1889 if not cl.GetIssue():
1890 parser.error('Need to upload first')
1891
1892 if not options.name:
1893 options.name = cl.GetBranch()
1894
1895 # Process --bot and --testfilter.
1896 if not options.bot:
1897 # Get try slaves from PRESUBMIT.py files if not specified.
ilevy@chromium.org0f58fa82012-11-05 01:45:20 +00001898 change = cl.GetChange(
1899 RunGit(['merge-base', cl.GetUpstreamBranch(), 'HEAD']).strip(),
1900 None)
maruel@chromium.org15192402012-09-06 12:38:29 +00001901 options.bot = presubmit_support.DoGetTrySlaves(
1902 change,
1903 change.LocalPaths(),
1904 settings.GetRoot(),
1905 None,
1906 None,
1907 options.verbose,
1908 sys.stdout)
1909 if not options.bot:
1910 parser.error('No default try builder to try, use --bot')
1911
1912 builders_and_tests = {}
1913 for bot in options.bot:
1914 if ':' in bot:
1915 builder, tests = bot.split(':', 1)
1916 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1917 elif ',' in bot:
1918 parser.error('Specify one bot per --bot flag')
1919 else:
1920 builders_and_tests.setdefault(bot, []).append('defaulttests')
1921
1922 if options.testfilter:
1923 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1924 builders_and_tests = dict(
1925 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1926 if t != ['compile'])
1927
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001928 if any('triggered' in b for b in builders_and_tests):
1929 print >> sys.stderr, (
1930 'ERROR You are trying to send a job to a triggered bot. This type of'
1931 ' bot requires an\ninitial job from a parent (usually a builder). '
1932 'Instead send your job to the parent.\n'
1933 'Bot list: %s' % builders_and_tests)
1934 return 1
1935
maruel@chromium.org15192402012-09-06 12:38:29 +00001936 patchset = cl.GetPatchset()
1937 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001938 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001939
1940 cl.RpcServer().trigger_try_jobs(
1941 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1942 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001943 print('Tried jobs on:')
1944 length = max(len(builder) for builder in builders_and_tests)
1945 for builder in sorted(builders_and_tests):
1946 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001947 return 0
1948
1949
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001950@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001951def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001952 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001953 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001954 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001955 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001956 return 0
1957
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001958 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001959 if args:
1960 # One arg means set upstream branch.
1961 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1962 cl = Changelist()
1963 print "Upstream branch set to " + cl.GetUpstreamBranch()
1964 else:
1965 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001966 return 0
1967
1968
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001969def CMDset_commit(parser, args):
1970 """set the commit bit"""
1971 _, args = parser.parse_args(args)
1972 if args:
1973 parser.error('Unrecognized args: %s' % ' '.join(args))
1974 cl = Changelist()
1975 cl.SetFlag('commit', '1')
1976 return 0
1977
1978
groby@chromium.org411034a2013-02-26 15:12:01 +00001979def CMDset_close(parser, args):
1980 """close the issue"""
1981 _, args = parser.parse_args(args)
1982 if args:
1983 parser.error('Unrecognized args: %s' % ' '.join(args))
1984 cl = Changelist()
1985 # Ensure there actually is an issue to close.
1986 cl.GetDescription()
1987 cl.CloseIssue()
1988 return 0
1989
1990
agable@chromium.orgfab8f822013-05-06 17:43:09 +00001991def CMDformat(parser, args):
1992 """run clang-format on the diff"""
1993 CLANG_EXTS = ['.cc', '.cpp', '.h']
1994 parser.add_option('--full', action='store_true', default=False)
1995 opts, args = parser.parse_args(args)
1996 if args:
1997 parser.error('Unrecognized args: %s' % ' '.join(args))
1998
digit@chromium.org29e47272013-05-17 17:01:46 +00001999 # Generate diff for the current branch's changes.
enne@chromium.org90d30c62013-05-29 16:09:49 +00002000 diff_cmd = ['diff', '--no-ext-diff', '--no-prefix']
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002001 if opts.full:
digit@chromium.org29e47272013-05-17 17:01:46 +00002002 # Only list the names of modified files.
2003 diff_cmd.append('--name-only')
2004 else:
2005 # Only generate context-less patches.
2006 diff_cmd.append('-U0')
2007
2008 # Grab the merge-base commit, i.e. the upstream commit of the current
2009 # branch when it was created or the last time it was rebased. This is
2010 # to cover the case where the user may have called "git fetch origin",
2011 # moving the origin branch to a newer commit, but hasn't rebased yet.
2012 upstream_commit = None
2013 cl = Changelist()
2014 upstream_branch = cl.GetUpstreamBranch()
2015 if upstream_branch:
2016 upstream_commit = RunGit(['merge-base', 'HEAD', upstream_branch])
2017 upstream_commit = upstream_commit.strip()
2018
2019 if not upstream_commit:
2020 DieWithError('Could not find base commit for this branch. '
2021 'Are you in detached state?')
2022
2023 diff_cmd.append(upstream_commit)
2024
2025 # Handle source file filtering.
2026 diff_cmd.append('--')
2027 diff_cmd += ['*' + ext for ext in CLANG_EXTS]
2028 diff_output = RunGit(diff_cmd)
2029
2030 if opts.full:
2031 # diff_output is a list of files to send to clang-format.
2032 files = diff_output.splitlines()
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002033 if not files:
2034 print "Nothing to format."
2035 return 0
digit@chromium.org29e47272013-05-17 17:01:46 +00002036 RunCommand(['clang-format', '-i', '-style', 'Chromium'] + files)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002037 else:
digit@chromium.org29e47272013-05-17 17:01:46 +00002038 # diff_output is a patch to send to clang-format-diff.py
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002039 cfd_path = os.path.join('/usr', 'lib', 'clang-format',
2040 'clang-format-diff.py')
2041 if not os.path.exists(cfd_path):
digit@chromium.org29e47272013-05-17 17:01:46 +00002042 DieWithError('Could not find clang-format-diff at %s.' % cfd_path)
2043 cmd = [sys.executable, cfd_path, '-style', 'Chromium']
2044 RunCommand(cmd, stdin=diff_output)
agable@chromium.orgfab8f822013-05-06 17:43:09 +00002045
2046 return 0
2047
2048
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002049### Glue code for subcommand handling.
2050
2051
2052def Commands():
2053 """Returns a dict of command and their handling function."""
2054 module = sys.modules[__name__]
2055 cmds = (fn[3:] for fn in dir(module) if fn.startswith('CMD'))
2056 return dict((cmd, getattr(module, 'CMD' + cmd)) for cmd in cmds)
2057
2058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002059def Command(name):
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002060 """Retrieves the function to handle a command."""
2061 commands = Commands()
2062 if name in commands:
2063 return commands[name]
2064
2065 # Try to be smart and look if there's something similar.
2066 commands_with_prefix = [c for c in commands if c.startswith(name)]
2067 if len(commands_with_prefix) == 1:
2068 return commands[commands_with_prefix[0]]
2069
2070 # A #closeenough approximation of levenshtein distance.
2071 def close_enough(a, b):
2072 return difflib.SequenceMatcher(a=a, b=b).ratio()
2073
2074 hamming_commands = sorted(
2075 ((close_enough(c, name), c) for c in commands),
2076 reverse=True)
2077 if (hamming_commands[0][0] - hamming_commands[1][0]) < 0.3:
2078 # Too ambiguous.
2079 return
2080
2081 if hamming_commands[0][0] < 0.8:
2082 # Not similar enough. Don't be a fool and run a random command.
2083 return
2084
2085 return commands[hamming_commands[0][1]]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002086
2087
2088def CMDhelp(parser, args):
2089 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00002090 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002091 if len(args) == 1:
2092 return main(args + ['--help'])
2093 parser.print_help()
2094 return 0
2095
2096
2097def GenUsage(parser, command):
2098 """Modify an OptParse object with the function's documentation."""
2099 obj = Command(command)
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002100 # Get back the real command name in case Command() guess the actual command
2101 # name.
2102 command = obj.__name__[3:]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002103 more = getattr(obj, 'usage_more', '')
2104 if command == 'help':
2105 command = '<command>'
2106 else:
2107 # OptParser.description prefer nicely non-formatted strings.
2108 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
2109 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
2110
2111
2112def main(argv):
2113 """Doesn't parse the arguments here, just find the right subcommand to
2114 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00002115 if sys.hexversion < 0x02060000:
2116 print >> sys.stderr, (
2117 '\nYour python version %s is unsupported, please upgrade.\n' %
2118 sys.version.split(' ', 1)[0])
2119 return 2
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002120
maruel@chromium.orgddd59412011-11-30 14:20:38 +00002121 # Reload settings.
2122 global settings
2123 settings = Settings()
2124
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002125 # Do it late so all commands are listed.
maruel@chromium.org967c0a82013-06-17 22:52:24 +00002126 commands = Commands()
2127 length = max(len(c) for c in commands)
2128 docs = sorted(
2129 (name, handler.__doc__.split('\n')[0].strip())
2130 for name, handler in commands.iteritems())
2131 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join(
2132 ' %-*s %s' % (length, name, doc) for name, doc in docs))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002133
2134 # Create the option parse and add --verbose support.
2135 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002136 parser.add_option(
2137 '-v', '--verbose', action='count', default=0,
2138 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002139 old_parser_args = parser.parse_args
2140 def Parse(args):
2141 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002142 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002143 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00002144 elif options.verbose:
2145 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002146 else:
2147 logging.basicConfig(level=logging.WARNING)
2148 return options, args
2149 parser.parse_args = Parse
2150
2151 if argv:
2152 command = Command(argv[0])
2153 if command:
2154 # "fix" the usage and the description now that we know the subcommand.
2155 GenUsage(parser, argv[0])
2156 try:
2157 return command(parser, argv[1:])
2158 except urllib2.HTTPError, e:
2159 if e.code != 500:
2160 raise
2161 DieWithError(
2162 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
2163 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
2164
2165 # Not a known command. Default to help.
2166 GenUsage(parser, 'help')
2167 return CMDhelp(parser, argv)
2168
2169
2170if __name__ == '__main__':
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002171 # These affect sys.stdout so do it outside of main() to simplify mocks in
2172 # unit testing.
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00002173 fix_encoding.fix_encoding()
maruel@chromium.org2e23ce32013-05-07 12:42:28 +00002174 colorama.init()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00002175 sys.exit(main(sys.argv[1:]))