blob: c24fdb6afc47926c51765b472da152e3b0018de4 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000010import logging
11import optparse
12import os
13import re
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000015import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000016import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import urllib2
18
19try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000020 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021except ImportError:
22 pass
23
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000024try:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000025 import simplejson as json # pylint: disable=F0401
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000026except ImportError:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000028 import json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029 except ImportError:
30 # Fall back to the packaged version.
maruel@chromium.orgb35c00c2011-03-31 00:43:35 +000031 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgfe79c312011-04-01 20:15:52 +000032 import simplejson as json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
34
35from third_party import upload
36import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000040import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
44
45
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000046DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000047POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
49
maruel@chromium.org90541732011-04-01 17:54:18 +000050
maruel@chromium.orgddd59412011-11-30 14:20:38 +000051# Initialized in main()
52settings = None
53
54
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000056 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057 sys.exit(1)
58
59
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061 try:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062 return subprocess2.check_output(args, shell=False, **kwargs)
63 except subprocess2.CalledProcessError, e:
64 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 'Command "%s" failed.\n%s' % (
67 ' '.join(args), error_message or e.stdout or ''))
68 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
70
71def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 """Returns stdout."""
73 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
75
76def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077 """Returns return code and stdout."""
78 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
79 return code, out[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def usage(more):
83 def hook(fn):
84 fn.usage_more = more
85 return fn
86 return hook
87
88
maruel@chromium.org90541732011-04-01 17:54:18 +000089def ask_for_data(prompt):
90 try:
91 return raw_input(prompt)
92 except KeyboardInterrupt:
93 # Hide the exception.
94 sys.exit(1)
95
96
bauerb@chromium.org866276c2011-03-18 20:09:31 +000097def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
98 """Return the corresponding git ref if |base_url| together with |glob_spec|
99 matches the full |url|.
100
101 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
102 """
103 fetch_suburl, as_ref = glob_spec.split(':')
104 if allow_wildcards:
105 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
106 if glob_match:
107 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
108 # "branches/{472,597,648}/src:refs/remotes/svn/*".
109 branch_re = re.escape(base_url)
110 if glob_match.group(1):
111 branch_re += '/' + re.escape(glob_match.group(1))
112 wildcard = glob_match.group(2)
113 if wildcard == '*':
114 branch_re += '([^/]*)'
115 else:
116 # Escape and replace surrounding braces with parentheses and commas
117 # with pipe symbols.
118 wildcard = re.escape(wildcard)
119 wildcard = re.sub('^\\\\{', '(', wildcard)
120 wildcard = re.sub('\\\\,', '|', wildcard)
121 wildcard = re.sub('\\\\}$', ')', wildcard)
122 branch_re += wildcard
123 if glob_match.group(3):
124 branch_re += re.escape(glob_match.group(3))
125 match = re.match(branch_re, url)
126 if match:
127 return re.sub('\*$', match.group(1), as_ref)
128
129 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
130 if fetch_suburl:
131 full_url = base_url + '/' + fetch_suburl
132 else:
133 full_url = base_url
134 if full_url == url:
135 return as_ref
136 return None
137
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000139class Settings(object):
140 def __init__(self):
141 self.default_server = None
142 self.cc = None
143 self.root = None
144 self.is_git_svn = None
145 self.svn_branch = None
146 self.tree_status_url = None
147 self.viewvc_url = None
148 self.updated = False
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000149 self.did_migrate_check = False
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150
151 def LazyUpdateIfNeeded(self):
152 """Updates the settings from a codereview.settings file, if available."""
153 if not self.updated:
154 cr_settings_file = FindCodereviewSettingsFile()
155 if cr_settings_file:
156 LoadCodereviewSettingsFromFile(cr_settings_file)
157 self.updated = True
158
159 def GetDefaultServerUrl(self, error_ok=False):
160 if not self.default_server:
161 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000162 self.default_server = gclient_utils.UpgradeToHttps(
163 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000164 if error_ok:
165 return self.default_server
166 if not self.default_server:
167 error_message = ('Could not find settings file. You must configure '
168 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000169 self.default_server = gclient_utils.UpgradeToHttps(
170 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000171 return self.default_server
172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000173 def GetRoot(self):
174 if not self.root:
175 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
176 return self.root
177
178 def GetIsGitSvn(self):
179 """Return true if this repo looks like it's using git-svn."""
180 if self.is_git_svn is None:
181 # If you have any "svn-remote.*" config keys, we think you're using svn.
182 self.is_git_svn = RunGitWithCode(
183 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
184 return self.is_git_svn
185
186 def GetSVNBranch(self):
187 if self.svn_branch is None:
188 if not self.GetIsGitSvn():
189 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
190
191 # Try to figure out which remote branch we're based on.
192 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000193 # 1) iterate through our branch history and find the svn URL.
194 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000195
196 # regexp matching the git-svn line that contains the URL.
197 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
198
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000199 # We don't want to go through all of history, so read a line from the
200 # pipe at a time.
201 # The -100 is an arbitrary limit so we don't search forever.
202 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000203 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000204 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000205 for line in proc.stdout:
206 match = git_svn_re.match(line)
207 if match:
208 url = match.group(1)
209 proc.stdout.close() # Cut pipe.
210 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000211
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000212 if url:
213 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
214 remotes = RunGit(['config', '--get-regexp',
215 r'^svn-remote\..*\.url']).splitlines()
216 for remote in remotes:
217 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000218 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000219 remote = match.group(1)
220 base_url = match.group(2)
221 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000222 ['config', 'svn-remote.%s.fetch' % remote],
223 error_ok=True).strip()
224 if fetch_spec:
225 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
226 if self.svn_branch:
227 break
228 branch_spec = RunGit(
229 ['config', 'svn-remote.%s.branches' % remote],
230 error_ok=True).strip()
231 if branch_spec:
232 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
233 if self.svn_branch:
234 break
235 tag_spec = RunGit(
236 ['config', 'svn-remote.%s.tags' % remote],
237 error_ok=True).strip()
238 if tag_spec:
239 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
240 if self.svn_branch:
241 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000242
243 if not self.svn_branch:
244 DieWithError('Can\'t guess svn branch -- try specifying it on the '
245 'command line')
246
247 return self.svn_branch
248
249 def GetTreeStatusUrl(self, error_ok=False):
250 if not self.tree_status_url:
251 error_message = ('You must configure your tree status URL by running '
252 '"git cl config".')
253 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
254 error_ok=error_ok,
255 error_message=error_message)
256 return self.tree_status_url
257
258 def GetViewVCUrl(self):
259 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000260 self.viewvc_url = gclient_utils.UpgradeToHttps(
261 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000262 return self.viewvc_url
263
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000264 def GetDefaultCCList(self):
265 return self._GetConfig('rietveld.cc', error_ok=True)
266
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000267 def _GetConfig(self, param, **kwargs):
268 self.LazyUpdateIfNeeded()
269 return RunGit(['config', param], **kwargs).strip()
270
271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272def CheckForMigration():
273 """Migrate from the old issue format, if found.
274
275 We used to store the branch<->issue mapping in a file in .git, but it's
276 better to store it in the .git/config, since deleting a branch deletes that
277 branch's entry there.
278 """
279
280 # Don't run more than once.
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000281 if settings.did_migrate_check:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000282 return
283
284 gitdir = RunGit(['rev-parse', '--git-dir']).strip()
285 storepath = os.path.join(gitdir, 'cl-mapping')
286 if os.path.exists(storepath):
287 print "old-style git-cl mapping file (%s) found; migrating." % storepath
288 store = open(storepath, 'r')
289 for line in store:
290 branch, issue = line.strip().split()
291 RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch),
292 issue])
293 store.close()
294 os.remove(storepath)
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000295 settings.did_migrate_check = True
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000296
297
298def ShortBranchName(branch):
299 """Convert a name like 'refs/heads/foo' to just 'foo'."""
300 return branch.replace('refs/heads/', '')
301
302
303class Changelist(object):
304 def __init__(self, branchref=None):
305 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000306 global settings
307 if not settings:
308 # Happens when git_cl.py is used as a utility library.
309 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 settings.GetDefaultServerUrl()
311 self.branchref = branchref
312 if self.branchref:
313 self.branch = ShortBranchName(self.branchref)
314 else:
315 self.branch = None
316 self.rietveld_server = None
317 self.upstream_branch = None
318 self.has_issue = False
319 self.issue = None
320 self.has_description = False
321 self.description = None
322 self.has_patchset = False
323 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000324 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000325 self.cc = None
326 self.watchers = ()
327
328 def GetCCList(self):
329 """Return the users cc'd on this CL.
330
331 Return is a string suitable for passing to gcl with the --cc flag.
332 """
333 if self.cc is None:
334 base_cc = settings .GetDefaultCCList()
335 more_cc = ','.join(self.watchers)
336 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
337 return self.cc
338
339 def SetWatchers(self, watchers):
340 """Set the list of email addresses that should be cc'd based on the changed
341 files in this CL.
342 """
343 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000344
345 def GetBranch(self):
346 """Returns the short branch name, e.g. 'master'."""
347 if not self.branch:
348 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
349 self.branch = ShortBranchName(self.branchref)
350 return self.branch
351
352 def GetBranchRef(self):
353 """Returns the full branch name, e.g. 'refs/heads/master'."""
354 self.GetBranch() # Poke the lazy loader.
355 return self.branchref
356
357 def FetchUpstreamTuple(self):
358 """Returns a tuple containg remote and remote ref,
359 e.g. 'origin', 'refs/heads/master'
360 """
361 remote = '.'
362 branch = self.GetBranch()
363 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
364 error_ok=True).strip()
365 if upstream_branch:
366 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
367 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000368 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
369 error_ok=True).strip()
370 if upstream_branch:
371 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000372 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000373 # Fall back on trying a git-svn upstream branch.
374 if settings.GetIsGitSvn():
375 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000376 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000377 # Else, try to guess the origin remote.
378 remote_branches = RunGit(['branch', '-r']).split()
379 if 'origin/master' in remote_branches:
380 # Fall back on origin/master if it exits.
381 remote = 'origin'
382 upstream_branch = 'refs/heads/master'
383 elif 'origin/trunk' in remote_branches:
384 # Fall back on origin/trunk if it exists. Generally a shared
385 # git-svn clone
386 remote = 'origin'
387 upstream_branch = 'refs/heads/trunk'
388 else:
389 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000390Either pass complete "git diff"-style arguments, like
391 git cl upload origin/master
392or verify this branch is set up to track another (via the --track argument to
393"git checkout -b ...").""")
394
395 return remote, upstream_branch
396
397 def GetUpstreamBranch(self):
398 if self.upstream_branch is None:
399 remote, upstream_branch = self.FetchUpstreamTuple()
400 if remote is not '.':
401 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
402 self.upstream_branch = upstream_branch
403 return self.upstream_branch
404
405 def GetRemoteUrl(self):
406 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
407
408 Returns None if there is no remote.
409 """
410 remote = self.FetchUpstreamTuple()[0]
411 if remote == '.':
412 return None
413 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
414
415 def GetIssue(self):
416 if not self.has_issue:
417 CheckForMigration()
418 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
419 if issue:
420 self.issue = issue
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000421 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000422 ['config', self._RietveldServer()], error_ok=True).strip())
423 else:
424 self.issue = None
425 if not self.rietveld_server:
426 self.rietveld_server = settings.GetDefaultServerUrl()
427 self.has_issue = True
428 return self.issue
429
430 def GetRietveldServer(self):
431 self.GetIssue()
432 return self.rietveld_server
433
434 def GetIssueURL(self):
435 """Get the URL for a particular issue."""
436 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
437
438 def GetDescription(self, pretty=False):
439 if not self.has_description:
440 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000441 issue = int(self.GetIssue())
442 try:
443 self.description = self.RpcServer().get_description(issue).strip()
444 except urllib2.HTTPError, e:
445 if e.code == 404:
446 DieWithError(
447 ('\nWhile fetching the description for issue %d, received a '
448 '404 (not found)\n'
449 'error. It is likely that you deleted this '
450 'issue on the server. If this is the\n'
451 'case, please run\n\n'
452 ' git cl issue 0\n\n'
453 'to clear the association with the deleted issue. Then run '
454 'this command again.') % issue)
455 else:
456 DieWithError(
457 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000458 self.has_description = True
459 if pretty:
460 wrapper = textwrap.TextWrapper()
461 wrapper.initial_indent = wrapper.subsequent_indent = ' '
462 return wrapper.fill(self.description)
463 return self.description
464
465 def GetPatchset(self):
466 if not self.has_patchset:
467 patchset = RunGit(['config', self._PatchsetSetting()],
468 error_ok=True).strip()
469 if patchset:
470 self.patchset = patchset
471 else:
472 self.patchset = None
473 self.has_patchset = True
474 return self.patchset
475
476 def SetPatchset(self, patchset):
477 """Set this branch's patchset. If patchset=0, clears the patchset."""
478 if patchset:
479 RunGit(['config', self._PatchsetSetting(), str(patchset)])
480 else:
481 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000482 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000483 self.has_patchset = False
484
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000485 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000486 patchset = self.RpcServer().get_issue_properties(
487 int(issue), False)['patchsets'][-1]
488 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000489 '/download/issue%s_%s.diff' % (issue, patchset))
490
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000491 def SetIssue(self, issue):
492 """Set this branch's issue. If issue=0, clears the issue."""
493 if issue:
494 RunGit(['config', self._IssueSetting(), str(issue)])
495 if self.rietveld_server:
496 RunGit(['config', self._RietveldServer(), self.rietveld_server])
497 else:
498 RunGit(['config', '--unset', self._IssueSetting()])
499 self.SetPatchset(0)
500 self.has_issue = False
501
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000502 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000503 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
504 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000505
506 # We use the sha1 of HEAD as a name of this change.
507 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000508 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000509 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000510 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000511 except subprocess2.CalledProcessError:
512 DieWithError(
513 ('\nFailed to diff against upstream branch %s!\n\n'
514 'This branch probably doesn\'t exist anymore. To reset the\n'
515 'tracking branch, please run\n'
516 ' git branch --set-upstream %s trunk\n'
517 'replacing trunk with origin/master or the relevant branch') %
518 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000519
520 issue = ConvertToInteger(self.GetIssue())
521 patchset = ConvertToInteger(self.GetPatchset())
522 if issue:
523 description = self.GetDescription()
524 else:
525 # If the change was never uploaded, use the log messages of all commits
526 # up to the branch point, as git cl upload will prefill the description
527 # with these log messages.
528 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
529 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000530
531 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000532 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000533 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000534 name,
535 description,
536 absroot,
537 files,
538 issue,
539 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000540 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000541
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000542 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
543 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
544 change = self.GetChange(upstream_branch, author)
545
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000546 # Apply watchlists on upload.
547 if not committing:
548 watchlist = watchlists.Watchlists(change.RepositoryRoot())
549 files = [f.LocalPath() for f in change.AffectedFiles()]
550 self.SetWatchers(watchlist.GetWatchersForPaths(files))
551
552 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000553 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000554 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000555 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000556 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000557 except presubmit_support.PresubmitFailure, e:
558 DieWithError(
559 ('%s\nMaybe your depot_tools is out of date?\n'
560 'If all fails, contact maruel@') % e)
561
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000562 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000563 """Updates the description and closes the issue."""
564 issue = int(self.GetIssue())
565 self.RpcServer().update_description(issue, self.description)
566 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000567
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000568 def SetFlag(self, flag, value):
569 """Patchset must match."""
570 if not self.GetPatchset():
571 DieWithError('The patchset needs to match. Send another patchset.')
572 try:
573 return self.RpcServer().set_flag(
574 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
575 except urllib2.HTTPError, e:
576 if e.code == 404:
577 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
578 if e.code == 403:
579 DieWithError(
580 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
581 'match?') % (self.GetIssue(), self.GetPatchset()))
582 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000583
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000584 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 """Returns an upload.RpcServer() to access this review's rietveld instance.
586 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000587 if not self._rpc_server:
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000588 self.GetIssue()
589 self._rpc_server = rietveld.Rietveld(self.rietveld_server, None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000590 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591
592 def _IssueSetting(self):
593 """Return the git setting that stores this change's issue."""
594 return 'branch.%s.rietveldissue' % self.GetBranch()
595
596 def _PatchsetSetting(self):
597 """Return the git setting that stores this change's most recent patchset."""
598 return 'branch.%s.rietveldpatchset' % self.GetBranch()
599
600 def _RietveldServer(self):
601 """Returns the git setting that stores this change's rietveld server."""
602 return 'branch.%s.rietveldserver' % self.GetBranch()
603
604
605def GetCodereviewSettingsInteractively():
606 """Prompt the user for settings."""
607 server = settings.GetDefaultServerUrl(error_ok=True)
608 prompt = 'Rietveld server (host[:port])'
609 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000610 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000611 if not server and not newserver:
612 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000613 if newserver:
614 newserver = gclient_utils.UpgradeToHttps(newserver)
615 if newserver != server:
616 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000618 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 prompt = caption
620 if initial:
621 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000622 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000623 if new_val == 'x':
624 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000625 elif new_val:
626 if is_url:
627 new_val = gclient_utils.UpgradeToHttps(new_val)
628 if new_val != initial:
629 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000630
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000631 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000632 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000633 'tree-status-url', False)
634 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000635
636 # TODO: configure a default branch to diff against, rather than this
637 # svn-based hackery.
638
639
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000640class ChangeDescription(object):
641 """Contains a parsed form of the change description."""
jam@chromium.org31083642012-01-27 03:14:45 +0000642 def __init__(self, subject, log_desc, reviewers):
643 self.subject = subject
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000644 self.log_desc = log_desc
645 self.reviewers = reviewers
646 self.description = self.log_desc
647
jam@chromium.org31083642012-01-27 03:14:45 +0000648 def Update(self):
649 initial_text = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000650# This will displayed on the codereview site.
651# The first line will also be used as the subject of the review.
652"""
jam@chromium.org31083642012-01-27 03:14:45 +0000653 initial_text += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000654 if ('\nR=' not in self.description and
655 '\nTBR=' not in self.description and
656 self.reviewers):
jam@chromium.org31083642012-01-27 03:14:45 +0000657 initial_text += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000658 if '\nBUG=' not in self.description:
jam@chromium.org31083642012-01-27 03:14:45 +0000659 initial_text += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000660 if '\nTEST=' not in self.description:
jam@chromium.org31083642012-01-27 03:14:45 +0000661 initial_text += '\nTEST='
662 initial_text = initial_text.rstrip('\n') + '\n'
663 content = gclient_utils.RunEditor(initial_text, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000664 if not content:
665 DieWithError('Running editor failed')
666 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
667 if not content:
668 DieWithError('No CL description, aborting')
jam@chromium.org31083642012-01-27 03:14:45 +0000669 self._ParseDescription(content)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000670
jam@chromium.org31083642012-01-27 03:14:45 +0000671 def _ParseDescription(self, description):
672 """Updates the list of reviewers and subject from the description."""
673 if not description:
674 self.description = description
675 return
676
677 self.description = description.strip('\n') + '\n'
678 self.subject = description.split('\n', 1)[0]
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000679 # Retrieves all reviewer lines
680 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
jam@chromium.org31083642012-01-27 03:14:45 +0000681 self.reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000682 i.group(2).strip() for i in regexp.finditer(self.description))
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000683
684 def IsEmpty(self):
685 return not self.description
686
687
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000688def FindCodereviewSettingsFile(filename='codereview.settings'):
689 """Finds the given file starting in the cwd and going up.
690
691 Only looks up to the top of the repository unless an
692 'inherit-review-settings-ok' file exists in the root of the repository.
693 """
694 inherit_ok_file = 'inherit-review-settings-ok'
695 cwd = os.getcwd()
696 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
697 if os.path.isfile(os.path.join(root, inherit_ok_file)):
698 root = '/'
699 while True:
700 if filename in os.listdir(cwd):
701 if os.path.isfile(os.path.join(cwd, filename)):
702 return open(os.path.join(cwd, filename))
703 if cwd == root:
704 break
705 cwd = os.path.dirname(cwd)
706
707
708def LoadCodereviewSettingsFromFile(fileobj):
709 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000710 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712 def SetProperty(name, setting, unset_error_ok=False):
713 fullname = 'rietveld.' + name
714 if setting in keyvals:
715 RunGit(['config', fullname, keyvals[setting]])
716 else:
717 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
718
719 SetProperty('server', 'CODE_REVIEW_SERVER')
720 # Only server setting is required. Other settings can be absent.
721 # In that case, we ignore errors raised during option deletion attempt.
722 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
723 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
724 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
725
726 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
727 #should be of the form
728 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
729 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
730 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
731 keyvals['ORIGIN_URL_CONFIG']])
732
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000733
734@usage('[repo root containing codereview.settings]')
735def CMDconfig(parser, args):
736 """edit configuration for this tree"""
737
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000738 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000739 if len(args) == 0:
740 GetCodereviewSettingsInteractively()
741 return 0
742
743 url = args[0]
744 if not url.endswith('codereview.settings'):
745 url = os.path.join(url, 'codereview.settings')
746
747 # Load code review settings and download hooks (if available).
748 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
749 return 0
750
751
752def CMDstatus(parser, args):
753 """show status of changelists"""
754 parser.add_option('--field',
755 help='print only specific field (desc|id|patch|url)')
756 (options, args) = parser.parse_args(args)
757
758 # TODO: maybe make show_branches a flag if necessary.
759 show_branches = not options.field
760
761 if show_branches:
762 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
763 if branches:
764 print 'Branches associated with reviews:'
765 for branch in sorted(branches.splitlines()):
766 cl = Changelist(branchref=branch)
767 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
768
769 cl = Changelist()
770 if options.field:
771 if options.field.startswith('desc'):
772 print cl.GetDescription()
773 elif options.field == 'id':
774 issueid = cl.GetIssue()
775 if issueid:
776 print issueid
777 elif options.field == 'patch':
778 patchset = cl.GetPatchset()
779 if patchset:
780 print patchset
781 elif options.field == 'url':
782 url = cl.GetIssueURL()
783 if url:
784 print url
785 else:
786 print
787 print 'Current branch:',
788 if not cl.GetIssue():
789 print 'no issue assigned.'
790 return 0
791 print cl.GetBranch()
792 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
793 print 'Issue description:'
794 print cl.GetDescription(pretty=True)
795 return 0
796
797
798@usage('[issue_number]')
799def CMDissue(parser, args):
800 """Set or display the current code review issue number.
801
802 Pass issue number 0 to clear the current issue.
803"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000804 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000805
806 cl = Changelist()
807 if len(args) > 0:
808 try:
809 issue = int(args[0])
810 except ValueError:
811 DieWithError('Pass a number to set the issue or none to list it.\n'
812 'Maybe you want to run git cl status?')
813 cl.SetIssue(issue)
814 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
815 return 0
816
817
818def CreateDescriptionFromLog(args):
819 """Pulls out the commit log to use as a base for the CL description."""
820 log_args = []
821 if len(args) == 1 and not args[0].endswith('.'):
822 log_args = [args[0] + '..']
823 elif len(args) == 1 and args[0].endswith('...'):
824 log_args = [args[0][:-1]]
825 elif len(args) == 2:
826 log_args = [args[0] + '..' + args[1]]
827 else:
828 log_args = args[:] # Hope for the best!
829 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
830
831
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000832def ConvertToInteger(inputval):
833 """Convert a string to integer, but returns either an int or None."""
834 try:
835 return int(inputval)
836 except (TypeError, ValueError):
837 return None
838
839
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000840def CMDpresubmit(parser, args):
841 """run presubmit tests on the current changelist"""
842 parser.add_option('--upload', action='store_true',
843 help='Run upload hook instead of the push/dcommit hook')
844 (options, args) = parser.parse_args(args)
845
846 # Make sure index is up-to-date before running diff-index.
847 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
848 if RunGit(['diff-index', 'HEAD']):
849 # TODO(maruel): Is this really necessary?
850 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
851 return 1
852
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000853 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854 if args:
855 base_branch = args[0]
856 else:
857 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000858 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000860 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000861 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000862 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000863 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864
865
866@usage('[args to "git diff"]')
867def CMDupload(parser, args):
868 """upload the current changelist to codereview"""
869 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
870 help='bypass upload presubmit hook')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000871 parser.add_option('-f', action='store_true', dest='force',
872 help="force yes to questions (don't prompt)")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873 parser.add_option('-m', dest='message', help='message for patch')
874 parser.add_option('-r', '--reviewers',
875 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000876 parser.add_option('--cc',
877 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878 parser.add_option('--send-mail', action='store_true',
879 help='send email to reviewer immediately')
880 parser.add_option("--emulate_svn_auto_props", action="store_true",
881 dest="emulate_svn_auto_props",
882 help="Emulate Subversion's auto properties feature.")
883 parser.add_option("--desc_from_logs", action="store_true",
884 dest="from_logs",
885 help="""Squashes git commit logs into change description and
886 uses message as subject""")
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000887 parser.add_option('-c', '--use-commit-queue', action='store_true',
888 help='tell the commit queue to commit this patchset')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000889 (options, args) = parser.parse_args(args)
890
891 # Make sure index is up-to-date before running diff-index.
892 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
893 if RunGit(['diff-index', 'HEAD']):
894 print 'Cannot upload with a dirty tree. You must commit locally first.'
895 return 1
896
897 cl = Changelist()
898 if args:
899 base_branch = args[0]
900 else:
901 # Default to diffing against the "upstream" branch.
902 base_branch = cl.GetUpstreamBranch()
903 args = [base_branch + "..."]
904
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000905 if not options.bypass_hooks:
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000906 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000907 may_prompt=not options.force,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000908 verbose=options.verbose,
909 author=None)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000910 if not hook_results.should_continue():
911 return 1
dpranke@chromium.org5ac21012011-03-16 02:58:25 +0000912 if not options.reviewers and hook_results.reviewers:
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000913 options.reviewers = hook_results.reviewers
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000914
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000915 # --no-ext-diff is broken in some versions of Git, so try to work around
916 # this by overriding the environment (but there is still a problem if the
917 # git config key "diff.external" is used).
918 env = os.environ.copy()
919 if 'GIT_EXTERNAL_DIFF' in env:
920 del env['GIT_EXTERNAL_DIFF']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000921 subprocess2.call(
922 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000923
924 upload_args = ['--assume_yes'] # Don't ask about untracked files.
925 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926 if options.emulate_svn_auto_props:
927 upload_args.append('--emulate_svn_auto_props')
jam@chromium.org31083642012-01-27 03:14:45 +0000928 if options.from_logs and not options.message:
929 print 'Must set message for subject line if using desc_from_logs'
930 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000931
932 change_desc = None
933
934 if cl.GetIssue():
935 if options.message:
936 upload_args.extend(['--message', options.message])
937 upload_args.extend(['--issue', cl.GetIssue()])
938 print ("This branch is associated with issue %s. "
939 "Adding patch to that issue." % cl.GetIssue())
940 else:
jam@chromium.org31083642012-01-27 03:14:45 +0000941 log_desc = CreateDescriptionFromLog(args)
942 change_desc = ChangeDescription(options.message, log_desc,
943 options.reviewers)
944 if not options.from_logs:
945 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000946
947 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948 print "Description is empty; aborting."
949 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000950
jam@chromium.org31083642012-01-27 03:14:45 +0000951 upload_args.extend(['--message', change_desc.subject])
952 upload_args.extend(['--description', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000953 if change_desc.reviewers:
954 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000955 if options.send_mail:
956 if not change_desc.reviewers:
957 DieWithError("Must specify reviewers to send email.")
958 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000959 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000960 if cc:
961 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000962
963 # Include the upstream repo's URL in the change -- this is useful for
964 # projects that have their source spread across multiple repos.
965 remote_url = None
966 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000967 # URL is dependent on the current directory.
968 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000969 if data:
970 keys = dict(line.split(': ', 1) for line in data.splitlines()
971 if ': ' in line)
972 remote_url = keys.get('URL', None)
973 else:
974 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
975 remote_url = (cl.GetRemoteUrl() + '@'
976 + cl.GetUpstreamBranch().split('/')[-1])
977 if remote_url:
978 upload_args.extend(['--base_url', remote_url])
979
980 try:
981 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000982 except KeyboardInterrupt:
983 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000984 except:
985 # If we got an exception after the user typed a description for their
986 # change, back up the description before re-raising.
987 if change_desc:
988 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
989 print '\nGot exception while uploading -- saving description to %s\n' \
990 % backup_path
991 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000992 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000993 backup_file.close()
994 raise
995
996 if not cl.GetIssue():
997 cl.SetIssue(issue)
998 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000999
1000 if options.use_commit_queue:
1001 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 return 0
1003
1004
1005def SendUpstream(parser, args, cmd):
1006 """Common code for CmdPush and CmdDCommit
1007
1008 Squashed commit into a single.
1009 Updates changelog with metadata (e.g. pointer to review).
1010 Pushes/dcommits the code upstream.
1011 Updates review and closes.
1012 """
1013 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1014 help='bypass upload presubmit hook')
1015 parser.add_option('-m', dest='message',
1016 help="override review description")
1017 parser.add_option('-f', action='store_true', dest='force',
1018 help="force yes to questions (don't prompt)")
1019 parser.add_option('-c', dest='contributor',
1020 help="external contributor for patch (appended to " +
1021 "description and used as author for git). Should be " +
1022 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001023 (options, args) = parser.parse_args(args)
1024 cl = Changelist()
1025
1026 if not args or cmd == 'push':
1027 # Default to merging against our best guess of the upstream branch.
1028 args = [cl.GetUpstreamBranch()]
1029
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001030 if options.contributor:
1031 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1032 print "Please provide contibutor as 'First Last <email@example.com>'"
1033 return 1
1034
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001035 base_branch = args[0]
1036
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001037 # Make sure index is up-to-date before running diff-index.
1038 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001039 if RunGit(['diff-index', 'HEAD']):
1040 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1041 return 1
1042
1043 # This rev-list syntax means "show all commits not in my branch that
1044 # are in base_branch".
1045 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1046 base_branch]).splitlines()
1047 if upstream_commits:
1048 print ('Base branch "%s" has %d commits '
1049 'not in this branch.' % (base_branch, len(upstream_commits)))
1050 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1051 return 1
1052
1053 if cmd == 'dcommit':
1054 # This is the revision `svn dcommit` will commit on top of.
1055 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1056 '--pretty=format:%H'])
1057 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1058 if extra_commits:
1059 print ('This branch has %d additional commits not upstreamed yet.'
1060 % len(extra_commits.splitlines()))
1061 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1062 'before attempting to %s.' % (base_branch, cmd))
1063 return 1
1064
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001065 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001066 author = None
1067 if options.contributor:
1068 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001069 hook_results = cl.RunHook(
1070 committing=True,
1071 upstream_branch=base_branch,
1072 may_prompt=not options.force,
1073 verbose=options.verbose,
1074 author=author)
1075 if not hook_results.should_continue():
1076 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001077
1078 if cmd == 'dcommit':
1079 # Check the tree status if the tree status URL is set.
1080 status = GetTreeStatus()
1081 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001082 print('The tree is closed. Please wait for it to reopen. Use '
1083 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084 return 1
1085 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001086 print('Unable to determine tree status. Please verify manually and '
1087 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001088 else:
1089 breakpad.SendStack(
1090 'GitClHooksBypassedCommit',
1091 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001092 (cl.GetRietveldServer(), cl.GetIssue()),
1093 verbose=False)
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:]))