blob: 9ba9062692ab63168ec0273bf8fa3ea91bf76fd7 [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' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001093 (cl.GetRietveldServer(), cl.GetIssue()),
1094 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095
1096 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001097 if not description and cl.GetIssue():
1098 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001099
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001100 if not description:
1101 print 'No description set.'
1102 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1103 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001104
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001105 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001106 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001107
1108 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 description += "\nPatch from %s." % options.contributor
1110 print 'Description:', repr(description)
1111
1112 branches = [base_branch, cl.GetBranchRef()]
1113 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001114 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001115 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001116
1117 # We want to squash all this branch's commits into one commit with the
1118 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001119 # We do this by doing a "reset --soft" to the base branch (which keeps
1120 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121 MERGE_BRANCH = 'git-cl-commit'
1122 # Delete the merge branch if it already exists.
1123 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1124 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1125 RunGit(['branch', '-D', MERGE_BRANCH])
1126
1127 # We might be in a directory that's present in this branch but not in the
1128 # trunk. Move up to the top of the tree so that git commands that expect a
1129 # valid CWD won't fail after we check out the merge branch.
1130 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1131 if rel_base_path:
1132 os.chdir(rel_base_path)
1133
1134 # Stuff our change into the merge branch.
1135 # We wrap in a try...finally block so if anything goes wrong,
1136 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001137 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001139 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1140 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001141 if options.contributor:
1142 RunGit(['commit', '--author', options.contributor, '-m', description])
1143 else:
1144 RunGit(['commit', '-m', description])
1145 if cmd == 'push':
1146 # push the merge branch.
1147 remote, branch = cl.FetchUpstreamTuple()
1148 retcode, output = RunGitWithCode(
1149 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1150 logging.debug(output)
1151 else:
1152 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001153 retcode, output = RunGitWithCode(['svn', 'dcommit',
1154 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155 finally:
1156 # And then swap back to the original branch and clean up.
1157 RunGit(['checkout', '-q', cl.GetBranch()])
1158 RunGit(['branch', '-D', MERGE_BRANCH])
1159
1160 if cl.GetIssue():
1161 if cmd == 'dcommit' and 'Committed r' in output:
1162 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1163 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001164 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1165 for l in output.splitlines(False))
1166 match = filter(None, match)
1167 if len(match) != 1:
1168 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1169 output)
1170 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001171 else:
1172 return 1
1173 viewvc_url = settings.GetViewVCUrl()
1174 if viewvc_url and revision:
1175 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1176 print ('Closing issue '
1177 '(you may be prompted for your codereview password)...')
1178 cl.CloseIssue()
1179 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001180
1181 if retcode == 0:
1182 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1183 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001184 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001185
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001186 return 0
1187
1188
1189@usage('[upstream branch to apply against]')
1190def CMDdcommit(parser, args):
1191 """commit the current changelist via git-svn"""
1192 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001193 message = """This doesn't appear to be an SVN repository.
1194If your project has a git mirror with an upstream SVN master, you probably need
1195to run 'git svn init', see your project's git mirror documentation.
1196If your project has a true writeable upstream repository, you probably want
1197to run 'git cl push' instead.
1198Choose wisely, if you get this wrong, your commit might appear to succeed but
1199will instead be silently ignored."""
1200 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001201 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001202 return SendUpstream(parser, args, 'dcommit')
1203
1204
1205@usage('[upstream branch to apply against]')
1206def CMDpush(parser, args):
1207 """commit the current changelist via git"""
1208 if settings.GetIsGitSvn():
1209 print('This appears to be an SVN repository.')
1210 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001211 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001212 return SendUpstream(parser, args, 'push')
1213
1214
1215@usage('<patch url or issue id>')
1216def CMDpatch(parser, args):
1217 """patch in a code review"""
1218 parser.add_option('-b', dest='newbranch',
1219 help='create a new branch off trunk for the patch')
1220 parser.add_option('-f', action='store_true', dest='force',
1221 help='with -b, clobber any existing branch')
1222 parser.add_option('--reject', action='store_true', dest='reject',
1223 help='allow failed patches and spew .rej files')
1224 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1225 help="don't commit after patch applies")
1226 (options, args) = parser.parse_args(args)
1227 if len(args) != 1:
1228 parser.print_help()
1229 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001230 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001232 # TODO(maruel): Use apply_issue.py
1233
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001234 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001235 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001236 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001237 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001238 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001239 # Assume it's a URL to the patch. Default to https.
1240 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001241 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001242 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 DieWithError('Must pass an issue ID or full URL for '
1244 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001245 issue = match.group(1)
1246 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001247
1248 if options.newbranch:
1249 if options.force:
1250 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001251 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 RunGit(['checkout', '-b', options.newbranch,
1253 Changelist().GetUpstreamBranch()])
1254
1255 # Switch up to the top-level directory, if necessary, in preparation for
1256 # applying the patch.
1257 top = RunGit(['rev-parse', '--show-cdup']).strip()
1258 if top:
1259 os.chdir(top)
1260
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001261 # Git patches have a/ at the beginning of source paths. We strip that out
1262 # with a sed script rather than the -p flag to patch so we can feed either
1263 # Git or svn-style patches into the same apply command.
1264 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001265 try:
1266 patch_data = subprocess2.check_output(
1267 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1268 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001269 DieWithError('Git patch mungling failed.')
1270 logging.info(patch_data)
1271 # We use "git apply" to apply the patch instead of "patch" so that we can
1272 # pick up file adds.
1273 # The --index flag means: also insert into the index (so we catch adds).
1274 cmd = ['git', 'apply', '--index', '-p0']
1275 if options.reject:
1276 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001277 try:
1278 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1279 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001280 DieWithError('Failed to apply the patch')
1281
1282 # If we had an issue, commit the current state and register the issue.
1283 if not options.nocommit:
1284 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1285 cl = Changelist()
1286 cl.SetIssue(issue)
1287 print "Committed patch."
1288 else:
1289 print "Patch applied to index."
1290 return 0
1291
1292
1293def CMDrebase(parser, args):
1294 """rebase current branch on top of svn repo"""
1295 # Provide a wrapper for git svn rebase to help avoid accidental
1296 # git svn dcommit.
1297 # It's the only command that doesn't use parser at all since we just defer
1298 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001299 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300
1301
1302def GetTreeStatus():
1303 """Fetches the tree status and returns either 'open', 'closed',
1304 'unknown' or 'unset'."""
1305 url = settings.GetTreeStatusUrl(error_ok=True)
1306 if url:
1307 status = urllib2.urlopen(url).read().lower()
1308 if status.find('closed') != -1 or status == '0':
1309 return 'closed'
1310 elif status.find('open') != -1 or status == '1':
1311 return 'open'
1312 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 return 'unset'
1314
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001315
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001316def GetTreeStatusReason():
1317 """Fetches the tree status from a json url and returns the message
1318 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001319 url = settings.GetTreeStatusUrl()
1320 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 connection = urllib2.urlopen(json_url)
1322 status = json.loads(connection.read())
1323 connection.close()
1324 return status['message']
1325
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001326
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001327def CMDtree(parser, args):
1328 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001329 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 status = GetTreeStatus()
1331 if 'unset' == status:
1332 print 'You must configure your tree status URL by running "git cl config".'
1333 return 2
1334
1335 print "The tree is %s" % status
1336 print
1337 print GetTreeStatusReason()
1338 if status != 'open':
1339 return 1
1340 return 0
1341
1342
1343def CMDupstream(parser, args):
1344 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001345 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001346 if args:
1347 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001348 cl = Changelist()
1349 print cl.GetUpstreamBranch()
1350 return 0
1351
1352
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001353def CMDset_commit(parser, args):
1354 """set the commit bit"""
1355 _, args = parser.parse_args(args)
1356 if args:
1357 parser.error('Unrecognized args: %s' % ' '.join(args))
1358 cl = Changelist()
1359 cl.SetFlag('commit', '1')
1360 return 0
1361
1362
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363def Command(name):
1364 return getattr(sys.modules[__name__], 'CMD' + name, None)
1365
1366
1367def CMDhelp(parser, args):
1368 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001369 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370 if len(args) == 1:
1371 return main(args + ['--help'])
1372 parser.print_help()
1373 return 0
1374
1375
1376def GenUsage(parser, command):
1377 """Modify an OptParse object with the function's documentation."""
1378 obj = Command(command)
1379 more = getattr(obj, 'usage_more', '')
1380 if command == 'help':
1381 command = '<command>'
1382 else:
1383 # OptParser.description prefer nicely non-formatted strings.
1384 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1385 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1386
1387
1388def main(argv):
1389 """Doesn't parse the arguments here, just find the right subcommand to
1390 execute."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001391 # Reload settings.
1392 global settings
1393 settings = Settings()
1394
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395 # Do it late so all commands are listed.
1396 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1397 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1398 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1399
1400 # Create the option parse and add --verbose support.
1401 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001402 parser.add_option(
1403 '-v', '--verbose', action='count', default=0,
1404 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001405 old_parser_args = parser.parse_args
1406 def Parse(args):
1407 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001408 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001410 elif options.verbose:
1411 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 else:
1413 logging.basicConfig(level=logging.WARNING)
1414 return options, args
1415 parser.parse_args = Parse
1416
1417 if argv:
1418 command = Command(argv[0])
1419 if command:
1420 # "fix" the usage and the description now that we know the subcommand.
1421 GenUsage(parser, argv[0])
1422 try:
1423 return command(parser, argv[1:])
1424 except urllib2.HTTPError, e:
1425 if e.code != 500:
1426 raise
1427 DieWithError(
1428 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1429 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1430
1431 # Not a known command. Default to help.
1432 GenUsage(parser, 'help')
1433 return CMDhelp(parser, argv)
1434
1435
1436if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001437 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001438 sys.exit(main(sys.argv[1:]))