blob: 99950320d6c76ca09e34173d0018feac84f431b3 [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
maruel@chromium.org4f6852c2012-04-20 20:39:20 +000010import json
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000011import logging
12import optparse
13import os
14import re
ukai@chromium.org78c4b982012-02-14 02:20:26 +000015import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000018import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
maruel@chromium.org2a74d372011-03-29 19:05:50 +000026
27from third_party import upload
28import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000029import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000030import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000032import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000034import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035import watchlists
36
37
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000038DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000039POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000040DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +000041GIT_INSTRUCTIONS_URL = 'http://code.google.com/p/chromium/wiki/UsingNewGit'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000042
maruel@chromium.org90541732011-04-01 17:54:18 +000043
maruel@chromium.orgddd59412011-11-30 14:20:38 +000044# Initialized in main()
45settings = None
46
47
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000049 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050 sys.exit(1)
51
52
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000053def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000054 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000055 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000056 except subprocess2.CalledProcessError, e:
57 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000058 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000059 'Command "%s" failed.\n%s' % (
60 ' '.join(args), error_message or e.stdout or ''))
61 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000062
63
64def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000065 """Returns stdout."""
66 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067
68
69def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000070 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000071 try:
72 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
73 return code, out[0]
74 except ValueError:
75 # When the subprocess fails, it returns None. That triggers a ValueError
76 # when trying to unpack the return value into (out, code).
77 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000078
79
80def usage(more):
81 def hook(fn):
82 fn.usage_more = more
83 return fn
84 return hook
85
86
maruel@chromium.org90541732011-04-01 17:54:18 +000087def ask_for_data(prompt):
88 try:
89 return raw_input(prompt)
90 except KeyboardInterrupt:
91 # Hide the exception.
92 sys.exit(1)
93
94
iannucci@chromium.org79540052012-10-19 23:15:26 +000095def git_set_branch_value(key, value):
96 branch = Changelist().GetBranch()
97 if branch:
98 git_key = 'branch.%s.%s' % (branch, key)
99 RunGit(['config', '--int', git_key, "%d" % value])
100
101
102def git_get_branch_default(key, default):
103 branch = Changelist().GetBranch()
104 if branch:
105 git_key = 'branch.%s.%s' % (branch, key)
106 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
107 try:
108 return int(stdout.strip())
109 except ValueError:
110 pass
111 return default
112
113
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000114def add_git_similarity(parser):
115 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000116 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000117 help='Sets the percentage that a pair of files need to match in order to'
118 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000119 parser.add_option(
120 '--find-copies', action='store_true',
121 help='Allows git to look for copies.')
122 parser.add_option(
123 '--no-find-copies', action='store_false', dest='find_copies',
124 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000125
126 old_parser_args = parser.parse_args
127 def Parse(args):
128 options, args = old_parser_args(args)
129
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000130 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000131 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000132 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000133 print('Note: Saving similarity of %d%% in git config.'
134 % options.similarity)
135 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000136
iannucci@chromium.org79540052012-10-19 23:15:26 +0000137 options.similarity = max(0, min(options.similarity, 100))
138
139 if options.find_copies is None:
140 options.find_copies = bool(
141 git_get_branch_default('git-find-copies', True))
142 else:
143 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000144
145 print('Using %d%% similarity for rename/copy detection. '
146 'Override with --similarity.' % options.similarity)
147
148 return options, args
149 parser.parse_args = Parse
150
151
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000152def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
153 """Return the corresponding git ref if |base_url| together with |glob_spec|
154 matches the full |url|.
155
156 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
157 """
158 fetch_suburl, as_ref = glob_spec.split(':')
159 if allow_wildcards:
160 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
161 if glob_match:
162 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
163 # "branches/{472,597,648}/src:refs/remotes/svn/*".
164 branch_re = re.escape(base_url)
165 if glob_match.group(1):
166 branch_re += '/' + re.escape(glob_match.group(1))
167 wildcard = glob_match.group(2)
168 if wildcard == '*':
169 branch_re += '([^/]*)'
170 else:
171 # Escape and replace surrounding braces with parentheses and commas
172 # with pipe symbols.
173 wildcard = re.escape(wildcard)
174 wildcard = re.sub('^\\\\{', '(', wildcard)
175 wildcard = re.sub('\\\\,', '|', wildcard)
176 wildcard = re.sub('\\\\}$', ')', wildcard)
177 branch_re += wildcard
178 if glob_match.group(3):
179 branch_re += re.escape(glob_match.group(3))
180 match = re.match(branch_re, url)
181 if match:
182 return re.sub('\*$', match.group(1), as_ref)
183
184 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
185 if fetch_suburl:
186 full_url = base_url + '/' + fetch_suburl
187 else:
188 full_url = base_url
189 if full_url == url:
190 return as_ref
191 return None
192
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000193
iannucci@chromium.org79540052012-10-19 23:15:26 +0000194def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000195 """Prints statistics about the change to the user."""
196 # --no-ext-diff is broken in some versions of Git, so try to work around
197 # this by overriding the environment (but there is still a problem if the
198 # git config key "diff.external" is used).
199 env = os.environ.copy()
200 if 'GIT_EXTERNAL_DIFF' in env:
201 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000202
203 if find_copies:
204 similarity_options = ['--find-copies-harder', '-l100000',
205 '-C%s' % similarity]
206 else:
207 similarity_options = ['-M%s' % similarity]
208
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000209 return subprocess2.call(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000210 ['git', 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
211 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000212
213
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000214class Settings(object):
215 def __init__(self):
216 self.default_server = None
217 self.cc = None
218 self.root = None
219 self.is_git_svn = None
220 self.svn_branch = None
221 self.tree_status_url = None
222 self.viewvc_url = None
223 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000224 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000225
226 def LazyUpdateIfNeeded(self):
227 """Updates the settings from a codereview.settings file, if available."""
228 if not self.updated:
229 cr_settings_file = FindCodereviewSettingsFile()
230 if cr_settings_file:
231 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000232 self.updated = True
233 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000234 self.updated = True
235
236 def GetDefaultServerUrl(self, error_ok=False):
237 if not self.default_server:
238 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000239 self.default_server = gclient_utils.UpgradeToHttps(
240 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000241 if error_ok:
242 return self.default_server
243 if not self.default_server:
244 error_message = ('Could not find settings file. You must configure '
245 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000246 self.default_server = gclient_utils.UpgradeToHttps(
247 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000248 return self.default_server
249
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000250 def GetRoot(self):
251 if not self.root:
252 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
253 return self.root
254
255 def GetIsGitSvn(self):
256 """Return true if this repo looks like it's using git-svn."""
257 if self.is_git_svn is None:
258 # If you have any "svn-remote.*" config keys, we think you're using svn.
259 self.is_git_svn = RunGitWithCode(
260 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
261 return self.is_git_svn
262
263 def GetSVNBranch(self):
264 if self.svn_branch is None:
265 if not self.GetIsGitSvn():
266 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
267
268 # Try to figure out which remote branch we're based on.
269 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000270 # 1) iterate through our branch history and find the svn URL.
271 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000272
273 # regexp matching the git-svn line that contains the URL.
274 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
275
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000276 # We don't want to go through all of history, so read a line from the
277 # pipe at a time.
278 # The -100 is an arbitrary limit so we don't search forever.
279 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000280 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000281 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000282 for line in proc.stdout:
283 match = git_svn_re.match(line)
284 if match:
285 url = match.group(1)
286 proc.stdout.close() # Cut pipe.
287 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000288
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000289 if url:
290 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
291 remotes = RunGit(['config', '--get-regexp',
292 r'^svn-remote\..*\.url']).splitlines()
293 for remote in remotes:
294 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000295 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000296 remote = match.group(1)
297 base_url = match.group(2)
298 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000299 ['config', 'svn-remote.%s.fetch' % remote],
300 error_ok=True).strip()
301 if fetch_spec:
302 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
303 if self.svn_branch:
304 break
305 branch_spec = RunGit(
306 ['config', 'svn-remote.%s.branches' % remote],
307 error_ok=True).strip()
308 if branch_spec:
309 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
310 if self.svn_branch:
311 break
312 tag_spec = RunGit(
313 ['config', 'svn-remote.%s.tags' % remote],
314 error_ok=True).strip()
315 if tag_spec:
316 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
317 if self.svn_branch:
318 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000319
320 if not self.svn_branch:
321 DieWithError('Can\'t guess svn branch -- try specifying it on the '
322 'command line')
323
324 return self.svn_branch
325
326 def GetTreeStatusUrl(self, error_ok=False):
327 if not self.tree_status_url:
328 error_message = ('You must configure your tree status URL by running '
329 '"git cl config".')
330 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
331 error_ok=error_ok,
332 error_message=error_message)
333 return self.tree_status_url
334
335 def GetViewVCUrl(self):
336 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000337 self.viewvc_url = gclient_utils.UpgradeToHttps(
338 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000339 return self.viewvc_url
340
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000341 def GetDefaultCCList(self):
342 return self._GetConfig('rietveld.cc', error_ok=True)
343
ukai@chromium.orge8077812012-02-03 03:41:46 +0000344 def GetIsGerrit(self):
345 """Return true if this repo is assosiated with gerrit code review system."""
346 if self.is_gerrit is None:
347 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
348 return self.is_gerrit
349
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000350 def _GetConfig(self, param, **kwargs):
351 self.LazyUpdateIfNeeded()
352 return RunGit(['config', param], **kwargs).strip()
353
354
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000355def ShortBranchName(branch):
356 """Convert a name like 'refs/heads/foo' to just 'foo'."""
357 return branch.replace('refs/heads/', '')
358
359
360class Changelist(object):
361 def __init__(self, branchref=None):
362 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000363 global settings
364 if not settings:
365 # Happens when git_cl.py is used as a utility library.
366 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000367 settings.GetDefaultServerUrl()
368 self.branchref = branchref
369 if self.branchref:
370 self.branch = ShortBranchName(self.branchref)
371 else:
372 self.branch = None
373 self.rietveld_server = None
374 self.upstream_branch = None
375 self.has_issue = False
376 self.issue = None
377 self.has_description = False
378 self.description = None
379 self.has_patchset = False
380 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000381 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000382 self.cc = None
383 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000384 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000385
386 def GetCCList(self):
387 """Return the users cc'd on this CL.
388
389 Return is a string suitable for passing to gcl with the --cc flag.
390 """
391 if self.cc is None:
392 base_cc = settings .GetDefaultCCList()
393 more_cc = ','.join(self.watchers)
394 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
395 return self.cc
396
397 def SetWatchers(self, watchers):
398 """Set the list of email addresses that should be cc'd based on the changed
399 files in this CL.
400 """
401 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402
403 def GetBranch(self):
404 """Returns the short branch name, e.g. 'master'."""
405 if not self.branch:
406 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
407 self.branch = ShortBranchName(self.branchref)
408 return self.branch
409
410 def GetBranchRef(self):
411 """Returns the full branch name, e.g. 'refs/heads/master'."""
412 self.GetBranch() # Poke the lazy loader.
413 return self.branchref
414
415 def FetchUpstreamTuple(self):
416 """Returns a tuple containg remote and remote ref,
417 e.g. 'origin', 'refs/heads/master'
418 """
419 remote = '.'
420 branch = self.GetBranch()
421 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
422 error_ok=True).strip()
423 if upstream_branch:
424 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
425 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000426 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
427 error_ok=True).strip()
428 if upstream_branch:
429 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000430 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000431 # Fall back on trying a git-svn upstream branch.
432 if settings.GetIsGitSvn():
433 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000434 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000435 # Else, try to guess the origin remote.
436 remote_branches = RunGit(['branch', '-r']).split()
437 if 'origin/master' in remote_branches:
438 # Fall back on origin/master if it exits.
439 remote = 'origin'
440 upstream_branch = 'refs/heads/master'
441 elif 'origin/trunk' in remote_branches:
442 # Fall back on origin/trunk if it exists. Generally a shared
443 # git-svn clone
444 remote = 'origin'
445 upstream_branch = 'refs/heads/trunk'
446 else:
447 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000448Either pass complete "git diff"-style arguments, like
449 git cl upload origin/master
450or verify this branch is set up to track another (via the --track argument to
451"git checkout -b ...").""")
452
453 return remote, upstream_branch
454
455 def GetUpstreamBranch(self):
456 if self.upstream_branch is None:
457 remote, upstream_branch = self.FetchUpstreamTuple()
458 if remote is not '.':
459 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
460 self.upstream_branch = upstream_branch
461 return self.upstream_branch
462
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000463 def GetRemote(self):
464 if not self._remote:
465 self._remote = self.FetchUpstreamTuple()[0]
466 if self._remote == '.':
467
468 remotes = RunGit(['remote'], error_ok=True).split()
469 if len(remotes) == 1:
470 self._remote, = remotes
471 elif 'origin' in remotes:
472 self._remote = 'origin'
473 logging.warning('Could not determine which remote this change is '
474 'associated with, so defaulting to "%s". This may '
475 'not be what you want. You may prevent this message '
476 'by running "git svn info" as documented here: %s',
477 self._remote,
478 GIT_INSTRUCTIONS_URL)
479 else:
480 logging.warn('Could not determine which remote this change is '
481 'associated with. You may prevent this message by '
482 'running "git svn info" as documented here: %s',
483 GIT_INSTRUCTIONS_URL)
484 return self._remote
485
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000486 def GetGitBaseUrlFromConfig(self):
487 """Return the configured base URL from branch.<branchname>.baseurl.
488
489 Returns None if it is not set.
490 """
491 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
492 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000493
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000494 def GetRemoteUrl(self):
495 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
496
497 Returns None if there is no remote.
498 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000499 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000500 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
501
502 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000503 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000504 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000505 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
506 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000507 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000508 else:
509 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000510 self.has_issue = True
511 return self.issue
512
513 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000514 if not self.rietveld_server:
515 # If we're on a branch then get the server potentially associated
516 # with that branch.
517 if self.GetIssue():
518 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
519 ['config', self._RietveldServer()], error_ok=True).strip())
520 if not self.rietveld_server:
521 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000522 return self.rietveld_server
523
524 def GetIssueURL(self):
525 """Get the URL for a particular issue."""
526 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
527
528 def GetDescription(self, pretty=False):
529 if not self.has_description:
530 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000531 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000532 try:
533 self.description = self.RpcServer().get_description(issue).strip()
534 except urllib2.HTTPError, e:
535 if e.code == 404:
536 DieWithError(
537 ('\nWhile fetching the description for issue %d, received a '
538 '404 (not found)\n'
539 'error. It is likely that you deleted this '
540 'issue on the server. If this is the\n'
541 'case, please run\n\n'
542 ' git cl issue 0\n\n'
543 'to clear the association with the deleted issue. Then run '
544 'this command again.') % issue)
545 else:
546 DieWithError(
547 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000548 self.has_description = True
549 if pretty:
550 wrapper = textwrap.TextWrapper()
551 wrapper.initial_indent = wrapper.subsequent_indent = ' '
552 return wrapper.fill(self.description)
553 return self.description
554
555 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000556 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000557 if not self.has_patchset:
558 patchset = RunGit(['config', self._PatchsetSetting()],
559 error_ok=True).strip()
560 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000561 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000562 else:
563 self.patchset = None
564 self.has_patchset = True
565 return self.patchset
566
567 def SetPatchset(self, patchset):
568 """Set this branch's patchset. If patchset=0, clears the patchset."""
569 if patchset:
570 RunGit(['config', self._PatchsetSetting(), str(patchset)])
571 else:
572 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000573 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000574 self.has_patchset = False
575
binji@chromium.org0281f522012-09-14 13:37:59 +0000576 def GetMostRecentPatchset(self, issue):
577 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000578 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000579
580 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000581 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000582 '/download/issue%s_%s.diff' % (issue, patchset))
583
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000584 def SetIssue(self, issue):
585 """Set this branch's issue. If issue=0, clears the issue."""
586 if issue:
587 RunGit(['config', self._IssueSetting(), str(issue)])
588 if self.rietveld_server:
589 RunGit(['config', self._RietveldServer(), self.rietveld_server])
590 else:
591 RunGit(['config', '--unset', self._IssueSetting()])
592 self.SetPatchset(0)
593 self.has_issue = False
594
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000595 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000596 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
597 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000598
599 # We use the sha1 of HEAD as a name of this change.
600 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000601 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000602 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000603 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000604 except subprocess2.CalledProcessError:
605 DieWithError(
606 ('\nFailed to diff against upstream branch %s!\n\n'
607 'This branch probably doesn\'t exist anymore. To reset the\n'
608 'tracking branch, please run\n'
609 ' git branch --set-upstream %s trunk\n'
610 'replacing trunk with origin/master or the relevant branch') %
611 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000612
maruel@chromium.org52424302012-08-29 15:14:30 +0000613 issue = self.GetIssue()
614 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000615 if issue:
616 description = self.GetDescription()
617 else:
618 # If the change was never uploaded, use the log messages of all commits
619 # up to the branch point, as git cl upload will prefill the description
620 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000621 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
622 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000623
624 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000625 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000626 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000627 name,
628 description,
629 absroot,
630 files,
631 issue,
632 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000633 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000634
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000635 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
636 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
637 change = self.GetChange(upstream_branch, author)
638
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000639 # Apply watchlists on upload.
640 if not committing:
641 watchlist = watchlists.Watchlists(change.RepositoryRoot())
642 files = [f.LocalPath() for f in change.AffectedFiles()]
643 self.SetWatchers(watchlist.GetWatchersForPaths(files))
644
645 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000646 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000647 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000648 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000649 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000650 except presubmit_support.PresubmitFailure, e:
651 DieWithError(
652 ('%s\nMaybe your depot_tools is out of date?\n'
653 'If all fails, contact maruel@') % e)
654
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000655 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000656 """Updates the description and closes the issue."""
maruel@chromium.org52424302012-08-29 15:14:30 +0000657 issue = self.GetIssue()
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000658 self.RpcServer().update_description(issue, self.description)
659 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000660
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000661 def SetFlag(self, flag, value):
662 """Patchset must match."""
663 if not self.GetPatchset():
664 DieWithError('The patchset needs to match. Send another patchset.')
665 try:
666 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000667 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000668 except urllib2.HTTPError, e:
669 if e.code == 404:
670 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
671 if e.code == 403:
672 DieWithError(
673 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
674 'match?') % (self.GetIssue(), self.GetPatchset()))
675 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000676
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000677 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000678 """Returns an upload.RpcServer() to access this review's rietveld instance.
679 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000680 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000681 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
682 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000683 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000684
685 def _IssueSetting(self):
686 """Return the git setting that stores this change's issue."""
687 return 'branch.%s.rietveldissue' % self.GetBranch()
688
689 def _PatchsetSetting(self):
690 """Return the git setting that stores this change's most recent patchset."""
691 return 'branch.%s.rietveldpatchset' % self.GetBranch()
692
693 def _RietveldServer(self):
694 """Returns the git setting that stores this change's rietveld server."""
695 return 'branch.%s.rietveldserver' % self.GetBranch()
696
697
698def GetCodereviewSettingsInteractively():
699 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000700 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000701 server = settings.GetDefaultServerUrl(error_ok=True)
702 prompt = 'Rietveld server (host[:port])'
703 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000704 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000705 if not server and not newserver:
706 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000707 if newserver:
708 newserver = gclient_utils.UpgradeToHttps(newserver)
709 if newserver != server:
710 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000711
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000712 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000713 prompt = caption
714 if initial:
715 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000716 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717 if new_val == 'x':
718 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000719 elif new_val:
720 if is_url:
721 new_val = gclient_utils.UpgradeToHttps(new_val)
722 if new_val != initial:
723 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000725 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000726 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000727 'tree-status-url', False)
728 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000729
730 # TODO: configure a default branch to diff against, rather than this
731 # svn-based hackery.
732
733
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000734class ChangeDescription(object):
735 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000736 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000737 self.log_desc = log_desc
738 self.reviewers = reviewers
739 self.description = self.log_desc
740
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000741 def Prompt(self):
742 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000743# This will displayed on the codereview site.
744# The first line will also be used as the subject of the review.
745"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000746 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000747 if ('\nR=' not in self.description and
748 '\nTBR=' not in self.description and
749 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000750 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000751 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000752 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000753 content = content.rstrip('\n') + '\n'
754 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000755 if not content:
756 DieWithError('Running editor failed')
757 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000758 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000759 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000760 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000761
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000762 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000763 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000764 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000765 # Retrieves all reviewer lines
766 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000767 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000768 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000769 if reviewers:
770 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000771
772 def IsEmpty(self):
773 return not self.description
774
775
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000776def FindCodereviewSettingsFile(filename='codereview.settings'):
777 """Finds the given file starting in the cwd and going up.
778
779 Only looks up to the top of the repository unless an
780 'inherit-review-settings-ok' file exists in the root of the repository.
781 """
782 inherit_ok_file = 'inherit-review-settings-ok'
783 cwd = os.getcwd()
784 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
785 if os.path.isfile(os.path.join(root, inherit_ok_file)):
786 root = '/'
787 while True:
788 if filename in os.listdir(cwd):
789 if os.path.isfile(os.path.join(cwd, filename)):
790 return open(os.path.join(cwd, filename))
791 if cwd == root:
792 break
793 cwd = os.path.dirname(cwd)
794
795
796def LoadCodereviewSettingsFromFile(fileobj):
797 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000798 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000799
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800 def SetProperty(name, setting, unset_error_ok=False):
801 fullname = 'rietveld.' + name
802 if setting in keyvals:
803 RunGit(['config', fullname, keyvals[setting]])
804 else:
805 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
806
807 SetProperty('server', 'CODE_REVIEW_SERVER')
808 # Only server setting is required. Other settings can be absent.
809 # In that case, we ignore errors raised during option deletion attempt.
810 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
811 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
812 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
813
ukai@chromium.orge8077812012-02-03 03:41:46 +0000814 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
815 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
816 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000817
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000818 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
819 #should be of the form
820 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
821 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
822 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
823 keyvals['ORIGIN_URL_CONFIG']])
824
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000825
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000826def urlretrieve(source, destination):
827 """urllib is broken for SSL connections via a proxy therefore we
828 can't use urllib.urlretrieve()."""
829 with open(destination, 'w') as f:
830 f.write(urllib2.urlopen(source).read())
831
832
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000833def DownloadHooks(force):
834 """downloads hooks
835
836 Args:
837 force: True to update hooks. False to install hooks if not present.
838 """
839 if not settings.GetIsGerrit():
840 return
841 server_url = settings.GetDefaultServerUrl()
842 src = '%s/tools/hooks/commit-msg' % server_url
843 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
844 if not os.access(dst, os.X_OK):
845 if os.path.exists(dst):
846 if not force:
847 return
848 os.remove(dst)
849 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000850 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000851 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
852 except Exception:
853 if os.path.exists(dst):
854 os.remove(dst)
855 DieWithError('\nFailed to download hooks from %s' % src)
856
857
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858@usage('[repo root containing codereview.settings]')
859def CMDconfig(parser, args):
860 """edit configuration for this tree"""
861
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000862 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863 if len(args) == 0:
864 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000865 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866 return 0
867
868 url = args[0]
869 if not url.endswith('codereview.settings'):
870 url = os.path.join(url, 'codereview.settings')
871
872 # Load code review settings and download hooks (if available).
873 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000874 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 return 0
876
877
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000878def CMDbaseurl(parser, args):
879 """get or set base-url for this branch"""
880 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
881 branch = ShortBranchName(branchref)
882 _, args = parser.parse_args(args)
883 if not args:
884 print("Current base-url:")
885 return RunGit(['config', 'branch.%s.base-url' % branch],
886 error_ok=False).strip()
887 else:
888 print("Setting base-url to %s" % args[0])
889 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
890 error_ok=False).strip()
891
892
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000893def CMDstatus(parser, args):
894 """show status of changelists"""
895 parser.add_option('--field',
896 help='print only specific field (desc|id|patch|url)')
897 (options, args) = parser.parse_args(args)
898
899 # TODO: maybe make show_branches a flag if necessary.
900 show_branches = not options.field
901
902 if show_branches:
903 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
904 if branches:
905 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000906 changes = (Changelist(branchref=b) for b in branches.splitlines())
907 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
908 alignment = max(5, max(len(b) for b in branches))
909 for branch in sorted(branches):
910 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911
912 cl = Changelist()
913 if options.field:
914 if options.field.startswith('desc'):
915 print cl.GetDescription()
916 elif options.field == 'id':
917 issueid = cl.GetIssue()
918 if issueid:
919 print issueid
920 elif options.field == 'patch':
921 patchset = cl.GetPatchset()
922 if patchset:
923 print patchset
924 elif options.field == 'url':
925 url = cl.GetIssueURL()
926 if url:
927 print url
928 else:
929 print
930 print 'Current branch:',
931 if not cl.GetIssue():
932 print 'no issue assigned.'
933 return 0
934 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000935 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936 print 'Issue description:'
937 print cl.GetDescription(pretty=True)
938 return 0
939
940
941@usage('[issue_number]')
942def CMDissue(parser, args):
943 """Set or display the current code review issue number.
944
945 Pass issue number 0 to clear the current issue.
946"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000947 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000948
949 cl = Changelist()
950 if len(args) > 0:
951 try:
952 issue = int(args[0])
953 except ValueError:
954 DieWithError('Pass a number to set the issue or none to list it.\n'
955 'Maybe you want to run git cl status?')
956 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +0000957 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000958 return 0
959
960
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000961def CMDcomments(parser, args):
962 """show review comments of the current changelist"""
963 (_, args) = parser.parse_args(args)
964 if args:
965 parser.error('Unsupported argument: %s' % args)
966
967 cl = Changelist()
968 if cl.GetIssue():
969 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
970 for message in sorted(data['messages'], key=lambda x: x['date']):
971 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
972 if message['text'].strip():
973 print '\n'.join(' ' + l for l in message['text'].splitlines())
974 return 0
975
976
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000977def CreateDescriptionFromLog(args):
978 """Pulls out the commit log to use as a base for the CL description."""
979 log_args = []
980 if len(args) == 1 and not args[0].endswith('.'):
981 log_args = [args[0] + '..']
982 elif len(args) == 1 and args[0].endswith('...'):
983 log_args = [args[0][:-1]]
984 elif len(args) == 2:
985 log_args = [args[0] + '..' + args[1]]
986 else:
987 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000988 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000989
990
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000991def CMDpresubmit(parser, args):
992 """run presubmit tests on the current changelist"""
993 parser.add_option('--upload', action='store_true',
994 help='Run upload hook instead of the push/dcommit hook')
sbc@chromium.org495ad152012-09-04 23:07:42 +0000995 parser.add_option('--force', action='store_true',
996 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000997 (options, args) = parser.parse_args(args)
998
999 # Make sure index is up-to-date before running diff-index.
1000 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
sbc@chromium.org495ad152012-09-04 23:07:42 +00001001 if not options.force and RunGit(['diff-index', 'HEAD']):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001002 # TODO(maruel): Is this really necessary?
sbc@chromium.org495ad152012-09-04 23:07:42 +00001003 print ('Cannot presubmit with a dirty tree.\n'
1004 'You must commit locally first (or use --force).')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 return 1
1006
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001007 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001008 if args:
1009 base_branch = args[0]
1010 else:
1011 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001012 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001013
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001014 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001015 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001016 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001017 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001018
1019
ukai@chromium.orge8077812012-02-03 03:41:46 +00001020def GerritUpload(options, args, cl):
1021 """upload the current branch to gerrit."""
1022 # We assume the remote called "origin" is the one we want.
1023 # It is probably not worthwhile to support different workflows.
1024 remote = 'origin'
1025 branch = 'master'
1026 if options.target_branch:
1027 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001028
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001029 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001030 if options.reviewers:
1031 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001032 change_desc = ChangeDescription(log_desc, options.reviewers)
1033 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +00001034 if change_desc.IsEmpty():
1035 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001036 return 1
1037
ukai@chromium.orge8077812012-02-03 03:41:46 +00001038 receive_options = []
1039 cc = cl.GetCCList().split(',')
1040 if options.cc:
1041 cc += options.cc.split(',')
1042 cc = filter(None, cc)
1043 if cc:
1044 receive_options += ['--cc=' + email for email in cc]
1045 if change_desc.reviewers:
1046 reviewers = filter(None, change_desc.reviewers.split(','))
1047 if reviewers:
1048 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049
ukai@chromium.orge8077812012-02-03 03:41:46 +00001050 git_command = ['push']
1051 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001052 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001053 ' '.join(receive_options))
1054 git_command += [remote, 'HEAD:refs/for/' + branch]
1055 RunGit(git_command)
1056 # TODO(ukai): parse Change-Id: and set issue number?
1057 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001058
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001059
ukai@chromium.orge8077812012-02-03 03:41:46 +00001060def RietveldUpload(options, args, cl):
1061 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001062 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1063 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064 if options.emulate_svn_auto_props:
1065 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066
1067 change_desc = None
1068
1069 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001070 if options.title:
1071 upload_args.extend(['--title', options.title])
1072 elif options.message:
1073 # TODO(rogerta): for now, the -m option will also set the --title option
1074 # for upload.py. Soon this will be changed to set the --message option.
1075 # Will wait until people are used to typing -t instead of -m.
1076 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001077 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001078 print ("This branch is associated with issue %s. "
1079 "Adding patch to that issue." % cl.GetIssue())
1080 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001081 if options.title:
1082 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001083 message = options.message or CreateDescriptionFromLog(args)
1084 change_desc = ChangeDescription(message, options.reviewers)
1085 if not options.force:
1086 change_desc.Prompt()
1087 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001088
1089 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001090 print "Description is empty; aborting."
1091 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001092
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001093 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001094 if change_desc.reviewers:
1095 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001096 if options.send_mail:
1097 if not change_desc.reviewers:
1098 DieWithError("Must specify reviewers to send email.")
1099 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001100 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001101 if cc:
1102 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001103
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001104 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001105 if not options.find_copies:
1106 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001107
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001108 # Include the upstream repo's URL in the change -- this is useful for
1109 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001110 remote_url = cl.GetGitBaseUrlFromConfig()
1111 if not remote_url:
1112 if settings.GetIsGitSvn():
1113 # URL is dependent on the current directory.
1114 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1115 if data:
1116 keys = dict(line.split(': ', 1) for line in data.splitlines()
1117 if ': ' in line)
1118 remote_url = keys.get('URL', None)
1119 else:
1120 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1121 remote_url = (cl.GetRemoteUrl() + '@'
1122 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001123 if remote_url:
1124 upload_args.extend(['--base_url', remote_url])
1125
1126 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001127 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001128 except KeyboardInterrupt:
1129 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001130 except:
1131 # If we got an exception after the user typed a description for their
1132 # change, back up the description before re-raising.
1133 if change_desc:
1134 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1135 print '\nGot exception while uploading -- saving description to %s\n' \
1136 % backup_path
1137 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001138 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139 backup_file.close()
1140 raise
1141
1142 if not cl.GetIssue():
1143 cl.SetIssue(issue)
1144 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001145
1146 if options.use_commit_queue:
1147 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001148 return 0
1149
1150
ukai@chromium.orge8077812012-02-03 03:41:46 +00001151@usage('[args to "git diff"]')
1152def CMDupload(parser, args):
1153 """upload the current changelist to codereview"""
1154 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1155 help='bypass upload presubmit hook')
1156 parser.add_option('-f', action='store_true', dest='force',
1157 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001158 parser.add_option('-m', dest='message', help='message for patchset')
1159 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001160 parser.add_option('-r', '--reviewers',
1161 help='reviewer email addresses')
1162 parser.add_option('--cc',
1163 help='cc email addresses')
1164 parser.add_option('--send-mail', action='store_true',
1165 help='send email to reviewer immediately')
1166 parser.add_option("--emulate_svn_auto_props", action="store_true",
1167 dest="emulate_svn_auto_props",
1168 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001169 parser.add_option('-c', '--use-commit-queue', action='store_true',
1170 help='tell the commit queue to commit this patchset')
1171 if settings.GetIsGerrit():
1172 parser.add_option('--target_branch', dest='target_branch', default='master',
1173 help='target branch to upload')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001174 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001175 (options, args) = parser.parse_args(args)
1176
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001177 # Print warning if the user used the -m/--message argument. This will soon
1178 # change to -t/--title.
1179 if options.message:
1180 print >> sys.stderr, (
1181 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1182 'In the near future, -m or --message will send a message instead.\n'
1183 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001184
ukai@chromium.orge8077812012-02-03 03:41:46 +00001185 # Make sure index is up-to-date before running diff-index.
1186 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1187 if RunGit(['diff-index', 'HEAD']):
1188 print 'Cannot upload with a dirty tree. You must commit locally first.'
1189 return 1
1190
1191 cl = Changelist()
1192 if args:
1193 # TODO(ukai): is it ok for gerrit case?
1194 base_branch = args[0]
1195 else:
1196 # Default to diffing against the "upstream" branch.
1197 base_branch = cl.GetUpstreamBranch()
1198 args = [base_branch + "..."]
1199
1200 if not options.bypass_hooks:
1201 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1202 may_prompt=not options.force,
1203 verbose=options.verbose,
1204 author=None)
1205 if not hook_results.should_continue():
1206 return 1
1207 if not options.reviewers and hook_results.reviewers:
1208 options.reviewers = hook_results.reviewers
1209
iannucci@chromium.org79540052012-10-19 23:15:26 +00001210 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001211 if settings.GetIsGerrit():
1212 return GerritUpload(options, args, cl)
1213 return RietveldUpload(options, args, cl)
1214
1215
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001216def IsSubmoduleMergeCommit(ref):
1217 # When submodules are added to the repo, we expect there to be a single
1218 # non-git-svn merge commit at remote HEAD with a signature comment.
1219 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001220 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001221 return RunGit(cmd) != ''
1222
1223
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224def SendUpstream(parser, args, cmd):
1225 """Common code for CmdPush and CmdDCommit
1226
1227 Squashed commit into a single.
1228 Updates changelog with metadata (e.g. pointer to review).
1229 Pushes/dcommits the code upstream.
1230 Updates review and closes.
1231 """
1232 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1233 help='bypass upload presubmit hook')
1234 parser.add_option('-m', dest='message',
1235 help="override review description")
1236 parser.add_option('-f', action='store_true', dest='force',
1237 help="force yes to questions (don't prompt)")
1238 parser.add_option('-c', dest='contributor',
1239 help="external contributor for patch (appended to " +
1240 "description and used as author for git). Should be " +
1241 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001242 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001243 (options, args) = parser.parse_args(args)
1244 cl = Changelist()
1245
1246 if not args or cmd == 'push':
1247 # Default to merging against our best guess of the upstream branch.
1248 args = [cl.GetUpstreamBranch()]
1249
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001250 if options.contributor:
1251 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1252 print "Please provide contibutor as 'First Last <email@example.com>'"
1253 return 1
1254
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001256 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001257
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001258 # Make sure index is up-to-date before running diff-index.
1259 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 if RunGit(['diff-index', 'HEAD']):
1261 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1262 return 1
1263
1264 # This rev-list syntax means "show all commits not in my branch that
1265 # are in base_branch".
1266 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1267 base_branch]).splitlines()
1268 if upstream_commits:
1269 print ('Base branch "%s" has %d commits '
1270 'not in this branch.' % (base_branch, len(upstream_commits)))
1271 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1272 return 1
1273
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001274 # This is the revision `svn dcommit` will commit on top of.
1275 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1276 '--pretty=format:%H'])
1277
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001278 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001279 # If the base_head is a submodule merge commit, the first parent of the
1280 # base_head should be a git-svn commit, which is what we're interested in.
1281 base_svn_head = base_branch
1282 if base_has_submodules:
1283 base_svn_head += '^1'
1284
1285 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001286 if extra_commits:
1287 print ('This branch has %d additional commits not upstreamed yet.'
1288 % len(extra_commits.splitlines()))
1289 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1290 'before attempting to %s.' % (base_branch, cmd))
1291 return 1
1292
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001293 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001294 author = None
1295 if options.contributor:
1296 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001297 hook_results = cl.RunHook(
1298 committing=True,
1299 upstream_branch=base_branch,
1300 may_prompt=not options.force,
1301 verbose=options.verbose,
1302 author=author)
1303 if not hook_results.should_continue():
1304 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305
1306 if cmd == 'dcommit':
1307 # Check the tree status if the tree status URL is set.
1308 status = GetTreeStatus()
1309 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001310 print('The tree is closed. Please wait for it to reopen. Use '
1311 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001312 return 1
1313 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001314 print('Unable to determine tree status. Please verify manually and '
1315 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001316 else:
1317 breakpad.SendStack(
1318 'GitClHooksBypassedCommit',
1319 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001320 (cl.GetRietveldServer(), cl.GetIssue()),
1321 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322
1323 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001324 if not description and cl.GetIssue():
1325 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001326
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001327 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001328 if not cl.GetIssue() and options.bypass_hooks:
1329 description = CreateDescriptionFromLog([base_branch])
1330 else:
1331 print 'No description set.'
1332 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1333 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001334
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001335 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001336 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001337
1338 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 description += "\nPatch from %s." % options.contributor
1340 print 'Description:', repr(description)
1341
1342 branches = [base_branch, cl.GetBranchRef()]
1343 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001344 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001345 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001346
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001347 # We want to squash all this branch's commits into one commit with the proper
1348 # description. We do this by doing a "reset --soft" to the base branch (which
1349 # keeps the working copy the same), then dcommitting that. If origin/master
1350 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1351 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001352 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001353 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1354 # Delete the branches if they exist.
1355 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1356 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1357 result = RunGitWithCode(showref_cmd)
1358 if result[0] == 0:
1359 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360
1361 # We might be in a directory that's present in this branch but not in the
1362 # trunk. Move up to the top of the tree so that git commands that expect a
1363 # valid CWD won't fail after we check out the merge branch.
1364 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1365 if rel_base_path:
1366 os.chdir(rel_base_path)
1367
1368 # Stuff our change into the merge branch.
1369 # We wrap in a try...finally block so if anything goes wrong,
1370 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001371 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001372 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001373 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1374 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001375 if options.contributor:
1376 RunGit(['commit', '--author', options.contributor, '-m', description])
1377 else:
1378 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001379 if base_has_submodules:
1380 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1381 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1382 RunGit(['checkout', CHERRY_PICK_BRANCH])
1383 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001384 if cmd == 'push':
1385 # push the merge branch.
1386 remote, branch = cl.FetchUpstreamTuple()
1387 retcode, output = RunGitWithCode(
1388 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1389 logging.debug(output)
1390 else:
1391 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001392 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001393 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001394 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001395 finally:
1396 # And then swap back to the original branch and clean up.
1397 RunGit(['checkout', '-q', cl.GetBranch()])
1398 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001399 if base_has_submodules:
1400 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401
1402 if cl.GetIssue():
1403 if cmd == 'dcommit' and 'Committed r' in output:
1404 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1405 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001406 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1407 for l in output.splitlines(False))
1408 match = filter(None, match)
1409 if len(match) != 1:
1410 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1411 output)
1412 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001413 else:
1414 return 1
1415 viewvc_url = settings.GetViewVCUrl()
1416 if viewvc_url and revision:
1417 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001418 elif revision:
1419 cl.description += ('\n\nCommitted: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001420 print ('Closing issue '
1421 '(you may be prompted for your codereview password)...')
1422 cl.CloseIssue()
1423 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001424
1425 if retcode == 0:
1426 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1427 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001428 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001429
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 return 0
1431
1432
1433@usage('[upstream branch to apply against]')
1434def CMDdcommit(parser, args):
1435 """commit the current changelist via git-svn"""
1436 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001437 message = """This doesn't appear to be an SVN repository.
1438If your project has a git mirror with an upstream SVN master, you probably need
1439to run 'git svn init', see your project's git mirror documentation.
1440If your project has a true writeable upstream repository, you probably want
1441to run 'git cl push' instead.
1442Choose wisely, if you get this wrong, your commit might appear to succeed but
1443will instead be silently ignored."""
1444 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001445 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 return SendUpstream(parser, args, 'dcommit')
1447
1448
1449@usage('[upstream branch to apply against]')
1450def CMDpush(parser, args):
1451 """commit the current changelist via git"""
1452 if settings.GetIsGitSvn():
1453 print('This appears to be an SVN repository.')
1454 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001455 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 return SendUpstream(parser, args, 'push')
1457
1458
1459@usage('<patch url or issue id>')
1460def CMDpatch(parser, args):
1461 """patch in a code review"""
1462 parser.add_option('-b', dest='newbranch',
1463 help='create a new branch off trunk for the patch')
1464 parser.add_option('-f', action='store_true', dest='force',
1465 help='with -b, clobber any existing branch')
1466 parser.add_option('--reject', action='store_true', dest='reject',
1467 help='allow failed patches and spew .rej files')
1468 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1469 help="don't commit after patch applies")
1470 (options, args) = parser.parse_args(args)
1471 if len(args) != 1:
1472 parser.print_help()
1473 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001474 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001476 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001477 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001478
maruel@chromium.org52424302012-08-29 15:14:30 +00001479 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001480 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001481 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001482 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001483 patchset = cl.GetMostRecentPatchset(issue)
1484 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001485 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001486 # Assume it's a URL to the patch. Default to https.
1487 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001488 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001489 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001490 DieWithError('Must pass an issue ID or full URL for '
1491 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001492 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001493 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001494 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001495
1496 if options.newbranch:
1497 if options.force:
1498 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001499 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001500 RunGit(['checkout', '-b', options.newbranch,
1501 Changelist().GetUpstreamBranch()])
1502
1503 # Switch up to the top-level directory, if necessary, in preparation for
1504 # applying the patch.
1505 top = RunGit(['rev-parse', '--show-cdup']).strip()
1506 if top:
1507 os.chdir(top)
1508
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001509 # Git patches have a/ at the beginning of source paths. We strip that out
1510 # with a sed script rather than the -p flag to patch so we can feed either
1511 # Git or svn-style patches into the same apply command.
1512 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001513 try:
1514 patch_data = subprocess2.check_output(
1515 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1516 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001517 DieWithError('Git patch mungling failed.')
1518 logging.info(patch_data)
1519 # We use "git apply" to apply the patch instead of "patch" so that we can
1520 # pick up file adds.
1521 # The --index flag means: also insert into the index (so we catch adds).
1522 cmd = ['git', 'apply', '--index', '-p0']
1523 if options.reject:
1524 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001525 try:
1526 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1527 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001528 DieWithError('Failed to apply the patch')
1529
1530 # If we had an issue, commit the current state and register the issue.
1531 if not options.nocommit:
1532 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1533 cl = Changelist()
1534 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001535 cl.SetPatchset(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001536 print "Committed patch."
1537 else:
1538 print "Patch applied to index."
1539 return 0
1540
1541
1542def CMDrebase(parser, args):
1543 """rebase current branch on top of svn repo"""
1544 # Provide a wrapper for git svn rebase to help avoid accidental
1545 # git svn dcommit.
1546 # It's the only command that doesn't use parser at all since we just defer
1547 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001548 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001549
1550
1551def GetTreeStatus():
1552 """Fetches the tree status and returns either 'open', 'closed',
1553 'unknown' or 'unset'."""
1554 url = settings.GetTreeStatusUrl(error_ok=True)
1555 if url:
1556 status = urllib2.urlopen(url).read().lower()
1557 if status.find('closed') != -1 or status == '0':
1558 return 'closed'
1559 elif status.find('open') != -1 or status == '1':
1560 return 'open'
1561 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001562 return 'unset'
1563
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001564
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001565def GetTreeStatusReason():
1566 """Fetches the tree status from a json url and returns the message
1567 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001568 url = settings.GetTreeStatusUrl()
1569 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001570 connection = urllib2.urlopen(json_url)
1571 status = json.loads(connection.read())
1572 connection.close()
1573 return status['message']
1574
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001575
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001576def CMDtree(parser, args):
1577 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001578 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001579 status = GetTreeStatus()
1580 if 'unset' == status:
1581 print 'You must configure your tree status URL by running "git cl config".'
1582 return 2
1583
1584 print "The tree is %s" % status
1585 print
1586 print GetTreeStatusReason()
1587 if status != 'open':
1588 return 1
1589 return 0
1590
1591
maruel@chromium.org15192402012-09-06 12:38:29 +00001592def CMDtry(parser, args):
1593 """Triggers a try job through Rietveld."""
1594 group = optparse.OptionGroup(parser, "Try job options")
1595 group.add_option(
1596 "-b", "--bot", action="append",
1597 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1598 "times to specify multiple builders. ex: "
1599 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1600 "the try server waterfall for the builders name and the tests "
1601 "available. Can also be used to specify gtest_filter, e.g. "
1602 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1603 group.add_option(
1604 "-r", "--revision",
1605 help="Revision to use for the try job; default: the "
1606 "revision will be determined by the try server; see "
1607 "its waterfall for more info")
1608 group.add_option(
1609 "-c", "--clobber", action="store_true", default=False,
1610 help="Force a clobber before building; e.g. don't do an "
1611 "incremental build")
1612 group.add_option(
1613 "--project",
1614 help="Override which project to use. Projects are defined "
1615 "server-side to define what default bot set to use")
1616 group.add_option(
1617 "-t", "--testfilter", action="append", default=[],
1618 help=("Apply a testfilter to all the selected builders. Unless the "
1619 "builders configurations are similar, use multiple "
1620 "--bot <builder>:<test> arguments."))
1621 group.add_option(
1622 "-n", "--name", help="Try job name; default to current branch name")
1623 parser.add_option_group(group)
1624 options, args = parser.parse_args(args)
1625
1626 if args:
1627 parser.error('Unknown arguments: %s' % args)
1628
1629 cl = Changelist()
1630 if not cl.GetIssue():
1631 parser.error('Need to upload first')
1632
1633 if not options.name:
1634 options.name = cl.GetBranch()
1635
1636 # Process --bot and --testfilter.
1637 if not options.bot:
1638 # Get try slaves from PRESUBMIT.py files if not specified.
1639 change = cl.GetChange(cl.GetUpstreamBranch(), None)
1640 options.bot = presubmit_support.DoGetTrySlaves(
1641 change,
1642 change.LocalPaths(),
1643 settings.GetRoot(),
1644 None,
1645 None,
1646 options.verbose,
1647 sys.stdout)
1648 if not options.bot:
1649 parser.error('No default try builder to try, use --bot')
1650
1651 builders_and_tests = {}
1652 for bot in options.bot:
1653 if ':' in bot:
1654 builder, tests = bot.split(':', 1)
1655 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1656 elif ',' in bot:
1657 parser.error('Specify one bot per --bot flag')
1658 else:
1659 builders_and_tests.setdefault(bot, []).append('defaulttests')
1660
1661 if options.testfilter:
1662 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1663 builders_and_tests = dict(
1664 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1665 if t != ['compile'])
1666
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001667 if any('triggered' in b for b in builders_and_tests):
1668 print >> sys.stderr, (
1669 'ERROR You are trying to send a job to a triggered bot. This type of'
1670 ' bot requires an\ninitial job from a parent (usually a builder). '
1671 'Instead send your job to the parent.\n'
1672 'Bot list: %s' % builders_and_tests)
1673 return 1
1674
maruel@chromium.org15192402012-09-06 12:38:29 +00001675 patchset = cl.GetPatchset()
1676 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001677 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001678
1679 cl.RpcServer().trigger_try_jobs(
1680 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1681 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001682 print('Tried jobs on:')
1683 length = max(len(builder) for builder in builders_and_tests)
1684 for builder in sorted(builders_and_tests):
1685 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001686 return 0
1687
1688
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001689@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001690def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001691 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001692 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001693 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001694 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001695 return 0
1696
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001697 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001698 if args:
1699 # One arg means set upstream branch.
1700 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1701 cl = Changelist()
1702 print "Upstream branch set to " + cl.GetUpstreamBranch()
1703 else:
1704 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001705 return 0
1706
1707
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001708def CMDset_commit(parser, args):
1709 """set the commit bit"""
1710 _, args = parser.parse_args(args)
1711 if args:
1712 parser.error('Unrecognized args: %s' % ' '.join(args))
1713 cl = Changelist()
1714 cl.SetFlag('commit', '1')
1715 return 0
1716
1717
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001718def Command(name):
1719 return getattr(sys.modules[__name__], 'CMD' + name, None)
1720
1721
1722def CMDhelp(parser, args):
1723 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001724 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001725 if len(args) == 1:
1726 return main(args + ['--help'])
1727 parser.print_help()
1728 return 0
1729
1730
1731def GenUsage(parser, command):
1732 """Modify an OptParse object with the function's documentation."""
1733 obj = Command(command)
1734 more = getattr(obj, 'usage_more', '')
1735 if command == 'help':
1736 command = '<command>'
1737 else:
1738 # OptParser.description prefer nicely non-formatted strings.
1739 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1740 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1741
1742
1743def main(argv):
1744 """Doesn't parse the arguments here, just find the right subcommand to
1745 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001746 if sys.hexversion < 0x02060000:
1747 print >> sys.stderr, (
1748 '\nYour python version %s is unsupported, please upgrade.\n' %
1749 sys.version.split(' ', 1)[0])
1750 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001751 # Reload settings.
1752 global settings
1753 settings = Settings()
1754
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001755 # Do it late so all commands are listed.
1756 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1757 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1758 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1759
1760 # Create the option parse and add --verbose support.
1761 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001762 parser.add_option(
1763 '-v', '--verbose', action='count', default=0,
1764 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001765 old_parser_args = parser.parse_args
1766 def Parse(args):
1767 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001768 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001769 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001770 elif options.verbose:
1771 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 else:
1773 logging.basicConfig(level=logging.WARNING)
1774 return options, args
1775 parser.parse_args = Parse
1776
1777 if argv:
1778 command = Command(argv[0])
1779 if command:
1780 # "fix" the usage and the description now that we know the subcommand.
1781 GenUsage(parser, argv[0])
1782 try:
1783 return command(parser, argv[1:])
1784 except urllib2.HTTPError, e:
1785 if e.code != 500:
1786 raise
1787 DieWithError(
1788 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1789 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1790
1791 # Not a known command. Default to help.
1792 GenUsage(parser, 'help')
1793 return CMDhelp(parser, argv)
1794
1795
1796if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001797 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001798 sys.exit(main(sys.argv[1:]))