blob: 0718b35df4cc3e451ab41f27adb3c29f12dbde21 [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000010import logging
11import optparse
12import os
13import re
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000015import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000016import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import urllib2
18
19try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000020 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021except ImportError:
22 pass
23
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000024try:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000025 import simplejson as json # pylint: disable=F0401
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000026except ImportError:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000028 import json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029 except ImportError:
30 # Fall back to the packaged version.
maruel@chromium.orgb35c00c2011-03-31 00:43:35 +000031 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgfe79c312011-04-01 20:15:52 +000032 import simplejson as json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
34
35from third_party import upload
36import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000040import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
44
45
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000046DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000047POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
49
maruel@chromium.org90541732011-04-01 17:54:18 +000050
maruel@chromium.orgddd59412011-11-30 14:20:38 +000051# Initialized in main()
52settings = None
53
54
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000056 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057 sys.exit(1)
58
59
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061 try:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062 return subprocess2.check_output(args, shell=False, **kwargs)
63 except subprocess2.CalledProcessError, e:
64 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 'Command "%s" failed.\n%s' % (
67 ' '.join(args), error_message or e.stdout or ''))
68 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
70
71def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 """Returns stdout."""
73 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
75
76def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077 """Returns return code and stdout."""
78 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
79 return code, out[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def usage(more):
83 def hook(fn):
84 fn.usage_more = more
85 return fn
86 return hook
87
88
maruel@chromium.org90541732011-04-01 17:54:18 +000089def ask_for_data(prompt):
90 try:
91 return raw_input(prompt)
92 except KeyboardInterrupt:
93 # Hide the exception.
94 sys.exit(1)
95
96
bauerb@chromium.org866276c2011-03-18 20:09:31 +000097def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
98 """Return the corresponding git ref if |base_url| together with |glob_spec|
99 matches the full |url|.
100
101 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
102 """
103 fetch_suburl, as_ref = glob_spec.split(':')
104 if allow_wildcards:
105 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
106 if glob_match:
107 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
108 # "branches/{472,597,648}/src:refs/remotes/svn/*".
109 branch_re = re.escape(base_url)
110 if glob_match.group(1):
111 branch_re += '/' + re.escape(glob_match.group(1))
112 wildcard = glob_match.group(2)
113 if wildcard == '*':
114 branch_re += '([^/]*)'
115 else:
116 # Escape and replace surrounding braces with parentheses and commas
117 # with pipe symbols.
118 wildcard = re.escape(wildcard)
119 wildcard = re.sub('^\\\\{', '(', wildcard)
120 wildcard = re.sub('\\\\,', '|', wildcard)
121 wildcard = re.sub('\\\\}$', ')', wildcard)
122 branch_re += wildcard
123 if glob_match.group(3):
124 branch_re += re.escape(glob_match.group(3))
125 match = re.match(branch_re, url)
126 if match:
127 return re.sub('\*$', match.group(1), as_ref)
128
129 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
130 if fetch_suburl:
131 full_url = base_url + '/' + fetch_suburl
132 else:
133 full_url = base_url
134 if full_url == url:
135 return as_ref
136 return None
137
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000139class Settings(object):
140 def __init__(self):
141 self.default_server = None
142 self.cc = None
143 self.root = None
144 self.is_git_svn = None
145 self.svn_branch = None
146 self.tree_status_url = None
147 self.viewvc_url = None
148 self.updated = False
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000149 self.did_migrate_check = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150
151 def LazyUpdateIfNeeded(self):
152 """Updates the settings from a codereview.settings file, if available."""
153 if not self.updated:
154 cr_settings_file = FindCodereviewSettingsFile()
155 if cr_settings_file:
156 LoadCodereviewSettingsFromFile(cr_settings_file)
157 self.updated = True
158
159 def GetDefaultServerUrl(self, error_ok=False):
160 if not self.default_server:
161 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000162 self.default_server = gclient_utils.UpgradeToHttps(
163 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000164 if error_ok:
165 return self.default_server
166 if not self.default_server:
167 error_message = ('Could not find settings file. You must configure '
168 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000169 self.default_server = gclient_utils.UpgradeToHttps(
170 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000171 return self.default_server
172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000173 def GetRoot(self):
174 if not self.root:
175 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
176 return self.root
177
178 def GetIsGitSvn(self):
179 """Return true if this repo looks like it's using git-svn."""
180 if self.is_git_svn is None:
181 # If you have any "svn-remote.*" config keys, we think you're using svn.
182 self.is_git_svn = RunGitWithCode(
183 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
184 return self.is_git_svn
185
186 def GetSVNBranch(self):
187 if self.svn_branch is None:
188 if not self.GetIsGitSvn():
189 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
190
191 # Try to figure out which remote branch we're based on.
192 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000193 # 1) iterate through our branch history and find the svn URL.
194 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000195
196 # regexp matching the git-svn line that contains the URL.
197 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
198
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000199 # We don't want to go through all of history, so read a line from the
200 # pipe at a time.
201 # The -100 is an arbitrary limit so we don't search forever.
202 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000203 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000204 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000205 for line in proc.stdout:
206 match = git_svn_re.match(line)
207 if match:
208 url = match.group(1)
209 proc.stdout.close() # Cut pipe.
210 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000211
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000212 if url:
213 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
214 remotes = RunGit(['config', '--get-regexp',
215 r'^svn-remote\..*\.url']).splitlines()
216 for remote in remotes:
217 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000218 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000219 remote = match.group(1)
220 base_url = match.group(2)
221 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000222 ['config', 'svn-remote.%s.fetch' % remote],
223 error_ok=True).strip()
224 if fetch_spec:
225 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
226 if self.svn_branch:
227 break
228 branch_spec = RunGit(
229 ['config', 'svn-remote.%s.branches' % remote],
230 error_ok=True).strip()
231 if branch_spec:
232 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
233 if self.svn_branch:
234 break
235 tag_spec = RunGit(
236 ['config', 'svn-remote.%s.tags' % remote],
237 error_ok=True).strip()
238 if tag_spec:
239 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
240 if self.svn_branch:
241 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000242
243 if not self.svn_branch:
244 DieWithError('Can\'t guess svn branch -- try specifying it on the '
245 'command line')
246
247 return self.svn_branch
248
249 def GetTreeStatusUrl(self, error_ok=False):
250 if not self.tree_status_url:
251 error_message = ('You must configure your tree status URL by running '
252 '"git cl config".')
253 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
254 error_ok=error_ok,
255 error_message=error_message)
256 return self.tree_status_url
257
258 def GetViewVCUrl(self):
259 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000260 self.viewvc_url = gclient_utils.UpgradeToHttps(
261 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000262 return self.viewvc_url
263
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000264 def GetDefaultCCList(self):
265 return self._GetConfig('rietveld.cc', error_ok=True)
266
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000267 def _GetConfig(self, param, **kwargs):
268 self.LazyUpdateIfNeeded()
269 return RunGit(['config', param], **kwargs).strip()
270
271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272def CheckForMigration():
273 """Migrate from the old issue format, if found.
274
275 We used to store the branch<->issue mapping in a file in .git, but it's
276 better to store it in the .git/config, since deleting a branch deletes that
277 branch's entry there.
278 """
279
280 # Don't run more than once.
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000281 if settings.did_migrate_check:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000282 return
283
284 gitdir = RunGit(['rev-parse', '--git-dir']).strip()
285 storepath = os.path.join(gitdir, 'cl-mapping')
286 if os.path.exists(storepath):
287 print "old-style git-cl mapping file (%s) found; migrating." % storepath
288 store = open(storepath, 'r')
289 for line in store:
290 branch, issue = line.strip().split()
291 RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch),
292 issue])
293 store.close()
294 os.remove(storepath)
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000295 settings.did_migrate_check = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000296
297
298def ShortBranchName(branch):
299 """Convert a name like 'refs/heads/foo' to just 'foo'."""
300 return branch.replace('refs/heads/', '')
301
302
303class Changelist(object):
304 def __init__(self, branchref=None):
305 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000306 global settings
307 if not settings:
308 # Happens when git_cl.py is used as a utility library.
309 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 settings.GetDefaultServerUrl()
311 self.branchref = branchref
312 if self.branchref:
313 self.branch = ShortBranchName(self.branchref)
314 else:
315 self.branch = None
316 self.rietveld_server = None
317 self.upstream_branch = None
318 self.has_issue = False
319 self.issue = None
320 self.has_description = False
321 self.description = None
322 self.has_patchset = False
323 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000324 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000325 self.cc = None
326 self.watchers = ()
327
328 def GetCCList(self):
329 """Return the users cc'd on this CL.
330
331 Return is a string suitable for passing to gcl with the --cc flag.
332 """
333 if self.cc is None:
334 base_cc = settings .GetDefaultCCList()
335 more_cc = ','.join(self.watchers)
336 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
337 return self.cc
338
339 def SetWatchers(self, watchers):
340 """Set the list of email addresses that should be cc'd based on the changed
341 files in this CL.
342 """
343 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000344
345 def GetBranch(self):
346 """Returns the short branch name, e.g. 'master'."""
347 if not self.branch:
348 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
349 self.branch = ShortBranchName(self.branchref)
350 return self.branch
351
352 def GetBranchRef(self):
353 """Returns the full branch name, e.g. 'refs/heads/master'."""
354 self.GetBranch() # Poke the lazy loader.
355 return self.branchref
356
357 def FetchUpstreamTuple(self):
358 """Returns a tuple containg remote and remote ref,
359 e.g. 'origin', 'refs/heads/master'
360 """
361 remote = '.'
362 branch = self.GetBranch()
363 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
364 error_ok=True).strip()
365 if upstream_branch:
366 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
367 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000368 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
369 error_ok=True).strip()
370 if upstream_branch:
371 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000372 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000373 # Fall back on trying a git-svn upstream branch.
374 if settings.GetIsGitSvn():
375 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000376 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000377 # Else, try to guess the origin remote.
378 remote_branches = RunGit(['branch', '-r']).split()
379 if 'origin/master' in remote_branches:
380 # Fall back on origin/master if it exits.
381 remote = 'origin'
382 upstream_branch = 'refs/heads/master'
383 elif 'origin/trunk' in remote_branches:
384 # Fall back on origin/trunk if it exists. Generally a shared
385 # git-svn clone
386 remote = 'origin'
387 upstream_branch = 'refs/heads/trunk'
388 else:
389 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000390Either pass complete "git diff"-style arguments, like
391 git cl upload origin/master
392or verify this branch is set up to track another (via the --track argument to
393"git checkout -b ...").""")
394
395 return remote, upstream_branch
396
397 def GetUpstreamBranch(self):
398 if self.upstream_branch is None:
399 remote, upstream_branch = self.FetchUpstreamTuple()
400 if remote is not '.':
401 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
402 self.upstream_branch = upstream_branch
403 return self.upstream_branch
404
405 def GetRemoteUrl(self):
406 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
407
408 Returns None if there is no remote.
409 """
410 remote = self.FetchUpstreamTuple()[0]
411 if remote == '.':
412 return None
413 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
414
415 def GetIssue(self):
416 if not self.has_issue:
417 CheckForMigration()
418 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
419 if issue:
420 self.issue = issue
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000421 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000422 ['config', self._RietveldServer()], error_ok=True).strip())
423 else:
424 self.issue = None
425 if not self.rietveld_server:
426 self.rietveld_server = settings.GetDefaultServerUrl()
427 self.has_issue = True
428 return self.issue
429
430 def GetRietveldServer(self):
431 self.GetIssue()
432 return self.rietveld_server
433
434 def GetIssueURL(self):
435 """Get the URL for a particular issue."""
436 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
437
438 def GetDescription(self, pretty=False):
439 if not self.has_description:
440 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000441 issue = int(self.GetIssue())
442 try:
443 self.description = self.RpcServer().get_description(issue).strip()
444 except urllib2.HTTPError, e:
445 if e.code == 404:
446 DieWithError(
447 ('\nWhile fetching the description for issue %d, received a '
448 '404 (not found)\n'
449 'error. It is likely that you deleted this '
450 'issue on the server. If this is the\n'
451 'case, please run\n\n'
452 ' git cl issue 0\n\n'
453 'to clear the association with the deleted issue. Then run '
454 'this command again.') % issue)
455 else:
456 DieWithError(
457 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 self.has_description = True
459 if pretty:
460 wrapper = textwrap.TextWrapper()
461 wrapper.initial_indent = wrapper.subsequent_indent = ' '
462 return wrapper.fill(self.description)
463 return self.description
464
465 def GetPatchset(self):
466 if not self.has_patchset:
467 patchset = RunGit(['config', self._PatchsetSetting()],
468 error_ok=True).strip()
469 if patchset:
470 self.patchset = patchset
471 else:
472 self.patchset = None
473 self.has_patchset = True
474 return self.patchset
475
476 def SetPatchset(self, patchset):
477 """Set this branch's patchset. If patchset=0, clears the patchset."""
478 if patchset:
479 RunGit(['config', self._PatchsetSetting(), str(patchset)])
480 else:
481 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000482 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000483 self.has_patchset = False
484
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000485 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000486 patchset = self.RpcServer().get_issue_properties(
487 int(issue), False)['patchsets'][-1]
488 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000489 '/download/issue%s_%s.diff' % (issue, patchset))
490
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 def SetIssue(self, issue):
492 """Set this branch's issue. If issue=0, clears the issue."""
493 if issue:
494 RunGit(['config', self._IssueSetting(), str(issue)])
495 if self.rietveld_server:
496 RunGit(['config', self._RietveldServer(), self.rietveld_server])
497 else:
498 RunGit(['config', '--unset', self._IssueSetting()])
499 self.SetPatchset(0)
500 self.has_issue = False
501
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000502 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000503 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
504 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000505
506 # We use the sha1 of HEAD as a name of this change.
507 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000508 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000509 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000510 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000511 except subprocess2.CalledProcessError:
512 DieWithError(
513 ('\nFailed to diff against upstream branch %s!\n\n'
514 'This branch probably doesn\'t exist anymore. To reset the\n'
515 'tracking branch, please run\n'
516 ' git branch --set-upstream %s trunk\n'
517 'replacing trunk with origin/master or the relevant branch') %
518 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000519
520 issue = ConvertToInteger(self.GetIssue())
521 patchset = ConvertToInteger(self.GetPatchset())
522 if issue:
523 description = self.GetDescription()
524 else:
525 # If the change was never uploaded, use the log messages of all commits
526 # up to the branch point, as git cl upload will prefill the description
527 # with these log messages.
528 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
529 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000530
531 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000532 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000533 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000534 name,
535 description,
536 absroot,
537 files,
538 issue,
539 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000540 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000541
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000542 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
543 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
544 change = self.GetChange(upstream_branch, author)
545
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000546 # Apply watchlists on upload.
547 if not committing:
548 watchlist = watchlists.Watchlists(change.RepositoryRoot())
549 files = [f.LocalPath() for f in change.AffectedFiles()]
550 self.SetWatchers(watchlist.GetWatchersForPaths(files))
551
552 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000553 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000554 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000555 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000556 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000557 except presubmit_support.PresubmitFailure, e:
558 DieWithError(
559 ('%s\nMaybe your depot_tools is out of date?\n'
560 'If all fails, contact maruel@') % e)
561
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000562 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000563 """Updates the description and closes the issue."""
564 issue = int(self.GetIssue())
565 self.RpcServer().update_description(issue, self.description)
566 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000567
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000568 def SetFlag(self, flag, value):
569 """Patchset must match."""
570 if not self.GetPatchset():
571 DieWithError('The patchset needs to match. Send another patchset.')
572 try:
573 return self.RpcServer().set_flag(
574 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
575 except urllib2.HTTPError, e:
576 if e.code == 404:
577 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
578 if e.code == 403:
579 DieWithError(
580 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
581 'match?') % (self.GetIssue(), self.GetPatchset()))
582 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000584 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 """Returns an upload.RpcServer() to access this review's rietveld instance.
586 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000587 if not self._rpc_server:
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000588 self.GetIssue()
589 self._rpc_server = rietveld.Rietveld(self.rietveld_server, None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000590 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591
592 def _IssueSetting(self):
593 """Return the git setting that stores this change's issue."""
594 return 'branch.%s.rietveldissue' % self.GetBranch()
595
596 def _PatchsetSetting(self):
597 """Return the git setting that stores this change's most recent patchset."""
598 return 'branch.%s.rietveldpatchset' % self.GetBranch()
599
600 def _RietveldServer(self):
601 """Returns the git setting that stores this change's rietveld server."""
602 return 'branch.%s.rietveldserver' % self.GetBranch()
603
604
605def GetCodereviewSettingsInteractively():
606 """Prompt the user for settings."""
607 server = settings.GetDefaultServerUrl(error_ok=True)
608 prompt = 'Rietveld server (host[:port])'
609 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000610 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 if not server and not newserver:
612 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000613 if newserver:
614 newserver = gclient_utils.UpgradeToHttps(newserver)
615 if newserver != server:
616 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000618 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 prompt = caption
620 if initial:
621 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000622 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 if new_val == 'x':
624 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000625 elif new_val:
626 if is_url:
627 new_val = gclient_utils.UpgradeToHttps(new_val)
628 if new_val != initial:
629 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000631 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000633 'tree-status-url', False)
634 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635
636 # TODO: configure a default branch to diff against, rather than this
637 # svn-based hackery.
638
639
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000640class ChangeDescription(object):
641 """Contains a parsed form of the change description."""
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000642 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000643 self.log_desc = log_desc
644 self.reviewers = reviewers
645 self.description = self.log_desc
646
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000647 def Prompt(self):
648 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000649# This will displayed on the codereview site.
650# The first line will also be used as the subject of the review.
651"""
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000652 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000653 if ('\nR=' not in self.description and
654 '\nTBR=' not in self.description and
655 self.reviewers):
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000656 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000657 if '\nBUG=' not in self.description:
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000658 content += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000659 if '\nTEST=' not in self.description:
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000660 content += '\nTEST='
661 content = content.rstrip('\n') + '\n'
662 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000663 if not content:
664 DieWithError('Running editor failed')
665 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
666 if not content:
667 DieWithError('No CL description, aborting')
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000668 self.description = content.strip('\n') + '\n'
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000669
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000670 def ParseDescription(self):
671 """Updates the list of reviewers."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000672 # Retrieves all reviewer lines
673 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000674 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000675 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000676 if reviewers:
677 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000678
679 def IsEmpty(self):
680 return not self.description
681
682
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000683def FindCodereviewSettingsFile(filename='codereview.settings'):
684 """Finds the given file starting in the cwd and going up.
685
686 Only looks up to the top of the repository unless an
687 'inherit-review-settings-ok' file exists in the root of the repository.
688 """
689 inherit_ok_file = 'inherit-review-settings-ok'
690 cwd = os.getcwd()
691 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
692 if os.path.isfile(os.path.join(root, inherit_ok_file)):
693 root = '/'
694 while True:
695 if filename in os.listdir(cwd):
696 if os.path.isfile(os.path.join(cwd, filename)):
697 return open(os.path.join(cwd, filename))
698 if cwd == root:
699 break
700 cwd = os.path.dirname(cwd)
701
702
703def LoadCodereviewSettingsFromFile(fileobj):
704 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000705 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000707 def SetProperty(name, setting, unset_error_ok=False):
708 fullname = 'rietveld.' + name
709 if setting in keyvals:
710 RunGit(['config', fullname, keyvals[setting]])
711 else:
712 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
713
714 SetProperty('server', 'CODE_REVIEW_SERVER')
715 # Only server setting is required. Other settings can be absent.
716 # In that case, we ignore errors raised during option deletion attempt.
717 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
718 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
719 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
720
721 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
722 #should be of the form
723 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
724 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
725 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
726 keyvals['ORIGIN_URL_CONFIG']])
727
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000728
729@usage('[repo root containing codereview.settings]')
730def CMDconfig(parser, args):
731 """edit configuration for this tree"""
732
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000733 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000734 if len(args) == 0:
735 GetCodereviewSettingsInteractively()
736 return 0
737
738 url = args[0]
739 if not url.endswith('codereview.settings'):
740 url = os.path.join(url, 'codereview.settings')
741
742 # Load code review settings and download hooks (if available).
743 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
744 return 0
745
746
747def CMDstatus(parser, args):
748 """show status of changelists"""
749 parser.add_option('--field',
750 help='print only specific field (desc|id|patch|url)')
751 (options, args) = parser.parse_args(args)
752
753 # TODO: maybe make show_branches a flag if necessary.
754 show_branches = not options.field
755
756 if show_branches:
757 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
758 if branches:
759 print 'Branches associated with reviews:'
760 for branch in sorted(branches.splitlines()):
761 cl = Changelist(branchref=branch)
762 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
763
764 cl = Changelist()
765 if options.field:
766 if options.field.startswith('desc'):
767 print cl.GetDescription()
768 elif options.field == 'id':
769 issueid = cl.GetIssue()
770 if issueid:
771 print issueid
772 elif options.field == 'patch':
773 patchset = cl.GetPatchset()
774 if patchset:
775 print patchset
776 elif options.field == 'url':
777 url = cl.GetIssueURL()
778 if url:
779 print url
780 else:
781 print
782 print 'Current branch:',
783 if not cl.GetIssue():
784 print 'no issue assigned.'
785 return 0
786 print cl.GetBranch()
787 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
788 print 'Issue description:'
789 print cl.GetDescription(pretty=True)
790 return 0
791
792
793@usage('[issue_number]')
794def CMDissue(parser, args):
795 """Set or display the current code review issue number.
796
797 Pass issue number 0 to clear the current issue.
798"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000799 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800
801 cl = Changelist()
802 if len(args) > 0:
803 try:
804 issue = int(args[0])
805 except ValueError:
806 DieWithError('Pass a number to set the issue or none to list it.\n'
807 'Maybe you want to run git cl status?')
808 cl.SetIssue(issue)
809 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
810 return 0
811
812
813def CreateDescriptionFromLog(args):
814 """Pulls out the commit log to use as a base for the CL description."""
815 log_args = []
816 if len(args) == 1 and not args[0].endswith('.'):
817 log_args = [args[0] + '..']
818 elif len(args) == 1 and args[0].endswith('...'):
819 log_args = [args[0][:-1]]
820 elif len(args) == 2:
821 log_args = [args[0] + '..' + args[1]]
822 else:
823 log_args = args[:] # Hope for the best!
824 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
825
826
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000827def ConvertToInteger(inputval):
828 """Convert a string to integer, but returns either an int or None."""
829 try:
830 return int(inputval)
831 except (TypeError, ValueError):
832 return None
833
834
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000835def CMDpresubmit(parser, args):
836 """run presubmit tests on the current changelist"""
837 parser.add_option('--upload', action='store_true',
838 help='Run upload hook instead of the push/dcommit hook')
839 (options, args) = parser.parse_args(args)
840
841 # Make sure index is up-to-date before running diff-index.
842 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
843 if RunGit(['diff-index', 'HEAD']):
844 # TODO(maruel): Is this really necessary?
845 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
846 return 1
847
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000848 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000849 if args:
850 base_branch = args[0]
851 else:
852 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000853 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000855 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000856 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000857 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000858 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859
860
861@usage('[args to "git diff"]')
862def CMDupload(parser, args):
863 """upload the current changelist to codereview"""
864 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
865 help='bypass upload presubmit hook')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000866 parser.add_option('-f', action='store_true', dest='force',
867 help="force yes to questions (don't prompt)")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000868 parser.add_option('-m', dest='message', help='message for patch')
869 parser.add_option('-r', '--reviewers',
870 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000871 parser.add_option('--cc',
872 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873 parser.add_option('--send-mail', action='store_true',
874 help='send email to reviewer immediately')
875 parser.add_option("--emulate_svn_auto_props", action="store_true",
876 dest="emulate_svn_auto_props",
877 help="Emulate Subversion's auto properties feature.")
878 parser.add_option("--desc_from_logs", action="store_true",
879 dest="from_logs",
880 help="""Squashes git commit logs into change description and
881 uses message as subject""")
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000882 parser.add_option('-c', '--use-commit-queue', action='store_true',
883 help='tell the commit queue to commit this patchset')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000884 (options, args) = parser.parse_args(args)
885
886 # Make sure index is up-to-date before running diff-index.
887 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
888 if RunGit(['diff-index', 'HEAD']):
889 print 'Cannot upload with a dirty tree. You must commit locally first.'
890 return 1
891
892 cl = Changelist()
893 if args:
894 base_branch = args[0]
895 else:
896 # Default to diffing against the "upstream" branch.
897 base_branch = cl.GetUpstreamBranch()
898 args = [base_branch + "..."]
899
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000900 if not options.bypass_hooks:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000901 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000902 may_prompt=not options.force,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000903 verbose=options.verbose,
904 author=None)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000905 if not hook_results.should_continue():
906 return 1
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000907 if not options.reviewers and hook_results.reviewers:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000908 options.reviewers = hook_results.reviewers
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000909
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000910 # --no-ext-diff is broken in some versions of Git, so try to work around
911 # this by overriding the environment (but there is still a problem if the
912 # git config key "diff.external" is used).
913 env = os.environ.copy()
914 if 'GIT_EXTERNAL_DIFF' in env:
915 del env['GIT_EXTERNAL_DIFF']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000916 subprocess2.call(
917 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000918
919 upload_args = ['--assume_yes'] # Don't ask about untracked files.
920 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000921 if options.emulate_svn_auto_props:
922 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923
924 change_desc = None
925
926 if cl.GetIssue():
927 if options.message:
928 upload_args.extend(['--message', options.message])
929 upload_args.extend(['--issue', cl.GetIssue()])
930 print ("This branch is associated with issue %s. "
931 "Adding patch to that issue." % cl.GetIssue())
932 else:
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000933 message = options.message or CreateDescriptionFromLog(args)
934 change_desc = ChangeDescription(message, options.reviewers)
935 if not options.force:
936 change_desc.Prompt()
937 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000938
939 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000940 print "Description is empty; aborting."
941 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000942
maruel@chromium.org2b40b892012-01-25 14:33:36 +0000943 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000944 if change_desc.reviewers:
945 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000946 if options.send_mail:
947 if not change_desc.reviewers:
948 DieWithError("Must specify reviewers to send email.")
949 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000950 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000951 if cc:
952 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000953
954 # Include the upstream repo's URL in the change -- this is useful for
955 # projects that have their source spread across multiple repos.
956 remote_url = None
957 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000958 # URL is dependent on the current directory.
959 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000960 if data:
961 keys = dict(line.split(': ', 1) for line in data.splitlines()
962 if ': ' in line)
963 remote_url = keys.get('URL', None)
964 else:
965 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
966 remote_url = (cl.GetRemoteUrl() + '@'
967 + cl.GetUpstreamBranch().split('/')[-1])
968 if remote_url:
969 upload_args.extend(['--base_url', remote_url])
970
971 try:
972 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000973 except KeyboardInterrupt:
974 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975 except:
976 # If we got an exception after the user typed a description for their
977 # change, back up the description before re-raising.
978 if change_desc:
979 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
980 print '\nGot exception while uploading -- saving description to %s\n' \
981 % backup_path
982 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000983 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000984 backup_file.close()
985 raise
986
987 if not cl.GetIssue():
988 cl.SetIssue(issue)
989 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000990
991 if options.use_commit_queue:
992 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000993 return 0
994
995
996def SendUpstream(parser, args, cmd):
997 """Common code for CmdPush and CmdDCommit
998
999 Squashed commit into a single.
1000 Updates changelog with metadata (e.g. pointer to review).
1001 Pushes/dcommits the code upstream.
1002 Updates review and closes.
1003 """
1004 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1005 help='bypass upload presubmit hook')
1006 parser.add_option('-m', dest='message',
1007 help="override review description")
1008 parser.add_option('-f', action='store_true', dest='force',
1009 help="force yes to questions (don't prompt)")
1010 parser.add_option('-c', dest='contributor',
1011 help="external contributor for patch (appended to " +
1012 "description and used as author for git). Should be " +
1013 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 (options, args) = parser.parse_args(args)
1015 cl = Changelist()
1016
1017 if not args or cmd == 'push':
1018 # Default to merging against our best guess of the upstream branch.
1019 args = [cl.GetUpstreamBranch()]
1020
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001021 if options.contributor:
1022 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1023 print "Please provide contibutor as 'First Last <email@example.com>'"
1024 return 1
1025
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001026 base_branch = args[0]
1027
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001028 # Make sure index is up-to-date before running diff-index.
1029 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001030 if RunGit(['diff-index', 'HEAD']):
1031 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1032 return 1
1033
1034 # This rev-list syntax means "show all commits not in my branch that
1035 # are in base_branch".
1036 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1037 base_branch]).splitlines()
1038 if upstream_commits:
1039 print ('Base branch "%s" has %d commits '
1040 'not in this branch.' % (base_branch, len(upstream_commits)))
1041 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1042 return 1
1043
1044 if cmd == 'dcommit':
1045 # This is the revision `svn dcommit` will commit on top of.
1046 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1047 '--pretty=format:%H'])
1048 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1049 if extra_commits:
1050 print ('This branch has %d additional commits not upstreamed yet.'
1051 % len(extra_commits.splitlines()))
1052 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1053 'before attempting to %s.' % (base_branch, cmd))
1054 return 1
1055
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001056 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001057 author = None
1058 if options.contributor:
1059 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001060 hook_results = cl.RunHook(
1061 committing=True,
1062 upstream_branch=base_branch,
1063 may_prompt=not options.force,
1064 verbose=options.verbose,
1065 author=author)
1066 if not hook_results.should_continue():
1067 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001068
1069 if cmd == 'dcommit':
1070 # Check the tree status if the tree status URL is set.
1071 status = GetTreeStatus()
1072 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001073 print('The tree is closed. Please wait for it to reopen. Use '
1074 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075 return 1
1076 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001077 print('Unable to determine tree status. Please verify manually and '
1078 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001079 else:
1080 breakpad.SendStack(
1081 'GitClHooksBypassedCommit',
1082 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001083 (cl.GetRietveldServer(), cl.GetIssue()),
1084 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001085
1086 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001087 if not description and cl.GetIssue():
1088 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001089
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001090 if not description:
1091 print 'No description set.'
1092 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1093 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001095 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001097
1098 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099 description += "\nPatch from %s." % options.contributor
1100 print 'Description:', repr(description)
1101
1102 branches = [base_branch, cl.GetBranchRef()]
1103 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001104 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001105 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106
1107 # We want to squash all this branch's commits into one commit with the
1108 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001109 # We do this by doing a "reset --soft" to the base branch (which keeps
1110 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001111 MERGE_BRANCH = 'git-cl-commit'
1112 # Delete the merge branch if it already exists.
1113 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1114 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1115 RunGit(['branch', '-D', MERGE_BRANCH])
1116
1117 # We might be in a directory that's present in this branch but not in the
1118 # trunk. Move up to the top of the tree so that git commands that expect a
1119 # valid CWD won't fail after we check out the merge branch.
1120 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1121 if rel_base_path:
1122 os.chdir(rel_base_path)
1123
1124 # Stuff our change into the merge branch.
1125 # We wrap in a try...finally block so if anything goes wrong,
1126 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001127 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001129 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1130 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001131 if options.contributor:
1132 RunGit(['commit', '--author', options.contributor, '-m', description])
1133 else:
1134 RunGit(['commit', '-m', description])
1135 if cmd == 'push':
1136 # push the merge branch.
1137 remote, branch = cl.FetchUpstreamTuple()
1138 retcode, output = RunGitWithCode(
1139 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1140 logging.debug(output)
1141 else:
1142 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001143 retcode, output = RunGitWithCode(['svn', 'dcommit',
1144 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 finally:
1146 # And then swap back to the original branch and clean up.
1147 RunGit(['checkout', '-q', cl.GetBranch()])
1148 RunGit(['branch', '-D', MERGE_BRANCH])
1149
1150 if cl.GetIssue():
1151 if cmd == 'dcommit' and 'Committed r' in output:
1152 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1153 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001154 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1155 for l in output.splitlines(False))
1156 match = filter(None, match)
1157 if len(match) != 1:
1158 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1159 output)
1160 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161 else:
1162 return 1
1163 viewvc_url = settings.GetViewVCUrl()
1164 if viewvc_url and revision:
1165 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1166 print ('Closing issue '
1167 '(you may be prompted for your codereview password)...')
1168 cl.CloseIssue()
1169 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001170
1171 if retcode == 0:
1172 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1173 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001174 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001175
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176 return 0
1177
1178
1179@usage('[upstream branch to apply against]')
1180def CMDdcommit(parser, args):
1181 """commit the current changelist via git-svn"""
1182 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001183 message = """This doesn't appear to be an SVN repository.
1184If your project has a git mirror with an upstream SVN master, you probably need
1185to run 'git svn init', see your project's git mirror documentation.
1186If your project has a true writeable upstream repository, you probably want
1187to run 'git cl push' instead.
1188Choose wisely, if you get this wrong, your commit might appear to succeed but
1189will instead be silently ignored."""
1190 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001191 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001192 return SendUpstream(parser, args, 'dcommit')
1193
1194
1195@usage('[upstream branch to apply against]')
1196def CMDpush(parser, args):
1197 """commit the current changelist via git"""
1198 if settings.GetIsGitSvn():
1199 print('This appears to be an SVN repository.')
1200 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001201 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 return SendUpstream(parser, args, 'push')
1203
1204
1205@usage('<patch url or issue id>')
1206def CMDpatch(parser, args):
1207 """patch in a code review"""
1208 parser.add_option('-b', dest='newbranch',
1209 help='create a new branch off trunk for the patch')
1210 parser.add_option('-f', action='store_true', dest='force',
1211 help='with -b, clobber any existing branch')
1212 parser.add_option('--reject', action='store_true', dest='reject',
1213 help='allow failed patches and spew .rej files')
1214 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1215 help="don't commit after patch applies")
1216 (options, args) = parser.parse_args(args)
1217 if len(args) != 1:
1218 parser.print_help()
1219 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001220 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001221
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001222 # TODO(maruel): Use apply_issue.py
1223
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001224 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001225 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001226 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001227 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001229 # Assume it's a URL to the patch. Default to https.
1230 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001231 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001232 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001233 DieWithError('Must pass an issue ID or full URL for '
1234 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001235 issue = match.group(1)
1236 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237
1238 if options.newbranch:
1239 if options.force:
1240 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001241 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 RunGit(['checkout', '-b', options.newbranch,
1243 Changelist().GetUpstreamBranch()])
1244
1245 # Switch up to the top-level directory, if necessary, in preparation for
1246 # applying the patch.
1247 top = RunGit(['rev-parse', '--show-cdup']).strip()
1248 if top:
1249 os.chdir(top)
1250
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 # Git patches have a/ at the beginning of source paths. We strip that out
1252 # with a sed script rather than the -p flag to patch so we can feed either
1253 # Git or svn-style patches into the same apply command.
1254 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001255 try:
1256 patch_data = subprocess2.check_output(
1257 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1258 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001259 DieWithError('Git patch mungling failed.')
1260 logging.info(patch_data)
1261 # We use "git apply" to apply the patch instead of "patch" so that we can
1262 # pick up file adds.
1263 # The --index flag means: also insert into the index (so we catch adds).
1264 cmd = ['git', 'apply', '--index', '-p0']
1265 if options.reject:
1266 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001267 try:
1268 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1269 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001270 DieWithError('Failed to apply the patch')
1271
1272 # If we had an issue, commit the current state and register the issue.
1273 if not options.nocommit:
1274 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1275 cl = Changelist()
1276 cl.SetIssue(issue)
1277 print "Committed patch."
1278 else:
1279 print "Patch applied to index."
1280 return 0
1281
1282
1283def CMDrebase(parser, args):
1284 """rebase current branch on top of svn repo"""
1285 # Provide a wrapper for git svn rebase to help avoid accidental
1286 # git svn dcommit.
1287 # It's the only command that doesn't use parser at all since we just defer
1288 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001289 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001290
1291
1292def GetTreeStatus():
1293 """Fetches the tree status and returns either 'open', 'closed',
1294 'unknown' or 'unset'."""
1295 url = settings.GetTreeStatusUrl(error_ok=True)
1296 if url:
1297 status = urllib2.urlopen(url).read().lower()
1298 if status.find('closed') != -1 or status == '0':
1299 return 'closed'
1300 elif status.find('open') != -1 or status == '1':
1301 return 'open'
1302 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 return 'unset'
1304
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001305
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001306def GetTreeStatusReason():
1307 """Fetches the tree status from a json url and returns the message
1308 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001309 url = settings.GetTreeStatusUrl()
1310 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001311 connection = urllib2.urlopen(json_url)
1312 status = json.loads(connection.read())
1313 connection.close()
1314 return status['message']
1315
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001316
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317def CMDtree(parser, args):
1318 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001319 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 status = GetTreeStatus()
1321 if 'unset' == status:
1322 print 'You must configure your tree status URL by running "git cl config".'
1323 return 2
1324
1325 print "The tree is %s" % status
1326 print
1327 print GetTreeStatusReason()
1328 if status != 'open':
1329 return 1
1330 return 0
1331
1332
1333def CMDupstream(parser, args):
1334 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001335 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001336 if args:
1337 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001338 cl = Changelist()
1339 print cl.GetUpstreamBranch()
1340 return 0
1341
1342
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001343def CMDset_commit(parser, args):
1344 """set the commit bit"""
1345 _, args = parser.parse_args(args)
1346 if args:
1347 parser.error('Unrecognized args: %s' % ' '.join(args))
1348 cl = Changelist()
1349 cl.SetFlag('commit', '1')
1350 return 0
1351
1352
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353def Command(name):
1354 return getattr(sys.modules[__name__], 'CMD' + name, None)
1355
1356
1357def CMDhelp(parser, args):
1358 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001359 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360 if len(args) == 1:
1361 return main(args + ['--help'])
1362 parser.print_help()
1363 return 0
1364
1365
1366def GenUsage(parser, command):
1367 """Modify an OptParse object with the function's documentation."""
1368 obj = Command(command)
1369 more = getattr(obj, 'usage_more', '')
1370 if command == 'help':
1371 command = '<command>'
1372 else:
1373 # OptParser.description prefer nicely non-formatted strings.
1374 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1375 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1376
1377
1378def main(argv):
1379 """Doesn't parse the arguments here, just find the right subcommand to
1380 execute."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001381 # Reload settings.
1382 global settings
1383 settings = Settings()
1384
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001385 # Do it late so all commands are listed.
1386 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1387 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1388 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1389
1390 # Create the option parse and add --verbose support.
1391 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001392 parser.add_option(
1393 '-v', '--verbose', action='count', default=0,
1394 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395 old_parser_args = parser.parse_args
1396 def Parse(args):
1397 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001398 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001399 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001400 elif options.verbose:
1401 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001402 else:
1403 logging.basicConfig(level=logging.WARNING)
1404 return options, args
1405 parser.parse_args = Parse
1406
1407 if argv:
1408 command = Command(argv[0])
1409 if command:
1410 # "fix" the usage and the description now that we know the subcommand.
1411 GenUsage(parser, argv[0])
1412 try:
1413 return command(parser, argv[1:])
1414 except urllib2.HTTPError, e:
1415 if e.code != 500:
1416 raise
1417 DieWithError(
1418 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1419 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1420
1421 # Not a known command. Default to help.
1422 GenUsage(parser, 'help')
1423 return CMDhelp(parser, argv)
1424
1425
1426if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001427 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001428 sys.exit(main(sys.argv[1:]))