blob: 945f48f141421dac3b8371a9825519e555492d8b [file] [log] [blame]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001#!/usr/bin/python
2# git-cl -- a git-command for integrating reviews on Rietveld
3# Copyright (C) 2008 Evan Martin <martine@danga.com>
4
5import errno
6import logging
7import optparse
8import os
9import re
10import subprocess
11import sys
12import tempfile
13import textwrap
14import upload
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000015import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import urllib2
17
18try:
19 import readline
20except ImportError:
21 pass
22
23try:
24 # Add the parent directory in case it's a depot_tools checkout.
25 depot_tools_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
26 sys.path.append(depot_tools_path)
27 import breakpad
28except ImportError:
29 pass
30
31DEFAULT_SERVER = 'http://codereview.appspot.com'
32PREDCOMMIT_HOOK = '.git/hooks/pre-cl-dcommit'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000033POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000034PREUPLOAD_HOOK = '.git/hooks/pre-cl-upload'
35DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
36
37def DieWithError(message):
38 print >>sys.stderr, message
39 sys.exit(1)
40
41
42def Popen(cmd, **kwargs):
43 """Wrapper for subprocess.Popen() that logs and watch for cygwin issues"""
44 logging.info('Popen: ' + ' '.join(cmd))
45 try:
46 return subprocess.Popen(cmd, **kwargs)
47 except OSError, e:
48 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
49 DieWithError(
50 'Visit '
51 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
52 'learn how to fix this error; you need to rebase your cygwin dlls')
53 raise
54
55
56def RunCommand(cmd, error_ok=False, error_message=None,
57 redirect_stdout=True, swallow_stderr=False):
58 if redirect_stdout:
59 stdout = subprocess.PIPE
60 else:
61 stdout = None
62 if swallow_stderr:
63 stderr = subprocess.PIPE
64 else:
65 stderr = None
66 proc = Popen(cmd, stdout=stdout, stderr=stderr)
67 output = proc.communicate()[0]
68 if not error_ok and proc.returncode != 0:
69 DieWithError('Command "%s" failed.\n' % (' '.join(cmd)) +
70 (error_message or output or ''))
71 return output
72
73
74def RunGit(args, **kwargs):
75 cmd = ['git'] + args
76 return RunCommand(cmd, **kwargs)
77
78
79def RunGitWithCode(args):
80 proc = Popen(['git'] + args, stdout=subprocess.PIPE)
81 output = proc.communicate()[0]
82 return proc.returncode, output
83
84
85def usage(more):
86 def hook(fn):
87 fn.usage_more = more
88 return fn
89 return hook
90
91
92def FixUrl(server):
93 """Fix a server url to defaults protocol to http:// if none is specified."""
94 if not server:
95 return server
96 if not re.match(r'[a-z]+\://.*', server):
97 return 'http://' + server
98 return server
99
100
101class Settings(object):
102 def __init__(self):
103 self.default_server = None
104 self.cc = None
105 self.root = None
106 self.is_git_svn = None
107 self.svn_branch = None
108 self.tree_status_url = None
109 self.viewvc_url = None
110 self.updated = False
111
112 def LazyUpdateIfNeeded(self):
113 """Updates the settings from a codereview.settings file, if available."""
114 if not self.updated:
115 cr_settings_file = FindCodereviewSettingsFile()
116 if cr_settings_file:
117 LoadCodereviewSettingsFromFile(cr_settings_file)
118 self.updated = True
119
120 def GetDefaultServerUrl(self, error_ok=False):
121 if not self.default_server:
122 self.LazyUpdateIfNeeded()
123 self.default_server = FixUrl(self._GetConfig('rietveld.server',
124 error_ok=True))
125 if error_ok:
126 return self.default_server
127 if not self.default_server:
128 error_message = ('Could not find settings file. You must configure '
129 'your review setup by running "git cl config".')
130 self.default_server = FixUrl(self._GetConfig(
131 'rietveld.server', error_message=error_message))
132 return self.default_server
133
134 def GetCCList(self):
135 """Return the users cc'd on this CL.
136
137 Return is a string suitable for passing to gcl with the --cc flag.
138 """
139 if self.cc is None:
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000140 base_cc = self._GetConfig('rietveld.cc', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000141 more_cc = self._GetConfig('rietveld.extracc', error_ok=True)
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000142 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000143 return self.cc
144
145 def GetRoot(self):
146 if not self.root:
147 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
148 return self.root
149
150 def GetIsGitSvn(self):
151 """Return true if this repo looks like it's using git-svn."""
152 if self.is_git_svn is None:
153 # If you have any "svn-remote.*" config keys, we think you're using svn.
154 self.is_git_svn = RunGitWithCode(
155 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
156 return self.is_git_svn
157
158 def GetSVNBranch(self):
159 if self.svn_branch is None:
160 if not self.GetIsGitSvn():
161 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
162
163 # Try to figure out which remote branch we're based on.
164 # Strategy:
165 # 1) find all git-svn branches and note their svn URLs.
166 # 2) iterate through our branch history and match up the URLs.
167
168 # regexp matching the git-svn line that contains the URL.
169 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
170
171 # Get the refname and svn url for all refs/remotes/*.
172 remotes = RunGit(['for-each-ref', '--format=%(refname)',
173 'refs/remotes']).splitlines()
174 svn_refs = {}
175 for ref in remotes:
176 match = git_svn_re.search(RunGit(['cat-file', '-p', ref]))
177 # Prefer origin/HEAD over all others.
178 if match and (match.group(1) not in svn_refs or
179 ref == "refs/remotes/origin/HEAD"):
180 svn_refs[match.group(1)] = ref
181
182 if len(svn_refs) == 1:
183 # Only one svn branch exists -- seems like a good candidate.
184 self.svn_branch = svn_refs.values()[0]
185 elif len(svn_refs) > 1:
186 # We have more than one remote branch available. We don't
187 # want to go through all of history, so read a line from the
188 # pipe at a time.
189 # The -100 is an arbitrary limit so we don't search forever.
190 cmd = ['git', 'log', '-100', '--pretty=medium']
191 proc = Popen(cmd, stdout=subprocess.PIPE)
192 for line in proc.stdout:
193 match = git_svn_re.match(line)
194 if match:
195 url = match.group(1)
196 if url in svn_refs:
197 self.svn_branch = svn_refs[url]
198 proc.stdout.close() # Cut pipe.
199 break
200
201 if not self.svn_branch:
202 DieWithError('Can\'t guess svn branch -- try specifying it on the '
203 'command line')
204
205 return self.svn_branch
206
207 def GetTreeStatusUrl(self, error_ok=False):
208 if not self.tree_status_url:
209 error_message = ('You must configure your tree status URL by running '
210 '"git cl config".')
211 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
212 error_ok=error_ok,
213 error_message=error_message)
214 return self.tree_status_url
215
216 def GetViewVCUrl(self):
217 if not self.viewvc_url:
218 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
219 return self.viewvc_url
220
221 def _GetConfig(self, param, **kwargs):
222 self.LazyUpdateIfNeeded()
223 return RunGit(['config', param], **kwargs).strip()
224
225
226settings = Settings()
227
228
229did_migrate_check = False
230def CheckForMigration():
231 """Migrate from the old issue format, if found.
232
233 We used to store the branch<->issue mapping in a file in .git, but it's
234 better to store it in the .git/config, since deleting a branch deletes that
235 branch's entry there.
236 """
237
238 # Don't run more than once.
239 global did_migrate_check
240 if did_migrate_check:
241 return
242
243 gitdir = RunGit(['rev-parse', '--git-dir']).strip()
244 storepath = os.path.join(gitdir, 'cl-mapping')
245 if os.path.exists(storepath):
246 print "old-style git-cl mapping file (%s) found; migrating." % storepath
247 store = open(storepath, 'r')
248 for line in store:
249 branch, issue = line.strip().split()
250 RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch),
251 issue])
252 store.close()
253 os.remove(storepath)
254 did_migrate_check = True
255
256
257def ShortBranchName(branch):
258 """Convert a name like 'refs/heads/foo' to just 'foo'."""
259 return branch.replace('refs/heads/', '')
260
261
262class Changelist(object):
263 def __init__(self, branchref=None):
264 # Poke settings so we get the "configure your server" message if necessary.
265 settings.GetDefaultServerUrl()
266 self.branchref = branchref
267 if self.branchref:
268 self.branch = ShortBranchName(self.branchref)
269 else:
270 self.branch = None
271 self.rietveld_server = None
272 self.upstream_branch = None
273 self.has_issue = False
274 self.issue = None
275 self.has_description = False
276 self.description = None
277 self.has_patchset = False
278 self.patchset = None
279
280 def GetBranch(self):
281 """Returns the short branch name, e.g. 'master'."""
282 if not self.branch:
283 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
284 self.branch = ShortBranchName(self.branchref)
285 return self.branch
286
287 def GetBranchRef(self):
288 """Returns the full branch name, e.g. 'refs/heads/master'."""
289 self.GetBranch() # Poke the lazy loader.
290 return self.branchref
291
292 def FetchUpstreamTuple(self):
293 """Returns a tuple containg remote and remote ref,
294 e.g. 'origin', 'refs/heads/master'
295 """
296 remote = '.'
297 branch = self.GetBranch()
298 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
299 error_ok=True).strip()
300 if upstream_branch:
301 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
302 else:
303 # Fall back on trying a git-svn upstream branch.
304 if settings.GetIsGitSvn():
305 upstream_branch = settings.GetSVNBranch()
306 else:
307 # Else, try to guess the origin remote.
308 remote_branches = RunGit(['branch', '-r']).split()
309 if 'origin/master' in remote_branches:
310 # Fall back on origin/master if it exits.
311 remote = 'origin'
312 upstream_branch = 'refs/heads/master'
313 elif 'origin/trunk' in remote_branches:
314 # Fall back on origin/trunk if it exists. Generally a shared
315 # git-svn clone
316 remote = 'origin'
317 upstream_branch = 'refs/heads/trunk'
318 else:
319 DieWithError("""Unable to determine default branch to diff against.
320Either pass complete "git diff"-style arguments, like
321 git cl upload origin/master
322or verify this branch is set up to track another (via the --track argument to
323"git checkout -b ...").""")
324
325 return remote, upstream_branch
326
327 def GetUpstreamBranch(self):
328 if self.upstream_branch is None:
329 remote, upstream_branch = self.FetchUpstreamTuple()
330 if remote is not '.':
331 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
332 self.upstream_branch = upstream_branch
333 return self.upstream_branch
334
335 def GetRemoteUrl(self):
336 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
337
338 Returns None if there is no remote.
339 """
340 remote = self.FetchUpstreamTuple()[0]
341 if remote == '.':
342 return None
343 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
344
345 def GetIssue(self):
346 if not self.has_issue:
347 CheckForMigration()
348 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
349 if issue:
350 self.issue = issue
351 self.rietveld_server = FixUrl(RunGit(
352 ['config', self._RietveldServer()], error_ok=True).strip())
353 else:
354 self.issue = None
355 if not self.rietveld_server:
356 self.rietveld_server = settings.GetDefaultServerUrl()
357 self.has_issue = True
358 return self.issue
359
360 def GetRietveldServer(self):
361 self.GetIssue()
362 return self.rietveld_server
363
364 def GetIssueURL(self):
365 """Get the URL for a particular issue."""
366 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
367
368 def GetDescription(self, pretty=False):
369 if not self.has_description:
370 if self.GetIssue():
371 path = '/' + self.GetIssue() + '/description'
372 rpc_server = self._RpcServer()
373 self.description = rpc_server.Send(path).strip()
374 self.has_description = True
375 if pretty:
376 wrapper = textwrap.TextWrapper()
377 wrapper.initial_indent = wrapper.subsequent_indent = ' '
378 return wrapper.fill(self.description)
379 return self.description
380
381 def GetPatchset(self):
382 if not self.has_patchset:
383 patchset = RunGit(['config', self._PatchsetSetting()],
384 error_ok=True).strip()
385 if patchset:
386 self.patchset = patchset
387 else:
388 self.patchset = None
389 self.has_patchset = True
390 return self.patchset
391
392 def SetPatchset(self, patchset):
393 """Set this branch's patchset. If patchset=0, clears the patchset."""
394 if patchset:
395 RunGit(['config', self._PatchsetSetting(), str(patchset)])
396 else:
397 RunGit(['config', '--unset', self._PatchsetSetting()],
398 swallow_stderr=True, error_ok=True)
399 self.has_patchset = False
400
401 def SetIssue(self, issue):
402 """Set this branch's issue. If issue=0, clears the issue."""
403 if issue:
404 RunGit(['config', self._IssueSetting(), str(issue)])
405 if self.rietveld_server:
406 RunGit(['config', self._RietveldServer(), self.rietveld_server])
407 else:
408 RunGit(['config', '--unset', self._IssueSetting()])
409 self.SetPatchset(0)
410 self.has_issue = False
411
412 def CloseIssue(self):
413 rpc_server = self._RpcServer()
414 # Newer versions of Rietveld require us to pass an XSRF token to POST, so
415 # we fetch it from the server. (The version used by Chromium has been
416 # modified so the token isn't required when closing an issue.)
417 xsrf_token = rpc_server.Send('/xsrf_token',
418 extra_headers={'X-Requesting-XSRF-Token': '1'})
419
420 # You cannot close an issue with a GET.
421 # We pass an empty string for the data so it is a POST rather than a GET.
422 data = [("description", self.description),
423 ("xsrf_token", xsrf_token)]
424 ctype, body = upload.EncodeMultipartFormData(data, [])
425 rpc_server.Send('/' + self.GetIssue() + '/close', body, ctype)
426
427 def _RpcServer(self):
428 """Returns an upload.RpcServer() to access this review's rietveld instance.
429 """
430 server = self.GetRietveldServer()
431 return upload.GetRpcServer(server, save_cookies=True)
432
433 def _IssueSetting(self):
434 """Return the git setting that stores this change's issue."""
435 return 'branch.%s.rietveldissue' % self.GetBranch()
436
437 def _PatchsetSetting(self):
438 """Return the git setting that stores this change's most recent patchset."""
439 return 'branch.%s.rietveldpatchset' % self.GetBranch()
440
441 def _RietveldServer(self):
442 """Returns the git setting that stores this change's rietveld server."""
443 return 'branch.%s.rietveldserver' % self.GetBranch()
444
445
446def GetCodereviewSettingsInteractively():
447 """Prompt the user for settings."""
448 server = settings.GetDefaultServerUrl(error_ok=True)
449 prompt = 'Rietveld server (host[:port])'
450 prompt += ' [%s]' % (server or DEFAULT_SERVER)
451 newserver = raw_input(prompt + ': ')
452 if not server and not newserver:
453 newserver = DEFAULT_SERVER
454 if newserver and newserver != server:
455 RunGit(['config', 'rietveld.server', newserver])
456
457 def SetProperty(initial, caption, name):
458 prompt = caption
459 if initial:
460 prompt += ' ("x" to clear) [%s]' % initial
461 new_val = raw_input(prompt + ': ')
462 if new_val == 'x':
463 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
464 elif new_val and new_val != initial:
465 RunGit(['config', 'rietveld.' + name, new_val])
466
467 SetProperty(settings.GetCCList(), 'CC list', 'cc')
468 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
469 'tree-status-url')
470 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
471
472 # TODO: configure a default branch to diff against, rather than this
473 # svn-based hackery.
474
475
476def FindCodereviewSettingsFile(filename='codereview.settings'):
477 """Finds the given file starting in the cwd and going up.
478
479 Only looks up to the top of the repository unless an
480 'inherit-review-settings-ok' file exists in the root of the repository.
481 """
482 inherit_ok_file = 'inherit-review-settings-ok'
483 cwd = os.getcwd()
484 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
485 if os.path.isfile(os.path.join(root, inherit_ok_file)):
486 root = '/'
487 while True:
488 if filename in os.listdir(cwd):
489 if os.path.isfile(os.path.join(cwd, filename)):
490 return open(os.path.join(cwd, filename))
491 if cwd == root:
492 break
493 cwd = os.path.dirname(cwd)
494
495
496def LoadCodereviewSettingsFromFile(fileobj):
497 """Parse a codereview.settings file and updates hooks."""
498 def DownloadToFile(url, filename):
499 filename = os.path.join(settings.GetRoot(), filename)
500 contents = urllib2.urlopen(url).read()
501 fileobj = open(filename, 'w')
502 fileobj.write(contents)
503 fileobj.close()
504 os.chmod(filename, 0755)
505 return 0
506
507 keyvals = {}
508 for line in fileobj.read().splitlines():
509 if not line or line.startswith("#"):
510 continue
511 k, v = line.split(": ", 1)
512 keyvals[k] = v
513
514 def GetProperty(name):
515 return keyvals.get(name)
516
517 def SetProperty(name, setting, unset_error_ok=False):
518 fullname = 'rietveld.' + name
519 if setting in keyvals:
520 RunGit(['config', fullname, keyvals[setting]])
521 else:
522 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
523
524 SetProperty('server', 'CODE_REVIEW_SERVER')
525 # Only server setting is required. Other settings can be absent.
526 # In that case, we ignore errors raised during option deletion attempt.
527 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
528 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
529 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
530
531 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
532 #should be of the form
533 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
534 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
535 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
536 keyvals['ORIGIN_URL_CONFIG']])
537
538 # Update the hooks if the local hook files aren't present already.
539 if GetProperty('GITCL_PREUPLOAD') and not os.path.isfile(PREUPLOAD_HOOK):
540 DownloadToFile(GetProperty('GITCL_PREUPLOAD'), PREUPLOAD_HOOK)
541 if GetProperty('GITCL_PREDCOMMIT') and not os.path.isfile(PREDCOMMIT_HOOK):
542 DownloadToFile(GetProperty('GITCL_PREDCOMMIT'), PREDCOMMIT_HOOK)
543 return 0
544
545
546@usage('[repo root containing codereview.settings]')
547def CMDconfig(parser, args):
548 """edit configuration for this tree"""
549
550 (options, args) = parser.parse_args(args)
551 if len(args) == 0:
552 GetCodereviewSettingsInteractively()
553 return 0
554
555 url = args[0]
556 if not url.endswith('codereview.settings'):
557 url = os.path.join(url, 'codereview.settings')
558
559 # Load code review settings and download hooks (if available).
560 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
561 return 0
562
563
564def CMDstatus(parser, args):
565 """show status of changelists"""
566 parser.add_option('--field',
567 help='print only specific field (desc|id|patch|url)')
568 (options, args) = parser.parse_args(args)
569
570 # TODO: maybe make show_branches a flag if necessary.
571 show_branches = not options.field
572
573 if show_branches:
574 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
575 if branches:
576 print 'Branches associated with reviews:'
577 for branch in sorted(branches.splitlines()):
578 cl = Changelist(branchref=branch)
579 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
580
581 cl = Changelist()
582 if options.field:
583 if options.field.startswith('desc'):
584 print cl.GetDescription()
585 elif options.field == 'id':
586 issueid = cl.GetIssue()
587 if issueid:
588 print issueid
589 elif options.field == 'patch':
590 patchset = cl.GetPatchset()
591 if patchset:
592 print patchset
593 elif options.field == 'url':
594 url = cl.GetIssueURL()
595 if url:
596 print url
597 else:
598 print
599 print 'Current branch:',
600 if not cl.GetIssue():
601 print 'no issue assigned.'
602 return 0
603 print cl.GetBranch()
604 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
605 print 'Issue description:'
606 print cl.GetDescription(pretty=True)
607 return 0
608
609
610@usage('[issue_number]')
611def CMDissue(parser, args):
612 """Set or display the current code review issue number.
613
614 Pass issue number 0 to clear the current issue.
615"""
616 (options, args) = parser.parse_args(args)
617
618 cl = Changelist()
619 if len(args) > 0:
620 try:
621 issue = int(args[0])
622 except ValueError:
623 DieWithError('Pass a number to set the issue or none to list it.\n'
624 'Maybe you want to run git cl status?')
625 cl.SetIssue(issue)
626 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
627 return 0
628
629
630def CreateDescriptionFromLog(args):
631 """Pulls out the commit log to use as a base for the CL description."""
632 log_args = []
633 if len(args) == 1 and not args[0].endswith('.'):
634 log_args = [args[0] + '..']
635 elif len(args) == 1 and args[0].endswith('...'):
636 log_args = [args[0][:-1]]
637 elif len(args) == 2:
638 log_args = [args[0] + '..' + args[1]]
639 else:
640 log_args = args[:] # Hope for the best!
641 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
642
643
644def UserEditedLog(starting_text):
645 """Given some starting text, let the user edit it and return the result."""
646 editor = os.getenv('EDITOR', 'vi')
647
648 (file_handle, filename) = tempfile.mkstemp()
649 fileobj = os.fdopen(file_handle, 'w')
650 fileobj.write(starting_text)
651 fileobj.close()
652
653 ret = subprocess.call(editor + ' ' + filename, shell=True)
654 if ret != 0:
655 os.remove(filename)
656 return
657
658 fileobj = open(filename)
659 text = fileobj.read()
660 fileobj.close()
661 os.remove(filename)
662 stripcomment_re = re.compile(r'^#.*$', re.MULTILINE)
663 return stripcomment_re.sub('', text).strip()
664
665
666def RunHook(hook, upstream_branch, error_ok=False):
667 """Run a given hook if it exists. By default, we fail on errors."""
668 hook = '%s/%s' % (settings.GetRoot(), hook)
669 if not os.path.exists(hook):
670 return
671 return RunCommand([hook, upstream_branch], error_ok=error_ok,
672 redirect_stdout=False)
673
674
675def CMDpresubmit(parser, args):
676 """run presubmit tests on the current changelist"""
677 parser.add_option('--upload', action='store_true',
678 help='Run upload hook instead of the push/dcommit hook')
679 (options, args) = parser.parse_args(args)
680
681 # Make sure index is up-to-date before running diff-index.
682 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
683 if RunGit(['diff-index', 'HEAD']):
684 # TODO(maruel): Is this really necessary?
685 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
686 return 1
687
688 if args:
689 base_branch = args[0]
690 else:
691 # Default to diffing against the "upstream" branch.
692 base_branch = Changelist().GetUpstreamBranch()
693
694 if options.upload:
695 print '*** Presubmit checks for UPLOAD would report: ***'
696 return not RunHook(PREUPLOAD_HOOK, upstream_branch=base_branch,
697 error_ok=True)
698 else:
699 print '*** Presubmit checks for DCOMMIT would report: ***'
700 return not RunHook(PREDCOMMIT_HOOK, upstream_branch=base_branch,
701 error_ok=True)
702
703
704@usage('[args to "git diff"]')
705def CMDupload(parser, args):
706 """upload the current changelist to codereview"""
707 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
708 help='bypass upload presubmit hook')
709 parser.add_option('-m', dest='message', help='message for patch')
710 parser.add_option('-r', '--reviewers',
711 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000712 parser.add_option('--cc',
713 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000714 parser.add_option('--send-mail', action='store_true',
715 help='send email to reviewer immediately')
716 parser.add_option("--emulate_svn_auto_props", action="store_true",
717 dest="emulate_svn_auto_props",
718 help="Emulate Subversion's auto properties feature.")
719 parser.add_option("--desc_from_logs", action="store_true",
720 dest="from_logs",
721 help="""Squashes git commit logs into change description and
722 uses message as subject""")
723 (options, args) = parser.parse_args(args)
724
725 # Make sure index is up-to-date before running diff-index.
726 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
727 if RunGit(['diff-index', 'HEAD']):
728 print 'Cannot upload with a dirty tree. You must commit locally first.'
729 return 1
730
731 cl = Changelist()
732 if args:
733 base_branch = args[0]
734 else:
735 # Default to diffing against the "upstream" branch.
736 base_branch = cl.GetUpstreamBranch()
737 args = [base_branch + "..."]
738
739 if not options.bypass_hooks:
740 RunHook(PREUPLOAD_HOOK, upstream_branch=base_branch, error_ok=False)
741
742 # --no-ext-diff is broken in some versions of Git, so try to work around
743 # this by overriding the environment (but there is still a problem if the
744 # git config key "diff.external" is used).
745 env = os.environ.copy()
746 if 'GIT_EXTERNAL_DIFF' in env:
747 del env['GIT_EXTERNAL_DIFF']
748 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args,
749 env=env)
750
751 upload_args = ['--assume_yes'] # Don't ask about untracked files.
752 upload_args.extend(['--server', cl.GetRietveldServer()])
753 if options.reviewers:
754 upload_args.extend(['--reviewers', options.reviewers])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 if options.emulate_svn_auto_props:
756 upload_args.append('--emulate_svn_auto_props')
757 if options.send_mail:
758 if not options.reviewers:
759 DieWithError("Must specify reviewers to send email.")
760 upload_args.append('--send_mail')
761 if options.from_logs and not options.message:
762 print 'Must set message for subject line if using desc_from_logs'
763 return 1
764
765 change_desc = None
766
767 if cl.GetIssue():
768 if options.message:
769 upload_args.extend(['--message', options.message])
770 upload_args.extend(['--issue', cl.GetIssue()])
771 print ("This branch is associated with issue %s. "
772 "Adding patch to that issue." % cl.GetIssue())
773 else:
774 log_desc = CreateDescriptionFromLog(args)
775 if options.from_logs:
776 # Uses logs as description and message as subject.
777 subject = options.message
778 change_desc = subject + '\n\n' + log_desc
779 else:
780 initial_text = """# Enter a description of the change.
781# This will displayed on the codereview site.
782# The first line will also be used as the subject of the review.
783"""
784 if 'BUG=' not in log_desc:
785 log_desc += '\nBUG='
786 if 'TEST=' not in log_desc:
787 log_desc += '\nTEST='
788 change_desc = UserEditedLog(initial_text + log_desc)
789 subject = ''
790 if change_desc:
791 subject = change_desc.splitlines()[0]
792 if not change_desc:
793 print "Description is empty; aborting."
794 return 1
795 upload_args.extend(['--message', subject])
796 upload_args.extend(['--description', change_desc])
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000797 cc = ','.join(filter(None, (settings.GetCCList(), options.cc)))
798 if cc:
799 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800
801 # Include the upstream repo's URL in the change -- this is useful for
802 # projects that have their source spread across multiple repos.
803 remote_url = None
804 if settings.GetIsGitSvn():
805 data = RunGit(['svn', 'info'])
806 if data:
807 keys = dict(line.split(': ', 1) for line in data.splitlines()
808 if ': ' in line)
809 remote_url = keys.get('URL', None)
810 else:
811 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
812 remote_url = (cl.GetRemoteUrl() + '@'
813 + cl.GetUpstreamBranch().split('/')[-1])
814 if remote_url:
815 upload_args.extend(['--base_url', remote_url])
816
817 try:
818 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
819 except:
820 # If we got an exception after the user typed a description for their
821 # change, back up the description before re-raising.
822 if change_desc:
823 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
824 print '\nGot exception while uploading -- saving description to %s\n' \
825 % backup_path
826 backup_file = open(backup_path, 'w')
827 backup_file.write(change_desc)
828 backup_file.close()
829 raise
830
831 if not cl.GetIssue():
832 cl.SetIssue(issue)
833 cl.SetPatchset(patchset)
834 return 0
835
836
837def SendUpstream(parser, args, cmd):
838 """Common code for CmdPush and CmdDCommit
839
840 Squashed commit into a single.
841 Updates changelog with metadata (e.g. pointer to review).
842 Pushes/dcommits the code upstream.
843 Updates review and closes.
844 """
845 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
846 help='bypass upload presubmit hook')
847 parser.add_option('-m', dest='message',
848 help="override review description")
849 parser.add_option('-f', action='store_true', dest='force',
850 help="force yes to questions (don't prompt)")
851 parser.add_option('-c', dest='contributor',
852 help="external contributor for patch (appended to " +
853 "description and used as author for git). Should be " +
854 "formatted as 'First Last <email@example.com>'")
855 parser.add_option('--tbr', action='store_true', dest='tbr',
856 help="short for 'to be reviewed', commit branch " +
857 "even without uploading for review")
858 (options, args) = parser.parse_args(args)
859 cl = Changelist()
860
861 if not args or cmd == 'push':
862 # Default to merging against our best guess of the upstream branch.
863 args = [cl.GetUpstreamBranch()]
864
865 base_branch = args[0]
866
chase@chromium.orgc76e6752011-01-10 18:17:12 +0000867 # Make sure index is up-to-date before running diff-index.
868 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000869 if RunGit(['diff-index', 'HEAD']):
870 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
871 return 1
872
873 # This rev-list syntax means "show all commits not in my branch that
874 # are in base_branch".
875 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
876 base_branch]).splitlines()
877 if upstream_commits:
878 print ('Base branch "%s" has %d commits '
879 'not in this branch.' % (base_branch, len(upstream_commits)))
880 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
881 return 1
882
883 if cmd == 'dcommit':
884 # This is the revision `svn dcommit` will commit on top of.
885 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
886 '--pretty=format:%H'])
887 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
888 if extra_commits:
889 print ('This branch has %d additional commits not upstreamed yet.'
890 % len(extra_commits.splitlines()))
891 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
892 'before attempting to %s.' % (base_branch, cmd))
893 return 1
894
895 if not options.force and not options.bypass_hooks:
896 RunHook(PREDCOMMIT_HOOK, upstream_branch=base_branch, error_ok=False)
897
898 if cmd == 'dcommit':
899 # Check the tree status if the tree status URL is set.
900 status = GetTreeStatus()
901 if 'closed' == status:
902 print ('The tree is closed. Please wait for it to reopen. Use '
903 '"git cl dcommit -f" to commit on a closed tree.')
904 return 1
905 elif 'unknown' == status:
906 print ('Unable to determine tree status. Please verify manually and '
907 'use "git cl dcommit -f" to commit on a closed tree.')
908
909 description = options.message
910 if not options.tbr:
911 # It is important to have these checks early. Not only for user
912 # convenience, but also because the cl object then caches the correct values
913 # of these fields even as we're juggling branches for setting up the commit.
914 if not cl.GetIssue():
915 print 'Current issue unknown -- has this branch been uploaded?'
916 print 'Use --tbr to commit without review.'
917 return 1
918
919 if not description:
920 description = cl.GetDescription()
921
922 if not description:
923 print 'No description set.'
924 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
925 return 1
926
927 description += "\n\nReview URL: %s" % cl.GetIssueURL()
928 else:
929 if not description:
930 # Submitting TBR. See if there's already a description in Rietveld, else
931 # create a template description. Eitherway, give the user a chance to edit
932 # it to fill in the TBR= field.
933 if cl.GetIssue():
934 description = cl.GetDescription()
935
936 if not description:
937 description = """# Enter a description of the change.
938# This will be used as the change log for the commit.
939
940"""
941 description += CreateDescriptionFromLog(args)
942
943 description = UserEditedLog(description + '\nTBR=')
944
945 if not description:
946 print "Description empty; aborting."
947 return 1
948
949 if options.contributor:
950 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
951 print "Please provide contibutor as 'First Last <email@example.com>'"
952 return 1
953 description += "\nPatch from %s." % options.contributor
954 print 'Description:', repr(description)
955
956 branches = [base_branch, cl.GetBranchRef()]
957 if not options.force:
958 subprocess.call(['git', 'diff', '--stat'] + branches)
959 raw_input("About to commit; enter to confirm.")
960
961 # We want to squash all this branch's commits into one commit with the
962 # proper description.
963 # We do this by doing a "merge --squash" into a new commit branch, then
964 # dcommitting that.
965 MERGE_BRANCH = 'git-cl-commit'
966 # Delete the merge branch if it already exists.
967 if RunGitWithCode(['show-ref', '--quiet', '--verify',
968 'refs/heads/' + MERGE_BRANCH])[0] == 0:
969 RunGit(['branch', '-D', MERGE_BRANCH])
970
971 # We might be in a directory that's present in this branch but not in the
972 # trunk. Move up to the top of the tree so that git commands that expect a
973 # valid CWD won't fail after we check out the merge branch.
974 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
975 if rel_base_path:
976 os.chdir(rel_base_path)
977
978 # Stuff our change into the merge branch.
979 # We wrap in a try...finally block so if anything goes wrong,
980 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +0000981 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000982 try:
983 RunGit(['checkout', '-q', '-b', MERGE_BRANCH, base_branch])
984 RunGit(['merge', '--squash', cl.GetBranchRef()])
985 if options.contributor:
986 RunGit(['commit', '--author', options.contributor, '-m', description])
987 else:
988 RunGit(['commit', '-m', description])
989 if cmd == 'push':
990 # push the merge branch.
991 remote, branch = cl.FetchUpstreamTuple()
992 retcode, output = RunGitWithCode(
993 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
994 logging.debug(output)
995 else:
996 # dcommit the merge branch.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +0000997 retcode, output = RunGitWithCode(['svn', 'dcommit', '--no-rebase'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 finally:
999 # And then swap back to the original branch and clean up.
1000 RunGit(['checkout', '-q', cl.GetBranch()])
1001 RunGit(['branch', '-D', MERGE_BRANCH])
1002
1003 if cl.GetIssue():
1004 if cmd == 'dcommit' and 'Committed r' in output:
1005 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1006 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001007 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1008 for l in output.splitlines(False))
1009 match = filter(None, match)
1010 if len(match) != 1:
1011 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1012 output)
1013 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014 else:
1015 return 1
1016 viewvc_url = settings.GetViewVCUrl()
1017 if viewvc_url and revision:
1018 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1019 print ('Closing issue '
1020 '(you may be prompted for your codereview password)...')
1021 cl.CloseIssue()
1022 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001023
1024 if retcode == 0:
1025 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1026 if os.path.isfile(hook):
1027 RunHook(hook, upstream_branch=base_branch, error_ok=True)
1028
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029 return 0
1030
1031
1032@usage('[upstream branch to apply against]')
1033def CMDdcommit(parser, args):
1034 """commit the current changelist via git-svn"""
1035 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001036 message = """This doesn't appear to be an SVN repository.
1037If your project has a git mirror with an upstream SVN master, you probably need
1038to run 'git svn init', see your project's git mirror documentation.
1039If your project has a true writeable upstream repository, you probably want
1040to run 'git cl push' instead.
1041Choose wisely, if you get this wrong, your commit might appear to succeed but
1042will instead be silently ignored."""
1043 print(message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001044 raw_input('[Press enter to dcommit or ctrl-C to quit]')
1045 return SendUpstream(parser, args, 'dcommit')
1046
1047
1048@usage('[upstream branch to apply against]')
1049def CMDpush(parser, args):
1050 """commit the current changelist via git"""
1051 if settings.GetIsGitSvn():
1052 print('This appears to be an SVN repository.')
1053 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
1054 raw_input('[Press enter to push or ctrl-C to quit]')
1055 return SendUpstream(parser, args, 'push')
1056
1057
1058@usage('<patch url or issue id>')
1059def CMDpatch(parser, args):
1060 """patch in a code review"""
1061 parser.add_option('-b', dest='newbranch',
1062 help='create a new branch off trunk for the patch')
1063 parser.add_option('-f', action='store_true', dest='force',
1064 help='with -b, clobber any existing branch')
1065 parser.add_option('--reject', action='store_true', dest='reject',
1066 help='allow failed patches and spew .rej files')
1067 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1068 help="don't commit after patch applies")
1069 (options, args) = parser.parse_args(args)
1070 if len(args) != 1:
1071 parser.print_help()
1072 return 1
1073 input = args[0]
1074
1075 if re.match(r'\d+', input):
1076 # Input is an issue id. Figure out the URL.
1077 issue = input
1078 server = settings.GetDefaultServerUrl()
1079 fetch = urllib2.urlopen('%s/%s' % (server, issue)).read()
1080 m = re.search(r'/download/issue[0-9]+_[0-9]+.diff', fetch)
1081 if not m:
1082 DieWithError('Must pass an issue ID or full URL for '
1083 '\'Download raw patch set\'')
1084 url = '%s%s' % (server, m.group(0).strip())
1085 else:
1086 # Assume it's a URL to the patch. Default to http.
1087 input = FixUrl(input)
1088 match = re.match(r'.*?/issue(\d+)_\d+.diff', input)
1089 if match:
1090 issue = match.group(1)
1091 url = input
1092 else:
1093 DieWithError('Must pass an issue ID or full URL for '
1094 '\'Download raw patch set\'')
1095
1096 if options.newbranch:
1097 if options.force:
1098 RunGit(['branch', '-D', options.newbranch],
1099 swallow_stderr=True, error_ok=True)
1100 RunGit(['checkout', '-b', options.newbranch,
1101 Changelist().GetUpstreamBranch()])
1102
1103 # Switch up to the top-level directory, if necessary, in preparation for
1104 # applying the patch.
1105 top = RunGit(['rev-parse', '--show-cdup']).strip()
1106 if top:
1107 os.chdir(top)
1108
1109 patch_data = urllib2.urlopen(url).read()
1110 # Git patches have a/ at the beginning of source paths. We strip that out
1111 # with a sed script rather than the -p flag to patch so we can feed either
1112 # Git or svn-style patches into the same apply command.
1113 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1114 sed_proc = Popen(['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'],
1115 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
1116 patch_data = sed_proc.communicate(patch_data)[0]
1117 if sed_proc.returncode:
1118 DieWithError('Git patch mungling failed.')
1119 logging.info(patch_data)
1120 # We use "git apply" to apply the patch instead of "patch" so that we can
1121 # pick up file adds.
1122 # The --index flag means: also insert into the index (so we catch adds).
1123 cmd = ['git', 'apply', '--index', '-p0']
1124 if options.reject:
1125 cmd.append('--reject')
1126 patch_proc = Popen(cmd, stdin=subprocess.PIPE)
1127 patch_proc.communicate(patch_data)
1128 if patch_proc.returncode:
1129 DieWithError('Failed to apply the patch')
1130
1131 # If we had an issue, commit the current state and register the issue.
1132 if not options.nocommit:
1133 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1134 cl = Changelist()
1135 cl.SetIssue(issue)
1136 print "Committed patch."
1137 else:
1138 print "Patch applied to index."
1139 return 0
1140
1141
1142def CMDrebase(parser, args):
1143 """rebase current branch on top of svn repo"""
1144 # Provide a wrapper for git svn rebase to help avoid accidental
1145 # git svn dcommit.
1146 # It's the only command that doesn't use parser at all since we just defer
1147 # execution to git-svn.
1148 RunGit(['svn', 'rebase'] + args, redirect_stdout=False)
1149 return 0
1150
1151
1152def GetTreeStatus():
1153 """Fetches the tree status and returns either 'open', 'closed',
1154 'unknown' or 'unset'."""
1155 url = settings.GetTreeStatusUrl(error_ok=True)
1156 if url:
1157 status = urllib2.urlopen(url).read().lower()
1158 if status.find('closed') != -1 or status == '0':
1159 return 'closed'
1160 elif status.find('open') != -1 or status == '1':
1161 return 'open'
1162 return 'unknown'
1163
1164 return 'unset'
1165
1166def GetTreeStatusReason():
1167 """Fetches the tree status from a json url and returns the message
1168 with the reason for the tree to be opened or closed."""
1169 # Don't import it at file level since simplejson is not installed by default
1170 # on python 2.5 and it is only used for git-cl tree which isn't often used,
1171 # forcing everyone to install simplejson isn't efficient.
1172 try:
1173 import simplejson as json
1174 except ImportError:
1175 try:
1176 import json
1177 # Some versions of python2.5 have an incomplete json module. Check to make
1178 # sure loads exists.
1179 json.loads
1180 except (ImportError, AttributeError):
1181 print >> sys.stderr, 'Please install simplejson'
1182 sys.exit(1)
1183
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001184 url = settings.GetTreeStatusUrl()
1185 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186 connection = urllib2.urlopen(json_url)
1187 status = json.loads(connection.read())
1188 connection.close()
1189 return status['message']
1190
1191def CMDtree(parser, args):
1192 """show the status of the tree"""
1193 (options, args) = parser.parse_args(args)
1194 status = GetTreeStatus()
1195 if 'unset' == status:
1196 print 'You must configure your tree status URL by running "git cl config".'
1197 return 2
1198
1199 print "The tree is %s" % status
1200 print
1201 print GetTreeStatusReason()
1202 if status != 'open':
1203 return 1
1204 return 0
1205
1206
1207def CMDupstream(parser, args):
1208 """print the name of the upstream branch, if any"""
1209 (options, args) = parser.parse_args(args)
1210 cl = Changelist()
1211 print cl.GetUpstreamBranch()
1212 return 0
1213
1214
1215def Command(name):
1216 return getattr(sys.modules[__name__], 'CMD' + name, None)
1217
1218
1219def CMDhelp(parser, args):
1220 """print list of commands or help for a specific command"""
1221 (options, args) = parser.parse_args(args)
1222 if len(args) == 1:
1223 return main(args + ['--help'])
1224 parser.print_help()
1225 return 0
1226
1227
1228def GenUsage(parser, command):
1229 """Modify an OptParse object with the function's documentation."""
1230 obj = Command(command)
1231 more = getattr(obj, 'usage_more', '')
1232 if command == 'help':
1233 command = '<command>'
1234 else:
1235 # OptParser.description prefer nicely non-formatted strings.
1236 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1237 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1238
1239
1240def main(argv):
1241 """Doesn't parse the arguments here, just find the right subcommand to
1242 execute."""
1243 # Do it late so all commands are listed.
1244 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1245 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1246 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1247
1248 # Create the option parse and add --verbose support.
1249 parser = optparse.OptionParser()
1250 parser.add_option('-v', '--verbose', action='store_true')
1251 old_parser_args = parser.parse_args
1252 def Parse(args):
1253 options, args = old_parser_args(args)
1254 if options.verbose:
1255 logging.basicConfig(level=logging.DEBUG)
1256 else:
1257 logging.basicConfig(level=logging.WARNING)
1258 return options, args
1259 parser.parse_args = Parse
1260
1261 if argv:
1262 command = Command(argv[0])
1263 if command:
1264 # "fix" the usage and the description now that we know the subcommand.
1265 GenUsage(parser, argv[0])
1266 try:
1267 return command(parser, argv[1:])
1268 except urllib2.HTTPError, e:
1269 if e.code != 500:
1270 raise
1271 DieWithError(
1272 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1273 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1274
1275 # Not a known command. Default to help.
1276 GenUsage(parser, 'help')
1277 return CMDhelp(parser, argv)
1278
1279
1280if __name__ == '__main__':
1281 sys.exit(main(sys.argv[1:]))