blob: 2922345af842c89d0cf5aa421acc0254ec379a36 [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:
553 output = presubmit_support.DoPresubmitChecks(change, committing,
554 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
562 # TODO(dpranke): We should propagate the error out instead of calling
563 # exit().
564 if not output.should_continue():
565 sys.exit(1)
566
567 return output
568
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000570 """Updates the description and closes the issue."""
571 issue = int(self.GetIssue())
572 self.RpcServer().update_description(issue, self.description)
573 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000575 def SetFlag(self, flag, value):
576 """Patchset must match."""
577 if not self.GetPatchset():
578 DieWithError('The patchset needs to match. Send another patchset.')
579 try:
580 return self.RpcServer().set_flag(
581 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
582 except urllib2.HTTPError, e:
583 if e.code == 404:
584 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
585 if e.code == 403:
586 DieWithError(
587 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
588 'match?') % (self.GetIssue(), self.GetPatchset()))
589 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000590
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000591 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000592 """Returns an upload.RpcServer() to access this review's rietveld instance.
593 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000594 if not self._rpc_server:
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000595 self.GetIssue()
596 self._rpc_server = rietveld.Rietveld(self.rietveld_server, None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000597 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598
599 def _IssueSetting(self):
600 """Return the git setting that stores this change's issue."""
601 return 'branch.%s.rietveldissue' % self.GetBranch()
602
603 def _PatchsetSetting(self):
604 """Return the git setting that stores this change's most recent patchset."""
605 return 'branch.%s.rietveldpatchset' % self.GetBranch()
606
607 def _RietveldServer(self):
608 """Returns the git setting that stores this change's rietveld server."""
609 return 'branch.%s.rietveldserver' % self.GetBranch()
610
611
612def GetCodereviewSettingsInteractively():
613 """Prompt the user for settings."""
614 server = settings.GetDefaultServerUrl(error_ok=True)
615 prompt = 'Rietveld server (host[:port])'
616 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000617 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000618 if not server and not newserver:
619 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000620 if newserver:
621 newserver = gclient_utils.UpgradeToHttps(newserver)
622 if newserver != server:
623 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000624
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000625 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000626 prompt = caption
627 if initial:
628 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000629 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630 if new_val == 'x':
631 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000632 elif new_val:
633 if is_url:
634 new_val = gclient_utils.UpgradeToHttps(new_val)
635 if new_val != initial:
636 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000637
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000638 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000639 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000640 'tree-status-url', False)
641 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000642
643 # TODO: configure a default branch to diff against, rather than this
644 # svn-based hackery.
645
646
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000647class ChangeDescription(object):
648 """Contains a parsed form of the change description."""
649 def __init__(self, subject, log_desc, reviewers):
650 self.subject = subject
651 self.log_desc = log_desc
652 self.reviewers = reviewers
653 self.description = self.log_desc
654
655 def Update(self):
656 initial_text = """# Enter a description of the change.
657# This will displayed on the codereview site.
658# The first line will also be used as the subject of the review.
659"""
660 initial_text += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000661 if ('\nR=' not in self.description and
662 '\nTBR=' not in self.description and
663 self.reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000664 initial_text += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000665 if '\nBUG=' not in self.description:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000666 initial_text += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000667 if '\nTEST=' not in self.description:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000668 initial_text += '\nTEST='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000669 initial_text = initial_text.rstrip('\n') + '\n'
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000670 content = gclient_utils.RunEditor(initial_text, True)
671 if not content:
672 DieWithError('Running editor failed')
673 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
674 if not content:
675 DieWithError('No CL description, aborting')
676 self._ParseDescription(content)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000677
678 def _ParseDescription(self, description):
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000679 """Updates the list of reviewers and subject from the description."""
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000680 if not description:
681 self.description = description
682 return
683
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000684 self.description = description.strip('\n') + '\n'
685 self.subject = description.split('\n', 1)[0]
686 # Retrieves all reviewer lines
687 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
688 self.reviewers = ','.join(
689 i.group(2).strip() for i in regexp.finditer(self.description))
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000690
691 def IsEmpty(self):
692 return not self.description
693
694
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695def FindCodereviewSettingsFile(filename='codereview.settings'):
696 """Finds the given file starting in the cwd and going up.
697
698 Only looks up to the top of the repository unless an
699 'inherit-review-settings-ok' file exists in the root of the repository.
700 """
701 inherit_ok_file = 'inherit-review-settings-ok'
702 cwd = os.getcwd()
703 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
704 if os.path.isfile(os.path.join(root, inherit_ok_file)):
705 root = '/'
706 while True:
707 if filename in os.listdir(cwd):
708 if os.path.isfile(os.path.join(cwd, filename)):
709 return open(os.path.join(cwd, filename))
710 if cwd == root:
711 break
712 cwd = os.path.dirname(cwd)
713
714
715def LoadCodereviewSettingsFromFile(fileobj):
716 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000717 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000719 def SetProperty(name, setting, unset_error_ok=False):
720 fullname = 'rietveld.' + name
721 if setting in keyvals:
722 RunGit(['config', fullname, keyvals[setting]])
723 else:
724 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
725
726 SetProperty('server', 'CODE_REVIEW_SERVER')
727 # Only server setting is required. Other settings can be absent.
728 # In that case, we ignore errors raised during option deletion attempt.
729 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
730 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
731 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
732
733 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
734 #should be of the form
735 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
736 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
737 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
738 keyvals['ORIGIN_URL_CONFIG']])
739
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740
741@usage('[repo root containing codereview.settings]')
742def CMDconfig(parser, args):
743 """edit configuration for this tree"""
744
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000745 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746 if len(args) == 0:
747 GetCodereviewSettingsInteractively()
748 return 0
749
750 url = args[0]
751 if not url.endswith('codereview.settings'):
752 url = os.path.join(url, 'codereview.settings')
753
754 # Load code review settings and download hooks (if available).
755 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
756 return 0
757
758
759def CMDstatus(parser, args):
760 """show status of changelists"""
761 parser.add_option('--field',
762 help='print only specific field (desc|id|patch|url)')
763 (options, args) = parser.parse_args(args)
764
765 # TODO: maybe make show_branches a flag if necessary.
766 show_branches = not options.field
767
768 if show_branches:
769 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
770 if branches:
771 print 'Branches associated with reviews:'
772 for branch in sorted(branches.splitlines()):
773 cl = Changelist(branchref=branch)
774 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
775
776 cl = Changelist()
777 if options.field:
778 if options.field.startswith('desc'):
779 print cl.GetDescription()
780 elif options.field == 'id':
781 issueid = cl.GetIssue()
782 if issueid:
783 print issueid
784 elif options.field == 'patch':
785 patchset = cl.GetPatchset()
786 if patchset:
787 print patchset
788 elif options.field == 'url':
789 url = cl.GetIssueURL()
790 if url:
791 print url
792 else:
793 print
794 print 'Current branch:',
795 if not cl.GetIssue():
796 print 'no issue assigned.'
797 return 0
798 print cl.GetBranch()
799 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
800 print 'Issue description:'
801 print cl.GetDescription(pretty=True)
802 return 0
803
804
805@usage('[issue_number]')
806def CMDissue(parser, args):
807 """Set or display the current code review issue number.
808
809 Pass issue number 0 to clear the current issue.
810"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000811 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000812
813 cl = Changelist()
814 if len(args) > 0:
815 try:
816 issue = int(args[0])
817 except ValueError:
818 DieWithError('Pass a number to set the issue or none to list it.\n'
819 'Maybe you want to run git cl status?')
820 cl.SetIssue(issue)
821 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
822 return 0
823
824
825def CreateDescriptionFromLog(args):
826 """Pulls out the commit log to use as a base for the CL description."""
827 log_args = []
828 if len(args) == 1 and not args[0].endswith('.'):
829 log_args = [args[0] + '..']
830 elif len(args) == 1 and args[0].endswith('...'):
831 log_args = [args[0][:-1]]
832 elif len(args) == 2:
833 log_args = [args[0] + '..' + args[1]]
834 else:
835 log_args = args[:] # Hope for the best!
836 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
837
838
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000839def ConvertToInteger(inputval):
840 """Convert a string to integer, but returns either an int or None."""
841 try:
842 return int(inputval)
843 except (TypeError, ValueError):
844 return None
845
846
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000847def CMDpresubmit(parser, args):
848 """run presubmit tests on the current changelist"""
849 parser.add_option('--upload', action='store_true',
850 help='Run upload hook instead of the push/dcommit hook')
851 (options, args) = parser.parse_args(args)
852
853 # Make sure index is up-to-date before running diff-index.
854 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
855 if RunGit(['diff-index', 'HEAD']):
856 # TODO(maruel): Is this really necessary?
857 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
858 return 1
859
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000860 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000861 if args:
862 base_branch = args[0]
863 else:
864 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000865 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000867 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000868 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000869 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000870 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000871
872
873@usage('[args to "git diff"]')
874def CMDupload(parser, args):
875 """upload the current changelist to codereview"""
876 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
877 help='bypass upload presubmit hook')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000878 parser.add_option('-f', action='store_true', dest='force',
879 help="force yes to questions (don't prompt)")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000880 parser.add_option('-m', dest='message', help='message for patch')
881 parser.add_option('-r', '--reviewers',
882 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000883 parser.add_option('--cc',
884 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000885 parser.add_option('--send-mail', action='store_true',
886 help='send email to reviewer immediately')
887 parser.add_option("--emulate_svn_auto_props", action="store_true",
888 dest="emulate_svn_auto_props",
889 help="Emulate Subversion's auto properties feature.")
890 parser.add_option("--desc_from_logs", action="store_true",
891 dest="from_logs",
892 help="""Squashes git commit logs into change description and
893 uses message as subject""")
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000894 parser.add_option('-c', '--use-commit-queue', action='store_true',
895 help='tell the commit queue to commit this patchset')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 (options, args) = parser.parse_args(args)
897
898 # Make sure index is up-to-date before running diff-index.
899 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
900 if RunGit(['diff-index', 'HEAD']):
901 print 'Cannot upload with a dirty tree. You must commit locally first.'
902 return 1
903
904 cl = Changelist()
905 if args:
906 base_branch = args[0]
907 else:
908 # Default to diffing against the "upstream" branch.
909 base_branch = cl.GetUpstreamBranch()
910 args = [base_branch + "..."]
911
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000912 if not options.bypass_hooks and not options.force:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000913 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000914 may_prompt=True,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000915 verbose=options.verbose,
916 author=None)
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000917 if not options.reviewers and hook_results.reviewers:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000918 options.reviewers = hook_results.reviewers
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000919
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000920
921 # --no-ext-diff is broken in some versions of Git, so try to work around
922 # this by overriding the environment (but there is still a problem if the
923 # git config key "diff.external" is used).
924 env = os.environ.copy()
925 if 'GIT_EXTERNAL_DIFF' in env:
926 del env['GIT_EXTERNAL_DIFF']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000927 subprocess2.call(
928 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929
930 upload_args = ['--assume_yes'] # Don't ask about untracked files.
931 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000932 if options.emulate_svn_auto_props:
933 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934 if options.from_logs and not options.message:
935 print 'Must set message for subject line if using desc_from_logs'
936 return 1
937
938 change_desc = None
939
940 if cl.GetIssue():
941 if options.message:
942 upload_args.extend(['--message', options.message])
943 upload_args.extend(['--issue', cl.GetIssue()])
944 print ("This branch is associated with issue %s. "
945 "Adding patch to that issue." % cl.GetIssue())
946 else:
947 log_desc = CreateDescriptionFromLog(args)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000948 change_desc = ChangeDescription(options.message, log_desc,
949 options.reviewers)
950 if not options.from_logs:
951 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000952
953 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000954 print "Description is empty; aborting."
955 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000956
957 upload_args.extend(['--message', change_desc.subject])
958 upload_args.extend(['--description', change_desc.description])
959 if change_desc.reviewers:
960 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000961 if options.send_mail:
962 if not change_desc.reviewers:
963 DieWithError("Must specify reviewers to send email.")
964 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000965 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000966 if cc:
967 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000968
969 # Include the upstream repo's URL in the change -- this is useful for
970 # projects that have their source spread across multiple repos.
971 remote_url = None
972 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000973 # URL is dependent on the current directory.
974 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000975 if data:
976 keys = dict(line.split(': ', 1) for line in data.splitlines()
977 if ': ' in line)
978 remote_url = keys.get('URL', None)
979 else:
980 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
981 remote_url = (cl.GetRemoteUrl() + '@'
982 + cl.GetUpstreamBranch().split('/')[-1])
983 if remote_url:
984 upload_args.extend(['--base_url', remote_url])
985
986 try:
987 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000988 except KeyboardInterrupt:
989 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000990 except:
991 # If we got an exception after the user typed a description for their
992 # change, back up the description before re-raising.
993 if change_desc:
994 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
995 print '\nGot exception while uploading -- saving description to %s\n' \
996 % backup_path
997 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000998 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000999 backup_file.close()
1000 raise
1001
1002 if not cl.GetIssue():
1003 cl.SetIssue(issue)
1004 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001005
1006 if options.use_commit_queue:
1007 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 return 0
1009
1010
1011def SendUpstream(parser, args, cmd):
1012 """Common code for CmdPush and CmdDCommit
1013
1014 Squashed commit into a single.
1015 Updates changelog with metadata (e.g. pointer to review).
1016 Pushes/dcommits the code upstream.
1017 Updates review and closes.
1018 """
1019 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1020 help='bypass upload presubmit hook')
1021 parser.add_option('-m', dest='message',
1022 help="override review description")
1023 parser.add_option('-f', action='store_true', dest='force',
1024 help="force yes to questions (don't prompt)")
1025 parser.add_option('-c', dest='contributor',
1026 help="external contributor for patch (appended to " +
1027 "description and used as author for git). Should be " +
1028 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001029 (options, args) = parser.parse_args(args)
1030 cl = Changelist()
1031
1032 if not args or cmd == 'push':
1033 # Default to merging against our best guess of the upstream branch.
1034 args = [cl.GetUpstreamBranch()]
1035
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001036 if options.contributor:
1037 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1038 print "Please provide contibutor as 'First Last <email@example.com>'"
1039 return 1
1040
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001041 base_branch = args[0]
1042
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001043 # Make sure index is up-to-date before running diff-index.
1044 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001045 if RunGit(['diff-index', 'HEAD']):
1046 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1047 return 1
1048
1049 # This rev-list syntax means "show all commits not in my branch that
1050 # are in base_branch".
1051 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1052 base_branch]).splitlines()
1053 if upstream_commits:
1054 print ('Base branch "%s" has %d commits '
1055 'not in this branch.' % (base_branch, len(upstream_commits)))
1056 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1057 return 1
1058
1059 if cmd == 'dcommit':
1060 # This is the revision `svn dcommit` will commit on top of.
1061 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1062 '--pretty=format:%H'])
1063 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1064 if extra_commits:
1065 print ('This branch has %d additional commits not upstreamed yet.'
1066 % len(extra_commits.splitlines()))
1067 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1068 'before attempting to %s.' % (base_branch, cmd))
1069 return 1
1070
dpranke@chromium.org5ac21012011-03-16 02:58:25 +00001071 if not options.bypass_hooks and not options.force:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001072 author = None
1073 if options.contributor:
1074 author = re.search(r'\<(.*)\>', options.contributor).group(1)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001075 cl.RunHook(committing=True, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001076 may_prompt=True, verbose=options.verbose,
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001077 author=author)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078
1079 if cmd == 'dcommit':
1080 # Check the tree status if the tree status URL is set.
1081 status = GetTreeStatus()
1082 if 'closed' == status:
1083 print ('The tree is closed. Please wait for it to reopen. Use '
1084 '"git cl dcommit -f" to commit on a closed tree.')
1085 return 1
1086 elif 'unknown' == status:
1087 print ('Unable to determine tree status. Please verify manually and '
1088 'use "git cl dcommit -f" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001089 else:
1090 breakpad.SendStack(
1091 'GitClHooksBypassedCommit',
1092 'Issue %s/%s bypassed hook when committing' %
1093 (cl.GetRietveldServer(), cl.GetIssue()))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001094
1095 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001096 if not description and cl.GetIssue():
1097 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001098
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001099 if not description:
1100 print 'No description set.'
1101 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1102 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001104 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106
1107 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 description += "\nPatch from %s." % options.contributor
1109 print 'Description:', repr(description)
1110
1111 branches = [base_branch, cl.GetBranchRef()]
1112 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001113 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001114 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001115
1116 # We want to squash all this branch's commits into one commit with the
1117 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001118 # We do this by doing a "reset --soft" to the base branch (which keeps
1119 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120 MERGE_BRANCH = 'git-cl-commit'
1121 # Delete the merge branch if it already exists.
1122 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1123 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1124 RunGit(['branch', '-D', MERGE_BRANCH])
1125
1126 # We might be in a directory that's present in this branch but not in the
1127 # trunk. Move up to the top of the tree so that git commands that expect a
1128 # valid CWD won't fail after we check out the merge branch.
1129 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1130 if rel_base_path:
1131 os.chdir(rel_base_path)
1132
1133 # Stuff our change into the merge branch.
1134 # We wrap in a try...finally block so if anything goes wrong,
1135 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001136 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001137 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001138 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1139 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 if options.contributor:
1141 RunGit(['commit', '--author', options.contributor, '-m', description])
1142 else:
1143 RunGit(['commit', '-m', description])
1144 if cmd == 'push':
1145 # push the merge branch.
1146 remote, branch = cl.FetchUpstreamTuple()
1147 retcode, output = RunGitWithCode(
1148 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1149 logging.debug(output)
1150 else:
1151 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001152 retcode, output = RunGitWithCode(['svn', 'dcommit',
1153 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 finally:
1155 # And then swap back to the original branch and clean up.
1156 RunGit(['checkout', '-q', cl.GetBranch()])
1157 RunGit(['branch', '-D', MERGE_BRANCH])
1158
1159 if cl.GetIssue():
1160 if cmd == 'dcommit' and 'Committed r' in output:
1161 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1162 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001163 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1164 for l in output.splitlines(False))
1165 match = filter(None, match)
1166 if len(match) != 1:
1167 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1168 output)
1169 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001170 else:
1171 return 1
1172 viewvc_url = settings.GetViewVCUrl()
1173 if viewvc_url and revision:
1174 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1175 print ('Closing issue '
1176 '(you may be prompted for your codereview password)...')
1177 cl.CloseIssue()
1178 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001179
1180 if retcode == 0:
1181 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1182 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001183 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001184
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185 return 0
1186
1187
1188@usage('[upstream branch to apply against]')
1189def CMDdcommit(parser, args):
1190 """commit the current changelist via git-svn"""
1191 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001192 message = """This doesn't appear to be an SVN repository.
1193If your project has a git mirror with an upstream SVN master, you probably need
1194to run 'git svn init', see your project's git mirror documentation.
1195If your project has a true writeable upstream repository, you probably want
1196to run 'git cl push' instead.
1197Choose wisely, if you get this wrong, your commit might appear to succeed but
1198will instead be silently ignored."""
1199 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001200 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 return SendUpstream(parser, args, 'dcommit')
1202
1203
1204@usage('[upstream branch to apply against]')
1205def CMDpush(parser, args):
1206 """commit the current changelist via git"""
1207 if settings.GetIsGitSvn():
1208 print('This appears to be an SVN repository.')
1209 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001210 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 return SendUpstream(parser, args, 'push')
1212
1213
1214@usage('<patch url or issue id>')
1215def CMDpatch(parser, args):
1216 """patch in a code review"""
1217 parser.add_option('-b', dest='newbranch',
1218 help='create a new branch off trunk for the patch')
1219 parser.add_option('-f', action='store_true', dest='force',
1220 help='with -b, clobber any existing branch')
1221 parser.add_option('--reject', action='store_true', dest='reject',
1222 help='allow failed patches and spew .rej files')
1223 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1224 help="don't commit after patch applies")
1225 (options, args) = parser.parse_args(args)
1226 if len(args) != 1:
1227 parser.print_help()
1228 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001229 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001230
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001231 # TODO(maruel): Use apply_issue.py
1232
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001233 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001234 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001235 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001236 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001237 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001238 # Assume it's a URL to the patch. Default to https.
1239 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001240 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001241 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 DieWithError('Must pass an issue ID or full URL for '
1243 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001244 issue = match.group(1)
1245 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246
1247 if options.newbranch:
1248 if options.force:
1249 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001250 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001251 RunGit(['checkout', '-b', options.newbranch,
1252 Changelist().GetUpstreamBranch()])
1253
1254 # Switch up to the top-level directory, if necessary, in preparation for
1255 # applying the patch.
1256 top = RunGit(['rev-parse', '--show-cdup']).strip()
1257 if top:
1258 os.chdir(top)
1259
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 # Git patches have a/ at the beginning of source paths. We strip that out
1261 # with a sed script rather than the -p flag to patch so we can feed either
1262 # Git or svn-style patches into the same apply command.
1263 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001264 try:
1265 patch_data = subprocess2.check_output(
1266 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1267 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001268 DieWithError('Git patch mungling failed.')
1269 logging.info(patch_data)
1270 # We use "git apply" to apply the patch instead of "patch" so that we can
1271 # pick up file adds.
1272 # The --index flag means: also insert into the index (so we catch adds).
1273 cmd = ['git', 'apply', '--index', '-p0']
1274 if options.reject:
1275 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001276 try:
1277 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1278 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 DieWithError('Failed to apply the patch')
1280
1281 # If we had an issue, commit the current state and register the issue.
1282 if not options.nocommit:
1283 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1284 cl = Changelist()
1285 cl.SetIssue(issue)
1286 print "Committed patch."
1287 else:
1288 print "Patch applied to index."
1289 return 0
1290
1291
1292def CMDrebase(parser, args):
1293 """rebase current branch on top of svn repo"""
1294 # Provide a wrapper for git svn rebase to help avoid accidental
1295 # git svn dcommit.
1296 # It's the only command that doesn't use parser at all since we just defer
1297 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001298 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299
1300
1301def GetTreeStatus():
1302 """Fetches the tree status and returns either 'open', 'closed',
1303 'unknown' or 'unset'."""
1304 url = settings.GetTreeStatusUrl(error_ok=True)
1305 if url:
1306 status = urllib2.urlopen(url).read().lower()
1307 if status.find('closed') != -1 or status == '0':
1308 return 'closed'
1309 elif status.find('open') != -1 or status == '1':
1310 return 'open'
1311 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 return 'unset'
1313
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001314
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001315def GetTreeStatusReason():
1316 """Fetches the tree status from a json url and returns the message
1317 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001318 url = settings.GetTreeStatusUrl()
1319 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001320 connection = urllib2.urlopen(json_url)
1321 status = json.loads(connection.read())
1322 connection.close()
1323 return status['message']
1324
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001325
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326def CMDtree(parser, args):
1327 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001328 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 status = GetTreeStatus()
1330 if 'unset' == status:
1331 print 'You must configure your tree status URL by running "git cl config".'
1332 return 2
1333
1334 print "The tree is %s" % status
1335 print
1336 print GetTreeStatusReason()
1337 if status != 'open':
1338 return 1
1339 return 0
1340
1341
1342def CMDupstream(parser, args):
1343 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001344 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001345 if args:
1346 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001347 cl = Changelist()
1348 print cl.GetUpstreamBranch()
1349 return 0
1350
1351
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001352def CMDset_commit(parser, args):
1353 """set the commit bit"""
1354 _, args = parser.parse_args(args)
1355 if args:
1356 parser.error('Unrecognized args: %s' % ' '.join(args))
1357 cl = Changelist()
1358 cl.SetFlag('commit', '1')
1359 return 0
1360
1361
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362def Command(name):
1363 return getattr(sys.modules[__name__], 'CMD' + name, None)
1364
1365
1366def CMDhelp(parser, args):
1367 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001368 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 if len(args) == 1:
1370 return main(args + ['--help'])
1371 parser.print_help()
1372 return 0
1373
1374
1375def GenUsage(parser, command):
1376 """Modify an OptParse object with the function's documentation."""
1377 obj = Command(command)
1378 more = getattr(obj, 'usage_more', '')
1379 if command == 'help':
1380 command = '<command>'
1381 else:
1382 # OptParser.description prefer nicely non-formatted strings.
1383 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1384 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1385
1386
1387def main(argv):
1388 """Doesn't parse the arguments here, just find the right subcommand to
1389 execute."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001390 # Reload settings.
1391 global settings
1392 settings = Settings()
1393
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001394 # Do it late so all commands are listed.
1395 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1396 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1397 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1398
1399 # Create the option parse and add --verbose support.
1400 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001401 parser.add_option(
1402 '-v', '--verbose', action='count', default=0,
1403 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404 old_parser_args = parser.parse_args
1405 def Parse(args):
1406 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001407 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001408 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001409 elif options.verbose:
1410 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 else:
1412 logging.basicConfig(level=logging.WARNING)
1413 return options, args
1414 parser.parse_args = Parse
1415
1416 if argv:
1417 command = Command(argv[0])
1418 if command:
1419 # "fix" the usage and the description now that we know the subcommand.
1420 GenUsage(parser, argv[0])
1421 try:
1422 return command(parser, argv[1:])
1423 except urllib2.HTTPError, e:
1424 if e.code != 500:
1425 raise
1426 DieWithError(
1427 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1428 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1429
1430 # Not a known command. Default to help.
1431 GenUsage(parser, 'help')
1432 return CMDhelp(parser, argv)
1433
1434
1435if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001436 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 sys.exit(main(sys.argv[1:]))