blob: 62f339d9a3e3e2534aa4801b72f5ccf7d635da15 [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'
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +000042CHANGE_ID = 'Change-Id:'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000043
maruel@chromium.org90541732011-04-01 17:54:18 +000044
maruel@chromium.orgddd59412011-11-30 14:20:38 +000045# Initialized in main()
46settings = None
47
48
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000049def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000050 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000051 sys.exit(1)
52
53
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000054def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055 try:
maruel@chromium.org373af802012-05-25 21:07:33 +000056 return subprocess2.check_output(args, shell=False, **kwargs)
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000057 except subprocess2.CalledProcessError, e:
58 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060 'Command "%s" failed.\n%s' % (
61 ' '.join(args), error_message or e.stdout or ''))
62 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063
64
65def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 """Returns stdout."""
67 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000068
69
70def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000071 """Returns return code and stdout."""
szager@chromium.org9bb85e22012-06-13 20:28:23 +000072 try:
73 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
74 return code, out[0]
75 except ValueError:
76 # When the subprocess fails, it returns None. That triggers a ValueError
77 # when trying to unpack the return value into (out, code).
78 return 1, ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000079
80
81def usage(more):
82 def hook(fn):
83 fn.usage_more = more
84 return fn
85 return hook
86
87
maruel@chromium.org90541732011-04-01 17:54:18 +000088def ask_for_data(prompt):
89 try:
90 return raw_input(prompt)
91 except KeyboardInterrupt:
92 # Hide the exception.
93 sys.exit(1)
94
95
iannucci@chromium.org79540052012-10-19 23:15:26 +000096def git_set_branch_value(key, value):
97 branch = Changelist().GetBranch()
98 if branch:
99 git_key = 'branch.%s.%s' % (branch, key)
100 RunGit(['config', '--int', git_key, "%d" % value])
101
102
103def git_get_branch_default(key, default):
104 branch = Changelist().GetBranch()
105 if branch:
106 git_key = 'branch.%s.%s' % (branch, key)
107 (_, stdout) = RunGitWithCode(['config', '--int', '--get', git_key])
108 try:
109 return int(stdout.strip())
110 except ValueError:
111 pass
112 return default
113
114
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000115def add_git_similarity(parser):
116 parser.add_option(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000117 '--similarity', metavar='SIM', type='int', action='store',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000118 help='Sets the percentage that a pair of files need to match in order to'
119 ' be considered copies (default 50)')
iannucci@chromium.org79540052012-10-19 23:15:26 +0000120 parser.add_option(
121 '--find-copies', action='store_true',
122 help='Allows git to look for copies.')
123 parser.add_option(
124 '--no-find-copies', action='store_false', dest='find_copies',
125 help='Disallows git from looking for copies.')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000126
127 old_parser_args = parser.parse_args
128 def Parse(args):
129 options, args = old_parser_args(args)
130
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000131 if options.similarity is None:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000132 options.similarity = git_get_branch_default('git-cl-similarity', 50)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000133 else:
iannucci@chromium.org79540052012-10-19 23:15:26 +0000134 print('Note: Saving similarity of %d%% in git config.'
135 % options.similarity)
136 git_set_branch_value('git-cl-similarity', options.similarity)
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000137
iannucci@chromium.org79540052012-10-19 23:15:26 +0000138 options.similarity = max(0, min(options.similarity, 100))
139
140 if options.find_copies is None:
141 options.find_copies = bool(
142 git_get_branch_default('git-find-copies', True))
143 else:
144 git_set_branch_value('git-find-copies', int(options.find_copies))
iannucci@chromium.org53937ba2012-10-02 18:20:43 +0000145
146 print('Using %d%% similarity for rename/copy detection. '
147 'Override with --similarity.' % options.similarity)
148
149 return options, args
150 parser.parse_args = Parse
151
152
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000153def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
154 """Return the corresponding git ref if |base_url| together with |glob_spec|
155 matches the full |url|.
156
157 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
158 """
159 fetch_suburl, as_ref = glob_spec.split(':')
160 if allow_wildcards:
161 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
162 if glob_match:
163 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
164 # "branches/{472,597,648}/src:refs/remotes/svn/*".
165 branch_re = re.escape(base_url)
166 if glob_match.group(1):
167 branch_re += '/' + re.escape(glob_match.group(1))
168 wildcard = glob_match.group(2)
169 if wildcard == '*':
170 branch_re += '([^/]*)'
171 else:
172 # Escape and replace surrounding braces with parentheses and commas
173 # with pipe symbols.
174 wildcard = re.escape(wildcard)
175 wildcard = re.sub('^\\\\{', '(', wildcard)
176 wildcard = re.sub('\\\\,', '|', wildcard)
177 wildcard = re.sub('\\\\}$', ')', wildcard)
178 branch_re += wildcard
179 if glob_match.group(3):
180 branch_re += re.escape(glob_match.group(3))
181 match = re.match(branch_re, url)
182 if match:
183 return re.sub('\*$', match.group(1), as_ref)
184
185 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
186 if fetch_suburl:
187 full_url = base_url + '/' + fetch_suburl
188 else:
189 full_url = base_url
190 if full_url == url:
191 return as_ref
192 return None
193
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000194
iannucci@chromium.org79540052012-10-19 23:15:26 +0000195def print_stats(similarity, find_copies, args):
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000196 """Prints statistics about the change to the user."""
197 # --no-ext-diff is broken in some versions of Git, so try to work around
198 # this by overriding the environment (but there is still a problem if the
199 # git config key "diff.external" is used).
200 env = os.environ.copy()
201 if 'GIT_EXTERNAL_DIFF' in env:
202 del env['GIT_EXTERNAL_DIFF']
iannucci@chromium.org79540052012-10-19 23:15:26 +0000203
204 if find_copies:
205 similarity_options = ['--find-copies-harder', '-l100000',
206 '-C%s' % similarity]
207 else:
208 similarity_options = ['-M%s' % similarity]
209
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000210 return subprocess2.call(
iannucci@chromium.org79540052012-10-19 23:15:26 +0000211 ['git', 'diff', '--no-ext-diff', '--stat'] + similarity_options + args,
212 env=env)
maruel@chromium.org49e3d802012-07-18 23:54:45 +0000213
214
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000215class Settings(object):
216 def __init__(self):
217 self.default_server = None
218 self.cc = None
219 self.root = None
220 self.is_git_svn = None
221 self.svn_branch = None
222 self.tree_status_url = None
223 self.viewvc_url = None
224 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000225 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000226
227 def LazyUpdateIfNeeded(self):
228 """Updates the settings from a codereview.settings file, if available."""
229 if not self.updated:
230 cr_settings_file = FindCodereviewSettingsFile()
231 if cr_settings_file:
232 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000233 self.updated = True
234 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000235 self.updated = True
236
237 def GetDefaultServerUrl(self, error_ok=False):
238 if not self.default_server:
239 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000240 self.default_server = gclient_utils.UpgradeToHttps(
241 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000242 if error_ok:
243 return self.default_server
244 if not self.default_server:
245 error_message = ('Could not find settings file. You must configure '
246 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000247 self.default_server = gclient_utils.UpgradeToHttps(
248 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000249 return self.default_server
250
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000251 def GetRoot(self):
252 if not self.root:
253 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
254 return self.root
255
256 def GetIsGitSvn(self):
257 """Return true if this repo looks like it's using git-svn."""
258 if self.is_git_svn is None:
259 # If you have any "svn-remote.*" config keys, we think you're using svn.
260 self.is_git_svn = RunGitWithCode(
261 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
262 return self.is_git_svn
263
264 def GetSVNBranch(self):
265 if self.svn_branch is None:
266 if not self.GetIsGitSvn():
267 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
268
269 # Try to figure out which remote branch we're based on.
270 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000271 # 1) iterate through our branch history and find the svn URL.
272 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000273
274 # regexp matching the git-svn line that contains the URL.
275 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
276
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000277 # We don't want to go through all of history, so read a line from the
278 # pipe at a time.
279 # The -100 is an arbitrary limit so we don't search forever.
280 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000281 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000282 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000283 for line in proc.stdout:
284 match = git_svn_re.match(line)
285 if match:
286 url = match.group(1)
287 proc.stdout.close() # Cut pipe.
288 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000289
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000290 if url:
291 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
292 remotes = RunGit(['config', '--get-regexp',
293 r'^svn-remote\..*\.url']).splitlines()
294 for remote in remotes:
295 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000296 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000297 remote = match.group(1)
298 base_url = match.group(2)
299 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000300 ['config', 'svn-remote.%s.fetch' % remote],
301 error_ok=True).strip()
302 if fetch_spec:
303 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
304 if self.svn_branch:
305 break
306 branch_spec = RunGit(
307 ['config', 'svn-remote.%s.branches' % remote],
308 error_ok=True).strip()
309 if branch_spec:
310 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
311 if self.svn_branch:
312 break
313 tag_spec = RunGit(
314 ['config', 'svn-remote.%s.tags' % remote],
315 error_ok=True).strip()
316 if tag_spec:
317 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
318 if self.svn_branch:
319 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000320
321 if not self.svn_branch:
322 DieWithError('Can\'t guess svn branch -- try specifying it on the '
323 'command line')
324
325 return self.svn_branch
326
327 def GetTreeStatusUrl(self, error_ok=False):
328 if not self.tree_status_url:
329 error_message = ('You must configure your tree status URL by running '
330 '"git cl config".')
331 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
332 error_ok=error_ok,
333 error_message=error_message)
334 return self.tree_status_url
335
336 def GetViewVCUrl(self):
337 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000338 self.viewvc_url = gclient_utils.UpgradeToHttps(
339 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000340 return self.viewvc_url
341
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000342 def GetDefaultCCList(self):
343 return self._GetConfig('rietveld.cc', error_ok=True)
344
ukai@chromium.orge8077812012-02-03 03:41:46 +0000345 def GetIsGerrit(self):
346 """Return true if this repo is assosiated with gerrit code review system."""
347 if self.is_gerrit is None:
348 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
349 return self.is_gerrit
350
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000351 def _GetConfig(self, param, **kwargs):
352 self.LazyUpdateIfNeeded()
353 return RunGit(['config', param], **kwargs).strip()
354
355
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000356def ShortBranchName(branch):
357 """Convert a name like 'refs/heads/foo' to just 'foo'."""
358 return branch.replace('refs/heads/', '')
359
360
361class Changelist(object):
362 def __init__(self, branchref=None):
363 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000364 global settings
365 if not settings:
366 # Happens when git_cl.py is used as a utility library.
367 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000368 settings.GetDefaultServerUrl()
369 self.branchref = branchref
370 if self.branchref:
371 self.branch = ShortBranchName(self.branchref)
372 else:
373 self.branch = None
374 self.rietveld_server = None
375 self.upstream_branch = None
376 self.has_issue = False
377 self.issue = None
378 self.has_description = False
379 self.description = None
380 self.has_patchset = False
381 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000382 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000383 self.cc = None
384 self.watchers = ()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000385 self._remote = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000386
387 def GetCCList(self):
388 """Return the users cc'd on this CL.
389
390 Return is a string suitable for passing to gcl with the --cc flag.
391 """
392 if self.cc is None:
393 base_cc = settings .GetDefaultCCList()
394 more_cc = ','.join(self.watchers)
395 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
396 return self.cc
397
398 def SetWatchers(self, watchers):
399 """Set the list of email addresses that should be cc'd based on the changed
400 files in this CL.
401 """
402 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000403
404 def GetBranch(self):
405 """Returns the short branch name, e.g. 'master'."""
406 if not self.branch:
407 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
408 self.branch = ShortBranchName(self.branchref)
409 return self.branch
410
411 def GetBranchRef(self):
412 """Returns the full branch name, e.g. 'refs/heads/master'."""
413 self.GetBranch() # Poke the lazy loader.
414 return self.branchref
415
416 def FetchUpstreamTuple(self):
417 """Returns a tuple containg remote and remote ref,
418 e.g. 'origin', 'refs/heads/master'
419 """
420 remote = '.'
421 branch = self.GetBranch()
422 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
423 error_ok=True).strip()
424 if upstream_branch:
425 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
426 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000427 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
428 error_ok=True).strip()
429 if upstream_branch:
430 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000431 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000432 # Fall back on trying a git-svn upstream branch.
433 if settings.GetIsGitSvn():
434 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000435 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000436 # Else, try to guess the origin remote.
437 remote_branches = RunGit(['branch', '-r']).split()
438 if 'origin/master' in remote_branches:
439 # Fall back on origin/master if it exits.
440 remote = 'origin'
441 upstream_branch = 'refs/heads/master'
442 elif 'origin/trunk' in remote_branches:
443 # Fall back on origin/trunk if it exists. Generally a shared
444 # git-svn clone
445 remote = 'origin'
446 upstream_branch = 'refs/heads/trunk'
447 else:
448 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000449Either pass complete "git diff"-style arguments, like
450 git cl upload origin/master
451or verify this branch is set up to track another (via the --track argument to
452"git checkout -b ...").""")
453
454 return remote, upstream_branch
455
456 def GetUpstreamBranch(self):
457 if self.upstream_branch is None:
458 remote, upstream_branch = self.FetchUpstreamTuple()
459 if remote is not '.':
460 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
461 self.upstream_branch = upstream_branch
462 return self.upstream_branch
463
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000464 def GetRemote(self):
465 if not self._remote:
466 self._remote = self.FetchUpstreamTuple()[0]
467 if self._remote == '.':
468
469 remotes = RunGit(['remote'], error_ok=True).split()
470 if len(remotes) == 1:
471 self._remote, = remotes
472 elif 'origin' in remotes:
473 self._remote = 'origin'
474 logging.warning('Could not determine which remote this change is '
475 'associated with, so defaulting to "%s". This may '
476 'not be what you want. You may prevent this message '
477 'by running "git svn info" as documented here: %s',
478 self._remote,
479 GIT_INSTRUCTIONS_URL)
480 else:
481 logging.warn('Could not determine which remote this change is '
482 'associated with. You may prevent this message by '
483 'running "git svn info" as documented here: %s',
484 GIT_INSTRUCTIONS_URL)
485 return self._remote
486
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000487 def GetGitBaseUrlFromConfig(self):
488 """Return the configured base URL from branch.<branchname>.baseurl.
489
490 Returns None if it is not set.
491 """
492 return RunGit(['config', 'branch.%s.base-url' % self.GetBranch()],
493 error_ok=True).strip()
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000494
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000495 def GetRemoteUrl(self):
496 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
497
498 Returns None if there is no remote.
499 """
jmbaker@chromium.orga2cbbbb2012-03-22 20:40:40 +0000500 remote = self.GetRemote()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000501 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
502
503 def GetIssue(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000504 """Returns the issue number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000505 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000506 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
507 if issue:
maruel@chromium.org52424302012-08-29 15:14:30 +0000508 self.issue = int(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000509 else:
510 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000511 self.has_issue = True
512 return self.issue
513
514 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000515 if not self.rietveld_server:
516 # If we're on a branch then get the server potentially associated
517 # with that branch.
518 if self.GetIssue():
519 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
520 ['config', self._RietveldServer()], error_ok=True).strip())
521 if not self.rietveld_server:
522 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000523 return self.rietveld_server
524
525 def GetIssueURL(self):
526 """Get the URL for a particular issue."""
527 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
528
529 def GetDescription(self, pretty=False):
530 if not self.has_description:
531 if self.GetIssue():
maruel@chromium.org52424302012-08-29 15:14:30 +0000532 issue = self.GetIssue()
miket@chromium.org183df1a2012-01-04 19:44:55 +0000533 try:
534 self.description = self.RpcServer().get_description(issue).strip()
535 except urllib2.HTTPError, e:
536 if e.code == 404:
537 DieWithError(
538 ('\nWhile fetching the description for issue %d, received a '
539 '404 (not found)\n'
540 'error. It is likely that you deleted this '
541 'issue on the server. If this is the\n'
542 'case, please run\n\n'
543 ' git cl issue 0\n\n'
544 'to clear the association with the deleted issue. Then run '
545 'this command again.') % issue)
546 else:
547 DieWithError(
548 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000549 self.has_description = True
550 if pretty:
551 wrapper = textwrap.TextWrapper()
552 wrapper.initial_indent = wrapper.subsequent_indent = ' '
553 return wrapper.fill(self.description)
554 return self.description
555
556 def GetPatchset(self):
maruel@chromium.org52424302012-08-29 15:14:30 +0000557 """Returns the patchset number as a int or None if not set."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000558 if not self.has_patchset:
559 patchset = RunGit(['config', self._PatchsetSetting()],
560 error_ok=True).strip()
561 if patchset:
maruel@chromium.org52424302012-08-29 15:14:30 +0000562 self.patchset = int(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000563 else:
564 self.patchset = None
565 self.has_patchset = True
566 return self.patchset
567
568 def SetPatchset(self, patchset):
569 """Set this branch's patchset. If patchset=0, clears the patchset."""
570 if patchset:
571 RunGit(['config', self._PatchsetSetting(), str(patchset)])
572 else:
573 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000574 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000575 self.has_patchset = False
576
binji@chromium.org0281f522012-09-14 13:37:59 +0000577 def GetMostRecentPatchset(self, issue):
578 return self.RpcServer().get_issue_properties(
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000579 int(issue), False)['patchsets'][-1]
binji@chromium.org0281f522012-09-14 13:37:59 +0000580
581 def GetPatchSetDiff(self, issue, patchset):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000582 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000583 '/download/issue%s_%s.diff' % (issue, patchset))
584
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000585 def SetIssue(self, issue):
586 """Set this branch's issue. If issue=0, clears the issue."""
587 if issue:
588 RunGit(['config', self._IssueSetting(), str(issue)])
589 if self.rietveld_server:
590 RunGit(['config', self._RietveldServer(), self.rietveld_server])
591 else:
592 RunGit(['config', '--unset', self._IssueSetting()])
593 self.SetPatchset(0)
594 self.has_issue = False
595
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000596 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000597 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
598 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000599
600 # We use the sha1 of HEAD as a name of this change.
601 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000602 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000603 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000604 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000605 except subprocess2.CalledProcessError:
606 DieWithError(
607 ('\nFailed to diff against upstream branch %s!\n\n'
608 'This branch probably doesn\'t exist anymore. To reset the\n'
609 'tracking branch, please run\n'
610 ' git branch --set-upstream %s trunk\n'
611 'replacing trunk with origin/master or the relevant branch') %
612 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000613
maruel@chromium.org52424302012-08-29 15:14:30 +0000614 issue = self.GetIssue()
615 patchset = self.GetPatchset()
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000616 if issue:
617 description = self.GetDescription()
618 else:
619 # If the change was never uploaded, use the log messages of all commits
620 # up to the branch point, as git cl upload will prefill the description
621 # with these log messages.
maruel@chromium.org373af802012-05-25 21:07:33 +0000622 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
623 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000624
625 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000626 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000627 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000628 name,
629 description,
630 absroot,
631 files,
632 issue,
633 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000634 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000635
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000636 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
637 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
638 change = self.GetChange(upstream_branch, author)
639
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000640 # Apply watchlists on upload.
641 if not committing:
642 watchlist = watchlists.Watchlists(change.RepositoryRoot())
643 files = [f.LocalPath() for f in change.AffectedFiles()]
644 self.SetWatchers(watchlist.GetWatchersForPaths(files))
645
646 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000647 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000648 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000649 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000650 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000651 except presubmit_support.PresubmitFailure, e:
652 DieWithError(
653 ('%s\nMaybe your depot_tools is out of date?\n'
654 'If all fails, contact maruel@') % e)
655
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000656 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000657 """Updates the description and closes the issue."""
maruel@chromium.org52424302012-08-29 15:14:30 +0000658 issue = self.GetIssue()
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000659 self.RpcServer().update_description(issue, self.description)
660 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000661
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000662 def SetFlag(self, flag, value):
663 """Patchset must match."""
664 if not self.GetPatchset():
665 DieWithError('The patchset needs to match. Send another patchset.')
666 try:
667 return self.RpcServer().set_flag(
maruel@chromium.org52424302012-08-29 15:14:30 +0000668 self.GetIssue(), self.GetPatchset(), flag, value)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000669 except urllib2.HTTPError, e:
670 if e.code == 404:
671 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
672 if e.code == 403:
673 DieWithError(
674 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
675 'match?') % (self.GetIssue(), self.GetPatchset()))
676 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000677
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000678 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000679 """Returns an upload.RpcServer() to access this review's rietveld instance.
680 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000681 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000682 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
683 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000684 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000685
686 def _IssueSetting(self):
687 """Return the git setting that stores this change's issue."""
688 return 'branch.%s.rietveldissue' % self.GetBranch()
689
690 def _PatchsetSetting(self):
691 """Return the git setting that stores this change's most recent patchset."""
692 return 'branch.%s.rietveldpatchset' % self.GetBranch()
693
694 def _RietveldServer(self):
695 """Returns the git setting that stores this change's rietveld server."""
696 return 'branch.%s.rietveldserver' % self.GetBranch()
697
698
699def GetCodereviewSettingsInteractively():
700 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000701 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000702 server = settings.GetDefaultServerUrl(error_ok=True)
703 prompt = 'Rietveld server (host[:port])'
704 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000705 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000706 if not server and not newserver:
707 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000708 if newserver:
709 newserver = gclient_utils.UpgradeToHttps(newserver)
710 if newserver != server:
711 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000712
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000713 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000714 prompt = caption
715 if initial:
716 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000717 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 if new_val == 'x':
719 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000720 elif new_val:
721 if is_url:
722 new_val = gclient_utils.UpgradeToHttps(new_val)
723 if new_val != initial:
724 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000726 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000727 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000728 'tree-status-url', False)
729 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000730
731 # TODO: configure a default branch to diff against, rather than this
732 # svn-based hackery.
733
734
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000735class ChangeDescription(object):
736 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000737 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000738 self.log_desc = log_desc
739 self.reviewers = reviewers
740 self.description = self.log_desc
741
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000742 def Prompt(self):
743 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000744# This will displayed on the codereview site.
745# The first line will also be used as the subject of the review.
746"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000747 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000748 if ('\nR=' not in self.description and
749 '\nTBR=' not in self.description and
750 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000751 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000752 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000753 content += '\nBUG='
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000754 content = content.rstrip('\n') + '\n'
755 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000756 if not content:
757 DieWithError('Running editor failed')
758 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000759 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000760 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000761 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000762
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000763 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000764 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000765 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000766 # Retrieves all reviewer lines
767 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000768 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000769 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000770 if reviewers:
771 self.reviewers = reviewers
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000772
773 def IsEmpty(self):
774 return not self.description
775
776
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000777def FindCodereviewSettingsFile(filename='codereview.settings'):
778 """Finds the given file starting in the cwd and going up.
779
780 Only looks up to the top of the repository unless an
781 'inherit-review-settings-ok' file exists in the root of the repository.
782 """
783 inherit_ok_file = 'inherit-review-settings-ok'
784 cwd = os.getcwd()
785 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
786 if os.path.isfile(os.path.join(root, inherit_ok_file)):
787 root = '/'
788 while True:
789 if filename in os.listdir(cwd):
790 if os.path.isfile(os.path.join(cwd, filename)):
791 return open(os.path.join(cwd, filename))
792 if cwd == root:
793 break
794 cwd = os.path.dirname(cwd)
795
796
797def LoadCodereviewSettingsFromFile(fileobj):
798 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000799 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000800
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000801 def SetProperty(name, setting, unset_error_ok=False):
802 fullname = 'rietveld.' + name
803 if setting in keyvals:
804 RunGit(['config', fullname, keyvals[setting]])
805 else:
806 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
807
808 SetProperty('server', 'CODE_REVIEW_SERVER')
809 # Only server setting is required. Other settings can be absent.
810 # In that case, we ignore errors raised during option deletion attempt.
811 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
812 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
813 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
814
ukai@chromium.orge8077812012-02-03 03:41:46 +0000815 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
816 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
817 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000818
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
820 #should be of the form
821 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
822 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
823 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
824 keyvals['ORIGIN_URL_CONFIG']])
825
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000826
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000827def urlretrieve(source, destination):
828 """urllib is broken for SSL connections via a proxy therefore we
829 can't use urllib.urlretrieve()."""
830 with open(destination, 'w') as f:
831 f.write(urllib2.urlopen(source).read())
832
833
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000834def DownloadHooks(force):
835 """downloads hooks
836
837 Args:
838 force: True to update hooks. False to install hooks if not present.
839 """
840 if not settings.GetIsGerrit():
841 return
842 server_url = settings.GetDefaultServerUrl()
843 src = '%s/tools/hooks/commit-msg' % server_url
844 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
845 if not os.access(dst, os.X_OK):
846 if os.path.exists(dst):
847 if not force:
848 return
849 os.remove(dst)
850 try:
joshua.lock@intel.com426f69b2012-08-02 23:41:49 +0000851 urlretrieve(src, dst)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000852 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
853 except Exception:
854 if os.path.exists(dst):
855 os.remove(dst)
856 DieWithError('\nFailed to download hooks from %s' % src)
857
858
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000859@usage('[repo root containing codereview.settings]')
860def CMDconfig(parser, args):
861 """edit configuration for this tree"""
862
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000863 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000864 if len(args) == 0:
865 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000866 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000867 return 0
868
869 url = args[0]
870 if not url.endswith('codereview.settings'):
871 url = os.path.join(url, 'codereview.settings')
872
873 # Load code review settings and download hooks (if available).
874 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000875 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000876 return 0
877
878
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +0000879def CMDbaseurl(parser, args):
880 """get or set base-url for this branch"""
881 branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
882 branch = ShortBranchName(branchref)
883 _, args = parser.parse_args(args)
884 if not args:
885 print("Current base-url:")
886 return RunGit(['config', 'branch.%s.base-url' % branch],
887 error_ok=False).strip()
888 else:
889 print("Setting base-url to %s" % args[0])
890 return RunGit(['config', 'branch.%s.base-url' % branch, args[0]],
891 error_ok=False).strip()
892
893
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000894def CMDstatus(parser, args):
895 """show status of changelists"""
896 parser.add_option('--field',
897 help='print only specific field (desc|id|patch|url)')
898 (options, args) = parser.parse_args(args)
899
900 # TODO: maybe make show_branches a flag if necessary.
901 show_branches = not options.field
902
903 if show_branches:
904 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
905 if branches:
906 print 'Branches associated with reviews:'
rch@chromium.org92d67162012-04-02 20:10:35 +0000907 changes = (Changelist(branchref=b) for b in branches.splitlines())
908 branches = dict((cl.GetBranch(), cl.GetIssue()) for cl in changes)
909 alignment = max(5, max(len(b) for b in branches))
910 for branch in sorted(branches):
911 print " %*s: %s" % (alignment, branch, branches[branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000912
913 cl = Changelist()
914 if options.field:
915 if options.field.startswith('desc'):
916 print cl.GetDescription()
917 elif options.field == 'id':
918 issueid = cl.GetIssue()
919 if issueid:
920 print issueid
921 elif options.field == 'patch':
922 patchset = cl.GetPatchset()
923 if patchset:
924 print patchset
925 elif options.field == 'url':
926 url = cl.GetIssueURL()
927 if url:
928 print url
929 else:
930 print
931 print 'Current branch:',
932 if not cl.GetIssue():
933 print 'no issue assigned.'
934 return 0
935 print cl.GetBranch()
maruel@chromium.org52424302012-08-29 15:14:30 +0000936 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000937 print 'Issue description:'
938 print cl.GetDescription(pretty=True)
939 return 0
940
941
942@usage('[issue_number]')
943def CMDissue(parser, args):
944 """Set or display the current code review issue number.
945
946 Pass issue number 0 to clear the current issue.
947"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000948 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000949
950 cl = Changelist()
951 if len(args) > 0:
952 try:
953 issue = int(args[0])
954 except ValueError:
955 DieWithError('Pass a number to set the issue or none to list it.\n'
956 'Maybe you want to run git cl status?')
957 cl.SetIssue(issue)
maruel@chromium.org52424302012-08-29 15:14:30 +0000958 print 'Issue number: %s (%s)' % (cl.GetIssue(), cl.GetIssueURL())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000959 return 0
960
961
maruel@chromium.org9977a2e2012-06-06 22:30:56 +0000962def CMDcomments(parser, args):
963 """show review comments of the current changelist"""
964 (_, args) = parser.parse_args(args)
965 if args:
966 parser.error('Unsupported argument: %s' % args)
967
968 cl = Changelist()
969 if cl.GetIssue():
970 data = cl.RpcServer().get_issue_properties(cl.GetIssue(), True)
971 for message in sorted(data['messages'], key=lambda x: x['date']):
972 print '\n%s %s' % (message['date'].split('.', 1)[0], message['sender'])
973 if message['text'].strip():
974 print '\n'.join(' ' + l for l in message['text'].splitlines())
975 return 0
976
977
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978def CreateDescriptionFromLog(args):
979 """Pulls out the commit log to use as a base for the CL description."""
980 log_args = []
981 if len(args) == 1 and not args[0].endswith('.'):
982 log_args = [args[0] + '..']
983 elif len(args) == 1 and args[0].endswith('...'):
984 log_args = [args[0][:-1]]
985 elif len(args) == 2:
986 log_args = [args[0] + '..' + args[1]]
987 else:
988 log_args = args[:] # Hope for the best!
maruel@chromium.org373af802012-05-25 21:07:33 +0000989 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000990
991
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000992def CMDpresubmit(parser, args):
993 """run presubmit tests on the current changelist"""
994 parser.add_option('--upload', action='store_true',
995 help='Run upload hook instead of the push/dcommit hook')
sbc@chromium.org495ad152012-09-04 23:07:42 +0000996 parser.add_option('--force', action='store_true',
997 help='Run checks even if tree is dirty')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000998 (options, args) = parser.parse_args(args)
999
1000 # Make sure index is up-to-date before running diff-index.
1001 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
sbc@chromium.org495ad152012-09-04 23:07:42 +00001002 if not options.force and RunGit(['diff-index', 'HEAD']):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001003 # TODO(maruel): Is this really necessary?
sbc@chromium.org495ad152012-09-04 23:07:42 +00001004 print ('Cannot presubmit with a dirty tree.\n'
1005 'You must commit locally first (or use --force).')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001006 return 1
1007
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001008 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001009 if args:
1010 base_branch = args[0]
1011 else:
1012 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001013 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001014
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +00001015 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001016 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +00001017 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +00001018 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001019
1020
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001021def AddChangeIdToCommitMessage(options, args):
1022 """Re-commits using the current message, assumes the commit hook is in
1023 place.
1024 """
1025 log_desc = options.message or CreateDescriptionFromLog(args)
1026 git_command = ['commit', '--amend', '-m', log_desc]
1027 RunGit(git_command)
1028 new_log_desc = CreateDescriptionFromLog(args)
1029 if CHANGE_ID in new_log_desc:
1030 print 'git-cl: Added Change-Id to commit message.'
1031 else:
1032 print >> sys.stderr, 'ERROR: Gerrit commit-msg hook not available.'
1033
1034
ukai@chromium.orge8077812012-02-03 03:41:46 +00001035def GerritUpload(options, args, cl):
1036 """upload the current branch to gerrit."""
1037 # We assume the remote called "origin" is the one we want.
1038 # It is probably not worthwhile to support different workflows.
1039 remote = 'origin'
1040 branch = 'master'
1041 if options.target_branch:
1042 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001043
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001044 log_desc = options.message or CreateDescriptionFromLog(args)
sivachandra@chromium.orgaebe87f2012-10-22 20:34:21 +00001045 if CHANGE_ID not in log_desc:
1046 AddChangeIdToCommitMessage(options, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001047 if options.reviewers:
1048 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001049 change_desc = ChangeDescription(log_desc, options.reviewers)
1050 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +00001051 if change_desc.IsEmpty():
1052 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001053 return 1
1054
ukai@chromium.orge8077812012-02-03 03:41:46 +00001055 receive_options = []
1056 cc = cl.GetCCList().split(',')
1057 if options.cc:
1058 cc += options.cc.split(',')
1059 cc = filter(None, cc)
1060 if cc:
1061 receive_options += ['--cc=' + email for email in cc]
1062 if change_desc.reviewers:
1063 reviewers = filter(None, change_desc.reviewers.split(','))
1064 if reviewers:
1065 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066
ukai@chromium.orge8077812012-02-03 03:41:46 +00001067 git_command = ['push']
1068 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +00001069 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +00001070 ' '.join(receive_options))
1071 git_command += [remote, 'HEAD:refs/for/' + branch]
1072 RunGit(git_command)
1073 # TODO(ukai): parse Change-Id: and set issue number?
1074 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001075
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001076
ukai@chromium.orge8077812012-02-03 03:41:46 +00001077def RietveldUpload(options, args, cl):
1078 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001079 upload_args = ['--assume_yes'] # Don't ask about untracked files.
1080 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001081 if options.emulate_svn_auto_props:
1082 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083
1084 change_desc = None
1085
1086 if cl.GetIssue():
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001087 if options.title:
1088 upload_args.extend(['--title', options.title])
1089 elif options.message:
1090 # TODO(rogerta): for now, the -m option will also set the --title option
1091 # for upload.py. Soon this will be changed to set the --message option.
1092 # Will wait until people are used to typing -t instead of -m.
1093 upload_args.extend(['--title', options.message])
maruel@chromium.org52424302012-08-29 15:14:30 +00001094 upload_args.extend(['--issue', str(cl.GetIssue())])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001095 print ("This branch is associated with issue %s. "
1096 "Adding patch to that issue." % cl.GetIssue())
1097 else:
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001098 if options.title:
1099 upload_args.extend(['--title', options.title])
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001100 message = options.message or CreateDescriptionFromLog(args)
1101 change_desc = ChangeDescription(message, options.reviewers)
1102 if not options.force:
1103 change_desc.Prompt()
1104 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001105
1106 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001107 print "Description is empty; aborting."
1108 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001109
maruel@chromium.org71e12a92012-02-14 02:34:15 +00001110 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001111 if change_desc.reviewers:
1112 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +00001113 if options.send_mail:
1114 if not change_desc.reviewers:
1115 DieWithError("Must specify reviewers to send email.")
1116 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +00001117 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +00001118 if cc:
1119 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001120
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001121 upload_args.extend(['--git_similarity', str(options.similarity)])
iannucci@chromium.org79540052012-10-19 23:15:26 +00001122 if not options.find_copies:
1123 upload_args.extend(['--git_no_find_copies'])
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001124
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 # Include the upstream repo's URL in the change -- this is useful for
1126 # projects that have their source spread across multiple repos.
kalmard@homejinni.com6b0051e2012-04-03 15:45:08 +00001127 remote_url = cl.GetGitBaseUrlFromConfig()
1128 if not remote_url:
1129 if settings.GetIsGitSvn():
1130 # URL is dependent on the current directory.
1131 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
1132 if data:
1133 keys = dict(line.split(': ', 1) for line in data.splitlines()
1134 if ': ' in line)
1135 remote_url = keys.get('URL', None)
1136 else:
1137 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
1138 remote_url = (cl.GetRemoteUrl() + '@'
1139 + cl.GetUpstreamBranch().split('/')[-1])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001140 if remote_url:
1141 upload_args.extend(['--base_url', remote_url])
1142
1143 try:
cmp@chromium.orgdb9b0e32012-06-06 19:10:17 +00001144 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +00001145 except KeyboardInterrupt:
1146 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147 except:
1148 # If we got an exception after the user typed a description for their
1149 # change, back up the description before re-raising.
1150 if change_desc:
1151 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
1152 print '\nGot exception while uploading -- saving description to %s\n' \
1153 % backup_path
1154 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001155 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156 backup_file.close()
1157 raise
1158
1159 if not cl.GetIssue():
1160 cl.SetIssue(issue)
1161 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001162
1163 if options.use_commit_queue:
1164 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001165 return 0
1166
1167
ukai@chromium.orge8077812012-02-03 03:41:46 +00001168@usage('[args to "git diff"]')
1169def CMDupload(parser, args):
1170 """upload the current changelist to codereview"""
1171 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1172 help='bypass upload presubmit hook')
1173 parser.add_option('-f', action='store_true', dest='force',
1174 help="force yes to questions (don't prompt)")
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001175 parser.add_option('-m', dest='message', help='message for patchset')
1176 parser.add_option('-t', dest='title', help='title for patchset')
ukai@chromium.orge8077812012-02-03 03:41:46 +00001177 parser.add_option('-r', '--reviewers',
1178 help='reviewer email addresses')
1179 parser.add_option('--cc',
1180 help='cc email addresses')
1181 parser.add_option('--send-mail', action='store_true',
1182 help='send email to reviewer immediately')
1183 parser.add_option("--emulate_svn_auto_props", action="store_true",
1184 dest="emulate_svn_auto_props",
1185 help="Emulate Subversion's auto properties feature.")
ukai@chromium.orge8077812012-02-03 03:41:46 +00001186 parser.add_option('-c', '--use-commit-queue', action='store_true',
1187 help='tell the commit queue to commit this patchset')
1188 if settings.GetIsGerrit():
1189 parser.add_option('--target_branch', dest='target_branch', default='master',
1190 help='target branch to upload')
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001191 add_git_similarity(parser)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001192 (options, args) = parser.parse_args(args)
1193
rogerta@chromium.org420d3b82012-05-14 18:41:38 +00001194 # Print warning if the user used the -m/--message argument. This will soon
1195 # change to -t/--title.
1196 if options.message:
1197 print >> sys.stderr, (
1198 '\nWARNING: Use -t or --title to set the title of the patchset.\n'
1199 'In the near future, -m or --message will send a message instead.\n'
1200 'See http://goo.gl/JGg0Z for details.\n')
maruel@chromium.org9977a2e2012-06-06 22:30:56 +00001201
ukai@chromium.orge8077812012-02-03 03:41:46 +00001202 # Make sure index is up-to-date before running diff-index.
1203 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1204 if RunGit(['diff-index', 'HEAD']):
1205 print 'Cannot upload with a dirty tree. You must commit locally first.'
1206 return 1
1207
1208 cl = Changelist()
1209 if args:
1210 # TODO(ukai): is it ok for gerrit case?
1211 base_branch = args[0]
1212 else:
1213 # Default to diffing against the "upstream" branch.
1214 base_branch = cl.GetUpstreamBranch()
1215 args = [base_branch + "..."]
1216
1217 if not options.bypass_hooks:
1218 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1219 may_prompt=not options.force,
1220 verbose=options.verbose,
1221 author=None)
1222 if not hook_results.should_continue():
1223 return 1
1224 if not options.reviewers and hook_results.reviewers:
1225 options.reviewers = hook_results.reviewers
1226
iannucci@chromium.org79540052012-10-19 23:15:26 +00001227 print_stats(options.similarity, options.find_copies, args)
ukai@chromium.orge8077812012-02-03 03:41:46 +00001228 if settings.GetIsGerrit():
1229 return GerritUpload(options, args, cl)
1230 return RietveldUpload(options, args, cl)
1231
1232
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001233def IsSubmoduleMergeCommit(ref):
1234 # When submodules are added to the repo, we expect there to be a single
1235 # non-git-svn merge commit at remote HEAD with a signature comment.
1236 pattern = '^SVN changes up to revision [0-9]*$'
szager@chromium.orge84b7542012-06-15 21:26:58 +00001237 cmd = ['rev-list', '--merges', '--grep=%s' % pattern, '%s^!' % ref]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001238 return RunGit(cmd) != ''
1239
1240
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001241def SendUpstream(parser, args, cmd):
1242 """Common code for CmdPush and CmdDCommit
1243
1244 Squashed commit into a single.
1245 Updates changelog with metadata (e.g. pointer to review).
1246 Pushes/dcommits the code upstream.
1247 Updates review and closes.
1248 """
1249 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1250 help='bypass upload presubmit hook')
1251 parser.add_option('-m', dest='message',
1252 help="override review description")
1253 parser.add_option('-f', action='store_true', dest='force',
1254 help="force yes to questions (don't prompt)")
1255 parser.add_option('-c', dest='contributor',
1256 help="external contributor for patch (appended to " +
1257 "description and used as author for git). Should be " +
1258 "formatted as 'First Last <email@example.com>'")
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001259 add_git_similarity(parser)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001260 (options, args) = parser.parse_args(args)
1261 cl = Changelist()
1262
1263 if not args or cmd == 'push':
1264 # Default to merging against our best guess of the upstream branch.
1265 args = [cl.GetUpstreamBranch()]
1266
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001267 if options.contributor:
1268 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1269 print "Please provide contibutor as 'First Last <email@example.com>'"
1270 return 1
1271
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272 base_branch = args[0]
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001273 base_has_submodules = IsSubmoduleMergeCommit(base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001275 # Make sure index is up-to-date before running diff-index.
1276 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001277 if RunGit(['diff-index', 'HEAD']):
1278 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1279 return 1
1280
1281 # This rev-list syntax means "show all commits not in my branch that
1282 # are in base_branch".
1283 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1284 base_branch]).splitlines()
1285 if upstream_commits:
1286 print ('Base branch "%s" has %d commits '
1287 'not in this branch.' % (base_branch, len(upstream_commits)))
1288 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1289 return 1
1290
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001291 # This is the revision `svn dcommit` will commit on top of.
1292 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1293 '--pretty=format:%H'])
1294
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001295 if cmd == 'dcommit':
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001296 # If the base_head is a submodule merge commit, the first parent of the
1297 # base_head should be a git-svn commit, which is what we're interested in.
1298 base_svn_head = base_branch
1299 if base_has_submodules:
1300 base_svn_head += '^1'
1301
1302 extra_commits = RunGit(['rev-list', '^' + svn_head, base_svn_head])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001303 if extra_commits:
1304 print ('This branch has %d additional commits not upstreamed yet.'
1305 % len(extra_commits.splitlines()))
1306 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1307 'before attempting to %s.' % (base_branch, cmd))
1308 return 1
1309
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001310 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001311 author = None
1312 if options.contributor:
1313 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001314 hook_results = cl.RunHook(
1315 committing=True,
1316 upstream_branch=base_branch,
1317 may_prompt=not options.force,
1318 verbose=options.verbose,
1319 author=author)
1320 if not hook_results.should_continue():
1321 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322
1323 if cmd == 'dcommit':
1324 # Check the tree status if the tree status URL is set.
1325 status = GetTreeStatus()
1326 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001327 print('The tree is closed. Please wait for it to reopen. Use '
1328 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001329 return 1
1330 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001331 print('Unable to determine tree status. Please verify manually and '
1332 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001333 else:
1334 breakpad.SendStack(
1335 'GitClHooksBypassedCommit',
1336 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001337 (cl.GetRietveldServer(), cl.GetIssue()),
1338 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339
1340 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001341 if not description and cl.GetIssue():
1342 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001343
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001344 if not description:
erg@chromium.org1a173982012-08-29 20:43:05 +00001345 if not cl.GetIssue() and options.bypass_hooks:
1346 description = CreateDescriptionFromLog([base_branch])
1347 else:
1348 print 'No description set.'
1349 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1350 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001351
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001352 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001353 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354
1355 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001356 description += "\nPatch from %s." % options.contributor
1357 print 'Description:', repr(description)
1358
1359 branches = [base_branch, cl.GetBranchRef()]
1360 if not options.force:
iannucci@chromium.org79540052012-10-19 23:15:26 +00001361 print_stats(options.similarity, options.find_copies, branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001362 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001363
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001364 # We want to squash all this branch's commits into one commit with the proper
1365 # description. We do this by doing a "reset --soft" to the base branch (which
1366 # keeps the working copy the same), then dcommitting that. If origin/master
1367 # has a submodule merge commit, we'll also need to cherry-pick the squashed
1368 # commit onto a branch based on the git-svn head.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001369 MERGE_BRANCH = 'git-cl-commit'
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001370 CHERRY_PICK_BRANCH = 'git-cl-cherry-pick'
1371 # Delete the branches if they exist.
1372 for branch in [MERGE_BRANCH, CHERRY_PICK_BRANCH]:
1373 showref_cmd = ['show-ref', '--quiet', '--verify', 'refs/heads/%s' % branch]
1374 result = RunGitWithCode(showref_cmd)
1375 if result[0] == 0:
1376 RunGit(['branch', '-D', branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377
1378 # We might be in a directory that's present in this branch but not in the
1379 # trunk. Move up to the top of the tree so that git commands that expect a
1380 # valid CWD won't fail after we check out the merge branch.
1381 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1382 if rel_base_path:
1383 os.chdir(rel_base_path)
1384
1385 # Stuff our change into the merge branch.
1386 # We wrap in a try...finally block so if anything goes wrong,
1387 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001388 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001390 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1391 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 if options.contributor:
1393 RunGit(['commit', '--author', options.contributor, '-m', description])
1394 else:
1395 RunGit(['commit', '-m', description])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001396 if base_has_submodules:
1397 cherry_pick_commit = RunGit(['rev-list', 'HEAD^!']).rstrip()
1398 RunGit(['branch', CHERRY_PICK_BRANCH, svn_head])
1399 RunGit(['checkout', CHERRY_PICK_BRANCH])
1400 RunGit(['cherry-pick', cherry_pick_commit])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001401 if cmd == 'push':
1402 # push the merge branch.
1403 remote, branch = cl.FetchUpstreamTuple()
1404 retcode, output = RunGitWithCode(
1405 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1406 logging.debug(output)
1407 else:
1408 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001409 retcode, output = RunGitWithCode(['svn', 'dcommit',
iannucci@chromium.org53937ba2012-10-02 18:20:43 +00001410 '-C%s' % options.similarity,
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001411 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001412 finally:
1413 # And then swap back to the original branch and clean up.
1414 RunGit(['checkout', '-q', cl.GetBranch()])
1415 RunGit(['branch', '-D', MERGE_BRANCH])
szager@chromium.org9bb85e22012-06-13 20:28:23 +00001416 if base_has_submodules:
1417 RunGit(['branch', '-D', CHERRY_PICK_BRANCH])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418
1419 if cl.GetIssue():
1420 if cmd == 'dcommit' and 'Committed r' in output:
1421 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1422 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001423 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1424 for l in output.splitlines(False))
1425 match = filter(None, match)
1426 if len(match) != 1:
1427 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1428 output)
1429 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001430 else:
1431 return 1
1432 viewvc_url = settings.GetViewVCUrl()
1433 if viewvc_url and revision:
1434 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
cmp@chromium.orgc22ea4b2012-10-09 22:42:00 +00001435 elif revision:
1436 cl.description += ('\n\nCommitted: ' + revision)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001437 print ('Closing issue '
1438 '(you may be prompted for your codereview password)...')
1439 cl.CloseIssue()
1440 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001441
1442 if retcode == 0:
1443 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1444 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001445 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001446
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001447 return 0
1448
1449
1450@usage('[upstream branch to apply against]')
1451def CMDdcommit(parser, args):
1452 """commit the current changelist via git-svn"""
1453 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001454 message = """This doesn't appear to be an SVN repository.
1455If your project has a git mirror with an upstream SVN master, you probably need
1456to run 'git svn init', see your project's git mirror documentation.
1457If your project has a true writeable upstream repository, you probably want
1458to run 'git cl push' instead.
1459Choose wisely, if you get this wrong, your commit might appear to succeed but
1460will instead be silently ignored."""
1461 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001462 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001463 return SendUpstream(parser, args, 'dcommit')
1464
1465
1466@usage('[upstream branch to apply against]')
1467def CMDpush(parser, args):
1468 """commit the current changelist via git"""
1469 if settings.GetIsGitSvn():
1470 print('This appears to be an SVN repository.')
1471 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001472 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001473 return SendUpstream(parser, args, 'push')
1474
1475
1476@usage('<patch url or issue id>')
1477def CMDpatch(parser, args):
1478 """patch in a code review"""
1479 parser.add_option('-b', dest='newbranch',
1480 help='create a new branch off trunk for the patch')
1481 parser.add_option('-f', action='store_true', dest='force',
1482 help='with -b, clobber any existing branch')
1483 parser.add_option('--reject', action='store_true', dest='reject',
1484 help='allow failed patches and spew .rej files')
1485 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1486 help="don't commit after patch applies")
1487 (options, args) = parser.parse_args(args)
1488 if len(args) != 1:
1489 parser.print_help()
1490 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001491 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001492
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001493 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001494 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001495
maruel@chromium.org52424302012-08-29 15:14:30 +00001496 if issue_arg.isdigit():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001497 # Input is an issue id. Figure out the URL.
binji@chromium.org0281f522012-09-14 13:37:59 +00001498 cl = Changelist()
maruel@chromium.org52424302012-08-29 15:14:30 +00001499 issue = int(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001500 patchset = cl.GetMostRecentPatchset(issue)
1501 patch_data = cl.GetPatchSetDiff(issue, patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001502 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001503 # Assume it's a URL to the patch. Default to https.
1504 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
binji@chromium.org0281f522012-09-14 13:37:59 +00001505 match = re.match(r'.*?/issue(\d+)_(\d+).diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001506 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001507 DieWithError('Must pass an issue ID or full URL for '
1508 '\'Download raw patch set\'')
maruel@chromium.org52424302012-08-29 15:14:30 +00001509 issue = int(match.group(1))
binji@chromium.org0281f522012-09-14 13:37:59 +00001510 patchset = int(match.group(2))
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001511 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001512
1513 if options.newbranch:
1514 if options.force:
1515 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001516 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001517 RunGit(['checkout', '-b', options.newbranch,
1518 Changelist().GetUpstreamBranch()])
1519
1520 # Switch up to the top-level directory, if necessary, in preparation for
1521 # applying the patch.
1522 top = RunGit(['rev-parse', '--show-cdup']).strip()
1523 if top:
1524 os.chdir(top)
1525
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001526 # Git patches have a/ at the beginning of source paths. We strip that out
1527 # with a sed script rather than the -p flag to patch so we can feed either
1528 # Git or svn-style patches into the same apply command.
1529 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001530 try:
1531 patch_data = subprocess2.check_output(
1532 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1533 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001534 DieWithError('Git patch mungling failed.')
1535 logging.info(patch_data)
1536 # We use "git apply" to apply the patch instead of "patch" so that we can
1537 # pick up file adds.
1538 # The --index flag means: also insert into the index (so we catch adds).
1539 cmd = ['git', 'apply', '--index', '-p0']
1540 if options.reject:
1541 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001542 try:
1543 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1544 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001545 DieWithError('Failed to apply the patch')
1546
1547 # If we had an issue, commit the current state and register the issue.
1548 if not options.nocommit:
1549 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1550 cl = Changelist()
1551 cl.SetIssue(issue)
binji@chromium.org0281f522012-09-14 13:37:59 +00001552 cl.SetPatchset(patchset)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001553 print "Committed patch."
1554 else:
1555 print "Patch applied to index."
1556 return 0
1557
1558
1559def CMDrebase(parser, args):
1560 """rebase current branch on top of svn repo"""
1561 # Provide a wrapper for git svn rebase to help avoid accidental
1562 # git svn dcommit.
1563 # It's the only command that doesn't use parser at all since we just defer
1564 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001565 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001566
1567
1568def GetTreeStatus():
1569 """Fetches the tree status and returns either 'open', 'closed',
1570 'unknown' or 'unset'."""
1571 url = settings.GetTreeStatusUrl(error_ok=True)
1572 if url:
1573 status = urllib2.urlopen(url).read().lower()
1574 if status.find('closed') != -1 or status == '0':
1575 return 'closed'
1576 elif status.find('open') != -1 or status == '1':
1577 return 'open'
1578 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001579 return 'unset'
1580
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001581
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001582def GetTreeStatusReason():
1583 """Fetches the tree status from a json url and returns the message
1584 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001585 url = settings.GetTreeStatusUrl()
1586 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001587 connection = urllib2.urlopen(json_url)
1588 status = json.loads(connection.read())
1589 connection.close()
1590 return status['message']
1591
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001592
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001593def CMDtree(parser, args):
1594 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001595 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001596 status = GetTreeStatus()
1597 if 'unset' == status:
1598 print 'You must configure your tree status URL by running "git cl config".'
1599 return 2
1600
1601 print "The tree is %s" % status
1602 print
1603 print GetTreeStatusReason()
1604 if status != 'open':
1605 return 1
1606 return 0
1607
1608
maruel@chromium.org15192402012-09-06 12:38:29 +00001609def CMDtry(parser, args):
1610 """Triggers a try job through Rietveld."""
1611 group = optparse.OptionGroup(parser, "Try job options")
1612 group.add_option(
1613 "-b", "--bot", action="append",
1614 help=("IMPORTANT: specify ONE builder per --bot flag. Use it multiple "
1615 "times to specify multiple builders. ex: "
1616 "'-bwin_rel:ui_tests,webkit_unit_tests -bwin_layout'. See "
1617 "the try server waterfall for the builders name and the tests "
1618 "available. Can also be used to specify gtest_filter, e.g. "
1619 "-bwin_rel:base_unittests:ValuesTest.*Value"))
1620 group.add_option(
1621 "-r", "--revision",
1622 help="Revision to use for the try job; default: the "
1623 "revision will be determined by the try server; see "
1624 "its waterfall for more info")
1625 group.add_option(
1626 "-c", "--clobber", action="store_true", default=False,
1627 help="Force a clobber before building; e.g. don't do an "
1628 "incremental build")
1629 group.add_option(
1630 "--project",
1631 help="Override which project to use. Projects are defined "
1632 "server-side to define what default bot set to use")
1633 group.add_option(
1634 "-t", "--testfilter", action="append", default=[],
1635 help=("Apply a testfilter to all the selected builders. Unless the "
1636 "builders configurations are similar, use multiple "
1637 "--bot <builder>:<test> arguments."))
1638 group.add_option(
1639 "-n", "--name", help="Try job name; default to current branch name")
1640 parser.add_option_group(group)
1641 options, args = parser.parse_args(args)
1642
1643 if args:
1644 parser.error('Unknown arguments: %s' % args)
1645
1646 cl = Changelist()
1647 if not cl.GetIssue():
1648 parser.error('Need to upload first')
1649
1650 if not options.name:
1651 options.name = cl.GetBranch()
1652
1653 # Process --bot and --testfilter.
1654 if not options.bot:
1655 # Get try slaves from PRESUBMIT.py files if not specified.
1656 change = cl.GetChange(cl.GetUpstreamBranch(), None)
1657 options.bot = presubmit_support.DoGetTrySlaves(
1658 change,
1659 change.LocalPaths(),
1660 settings.GetRoot(),
1661 None,
1662 None,
1663 options.verbose,
1664 sys.stdout)
1665 if not options.bot:
1666 parser.error('No default try builder to try, use --bot')
1667
1668 builders_and_tests = {}
1669 for bot in options.bot:
1670 if ':' in bot:
1671 builder, tests = bot.split(':', 1)
1672 builders_and_tests.setdefault(builder, []).extend(tests.split(','))
1673 elif ',' in bot:
1674 parser.error('Specify one bot per --bot flag')
1675 else:
1676 builders_and_tests.setdefault(bot, []).append('defaulttests')
1677
1678 if options.testfilter:
1679 forced_tests = sum((t.split(',') for t in options.testfilter), [])
1680 builders_and_tests = dict(
1681 (b, forced_tests) for b, t in builders_and_tests.iteritems()
1682 if t != ['compile'])
1683
ilevy@chromium.orgf3b21232012-09-24 20:48:55 +00001684 if any('triggered' in b for b in builders_and_tests):
1685 print >> sys.stderr, (
1686 'ERROR You are trying to send a job to a triggered bot. This type of'
1687 ' bot requires an\ninitial job from a parent (usually a builder). '
1688 'Instead send your job to the parent.\n'
1689 'Bot list: %s' % builders_and_tests)
1690 return 1
1691
maruel@chromium.org15192402012-09-06 12:38:29 +00001692 patchset = cl.GetPatchset()
1693 if not cl.GetPatchset():
binji@chromium.org0281f522012-09-14 13:37:59 +00001694 patchset = cl.GetMostRecentPatchset(cl.GetIssue())
maruel@chromium.org15192402012-09-06 12:38:29 +00001695
1696 cl.RpcServer().trigger_try_jobs(
1697 cl.GetIssue(), patchset, options.name, options.clobber, options.revision,
1698 builders_and_tests)
maruel@chromium.org072d94b2012-09-20 19:20:08 +00001699 print('Tried jobs on:')
1700 length = max(len(builder) for builder in builders_and_tests)
1701 for builder in sorted(builders_and_tests):
1702 print ' %*s: %s' % (length, builder, ','.join(builders_and_tests[builder]))
maruel@chromium.org15192402012-09-06 12:38:29 +00001703 return 0
1704
1705
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001706@usage('[new upstream branch]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001707def CMDupstream(parser, args):
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001708 """prints or sets the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001709 _, args = parser.parse_args(args)
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001710 if len(args) > 1:
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001711 parser.error('Unrecognized args: %s' % ' '.join(args))
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001712 return 0
1713
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001714 cl = Changelist()
brettw@chromium.orgac0ba332012-08-09 23:42:53 +00001715 if args:
1716 # One arg means set upstream branch.
1717 RunGit(['branch', '--set-upstream', cl.GetBranch(), args[0]])
1718 cl = Changelist()
1719 print "Upstream branch set to " + cl.GetUpstreamBranch()
1720 else:
1721 print cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001722 return 0
1723
1724
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001725def CMDset_commit(parser, args):
1726 """set the commit bit"""
1727 _, args = parser.parse_args(args)
1728 if args:
1729 parser.error('Unrecognized args: %s' % ' '.join(args))
1730 cl = Changelist()
1731 cl.SetFlag('commit', '1')
1732 return 0
1733
1734
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001735def Command(name):
1736 return getattr(sys.modules[__name__], 'CMD' + name, None)
1737
1738
1739def CMDhelp(parser, args):
1740 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001741 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001742 if len(args) == 1:
1743 return main(args + ['--help'])
1744 parser.print_help()
1745 return 0
1746
1747
1748def GenUsage(parser, command):
1749 """Modify an OptParse object with the function's documentation."""
1750 obj = Command(command)
1751 more = getattr(obj, 'usage_more', '')
1752 if command == 'help':
1753 command = '<command>'
1754 else:
1755 # OptParser.description prefer nicely non-formatted strings.
1756 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1757 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1758
1759
1760def main(argv):
1761 """Doesn't parse the arguments here, just find the right subcommand to
1762 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001763 if sys.hexversion < 0x02060000:
1764 print >> sys.stderr, (
1765 '\nYour python version %s is unsupported, please upgrade.\n' %
1766 sys.version.split(' ', 1)[0])
1767 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001768 # Reload settings.
1769 global settings
1770 settings = Settings()
1771
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001772 # Do it late so all commands are listed.
1773 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1774 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1775 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1776
1777 # Create the option parse and add --verbose support.
1778 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001779 parser.add_option(
1780 '-v', '--verbose', action='count', default=0,
1781 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001782 old_parser_args = parser.parse_args
1783 def Parse(args):
1784 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001785 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001786 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001787 elif options.verbose:
1788 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001789 else:
1790 logging.basicConfig(level=logging.WARNING)
1791 return options, args
1792 parser.parse_args = Parse
1793
1794 if argv:
1795 command = Command(argv[0])
1796 if command:
1797 # "fix" the usage and the description now that we know the subcommand.
1798 GenUsage(parser, argv[0])
1799 try:
1800 return command(parser, argv[1:])
1801 except urllib2.HTTPError, e:
1802 if e.code != 500:
1803 raise
1804 DieWithError(
1805 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1806 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1807
1808 # Not a known command. Default to help.
1809 GenUsage(parser, 'help')
1810 return CMDhelp(parser, argv)
1811
1812
1813if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001814 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001815 sys.exit(main(sys.argv[1:]))