blob: 21ade2a28e9987cfdc76425e64f8f0f027b6b1e6 [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.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026
27from third_party import upload
28import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000029import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000032import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000034import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import watchlists
36
37
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000038DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000039POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000041GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000042CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000043
maruel@chromium.org90541732011-04-01 17:54:18 +000044
maruel@chromium.orgddd59412011-11-30 14:20:38 +000045# Initialized in main()
46settings = None
47
48
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000050 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051 sys.exit(1)
52
53
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000056 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057 except subprocess2.CalledProcessError, e:
58 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060 'Command "%s" failed.\n%s' % (
61 ' '.join(args), error_message or e.stdout or ''))
62 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063
64
65def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 """Returns stdout."""
67 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068
69
70def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000072 try:
73 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
74 return code, out[0]
75 except ValueError:
76 # When the subprocess fails, it returns None. That triggers a ValueError
77 # when trying to unpack the return value into (out, code).
78 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
80
81def usage(more):
82 def hook(fn):
83 fn.usage_more = more
84 return fn
85 return hook
86
87
maruel@chromium.org90541732011-04-01 17:54:18 +000088def ask_for_data(prompt):
89 try:
90 return raw_input(prompt)
91 except KeyboardInterrupt:
92 # Hide the exception.
93 sys.exit(1)
94
95
iannucci@chromium.org79540052012-10-19 23:15:26 +000096def git_set_branch_value(key, value):
97 branch = Changelist().GetBranch()
98 if branch:
99 git_key = 'branch.%s.%s' % (branch, key)
100 RunGit(['config', '--int', git_key, "%d" % value])
101
102
103def git_get_branch_default(key, default):
104 branch = Changelist().GetBranch()
105 if branch:
106 git_key = 'branch.%s.%s' % (branch, key)
107 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
108 try:
109 return int(stdout.strip())
110 except ValueError:
111 pass
112 return default
113
114
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000115def add_git_similarity(parser):
116 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000117 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000118 help='Sets the percentage that a pair of files need to match in order to'
119 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000120 parser.add_option(
121 '--find-copies', action='store_true',
122 help='Allows git to look for copies.')
123 parser.add_option(
124 '--no-find-copies', action='store_false', dest='find_copies',
125 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000126
127 old_parser_args = parser.parse_args
128 def Parse(args):
129 options, args = old_parser_args(args)
130
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000131 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000132 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000133 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000134 print('Note: Saving similarity of %d%% in git config.'
135 % options.similarity)
136 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138 options.similarity = max(0, min(options.similarity, 100))
139
140 if options.find_copies is None:
141 options.find_copies = bool(
142 git_get_branch_default('git-find-copies', True))
143 else:
144 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000145
146 print('Using %d%% similarity for rename/copy detection. '
147 'Override with --similarity.' % options.similarity)
148
149 return options, args
150 parser.parse_args = Parse
151
152
ukai@chromium.org259e4682012-10-25 07:36:33 +0000153def is_dirty_git_tree(cmd):
154 # Make sure index is up-to-date before running diff-index.
155 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
156 dirty = RunGit(['diff-index', '--name-status', 'HEAD'])
157 if dirty:
158 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
159 print 'Uncommitted files: (git diff-index --name-status HEAD)'
160 print dirty[:4096]
161 if len(dirty) > 4096:
162 print '... (run "git diff-index --name-status HEAD" to see full output).'
163 return True
164 return False
165
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000166def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
167 """Return the corresponding git ref if |base_url| together with |glob_spec|
168 matches the full |url|.
169
170 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
171 """
172 fetch_suburl, as_ref = glob_spec.split(':')
173 if allow_wildcards:
174 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
175 if glob_match:
176 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
177 # "branches/{472,597,648}/src:refs/remotes/svn/*".
178 branch_re = re.escape(base_url)
179 if glob_match.group(1):
180 branch_re += '/' + re.escape(glob_match.group(1))
181 wildcard = glob_match.group(2)
182 if wildcard == '*':
183 branch_re += '([^/]*)'
184 else:
185 # Escape and replace surrounding braces with parentheses and commas
186 # with pipe symbols.
187 wildcard = re.escape(wildcard)
188 wildcard = re.sub('^\\\\{', '(', wildcard)
189 wildcard = re.sub('\\\\,', '|', wildcard)
190 wildcard = re.sub('\\\\}$', ')', wildcard)
191 branch_re += wildcard
192 if glob_match.group(3):
193 branch_re += re.escape(glob_match.group(3))
194 match = re.match(branch_re, url)
195 if match:
196 return re.sub('\*$', match.group(1), as_ref)
197
198 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
199 if fetch_suburl:
200 full_url = base_url + '/' + fetch_suburl
201 else:
202 full_url = base_url
203 if full_url == url:
204 return as_ref
205 return None
206
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000207
iannucci@chromium.org79540052012-10-19 23:15:26 +0000208def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000209 """Prints statistics about the change to the user."""
210 # --no-ext-diff is broken in some versions of Git, so try to work around
211 # this by overriding the environment (but there is still a problem if the
212 # git config key "diff.external" is used).
213 env = os.environ.copy()
214 if 'GIT_EXTERNAL_DIFF' in env:
215 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000216
217 if find_copies:
218 similarity_options = ['--find-copies-harder', '-l100000',
219 '-C%s' % similarity]
220 else:
221 similarity_options = ['-M%s' % similarity]
222
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000223 return subprocess2.call(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000224 ['git', 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
225 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000226
227
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000228class Settings(object):
229 def __init__(self):
230 self.default_server = None
231 self.cc = None
232 self.root = None
233 self.is_git_svn = None
234 self.svn_branch = None
235 self.tree_status_url = None
236 self.viewvc_url = None
237 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000238 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000239
240 def LazyUpdateIfNeeded(self):
241 """Updates the settings from a codereview.settings file, if available."""
242 if not self.updated:
243 cr_settings_file = FindCodereviewSettingsFile()
244 if cr_settings_file:
245 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000246 self.updated = True
247 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000248 self.updated = True
249
250 def GetDefaultServerUrl(self, error_ok=False):
251 if not self.default_server:
252 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000253 self.default_server = gclient_utils.UpgradeToHttps(
254 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000255 if error_ok:
256 return self.default_server
257 if not self.default_server:
258 error_message = ('Could not find settings file. You must configure '
259 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000260 self.default_server = gclient_utils.UpgradeToHttps(
261 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000262 return self.default_server
263
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000264 def GetRoot(self):
265 if not self.root:
266 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
267 return self.root
268
269 def GetIsGitSvn(self):
270 """Return true if this repo looks like it's using git-svn."""
271 if self.is_git_svn is None:
272 # If you have any "svn-remote.*" config keys, we think you're using svn.
273 self.is_git_svn = RunGitWithCode(
274 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
275 return self.is_git_svn
276
277 def GetSVNBranch(self):
278 if self.svn_branch is None:
279 if not self.GetIsGitSvn():
280 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
281
282 # Try to figure out which remote branch we're based on.
283 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000284 # 1) iterate through our branch history and find the svn URL.
285 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000286
287 # regexp matching the git-svn line that contains the URL.
288 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
289
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000290 # We don't want to go through all of history, so read a line from the
291 # pipe at a time.
292 # The -100 is an arbitrary limit so we don't search forever.
293 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000294 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000295 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000296 for line in proc.stdout:
297 match = git_svn_re.match(line)
298 if match:
299 url = match.group(1)
300 proc.stdout.close() # Cut pipe.
301 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000302
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000303 if url:
304 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
305 remotes = RunGit(['config', '--get-regexp',
306 r'^svn-remote\..*\.url']).splitlines()
307 for remote in remotes:
308 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000309 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000310 remote = match.group(1)
311 base_url = match.group(2)
312 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000313 ['config', 'svn-remote.%s.fetch' % remote],
314 error_ok=True).strip()
315 if fetch_spec:
316 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
317 if self.svn_branch:
318 break
319 branch_spec = RunGit(
320 ['config', 'svn-remote.%s.branches' % remote],
321 error_ok=True).strip()
322 if branch_spec:
323 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
324 if self.svn_branch:
325 break
326 tag_spec = RunGit(
327 ['config', 'svn-remote.%s.tags' % remote],
328 error_ok=True).strip()
329 if tag_spec:
330 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
331 if self.svn_branch:
332 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000333
334 if not self.svn_branch:
335 DieWithError('Can\'t guess svn branch -- try specifying it on the '
336 'command line')
337
338 return self.svn_branch
339
340 def GetTreeStatusUrl(self, error_ok=False):
341 if not self.tree_status_url:
342 error_message = ('You must configure your tree status URL by running '
343 '"git cl config".')
344 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
345 error_ok=error_ok,
346 error_message=error_message)
347 return self.tree_status_url
348
349 def GetViewVCUrl(self):
350 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000351 self.viewvc_url = gclient_utils.UpgradeToHttps(
352 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000353 return self.viewvc_url
354
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000355 def GetDefaultCCList(self):
356 return self._GetConfig('rietveld.cc', error_ok=True)
357
ukai@chromium.orge8077812012-02-03 03:41:46 +0000358 def GetIsGerrit(self):
359 """Return true if this repo is assosiated with gerrit code review system."""
360 if self.is_gerrit is None:
361 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
362 return self.is_gerrit
363
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000364 def _GetConfig(self, param, **kwargs):
365 self.LazyUpdateIfNeeded()
366 return RunGit(['config', param], **kwargs).strip()
367
368
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000369def ShortBranchName(branch):
370 """Convert a name like 'refs/heads/foo' to just 'foo'."""
371 return branch.replace('refs/heads/', '')
372
373
374class Changelist(object):
375 def __init__(self, branchref=None):
376 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000377 global settings
378 if not settings:
379 # Happens when git_cl.py is used as a utility library.
380 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000381 settings.GetDefaultServerUrl()
382 self.branchref = branchref
383 if self.branchref:
384 self.branch = ShortBranchName(self.branchref)
385 else:
386 self.branch = None
387 self.rietveld_server = None
388 self.upstream_branch = None
389 self.has_issue = False
390 self.issue = None
391 self.has_description = False
392 self.description = None
393 self.has_patchset = False
394 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000395 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000396 self.cc = None
397 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000398 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000399
400 def GetCCList(self):
401 """Return the users cc'd on this CL.
402
403 Return is a string suitable for passing to gcl with the --cc flag.
404 """
405 if self.cc is None:
406 base_cc = settings .GetDefaultCCList()
407 more_cc = ','.join(self.watchers)
408 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
409 return self.cc
410
411 def SetWatchers(self, watchers):
412 """Set the list of email addresses that should be cc'd based on the changed
413 files in this CL.
414 """
415 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000416
417 def GetBranch(self):
418 """Returns the short branch name, e.g. 'master'."""
419 if not self.branch:
420 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
421 self.branch = ShortBranchName(self.branchref)
422 return self.branch
423
424 def GetBranchRef(self):
425 """Returns the full branch name, e.g. 'refs/heads/master'."""
426 self.GetBranch() # Poke the lazy loader.
427 return self.branchref
428
429 def FetchUpstreamTuple(self):
430 """Returns a tuple containg remote and remote ref,
431 e.g. 'origin', 'refs/heads/master'
432 """
433 remote = '.'
434 branch = self.GetBranch()
435 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
436 error_ok=True).strip()
437 if upstream_branch:
438 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
439 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000440 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
441 error_ok=True).strip()
442 if upstream_branch:
443 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000444 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000445 # Fall back on trying a git-svn upstream branch.
446 if settings.GetIsGitSvn():
447 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000448 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000449 # Else, try to guess the origin remote.
450 remote_branches = RunGit(['branch', '-r']).split()
451 if 'origin/master' in remote_branches:
452 # Fall back on origin/master if it exits.
453 remote = 'origin'
454 upstream_branch = 'refs/heads/master'
455 elif 'origin/trunk' in remote_branches:
456 # Fall back on origin/trunk if it exists. Generally a shared
457 # git-svn clone
458 remote = 'origin'
459 upstream_branch = 'refs/heads/trunk'
460 else:
461 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000462Either pass complete "git diff"-style arguments, like
463 git cl upload origin/master
464or verify this branch is set up to track another (via the --track argument to
465"git checkout -b ...").""")
466
467 return remote, upstream_branch
468
469 def GetUpstreamBranch(self):
470 if self.upstream_branch is None:
471 remote, upstream_branch = self.FetchUpstreamTuple()
472 if remote is not '.':
473 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
474 self.upstream_branch = upstream_branch
475 return self.upstream_branch
476
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000477 def GetRemote(self):
478 if not self._remote:
479 self._remote = self.FetchUpstreamTuple()[0]
480 if self._remote == '.':
481
482 remotes = RunGit(['remote'], error_ok=True).split()
483 if len(remotes) == 1:
484 self._remote, = remotes
485 elif 'origin' in remotes:
486 self._remote = 'origin'
487 logging.warning('Could not determine which remote this change is '
488 'associated with, so defaulting to "%s". This may '
489 'not be what you want. You may prevent this message '
490 'by running "git svn info" as documented here: %s',
491 self._remote,
492 GIT_INSTRUCTIONS_URL)
493 else:
494 logging.warn('Could not determine which remote this change is '
495 'associated with. You may prevent this message by '
496 'running "git svn info" as documented here: %s',
497 GIT_INSTRUCTIONS_URL)
498 return self._remote
499
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000500 def GetGitBaseUrlFromConfig(self):
501 """Return the configured base URL from branch.<branchname>.baseurl.
502
503 Returns None if it is not set.
504 """
505 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
506 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000507
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000508 def GetRemoteUrl(self):
509 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
510
511 Returns None if there is no remote.
512 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000513 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000514 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
515
516 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000517 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000518 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000519 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
520 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000521 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000522 else:
523 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000524 self.has_issue = True
525 return self.issue
526
527 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000528 if not self.rietveld_server:
529 # If we're on a branch then get the server potentially associated
530 # with that branch.
531 if self.GetIssue():
532 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
533 ['config', self._RietveldServer()], error_ok=True).strip())
534 if not self.rietveld_server:
535 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000536 return self.rietveld_server
537
538 def GetIssueURL(self):
539 """Get the URL for a particular issue."""
540 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
541
542 def GetDescription(self, pretty=False):
543 if not self.has_description:
544 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000545 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000546 try:
547 self.description = self.RpcServer().get_description(issue).strip()
548 except urllib2.HTTPError, e:
549 if e.code == 404:
550 DieWithError(
551 ('\nWhile fetching the description for issue %d, received a '
552 '404 (not found)\n'
553 'error. It is likely that you deleted this '
554 'issue on the server. If this is the\n'
555 'case, please run\n\n'
556 ' git cl issue 0\n\n'
557 'to clear the association with the deleted issue. Then run '
558 'this command again.') % issue)
559 else:
560 DieWithError(
561 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000562 self.has_description = True
563 if pretty:
564 wrapper = textwrap.TextWrapper()
565 wrapper.initial_indent = wrapper.subsequent_indent = ' '
566 return wrapper.fill(self.description)
567 return self.description
568
569 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000570 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000571 if not self.has_patchset:
572 patchset = RunGit(['config', self._PatchsetSetting()],
573 error_ok=True).strip()
574 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000575 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000576 else:
577 self.patchset = None
578 self.has_patchset = True
579 return self.patchset
580
581 def SetPatchset(self, patchset):
582 """Set this branch's patchset. If patchset=0, clears the patchset."""
583 if patchset:
584 RunGit(['config', self._PatchsetSetting(), str(patchset)])
585 else:
586 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000587 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000588 self.has_patchset = False
589
binji@chromium.org0281f522012-09-14 13:37:59 +0000590 def GetMostRecentPatchset(self, issue):
591 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000592 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000593
594 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000595 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000596 '/download/issue%s_%s.diff' % (issue, patchset))
597
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598 def SetIssue(self, issue):
599 """Set this branch's issue. If issue=0, clears the issue."""
600 if issue:
601 RunGit(['config', self._IssueSetting(), str(issue)])
602 if self.rietveld_server:
603 RunGit(['config', self._RietveldServer(), self.rietveld_server])
604 else:
605 RunGit(['config', '--unset', self._IssueSetting()])
606 self.SetPatchset(0)
607 self.has_issue = False
608
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000609 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000610 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
611 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000612
613 # We use the sha1 of HEAD as a name of this change.
614 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000615 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000616 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000617 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000618 except subprocess2.CalledProcessError:
619 DieWithError(
620 ('\nFailed to diff against upstream branch %s!\n\n'
621 'This branch probably doesn\'t exist anymore. To reset the\n'
622 'tracking branch, please run\n'
623 ' git branch --set-upstream %s trunk\n'
624 'replacing trunk with origin/master or the relevant branch') %
625 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000626
maruel@chromium.org52424302012-08-29 15:14:30 +0000627 issue = self.GetIssue()
628 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000629 if issue:
630 description = self.GetDescription()
631 else:
632 # If the change was never uploaded, use the log messages of all commits
633 # up to the branch point, as git cl upload will prefill the description
634 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000635 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
636 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000637
638 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000639 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000640 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000641 name,
642 description,
643 absroot,
644 files,
645 issue,
646 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000647 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000648
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000649 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
650 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
651 change = self.GetChange(upstream_branch, author)
652
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000653 # Apply watchlists on upload.
654 if not committing:
655 watchlist = watchlists.Watchlists(change.RepositoryRoot())
656 files = [f.LocalPath() for f in change.AffectedFiles()]
657 self.SetWatchers(watchlist.GetWatchersForPaths(files))
658
659 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000660 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000661 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000662 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000663 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000664 except presubmit_support.PresubmitFailure, e:
665 DieWithError(
666 ('%s\nMaybe your depot_tools is out of date?\n'
667 'If all fails, contact maruel@') % e)
668
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000669 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000670 """Updates the description and closes the issue."""
maruel@chromium.org52424302012-08-29 15:14:30 +0000671 issue = self.GetIssue()
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000672 self.RpcServer().update_description(issue, self.description)
673 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000674
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000675 def SetFlag(self, flag, value):
676 """Patchset must match."""
677 if not self.GetPatchset():
678 DieWithError('The patchset needs to match. Send another patchset.')
679 try:
680 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000681 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000682 except urllib2.HTTPError, e:
683 if e.code == 404:
684 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
685 if e.code == 403:
686 DieWithError(
687 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
688 'match?') % (self.GetIssue(), self.GetPatchset()))
689 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000690
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000691 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 """Returns an upload.RpcServer() to access this review's rietveld instance.
693 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000694 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000695 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
696 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000697 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000698
699 def _IssueSetting(self):
700 """Return the git setting that stores this change's issue."""
701 return 'branch.%s.rietveldissue' % self.GetBranch()
702
703 def _PatchsetSetting(self):
704 """Return the git setting that stores this change's most recent patchset."""
705 return 'branch.%s.rietveldpatchset' % self.GetBranch()
706
707 def _RietveldServer(self):
708 """Returns the git setting that stores this change's rietveld server."""
709 return 'branch.%s.rietveldserver' % self.GetBranch()
710
711
712def GetCodereviewSettingsInteractively():
713 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000714 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 server = settings.GetDefaultServerUrl(error_ok=True)
716 prompt = 'Rietveld server (host[:port])'
717 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000718 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 if not server and not newserver:
720 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000721 if newserver:
722 newserver = gclient_utils.UpgradeToHttps(newserver)
723 if newserver != server:
724 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000726 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727 prompt = caption
728 if initial:
729 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000730 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 if new_val == 'x':
732 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000733 elif new_val:
734 if is_url:
735 new_val = gclient_utils.UpgradeToHttps(new_val)
736 if new_val != initial:
737 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000738
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000739 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000741 'tree-status-url', False)
742 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743
744 # TODO: configure a default branch to diff against, rather than this
745 # svn-based hackery.
746
747
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000748class ChangeDescription(object):
749 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000750 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000751 self.log_desc = log_desc
752 self.reviewers = reviewers
753 self.description = self.log_desc
754
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000755 def Prompt(self):
756 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000757# This will displayed on the codereview site.
758# The first line will also be used as the subject of the review.
759"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000760 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000761 if ('\nR=' not in self.description and
762 '\nTBR=' not in self.description and
763 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000764 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000765 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000766 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000767 content = content.rstrip('\n') + '\n'
768 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000769 if not content:
770 DieWithError('Running editor failed')
771 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000772 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000773 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000774 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000775
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000776 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000777 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000778 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000779 # Retrieves all reviewer lines
780 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000781 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000782 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000783 if reviewers:
784 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000785
786 def IsEmpty(self):
787 return not self.description
788
789
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000790def FindCodereviewSettingsFile(filename='codereview.settings'):
791 """Finds the given file starting in the cwd and going up.
792
793 Only looks up to the top of the repository unless an
794 'inherit-review-settings-ok' file exists in the root of the repository.
795 """
796 inherit_ok_file = 'inherit-review-settings-ok'
797 cwd = os.getcwd()
798 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
799 if os.path.isfile(os.path.join(root, inherit_ok_file)):
800 root = '/'
801 while True:
802 if filename in os.listdir(cwd):
803 if os.path.isfile(os.path.join(cwd, filename)):
804 return open(os.path.join(cwd, filename))
805 if cwd == root:
806 break
807 cwd = os.path.dirname(cwd)
808
809
810def LoadCodereviewSettingsFromFile(fileobj):
811 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000812 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000813
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000814 def SetProperty(name, setting, unset_error_ok=False):
815 fullname = 'rietveld.' + name
816 if setting in keyvals:
817 RunGit(['config', fullname, keyvals[setting]])
818 else:
819 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
820
821 SetProperty('server', 'CODE_REVIEW_SERVER')
822 # Only server setting is required. Other settings can be absent.
823 # In that case, we ignore errors raised during option deletion attempt.
824 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
825 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
826 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
827
ukai@chromium.orge8077812012-02-03 03:41:46 +0000828 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
829 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
830 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
833 #should be of the form
834 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
835 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
836 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
837 keyvals['ORIGIN_URL_CONFIG']])
838
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000839
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000840def urlretrieve(source, destination):
841 """urllib is broken for SSL connections via a proxy therefore we
842 can't use urllib.urlretrieve()."""
843 with open(destination, 'w') as f:
844 f.write(urllib2.urlopen(source).read())
845
846
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000847def DownloadHooks(force):
848 """downloads hooks
849
850 Args:
851 force: True to update hooks. False to install hooks if not present.
852 """
853 if not settings.GetIsGerrit():
854 return
855 server_url = settings.GetDefaultServerUrl()
856 src = '%s/tools/hooks/commit-msg' % server_url
857 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
858 if not os.access(dst, os.X_OK):
859 if os.path.exists(dst):
860 if not force:
861 return
862 os.remove(dst)
863 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000864 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000865 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
866 except Exception:
867 if os.path.exists(dst):
868 os.remove(dst)
869 DieWithError('\nFailed to download hooks from %s' % src)
870
871
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872@usage('[repo root containing codereview.settings]')
873def CMDconfig(parser, args):
874 """edit configuration for this tree"""
875
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000876 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000877 if len(args) == 0:
878 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000879 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 return 0
881
882 url = args[0]
883 if not url.endswith('codereview.settings'):
884 url = os.path.join(url, 'codereview.settings')
885
886 # Load code review settings and download hooks (if available).
887 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000888 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889 return 0
890
891
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000892def CMDbaseurl(parser, args):
893 """get or set base-url for this branch"""
894 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
895 branch = ShortBranchName(branchref)
896 _, args = parser.parse_args(args)
897 if not args:
898 print("Current base-url:")
899 return RunGit(['config', 'branch.%s.base-url' % branch],
900 error_ok=False).strip()
901 else:
902 print("Setting base-url to %s" % args[0])
903 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
904 error_ok=False).strip()
905
906
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000907def CMDstatus(parser, args):
908 """show status of changelists"""
909 parser.add_option('--field',
910 help='print only specific field (desc|id|patch|url)')
911 (options, args) = parser.parse_args(args)
912
913 # TODO: maybe make show_branches a flag if necessary.
914 show_branches = not options.field
915
916 if show_branches:
917 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
918 if branches:
919 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000920 changes = (Changelist(branchref=b) for b in branches.splitlines())
921 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
922 alignment = max(5, max(len(b) for b in branches))
923 for branch in sorted(branches):
924 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925
926 cl = Changelist()
927 if options.field:
928 if options.field.startswith('desc'):
929 print cl.GetDescription()
930 elif options.field == 'id':
931 issueid = cl.GetIssue()
932 if issueid:
933 print issueid
934 elif options.field == 'patch':
935 patchset = cl.GetPatchset()
936 if patchset:
937 print patchset
938 elif options.field == 'url':
939 url = cl.GetIssueURL()
940 if url:
941 print url
942 else:
943 print
944 print 'Current branch:',
945 if not cl.GetIssue():
946 print 'no issue assigned.'
947 return 0
948 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000949 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000950 print 'Issue description:'
951 print cl.GetDescription(pretty=True)
952 return 0
953
954
955@usage('[issue_number]')
956def CMDissue(parser, args):
957 """Set or display the current code review issue number.
958
959 Pass issue number 0 to clear the current issue.
960"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000961 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962
963 cl = Changelist()
964 if len(args) > 0:
965 try:
966 issue = int(args[0])
967 except ValueError:
968 DieWithError('Pass a number to set the issue or none to list it.\n'
969 'Maybe you want to run git cl status?')
970 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +0000971 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 return 0
973
974
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000975def CMDcomments(parser, args):
976 """show review comments of the current changelist"""
977 (_, args) = parser.parse_args(args)
978 if args:
979 parser.error('Unsupported argument: %s' % args)
980
981 cl = Changelist()
982 if cl.GetIssue():
983 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
984 for message in sorted(data['messages'], key=lambda x: x['date']):
985 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
986 if message['text'].strip():
987 print '\n'.join(' ' + l for l in message['text'].splitlines())
988 return 0
989
990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991def CreateDescriptionFromLog(args):
992 """Pulls out the commit log to use as a base for the CL description."""
993 log_args = []
994 if len(args) == 1 and not args[0].endswith('.'):
995 log_args = [args[0] + '..']
996 elif len(args) == 1 and args[0].endswith('...'):
997 log_args = [args[0][:-1]]
998 elif len(args) == 2:
999 log_args = [args[0] + '..' + args[1]]
1000 else:
1001 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +00001002 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003
1004
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005def CMDpresubmit(parser, args):
1006 """run presubmit tests on the current changelist"""
1007 parser.add_option('--upload', action='store_true',
1008 help='Run upload hook instead of the push/dcommit hook')
sbc@chromium.org495ad152012-09-04 23:07:42 +00001009 parser.add_option('--force', action='store_true',
1010 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001011 (options, args) = parser.parse_args(args)
1012
ukai@chromium.org259e4682012-10-25 07:36:33 +00001013 if not options.force and is_dirty_git_tree('presubmit'):
1014 print 'use --force to check even if tree is dirty.'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001015 return 1
1016
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001017 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018 if args:
1019 base_branch = args[0]
1020 else:
1021 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001022 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001024 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001025 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001026 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001027 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028
1029
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001030def AddChangeIdToCommitMessage(options, args):
1031 """Re-commits using the current message, assumes the commit hook is in
1032 place.
1033 """
1034 log_desc = options.message or CreateDescriptionFromLog(args)
1035 git_command = ['commit', '--amend', '-m', log_desc]
1036 RunGit(git_command)
1037 new_log_desc = CreateDescriptionFromLog(args)
1038 if CHANGE_ID in new_log_desc:
1039 print 'git-cl: Added Change-Id to commit message.'
1040 else:
1041 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1042
1043
ukai@chromium.orge8077812012-02-03 03:41:46 +00001044def GerritUpload(options, args, cl):
1045 """upload the current branch to gerrit."""
1046 # We assume the remote called "origin" is the one we want.
1047 # It is probably not worthwhile to support different workflows.
1048 remote = 'origin'
1049 branch = 'master'
1050 if options.target_branch:
1051 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001052
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001053 log_desc = options.message or CreateDescriptionFromLog(args)
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001054 if CHANGE_ID not in log_desc:
1055 AddChangeIdToCommitMessage(options, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001056 if options.reviewers:
1057 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001058 change_desc = ChangeDescription(log_desc, options.reviewers)
1059 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +00001060 if change_desc.IsEmpty():
1061 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001062 return 1
1063
ukai@chromium.orge8077812012-02-03 03:41:46 +00001064 receive_options = []
1065 cc = cl.GetCCList().split(',')
1066 if options.cc:
1067 cc += options.cc.split(',')
1068 cc = filter(None, cc)
1069 if cc:
1070 receive_options += ['--cc=' + email for email in cc]
1071 if change_desc.reviewers:
1072 reviewers = filter(None, change_desc.reviewers.split(','))
1073 if reviewers:
1074 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075
ukai@chromium.orge8077812012-02-03 03:41:46 +00001076 git_command = ['push']
1077 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001078 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001079 ' '.join(receive_options))
1080 git_command += [remote, 'HEAD:refs/for/' + branch]
1081 RunGit(git_command)
1082 # TODO(ukai): parse Change-Id: and set issue number?
1083 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001084
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085
ukai@chromium.orge8077812012-02-03 03:41:46 +00001086def RietveldUpload(options, args, cl):
1087 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001088 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1089 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 if options.emulate_svn_auto_props:
1091 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001092
1093 change_desc = None
1094
1095 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001096 if options.title:
1097 upload_args.extend(['--title', options.title])
1098 elif options.message:
1099 # TODO(rogerta): for now, the -m option will also set the --title option
1100 # for upload.py. Soon this will be changed to set the --message option.
1101 # Will wait until people are used to typing -t instead of -m.
1102 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001103 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104 print ("This branch is associated with issue %s. "
1105 "Adding patch to that issue." % cl.GetIssue())
1106 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001107 if options.title:
1108 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001109 message = options.message or CreateDescriptionFromLog(args)
1110 change_desc = ChangeDescription(message, options.reviewers)
1111 if not options.force:
1112 change_desc.Prompt()
1113 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001114
1115 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116 print "Description is empty; aborting."
1117 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001118
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001119 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001120 if change_desc.reviewers:
1121 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001122 if options.send_mail:
1123 if not change_desc.reviewers:
1124 DieWithError("Must specify reviewers to send email.")
1125 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001126 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001127 if cc:
1128 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001129
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001130 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001131 if not options.find_copies:
1132 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001133
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001134 # Include the upstream repo's URL in the change -- this is useful for
1135 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001136 remote_url = cl.GetGitBaseUrlFromConfig()
1137 if not remote_url:
1138 if settings.GetIsGitSvn():
1139 # URL is dependent on the current directory.
1140 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1141 if data:
1142 keys = dict(line.split(': ', 1) for line in data.splitlines()
1143 if ': ' in line)
1144 remote_url = keys.get('URL', None)
1145 else:
1146 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1147 remote_url = (cl.GetRemoteUrl() + '@'
1148 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 if remote_url:
1150 upload_args.extend(['--base_url', remote_url])
1151
1152 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001153 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001154 except KeyboardInterrupt:
1155 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 except:
1157 # If we got an exception after the user typed a description for their
1158 # change, back up the description before re-raising.
1159 if change_desc:
1160 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1161 print '\nGot exception while uploading -- saving description to %s\n' \
1162 % backup_path
1163 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001164 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 backup_file.close()
1166 raise
1167
1168 if not cl.GetIssue():
1169 cl.SetIssue(issue)
1170 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001171
1172 if options.use_commit_queue:
1173 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001174 return 0
1175
1176
ukai@chromium.orge8077812012-02-03 03:41:46 +00001177@usage('[args to "git diff"]')
1178def CMDupload(parser, args):
1179 """upload the current changelist to codereview"""
1180 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1181 help='bypass upload presubmit hook')
1182 parser.add_option('-f', action='store_true', dest='force',
1183 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001184 parser.add_option('-m', dest='message', help='message for patchset')
1185 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001186 parser.add_option('-r', '--reviewers',
1187 help='reviewer email addresses')
1188 parser.add_option('--cc',
1189 help='cc email addresses')
1190 parser.add_option('--send-mail', action='store_true',
1191 help='send email to reviewer immediately')
1192 parser.add_option("--emulate_svn_auto_props", action="store_true",
1193 dest="emulate_svn_auto_props",
1194 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001195 parser.add_option('-c', '--use-commit-queue', action='store_true',
1196 help='tell the commit queue to commit this patchset')
1197 if settings.GetIsGerrit():
1198 parser.add_option('--target_branch', dest='target_branch', default='master',
1199 help='target branch to upload')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001200 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001201 (options, args) = parser.parse_args(args)
1202
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001203 # Print warning if the user used the -m/--message argument. This will soon
1204 # change to -t/--title.
1205 if options.message:
1206 print >> sys.stderr, (
1207 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1208 'In the near future, -m or --message will send a message instead.\n'
1209 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001210
ukai@chromium.org259e4682012-10-25 07:36:33 +00001211 if is_dirty_git_tree('upload'):
ukai@chromium.orge8077812012-02-03 03:41:46 +00001212 return 1
1213
1214 cl = Changelist()
1215 if args:
1216 # TODO(ukai): is it ok for gerrit case?
1217 base_branch = args[0]
1218 else:
1219 # Default to diffing against the "upstream" branch.
1220 base_branch = cl.GetUpstreamBranch()
1221 args = [base_branch + "..."]
1222
1223 if not options.bypass_hooks:
1224 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1225 may_prompt=not options.force,
1226 verbose=options.verbose,
1227 author=None)
1228 if not hook_results.should_continue():
1229 return 1
1230 if not options.reviewers and hook_results.reviewers:
1231 options.reviewers = hook_results.reviewers
1232
iannucci@chromium.org79540052012-10-19 23:15:26 +00001233 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001234 if settings.GetIsGerrit():
1235 return GerritUpload(options, args, cl)
1236 return RietveldUpload(options, args, cl)
1237
1238
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001239def IsSubmoduleMergeCommit(ref):
1240 # When submodules are added to the repo, we expect there to be a single
1241 # non-git-svn merge commit at remote HEAD with a signature comment.
1242 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001243 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001244 return RunGit(cmd) != ''
1245
1246
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247def SendUpstream(parser, args, cmd):
1248 """Common code for CmdPush and CmdDCommit
1249
1250 Squashed commit into a single.
1251 Updates changelog with metadata (e.g. pointer to review).
1252 Pushes/dcommits the code upstream.
1253 Updates review and closes.
1254 """
1255 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1256 help='bypass upload presubmit hook')
1257 parser.add_option('-m', dest='message',
1258 help="override review description")
1259 parser.add_option('-f', action='store_true', dest='force',
1260 help="force yes to questions (don't prompt)")
1261 parser.add_option('-c', dest='contributor',
1262 help="external contributor for patch (appended to " +
1263 "description and used as author for git). Should be " +
1264 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001265 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001266 (options, args) = parser.parse_args(args)
1267 cl = Changelist()
1268
1269 if not args or cmd == 'push':
1270 # Default to merging against our best guess of the upstream branch.
1271 args = [cl.GetUpstreamBranch()]
1272
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001273 if options.contributor:
1274 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1275 print "Please provide contibutor as 'First Last <email@example.com>'"
1276 return 1
1277
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001279 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280
ukai@chromium.org259e4682012-10-25 07:36:33 +00001281 if is_dirty_git_tree(cmd):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 return 1
1283
1284 # This rev-list syntax means "show all commits not in my branch that
1285 # are in base_branch".
1286 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1287 base_branch]).splitlines()
1288 if upstream_commits:
1289 print ('Base branch "%s" has %d commits '
1290 'not in this branch.' % (base_branch, len(upstream_commits)))
1291 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1292 return 1
1293
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001294 # This is the revision `svn dcommit` will commit on top of.
1295 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1296 '--pretty=format:%H'])
1297
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001298 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001299 # If the base_head is a submodule merge commit, the first parent of the
1300 # base_head should be a git-svn commit, which is what we're interested in.
1301 base_svn_head = base_branch
1302 if base_has_submodules:
1303 base_svn_head += '^1'
1304
1305 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306 if extra_commits:
1307 print ('This branch has %d additional commits not upstreamed yet.'
1308 % len(extra_commits.splitlines()))
1309 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1310 'before attempting to %s.' % (base_branch, cmd))
1311 return 1
1312
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001313 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001314 author = None
1315 if options.contributor:
1316 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001317 hook_results = cl.RunHook(
1318 committing=True,
1319 upstream_branch=base_branch,
1320 may_prompt=not options.force,
1321 verbose=options.verbose,
1322 author=author)
1323 if not hook_results.should_continue():
1324 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001325
1326 if cmd == 'dcommit':
1327 # Check the tree status if the tree status URL is set.
1328 status = GetTreeStatus()
1329 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001330 print('The tree is closed. Please wait for it to reopen. Use '
1331 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001332 return 1
1333 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001334 print('Unable to determine tree status. Please verify manually and '
1335 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001336 else:
1337 breakpad.SendStack(
1338 'GitClHooksBypassedCommit',
1339 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001340 (cl.GetRietveldServer(), cl.GetIssue()),
1341 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001342
1343 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001344 if not description and cl.GetIssue():
1345 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001347 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001348 if not cl.GetIssue() and options.bypass_hooks:
1349 description = CreateDescriptionFromLog([base_branch])
1350 else:
1351 print 'No description set.'
1352 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1353 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001355 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357
1358 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001359 description += "\nPatch from %s." % options.contributor
1360 print 'Description:', repr(description)
1361
1362 branches = [base_branch, cl.GetBranchRef()]
1363 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001364 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001365 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001366
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001367 # We want to squash all this branch's commits into one commit with the proper
1368 # description. We do this by doing a "reset --soft" to the base branch (which
1369 # keeps the working copy the same), then dcommitting that. If origin/master
1370 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1371 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001373 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1374 # Delete the branches if they exist.
1375 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1376 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1377 result = RunGitWithCode(showref_cmd)
1378 if result[0] == 0:
1379 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001380
1381 # We might be in a directory that's present in this branch but not in the
1382 # trunk. Move up to the top of the tree so that git commands that expect a
1383 # valid CWD won't fail after we check out the merge branch.
1384 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1385 if rel_base_path:
1386 os.chdir(rel_base_path)
1387
1388 # Stuff our change into the merge branch.
1389 # We wrap in a try...finally block so if anything goes wrong,
1390 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001391 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001393 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1394 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395 if options.contributor:
1396 RunGit(['commit', '--author', options.contributor, '-m', description])
1397 else:
1398 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001399 if base_has_submodules:
1400 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1401 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1402 RunGit(['checkout', CHERRY_PICK_BRANCH])
1403 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 if cmd == 'push':
1405 # push the merge branch.
1406 remote, branch = cl.FetchUpstreamTuple()
1407 retcode, output = RunGitWithCode(
1408 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1409 logging.debug(output)
1410 else:
1411 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001412 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001413 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001414 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001415 finally:
1416 # And then swap back to the original branch and clean up.
1417 RunGit(['checkout', '-q', cl.GetBranch()])
1418 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001419 if base_has_submodules:
1420 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001421
1422 if cl.GetIssue():
1423 if cmd == 'dcommit' and 'Committed r' in output:
1424 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1425 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001426 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1427 for l in output.splitlines(False))
1428 match = filter(None, match)
1429 if len(match) != 1:
1430 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1431 output)
1432 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433 else:
1434 return 1
1435 viewvc_url = settings.GetViewVCUrl()
1436 if viewvc_url and revision:
1437 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001438 elif revision:
1439 cl.description += ('\n\nCommitted: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 print ('Closing issue '
1441 '(you may be prompted for your codereview password)...')
1442 cl.CloseIssue()
1443 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001444
1445 if retcode == 0:
1446 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1447 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001448 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001449
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 return 0
1451
1452
1453@usage('[upstream branch to apply against]')
1454def CMDdcommit(parser, args):
1455 """commit the current changelist via git-svn"""
1456 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001457 message = """This doesn't appear to be an SVN repository.
1458If your project has a git mirror with an upstream SVN master, you probably need
1459to run 'git svn init', see your project's git mirror documentation.
1460If your project has a true writeable upstream repository, you probably want
1461to run 'git cl push' instead.
1462Choose wisely, if you get this wrong, your commit might appear to succeed but
1463will instead be silently ignored."""
1464 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001465 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001466 return SendUpstream(parser, args, 'dcommit')
1467
1468
1469@usage('[upstream branch to apply against]')
1470def CMDpush(parser, args):
1471 """commit the current changelist via git"""
1472 if settings.GetIsGitSvn():
1473 print('This appears to be an SVN repository.')
1474 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001475 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001476 return SendUpstream(parser, args, 'push')
1477
1478
1479@usage('<patch url or issue id>')
1480def CMDpatch(parser, args):
1481 """patch in a code review"""
1482 parser.add_option('-b', dest='newbranch',
1483 help='create a new branch off trunk for the patch')
1484 parser.add_option('-f', action='store_true', dest='force',
1485 help='with -b, clobber any existing branch')
1486 parser.add_option('--reject', action='store_true', dest='reject',
1487 help='allow failed patches and spew .rej files')
1488 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1489 help="don't commit after patch applies")
1490 (options, args) = parser.parse_args(args)
1491 if len(args) != 1:
1492 parser.print_help()
1493 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001494 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001496 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001497 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001498
maruel@chromium.org52424302012-08-29 15:14:30 +00001499 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001501 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001502 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001503 patchset = cl.GetMostRecentPatchset(issue)
1504 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001505 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001506 # Assume it's a URL to the patch. Default to https.
1507 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001508 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001509 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001510 DieWithError('Must pass an issue ID or full URL for '
1511 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001512 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001513 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001514 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001515
1516 if options.newbranch:
1517 if options.force:
1518 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001519 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001520 RunGit(['checkout', '-b', options.newbranch,
1521 Changelist().GetUpstreamBranch()])
1522
1523 # Switch up to the top-level directory, if necessary, in preparation for
1524 # applying the patch.
1525 top = RunGit(['rev-parse', '--show-cdup']).strip()
1526 if top:
1527 os.chdir(top)
1528
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001529 # Git patches have a/ at the beginning of source paths. We strip that out
1530 # with a sed script rather than the -p flag to patch so we can feed either
1531 # Git or svn-style patches into the same apply command.
1532 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001533 try:
1534 patch_data = subprocess2.check_output(
1535 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1536 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001537 DieWithError('Git patch mungling failed.')
1538 logging.info(patch_data)
1539 # We use "git apply" to apply the patch instead of "patch" so that we can
1540 # pick up file adds.
1541 # The --index flag means: also insert into the index (so we catch adds).
1542 cmd = ['git', 'apply', '--index', '-p0']
1543 if options.reject:
1544 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001545 try:
1546 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1547 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001548 DieWithError('Failed to apply the patch')
1549
1550 # If we had an issue, commit the current state and register the issue.
1551 if not options.nocommit:
1552 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1553 cl = Changelist()
1554 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001555 cl.SetPatchset(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001556 print "Committed patch."
1557 else:
1558 print "Patch applied to index."
1559 return 0
1560
1561
1562def CMDrebase(parser, args):
1563 """rebase current branch on top of svn repo"""
1564 # Provide a wrapper for git svn rebase to help avoid accidental
1565 # git svn dcommit.
1566 # It's the only command that doesn't use parser at all since we just defer
1567 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001568 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001569
1570
1571def GetTreeStatus():
1572 """Fetches the tree status and returns either 'open', 'closed',
1573 'unknown' or 'unset'."""
1574 url = settings.GetTreeStatusUrl(error_ok=True)
1575 if url:
1576 status = urllib2.urlopen(url).read().lower()
1577 if status.find('closed') != -1 or status == '0':
1578 return 'closed'
1579 elif status.find('open') != -1 or status == '1':
1580 return 'open'
1581 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582 return 'unset'
1583
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001584
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001585def GetTreeStatusReason():
1586 """Fetches the tree status from a json url and returns the message
1587 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001588 url = settings.GetTreeStatusUrl()
1589 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001590 connection = urllib2.urlopen(json_url)
1591 status = json.loads(connection.read())
1592 connection.close()
1593 return status['message']
1594
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001595
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001596def CMDtree(parser, args):
1597 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001598 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001599 status = GetTreeStatus()
1600 if 'unset' == status:
1601 print 'You must configure your tree status URL by running "git cl config".'
1602 return 2
1603
1604 print "The tree is %s" % status
1605 print
1606 print GetTreeStatusReason()
1607 if status != 'open':
1608 return 1
1609 return 0
1610
1611
maruel@chromium.org15192402012-09-06 12:38:29 +00001612def CMDtry(parser, args):
1613 """Triggers a try job through Rietveld."""
1614 group = optparse.OptionGroup(parser, "Try job options")
1615 group.add_option(
1616 "-b", "--bot", action="append",
1617 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1618 "times to specify multiple builders. ex: "
1619 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1620 "the try server waterfall for the builders name and the tests "
1621 "available. Can also be used to specify gtest_filter, e.g. "
1622 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1623 group.add_option(
1624 "-r", "--revision",
1625 help="Revision to use for the try job; default: the "
1626 "revision will be determined by the try server; see "
1627 "its waterfall for more info")
1628 group.add_option(
1629 "-c", "--clobber", action="store_true", default=False,
1630 help="Force a clobber before building; e.g. don't do an "
1631 "incremental build")
1632 group.add_option(
1633 "--project",
1634 help="Override which project to use. Projects are defined "
1635 "server-side to define what default bot set to use")
1636 group.add_option(
1637 "-t", "--testfilter", action="append", default=[],
1638 help=("Apply a testfilter to all the selected builders. Unless the "
1639 "builders configurations are similar, use multiple "
1640 "--bot <builder>:<test> arguments."))
1641 group.add_option(
1642 "-n", "--name", help="Try job name; default to current branch name")
1643 parser.add_option_group(group)
1644 options, args = parser.parse_args(args)
1645
1646 if args:
1647 parser.error('Unknown arguments: %s' % args)
1648
1649 cl = Changelist()
1650 if not cl.GetIssue():
1651 parser.error('Need to upload first')
1652
1653 if not options.name:
1654 options.name = cl.GetBranch()
1655
1656 # Process --bot and --testfilter.
1657 if not options.bot:
1658 # Get try slaves from PRESUBMIT.py files if not specified.
1659 change = cl.GetChange(cl.GetUpstreamBranch(), None)
1660 options.bot = presubmit_support.DoGetTrySlaves(
1661 change,
1662 change.LocalPaths(),
1663 settings.GetRoot(),
1664 None,
1665 None,
1666 options.verbose,
1667 sys.stdout)
1668 if not options.bot:
1669 parser.error('No default try builder to try, use --bot')
1670
1671 builders_and_tests = {}
1672 for bot in options.bot:
1673 if ':' in bot:
1674 builder, tests = bot.split(':', 1)
1675 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1676 elif ',' in bot:
1677 parser.error('Specify one bot per --bot flag')
1678 else:
1679 builders_and_tests.setdefault(bot, []).append('defaulttests')
1680
1681 if options.testfilter:
1682 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1683 builders_and_tests = dict(
1684 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1685 if t != ['compile'])
1686
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001687 if any('triggered' in b for b in builders_and_tests):
1688 print >> sys.stderr, (
1689 'ERROR You are trying to send a job to a triggered bot. This type of'
1690 ' bot requires an\ninitial job from a parent (usually a builder). '
1691 'Instead send your job to the parent.\n'
1692 'Bot list: %s' % builders_and_tests)
1693 return 1
1694
maruel@chromium.org15192402012-09-06 12:38:29 +00001695 patchset = cl.GetPatchset()
1696 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001697 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001698
1699 cl.RpcServer().trigger_try_jobs(
1700 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1701 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001702 print('Tried jobs on:')
1703 length = max(len(builder) for builder in builders_and_tests)
1704 for builder in sorted(builders_and_tests):
1705 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001706 return 0
1707
1708
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001709@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001710def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001711 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001712 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001713 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001714 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001715 return 0
1716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001717 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001718 if args:
1719 # One arg means set upstream branch.
1720 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1721 cl = Changelist()
1722 print "Upstream branch set to " + cl.GetUpstreamBranch()
1723 else:
1724 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725 return 0
1726
1727
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001728def CMDset_commit(parser, args):
1729 """set the commit bit"""
1730 _, args = parser.parse_args(args)
1731 if args:
1732 parser.error('Unrecognized args: %s' % ' '.join(args))
1733 cl = Changelist()
1734 cl.SetFlag('commit', '1')
1735 return 0
1736
1737
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001738def Command(name):
1739 return getattr(sys.modules[__name__], 'CMD' + name, None)
1740
1741
1742def CMDhelp(parser, args):
1743 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001744 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001745 if len(args) == 1:
1746 return main(args + ['--help'])
1747 parser.print_help()
1748 return 0
1749
1750
1751def GenUsage(parser, command):
1752 """Modify an OptParse object with the function's documentation."""
1753 obj = Command(command)
1754 more = getattr(obj, 'usage_more', '')
1755 if command == 'help':
1756 command = '<command>'
1757 else:
1758 # OptParser.description prefer nicely non-formatted strings.
1759 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1760 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1761
1762
1763def main(argv):
1764 """Doesn't parse the arguments here, just find the right subcommand to
1765 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001766 if sys.hexversion < 0x02060000:
1767 print >> sys.stderr, (
1768 '\nYour python version %s is unsupported, please upgrade.\n' %
1769 sys.version.split(' ', 1)[0])
1770 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001771 # Reload settings.
1772 global settings
1773 settings = Settings()
1774
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001775 # Do it late so all commands are listed.
1776 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1777 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1778 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1779
1780 # Create the option parse and add --verbose support.
1781 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001782 parser.add_option(
1783 '-v', '--verbose', action='count', default=0,
1784 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001785 old_parser_args = parser.parse_args
1786 def Parse(args):
1787 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001788 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001790 elif options.verbose:
1791 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001792 else:
1793 logging.basicConfig(level=logging.WARNING)
1794 return options, args
1795 parser.parse_args = Parse
1796
1797 if argv:
1798 command = Command(argv[0])
1799 if command:
1800 # "fix" the usage and the description now that we know the subcommand.
1801 GenUsage(parser, argv[0])
1802 try:
1803 return command(parser, argv[1:])
1804 except urllib2.HTTPError, e:
1805 if e.code != 500:
1806 raise
1807 DieWithError(
1808 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1809 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1810
1811 # Not a known command. Default to help.
1812 GenUsage(parser, 'help')
1813 return CMDhelp(parser, argv)
1814
1815
1816if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001817 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001818 sys.exit(main(sys.argv[1:]))