blob: 268703034359969ef2b36c3efdfcf9e9da4e0e94 [file] [log] [blame]
maruel@chromium.org725f1c32011-04-01 20:24:54 +00001#!/usr/bin/env python
miket@chromium.org183df1a2012-01-04 19:44:55 +00002# Copyright (c) 2012 The Chromium Authors. All rights reserved.
maruel@chromium.org725f1c32011-04-01 20:24:54 +00003# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00006# Copyright (C) 2008 Evan Martin <martine@danga.com>
7
maruel@chromium.org725f1c32011-04-01 20:24:54 +00008"""A git-command for integrating reviews on Rietveld."""
9
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000010import logging
11import optparse
12import os
13import re
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000014import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000015import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000016import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000017import urllib2
18
19try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000020 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000021except ImportError:
22 pass
23
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000024try:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000025 import simplejson as json # pylint: disable=F0401
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000026except ImportError:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000028 import json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029 except ImportError:
30 # Fall back to the packaged version.
maruel@chromium.orgb35c00c2011-03-31 00:43:35 +000031 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgfe79c312011-04-01 20:15:52 +000032 import simplejson as json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000033
34
35from third_party import upload
36import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000037import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000038import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000039import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000040import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000042import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import watchlists
44
45
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000046DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000047POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000048DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
49
maruel@chromium.org90541732011-04-01 17:54:18 +000050
maruel@chromium.orgddd59412011-11-30 14:20:38 +000051# Initialized in main()
52settings = None
53
54
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000055def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000056 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057 sys.exit(1)
58
59
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000060def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000061 try:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062 return subprocess2.check_output(args, shell=False, **kwargs)
63 except subprocess2.CalledProcessError, e:
64 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000065 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000066 'Command "%s" failed.\n%s' % (
67 ' '.join(args), error_message or e.stdout or ''))
68 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069
70
71def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000072 """Returns stdout."""
73 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000074
75
76def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000077 """Returns return code and stdout."""
78 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
79 return code, out[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000080
81
82def usage(more):
83 def hook(fn):
84 fn.usage_more = more
85 return fn
86 return hook
87
88
maruel@chromium.org90541732011-04-01 17:54:18 +000089def ask_for_data(prompt):
90 try:
91 return raw_input(prompt)
92 except KeyboardInterrupt:
93 # Hide the exception.
94 sys.exit(1)
95
96
bauerb@chromium.org866276c2011-03-18 20:09:31 +000097def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
98 """Return the corresponding git ref if |base_url| together with |glob_spec|
99 matches the full |url|.
100
101 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
102 """
103 fetch_suburl, as_ref = glob_spec.split(':')
104 if allow_wildcards:
105 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
106 if glob_match:
107 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
108 # "branches/{472,597,648}/src:refs/remotes/svn/*".
109 branch_re = re.escape(base_url)
110 if glob_match.group(1):
111 branch_re += '/' + re.escape(glob_match.group(1))
112 wildcard = glob_match.group(2)
113 if wildcard == '*':
114 branch_re += '([^/]*)'
115 else:
116 # Escape and replace surrounding braces with parentheses and commas
117 # with pipe symbols.
118 wildcard = re.escape(wildcard)
119 wildcard = re.sub('^\\\\{', '(', wildcard)
120 wildcard = re.sub('\\\\,', '|', wildcard)
121 wildcard = re.sub('\\\\}$', ')', wildcard)
122 branch_re += wildcard
123 if glob_match.group(3):
124 branch_re += re.escape(glob_match.group(3))
125 match = re.match(branch_re, url)
126 if match:
127 return re.sub('\*$', match.group(1), as_ref)
128
129 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
130 if fetch_suburl:
131 full_url = base_url + '/' + fetch_suburl
132 else:
133 full_url = base_url
134 if full_url == url:
135 return as_ref
136 return None
137
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000138
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000139class Settings(object):
140 def __init__(self):
141 self.default_server = None
142 self.cc = None
143 self.root = None
144 self.is_git_svn = None
145 self.svn_branch = None
146 self.tree_status_url = None
147 self.viewvc_url = None
148 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000149 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000150
151 def LazyUpdateIfNeeded(self):
152 """Updates the settings from a codereview.settings file, if available."""
153 if not self.updated:
154 cr_settings_file = FindCodereviewSettingsFile()
155 if cr_settings_file:
156 LoadCodereviewSettingsFromFile(cr_settings_file)
157 self.updated = True
158
159 def GetDefaultServerUrl(self, error_ok=False):
160 if not self.default_server:
161 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000162 self.default_server = gclient_utils.UpgradeToHttps(
163 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000164 if error_ok:
165 return self.default_server
166 if not self.default_server:
167 error_message = ('Could not find settings file. You must configure '
168 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000169 self.default_server = gclient_utils.UpgradeToHttps(
170 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000171 return self.default_server
172
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000173 def GetRoot(self):
174 if not self.root:
175 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
176 return self.root
177
178 def GetIsGitSvn(self):
179 """Return true if this repo looks like it's using git-svn."""
180 if self.is_git_svn is None:
181 # If you have any "svn-remote.*" config keys, we think you're using svn.
182 self.is_git_svn = RunGitWithCode(
183 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
184 return self.is_git_svn
185
186 def GetSVNBranch(self):
187 if self.svn_branch is None:
188 if not self.GetIsGitSvn():
189 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
190
191 # Try to figure out which remote branch we're based on.
192 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000193 # 1) iterate through our branch history and find the svn URL.
194 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000195
196 # regexp matching the git-svn line that contains the URL.
197 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
198
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000199 # We don't want to go through all of history, so read a line from the
200 # pipe at a time.
201 # The -100 is an arbitrary limit so we don't search forever.
202 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000203 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000204 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000205 for line in proc.stdout:
206 match = git_svn_re.match(line)
207 if match:
208 url = match.group(1)
209 proc.stdout.close() # Cut pipe.
210 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000211
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000212 if url:
213 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
214 remotes = RunGit(['config', '--get-regexp',
215 r'^svn-remote\..*\.url']).splitlines()
216 for remote in remotes:
217 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000218 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000219 remote = match.group(1)
220 base_url = match.group(2)
221 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000222 ['config', 'svn-remote.%s.fetch' % remote],
223 error_ok=True).strip()
224 if fetch_spec:
225 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
226 if self.svn_branch:
227 break
228 branch_spec = RunGit(
229 ['config', 'svn-remote.%s.branches' % remote],
230 error_ok=True).strip()
231 if branch_spec:
232 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
233 if self.svn_branch:
234 break
235 tag_spec = RunGit(
236 ['config', 'svn-remote.%s.tags' % remote],
237 error_ok=True).strip()
238 if tag_spec:
239 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
240 if self.svn_branch:
241 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000242
243 if not self.svn_branch:
244 DieWithError('Can\'t guess svn branch -- try specifying it on the '
245 'command line')
246
247 return self.svn_branch
248
249 def GetTreeStatusUrl(self, error_ok=False):
250 if not self.tree_status_url:
251 error_message = ('You must configure your tree status URL by running '
252 '"git cl config".')
253 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
254 error_ok=error_ok,
255 error_message=error_message)
256 return self.tree_status_url
257
258 def GetViewVCUrl(self):
259 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000260 self.viewvc_url = gclient_utils.UpgradeToHttps(
261 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000262 return self.viewvc_url
263
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000264 def GetDefaultCCList(self):
265 return self._GetConfig('rietveld.cc', error_ok=True)
266
ukai@chromium.orge8077812012-02-03 03:41:46 +0000267 def GetIsGerrit(self):
268 """Return true if this repo is assosiated with gerrit code review system."""
269 if self.is_gerrit is None:
270 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
271 return self.is_gerrit
272
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000273 def _GetConfig(self, param, **kwargs):
274 self.LazyUpdateIfNeeded()
275 return RunGit(['config', param], **kwargs).strip()
276
277
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000278def ShortBranchName(branch):
279 """Convert a name like 'refs/heads/foo' to just 'foo'."""
280 return branch.replace('refs/heads/', '')
281
282
283class Changelist(object):
284 def __init__(self, branchref=None):
285 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000286 global settings
287 if not settings:
288 # Happens when git_cl.py is used as a utility library.
289 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000290 settings.GetDefaultServerUrl()
291 self.branchref = branchref
292 if self.branchref:
293 self.branch = ShortBranchName(self.branchref)
294 else:
295 self.branch = None
296 self.rietveld_server = None
297 self.upstream_branch = None
298 self.has_issue = False
299 self.issue = None
300 self.has_description = False
301 self.description = None
302 self.has_patchset = False
303 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000304 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000305 self.cc = None
306 self.watchers = ()
307
308 def GetCCList(self):
309 """Return the users cc'd on this CL.
310
311 Return is a string suitable for passing to gcl with the --cc flag.
312 """
313 if self.cc is None:
314 base_cc = settings .GetDefaultCCList()
315 more_cc = ','.join(self.watchers)
316 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
317 return self.cc
318
319 def SetWatchers(self, watchers):
320 """Set the list of email addresses that should be cc'd based on the changed
321 files in this CL.
322 """
323 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000324
325 def GetBranch(self):
326 """Returns the short branch name, e.g. 'master'."""
327 if not self.branch:
328 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
329 self.branch = ShortBranchName(self.branchref)
330 return self.branch
331
332 def GetBranchRef(self):
333 """Returns the full branch name, e.g. 'refs/heads/master'."""
334 self.GetBranch() # Poke the lazy loader.
335 return self.branchref
336
337 def FetchUpstreamTuple(self):
338 """Returns a tuple containg remote and remote ref,
339 e.g. 'origin', 'refs/heads/master'
340 """
341 remote = '.'
342 branch = self.GetBranch()
343 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
344 error_ok=True).strip()
345 if upstream_branch:
346 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
347 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000348 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
349 error_ok=True).strip()
350 if upstream_branch:
351 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000352 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000353 # Fall back on trying a git-svn upstream branch.
354 if settings.GetIsGitSvn():
355 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000356 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000357 # Else, try to guess the origin remote.
358 remote_branches = RunGit(['branch', '-r']).split()
359 if 'origin/master' in remote_branches:
360 # Fall back on origin/master if it exits.
361 remote = 'origin'
362 upstream_branch = 'refs/heads/master'
363 elif 'origin/trunk' in remote_branches:
364 # Fall back on origin/trunk if it exists. Generally a shared
365 # git-svn clone
366 remote = 'origin'
367 upstream_branch = 'refs/heads/trunk'
368 else:
369 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000370Either pass complete "git diff"-style arguments, like
371 git cl upload origin/master
372or verify this branch is set up to track another (via the --track argument to
373"git checkout -b ...").""")
374
375 return remote, upstream_branch
376
377 def GetUpstreamBranch(self):
378 if self.upstream_branch is None:
379 remote, upstream_branch = self.FetchUpstreamTuple()
380 if remote is not '.':
381 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
382 self.upstream_branch = upstream_branch
383 return self.upstream_branch
384
385 def GetRemoteUrl(self):
386 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
387
388 Returns None if there is no remote.
389 """
390 remote = self.FetchUpstreamTuple()[0]
391 if remote == '.':
392 return None
393 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
394
395 def GetIssue(self):
396 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000397 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
398 if issue:
399 self.issue = issue
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000400 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000401 ['config', self._RietveldServer()], error_ok=True).strip())
402 else:
403 self.issue = None
404 if not self.rietveld_server:
405 self.rietveld_server = settings.GetDefaultServerUrl()
406 self.has_issue = True
407 return self.issue
408
409 def GetRietveldServer(self):
410 self.GetIssue()
411 return self.rietveld_server
412
413 def GetIssueURL(self):
414 """Get the URL for a particular issue."""
415 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
416
417 def GetDescription(self, pretty=False):
418 if not self.has_description:
419 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000420 issue = int(self.GetIssue())
421 try:
422 self.description = self.RpcServer().get_description(issue).strip()
423 except urllib2.HTTPError, e:
424 if e.code == 404:
425 DieWithError(
426 ('\nWhile fetching the description for issue %d, received a '
427 '404 (not found)\n'
428 'error. It is likely that you deleted this '
429 'issue on the server. If this is the\n'
430 'case, please run\n\n'
431 ' git cl issue 0\n\n'
432 'to clear the association with the deleted issue. Then run '
433 'this command again.') % issue)
434 else:
435 DieWithError(
436 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000437 self.has_description = True
438 if pretty:
439 wrapper = textwrap.TextWrapper()
440 wrapper.initial_indent = wrapper.subsequent_indent = ' '
441 return wrapper.fill(self.description)
442 return self.description
443
444 def GetPatchset(self):
445 if not self.has_patchset:
446 patchset = RunGit(['config', self._PatchsetSetting()],
447 error_ok=True).strip()
448 if patchset:
449 self.patchset = patchset
450 else:
451 self.patchset = None
452 self.has_patchset = True
453 return self.patchset
454
455 def SetPatchset(self, patchset):
456 """Set this branch's patchset. If patchset=0, clears the patchset."""
457 if patchset:
458 RunGit(['config', self._PatchsetSetting(), str(patchset)])
459 else:
460 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000461 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000462 self.has_patchset = False
463
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000464 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000465 patchset = self.RpcServer().get_issue_properties(
466 int(issue), False)['patchsets'][-1]
467 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000468 '/download/issue%s_%s.diff' % (issue, patchset))
469
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000470 def SetIssue(self, issue):
471 """Set this branch's issue. If issue=0, clears the issue."""
472 if issue:
473 RunGit(['config', self._IssueSetting(), str(issue)])
474 if self.rietveld_server:
475 RunGit(['config', self._RietveldServer(), self.rietveld_server])
476 else:
477 RunGit(['config', '--unset', self._IssueSetting()])
478 self.SetPatchset(0)
479 self.has_issue = False
480
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000481 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000482 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
483 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000484
485 # We use the sha1 of HEAD as a name of this change.
486 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000487 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000488 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000489 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000490 except subprocess2.CalledProcessError:
491 DieWithError(
492 ('\nFailed to diff against upstream branch %s!\n\n'
493 'This branch probably doesn\'t exist anymore. To reset the\n'
494 'tracking branch, please run\n'
495 ' git branch --set-upstream %s trunk\n'
496 'replacing trunk with origin/master or the relevant branch') %
497 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000498
499 issue = ConvertToInteger(self.GetIssue())
500 patchset = ConvertToInteger(self.GetPatchset())
501 if issue:
502 description = self.GetDescription()
503 else:
504 # If the change was never uploaded, use the log messages of all commits
505 # up to the branch point, as git cl upload will prefill the description
506 # with these log messages.
507 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
508 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000509
510 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000511 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000512 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000513 name,
514 description,
515 absroot,
516 files,
517 issue,
518 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000519 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000520
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000521 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
522 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
523 change = self.GetChange(upstream_branch, author)
524
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000525 # Apply watchlists on upload.
526 if not committing:
527 watchlist = watchlists.Watchlists(change.RepositoryRoot())
528 files = [f.LocalPath() for f in change.AffectedFiles()]
529 self.SetWatchers(watchlist.GetWatchersForPaths(files))
530
531 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000532 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000533 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000534 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000535 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000536 except presubmit_support.PresubmitFailure, e:
537 DieWithError(
538 ('%s\nMaybe your depot_tools is out of date?\n'
539 'If all fails, contact maruel@') % e)
540
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000541 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000542 """Updates the description and closes the issue."""
543 issue = int(self.GetIssue())
544 self.RpcServer().update_description(issue, self.description)
545 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000546
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000547 def SetFlag(self, flag, value):
548 """Patchset must match."""
549 if not self.GetPatchset():
550 DieWithError('The patchset needs to match. Send another patchset.')
551 try:
552 return self.RpcServer().set_flag(
553 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
554 except urllib2.HTTPError, e:
555 if e.code == 404:
556 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
557 if e.code == 403:
558 DieWithError(
559 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
560 'match?') % (self.GetIssue(), self.GetPatchset()))
561 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000562
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000563 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000564 """Returns an upload.RpcServer() to access this review's rietveld instance.
565 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000566 if not self._rpc_server:
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000567 self.GetIssue()
568 self._rpc_server = rietveld.Rietveld(self.rietveld_server, None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000569 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000570
571 def _IssueSetting(self):
572 """Return the git setting that stores this change's issue."""
573 return 'branch.%s.rietveldissue' % self.GetBranch()
574
575 def _PatchsetSetting(self):
576 """Return the git setting that stores this change's most recent patchset."""
577 return 'branch.%s.rietveldpatchset' % self.GetBranch()
578
579 def _RietveldServer(self):
580 """Returns the git setting that stores this change's rietveld server."""
581 return 'branch.%s.rietveldserver' % self.GetBranch()
582
583
584def GetCodereviewSettingsInteractively():
585 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000586 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000587 server = settings.GetDefaultServerUrl(error_ok=True)
588 prompt = 'Rietveld server (host[:port])'
589 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000590 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000591 if not server and not newserver:
592 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000593 if newserver:
594 newserver = gclient_utils.UpgradeToHttps(newserver)
595 if newserver != server:
596 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000597
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000598 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000599 prompt = caption
600 if initial:
601 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000602 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000603 if new_val == 'x':
604 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000605 elif new_val:
606 if is_url:
607 new_val = gclient_utils.UpgradeToHttps(new_val)
608 if new_val != initial:
609 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000611 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000612 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000613 'tree-status-url', False)
614 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000615
616 # TODO: configure a default branch to diff against, rather than this
617 # svn-based hackery.
618
619
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000620class ChangeDescription(object):
621 """Contains a parsed form of the change description."""
jam@chromium.org31083642012-01-27 03:14:45 +0000622 def __init__(self, subject, log_desc, reviewers):
623 self.subject = subject
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000624 self.log_desc = log_desc
625 self.reviewers = reviewers
626 self.description = self.log_desc
627
jam@chromium.org31083642012-01-27 03:14:45 +0000628 def Update(self):
629 initial_text = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000630# This will displayed on the codereview site.
631# The first line will also be used as the subject of the review.
632"""
jam@chromium.org31083642012-01-27 03:14:45 +0000633 initial_text += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000634 if ('\nR=' not in self.description and
635 '\nTBR=' not in self.description and
636 self.reviewers):
jam@chromium.org31083642012-01-27 03:14:45 +0000637 initial_text += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000638 if '\nBUG=' not in self.description:
jam@chromium.org31083642012-01-27 03:14:45 +0000639 initial_text += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000640 if '\nTEST=' not in self.description:
jam@chromium.org31083642012-01-27 03:14:45 +0000641 initial_text += '\nTEST='
642 initial_text = initial_text.rstrip('\n') + '\n'
643 content = gclient_utils.RunEditor(initial_text, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000644 if not content:
645 DieWithError('Running editor failed')
646 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
647 if not content:
648 DieWithError('No CL description, aborting')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000649 self.ParseDescription(content)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000650
ukai@chromium.orge8077812012-02-03 03:41:46 +0000651 def ParseDescription(self, description):
jam@chromium.org31083642012-01-27 03:14:45 +0000652 """Updates the list of reviewers and subject from the description."""
653 if not description:
654 self.description = description
655 return
656
657 self.description = description.strip('\n') + '\n'
658 self.subject = description.split('\n', 1)[0]
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000659 # Retrieves all reviewer lines
660 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
jam@chromium.org31083642012-01-27 03:14:45 +0000661 self.reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000662 i.group(2).strip() for i in regexp.finditer(self.description))
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000663
664 def IsEmpty(self):
665 return not self.description
666
667
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000668def FindCodereviewSettingsFile(filename='codereview.settings'):
669 """Finds the given file starting in the cwd and going up.
670
671 Only looks up to the top of the repository unless an
672 'inherit-review-settings-ok' file exists in the root of the repository.
673 """
674 inherit_ok_file = 'inherit-review-settings-ok'
675 cwd = os.getcwd()
676 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
677 if os.path.isfile(os.path.join(root, inherit_ok_file)):
678 root = '/'
679 while True:
680 if filename in os.listdir(cwd):
681 if os.path.isfile(os.path.join(cwd, filename)):
682 return open(os.path.join(cwd, filename))
683 if cwd == root:
684 break
685 cwd = os.path.dirname(cwd)
686
687
688def LoadCodereviewSettingsFromFile(fileobj):
689 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000690 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000691
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000692 def SetProperty(name, setting, unset_error_ok=False):
693 fullname = 'rietveld.' + name
694 if setting in keyvals:
695 RunGit(['config', fullname, keyvals[setting]])
696 else:
697 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
698
699 SetProperty('server', 'CODE_REVIEW_SERVER')
700 # Only server setting is required. Other settings can be absent.
701 # In that case, we ignore errors raised during option deletion attempt.
702 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
703 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
704 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
705
ukai@chromium.orge8077812012-02-03 03:41:46 +0000706 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
707 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
708 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
709 # Install the standard commit-msg hook.
710 RunCommand(['scp', '-p', '-P', keyvals['GERRIT_PORT'],
711 '%s:hooks/commit-msg' % keyvals['GERRIT_HOST'],
712 os.path.join(settings.GetRoot(),
713 '.git', 'hooks', 'commit-msg')])
714
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000715 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
716 #should be of the form
717 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
718 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
719 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
720 keyvals['ORIGIN_URL_CONFIG']])
721
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000722
723@usage('[repo root containing codereview.settings]')
724def CMDconfig(parser, args):
725 """edit configuration for this tree"""
726
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000727 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000728 if len(args) == 0:
729 GetCodereviewSettingsInteractively()
730 return 0
731
732 url = args[0]
733 if not url.endswith('codereview.settings'):
734 url = os.path.join(url, 'codereview.settings')
735
736 # Load code review settings and download hooks (if available).
737 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
738 return 0
739
740
741def CMDstatus(parser, args):
742 """show status of changelists"""
743 parser.add_option('--field',
744 help='print only specific field (desc|id|patch|url)')
745 (options, args) = parser.parse_args(args)
746
747 # TODO: maybe make show_branches a flag if necessary.
748 show_branches = not options.field
749
750 if show_branches:
751 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
752 if branches:
753 print 'Branches associated with reviews:'
754 for branch in sorted(branches.splitlines()):
755 cl = Changelist(branchref=branch)
756 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
757
758 cl = Changelist()
759 if options.field:
760 if options.field.startswith('desc'):
761 print cl.GetDescription()
762 elif options.field == 'id':
763 issueid = cl.GetIssue()
764 if issueid:
765 print issueid
766 elif options.field == 'patch':
767 patchset = cl.GetPatchset()
768 if patchset:
769 print patchset
770 elif options.field == 'url':
771 url = cl.GetIssueURL()
772 if url:
773 print url
774 else:
775 print
776 print 'Current branch:',
777 if not cl.GetIssue():
778 print 'no issue assigned.'
779 return 0
780 print cl.GetBranch()
781 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
782 print 'Issue description:'
783 print cl.GetDescription(pretty=True)
784 return 0
785
786
787@usage('[issue_number]')
788def CMDissue(parser, args):
789 """Set or display the current code review issue number.
790
791 Pass issue number 0 to clear the current issue.
792"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000793 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000794
795 cl = Changelist()
796 if len(args) > 0:
797 try:
798 issue = int(args[0])
799 except ValueError:
800 DieWithError('Pass a number to set the issue or none to list it.\n'
801 'Maybe you want to run git cl status?')
802 cl.SetIssue(issue)
803 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
804 return 0
805
806
807def CreateDescriptionFromLog(args):
808 """Pulls out the commit log to use as a base for the CL description."""
809 log_args = []
810 if len(args) == 1 and not args[0].endswith('.'):
811 log_args = [args[0] + '..']
812 elif len(args) == 1 and args[0].endswith('...'):
813 log_args = [args[0][:-1]]
814 elif len(args) == 2:
815 log_args = [args[0] + '..' + args[1]]
816 else:
817 log_args = args[:] # Hope for the best!
818 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
819
820
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000821def ConvertToInteger(inputval):
822 """Convert a string to integer, but returns either an int or None."""
823 try:
824 return int(inputval)
825 except (TypeError, ValueError):
826 return None
827
828
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000829def CMDpresubmit(parser, args):
830 """run presubmit tests on the current changelist"""
831 parser.add_option('--upload', action='store_true',
832 help='Run upload hook instead of the push/dcommit hook')
833 (options, args) = parser.parse_args(args)
834
835 # Make sure index is up-to-date before running diff-index.
836 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
837 if RunGit(['diff-index', 'HEAD']):
838 # TODO(maruel): Is this really necessary?
839 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
840 return 1
841
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000842 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000843 if args:
844 base_branch = args[0]
845 else:
846 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000847 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000849 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000850 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000851 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000852 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000853
854
ukai@chromium.orge8077812012-02-03 03:41:46 +0000855def GerritUpload(options, args, cl):
856 """upload the current branch to gerrit."""
857 # We assume the remote called "origin" is the one we want.
858 # It is probably not worthwhile to support different workflows.
859 remote = 'origin'
860 branch = 'master'
861 if options.target_branch:
862 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000863
ukai@chromium.orge8077812012-02-03 03:41:46 +0000864 log_desc = CreateDescriptionFromLog(args)
865 if options.reviewers:
866 log_desc += '\nR=' + options.reviewers
867 change_desc = ChangeDescription(options.message, log_desc,
868 options.reviewers)
869 change_desc.ParseDescription(log_desc)
870 if change_desc.IsEmpty():
871 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 return 1
873
ukai@chromium.orge8077812012-02-03 03:41:46 +0000874 receive_options = []
875 cc = cl.GetCCList().split(',')
876 if options.cc:
877 cc += options.cc.split(',')
878 cc = filter(None, cc)
879 if cc:
880 receive_options += ['--cc=' + email for email in cc]
881 if change_desc.reviewers:
882 reviewers = filter(None, change_desc.reviewers.split(','))
883 if reviewers:
884 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000885
ukai@chromium.orge8077812012-02-03 03:41:46 +0000886 git_command = ['push']
887 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000888 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000889 ' '.join(receive_options))
890 git_command += [remote, 'HEAD:refs/for/' + branch]
891 RunGit(git_command)
892 # TODO(ukai): parse Change-Id: and set issue number?
893 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000894
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000895
ukai@chromium.orge8077812012-02-03 03:41:46 +0000896def RietveldUpload(options, args, cl):
897 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898 upload_args = ['--assume_yes'] # Don't ask about untracked files.
899 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000900 if options.emulate_svn_auto_props:
901 upload_args.append('--emulate_svn_auto_props')
jam@chromium.org31083642012-01-27 03:14:45 +0000902 if options.from_logs and not options.message:
903 print 'Must set message for subject line if using desc_from_logs'
904 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000905
906 change_desc = None
907
908 if cl.GetIssue():
909 if options.message:
910 upload_args.extend(['--message', options.message])
911 upload_args.extend(['--issue', cl.GetIssue()])
912 print ("This branch is associated with issue %s. "
913 "Adding patch to that issue." % cl.GetIssue())
914 else:
jam@chromium.org31083642012-01-27 03:14:45 +0000915 log_desc = CreateDescriptionFromLog(args)
916 change_desc = ChangeDescription(options.message, log_desc,
917 options.reviewers)
918 if not options.from_logs:
919 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000920
921 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 print "Description is empty; aborting."
923 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000924
jam@chromium.org31083642012-01-27 03:14:45 +0000925 upload_args.extend(['--message', change_desc.subject])
926 upload_args.extend(['--description', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000927 if change_desc.reviewers:
928 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000929 if options.send_mail:
930 if not change_desc.reviewers:
931 DieWithError("Must specify reviewers to send email.")
932 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000933 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000934 if cc:
935 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000936
937 # Include the upstream repo's URL in the change -- this is useful for
938 # projects that have their source spread across multiple repos.
939 remote_url = None
940 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000941 # URL is dependent on the current directory.
942 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000943 if data:
944 keys = dict(line.split(': ', 1) for line in data.splitlines()
945 if ': ' in line)
946 remote_url = keys.get('URL', None)
947 else:
948 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
949 remote_url = (cl.GetRemoteUrl() + '@'
950 + cl.GetUpstreamBranch().split('/')[-1])
951 if remote_url:
952 upload_args.extend(['--base_url', remote_url])
953
954 try:
955 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000956 except KeyboardInterrupt:
957 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000958 except:
959 # If we got an exception after the user typed a description for their
960 # change, back up the description before re-raising.
961 if change_desc:
962 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
963 print '\nGot exception while uploading -- saving description to %s\n' \
964 % backup_path
965 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000966 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000967 backup_file.close()
968 raise
969
970 if not cl.GetIssue():
971 cl.SetIssue(issue)
972 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000973
974 if options.use_commit_queue:
975 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000976 return 0
977
978
ukai@chromium.orge8077812012-02-03 03:41:46 +0000979@usage('[args to "git diff"]')
980def CMDupload(parser, args):
981 """upload the current changelist to codereview"""
982 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
983 help='bypass upload presubmit hook')
984 parser.add_option('-f', action='store_true', dest='force',
985 help="force yes to questions (don't prompt)")
986 parser.add_option('-m', dest='message', help='message for patch')
987 parser.add_option('-r', '--reviewers',
988 help='reviewer email addresses')
989 parser.add_option('--cc',
990 help='cc email addresses')
991 parser.add_option('--send-mail', action='store_true',
992 help='send email to reviewer immediately')
993 parser.add_option("--emulate_svn_auto_props", action="store_true",
994 dest="emulate_svn_auto_props",
995 help="Emulate Subversion's auto properties feature.")
996 parser.add_option("--desc_from_logs", action="store_true",
997 dest="from_logs",
998 help="""Squashes git commit logs into change description and
999 uses message as subject""")
1000 parser.add_option('-c', '--use-commit-queue', action='store_true',
1001 help='tell the commit queue to commit this patchset')
1002 if settings.GetIsGerrit():
1003 parser.add_option('--target_branch', dest='target_branch', default='master',
1004 help='target branch to upload')
1005 (options, args) = parser.parse_args(args)
1006
1007 # Make sure index is up-to-date before running diff-index.
1008 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1009 if RunGit(['diff-index', 'HEAD']):
1010 print 'Cannot upload with a dirty tree. You must commit locally first.'
1011 return 1
1012
1013 cl = Changelist()
1014 if args:
1015 # TODO(ukai): is it ok for gerrit case?
1016 base_branch = args[0]
1017 else:
1018 # Default to diffing against the "upstream" branch.
1019 base_branch = cl.GetUpstreamBranch()
1020 args = [base_branch + "..."]
1021
1022 if not options.bypass_hooks:
1023 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1024 may_prompt=not options.force,
1025 verbose=options.verbose,
1026 author=None)
1027 if not hook_results.should_continue():
1028 return 1
1029 if not options.reviewers and hook_results.reviewers:
1030 options.reviewers = hook_results.reviewers
1031
1032 # --no-ext-diff is broken in some versions of Git, so try to work around
1033 # this by overriding the environment (but there is still a problem if the
1034 # git config key "diff.external" is used).
1035 env = os.environ.copy()
1036 if 'GIT_EXTERNAL_DIFF' in env:
1037 del env['GIT_EXTERNAL_DIFF']
1038 subprocess2.call(
1039 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
1040
1041 if settings.GetIsGerrit():
1042 return GerritUpload(options, args, cl)
1043 return RietveldUpload(options, args, cl)
1044
1045
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001046def SendUpstream(parser, args, cmd):
1047 """Common code for CmdPush and CmdDCommit
1048
1049 Squashed commit into a single.
1050 Updates changelog with metadata (e.g. pointer to review).
1051 Pushes/dcommits the code upstream.
1052 Updates review and closes.
1053 """
1054 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1055 help='bypass upload presubmit hook')
1056 parser.add_option('-m', dest='message',
1057 help="override review description")
1058 parser.add_option('-f', action='store_true', dest='force',
1059 help="force yes to questions (don't prompt)")
1060 parser.add_option('-c', dest='contributor',
1061 help="external contributor for patch (appended to " +
1062 "description and used as author for git). Should be " +
1063 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001064 (options, args) = parser.parse_args(args)
1065 cl = Changelist()
1066
1067 if not args or cmd == 'push':
1068 # Default to merging against our best guess of the upstream branch.
1069 args = [cl.GetUpstreamBranch()]
1070
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001071 if options.contributor:
1072 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1073 print "Please provide contibutor as 'First Last <email@example.com>'"
1074 return 1
1075
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001076 base_branch = args[0]
1077
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001078 # Make sure index is up-to-date before running diff-index.
1079 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001080 if RunGit(['diff-index', 'HEAD']):
1081 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1082 return 1
1083
1084 # This rev-list syntax means "show all commits not in my branch that
1085 # are in base_branch".
1086 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1087 base_branch]).splitlines()
1088 if upstream_commits:
1089 print ('Base branch "%s" has %d commits '
1090 'not in this branch.' % (base_branch, len(upstream_commits)))
1091 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1092 return 1
1093
1094 if cmd == 'dcommit':
1095 # This is the revision `svn dcommit` will commit on top of.
1096 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1097 '--pretty=format:%H'])
1098 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1099 if extra_commits:
1100 print ('This branch has %d additional commits not upstreamed yet.'
1101 % len(extra_commits.splitlines()))
1102 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1103 'before attempting to %s.' % (base_branch, cmd))
1104 return 1
1105
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001106 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001107 author = None
1108 if options.contributor:
1109 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001110 hook_results = cl.RunHook(
1111 committing=True,
1112 upstream_branch=base_branch,
1113 may_prompt=not options.force,
1114 verbose=options.verbose,
1115 author=author)
1116 if not hook_results.should_continue():
1117 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001118
1119 if cmd == 'dcommit':
1120 # Check the tree status if the tree status URL is set.
1121 status = GetTreeStatus()
1122 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001123 print('The tree is closed. Please wait for it to reopen. Use '
1124 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001125 return 1
1126 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001127 print('Unable to determine tree status. Please verify manually and '
1128 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001129 else:
1130 breakpad.SendStack(
1131 'GitClHooksBypassedCommit',
1132 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001133 (cl.GetRietveldServer(), cl.GetIssue()),
1134 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001135
1136 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001137 if not description and cl.GetIssue():
1138 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001139
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001140 if not description:
1141 print 'No description set.'
1142 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1143 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001144
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001145 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001146 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147
1148 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 description += "\nPatch from %s." % options.contributor
1150 print 'Description:', repr(description)
1151
1152 branches = [base_branch, cl.GetBranchRef()]
1153 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001154 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001155 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001156
1157 # We want to squash all this branch's commits into one commit with the
1158 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001159 # We do this by doing a "reset --soft" to the base branch (which keeps
1160 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001161 MERGE_BRANCH = 'git-cl-commit'
1162 # Delete the merge branch if it already exists.
1163 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1164 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1165 RunGit(['branch', '-D', MERGE_BRANCH])
1166
1167 # We might be in a directory that's present in this branch but not in the
1168 # trunk. Move up to the top of the tree so that git commands that expect a
1169 # valid CWD won't fail after we check out the merge branch.
1170 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1171 if rel_base_path:
1172 os.chdir(rel_base_path)
1173
1174 # Stuff our change into the merge branch.
1175 # We wrap in a try...finally block so if anything goes wrong,
1176 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001177 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001179 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1180 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 if options.contributor:
1182 RunGit(['commit', '--author', options.contributor, '-m', description])
1183 else:
1184 RunGit(['commit', '-m', description])
1185 if cmd == 'push':
1186 # push the merge branch.
1187 remote, branch = cl.FetchUpstreamTuple()
1188 retcode, output = RunGitWithCode(
1189 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1190 logging.debug(output)
1191 else:
1192 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001193 retcode, output = RunGitWithCode(['svn', 'dcommit',
1194 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001195 finally:
1196 # And then swap back to the original branch and clean up.
1197 RunGit(['checkout', '-q', cl.GetBranch()])
1198 RunGit(['branch', '-D', MERGE_BRANCH])
1199
1200 if cl.GetIssue():
1201 if cmd == 'dcommit' and 'Committed r' in output:
1202 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1203 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001204 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1205 for l in output.splitlines(False))
1206 match = filter(None, match)
1207 if len(match) != 1:
1208 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1209 output)
1210 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001211 else:
1212 return 1
1213 viewvc_url = settings.GetViewVCUrl()
1214 if viewvc_url and revision:
1215 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1216 print ('Closing issue '
1217 '(you may be prompted for your codereview password)...')
1218 cl.CloseIssue()
1219 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001220
1221 if retcode == 0:
1222 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1223 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001224 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001225
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001226 return 0
1227
1228
1229@usage('[upstream branch to apply against]')
1230def CMDdcommit(parser, args):
1231 """commit the current changelist via git-svn"""
1232 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001233 message = """This doesn't appear to be an SVN repository.
1234If your project has a git mirror with an upstream SVN master, you probably need
1235to run 'git svn init', see your project's git mirror documentation.
1236If your project has a true writeable upstream repository, you probably want
1237to run 'git cl push' instead.
1238Choose wisely, if you get this wrong, your commit might appear to succeed but
1239will instead be silently ignored."""
1240 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001241 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001242 return SendUpstream(parser, args, 'dcommit')
1243
1244
1245@usage('[upstream branch to apply against]')
1246def CMDpush(parser, args):
1247 """commit the current changelist via git"""
1248 if settings.GetIsGitSvn():
1249 print('This appears to be an SVN repository.')
1250 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001251 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001252 return SendUpstream(parser, args, 'push')
1253
1254
1255@usage('<patch url or issue id>')
1256def CMDpatch(parser, args):
1257 """patch in a code review"""
1258 parser.add_option('-b', dest='newbranch',
1259 help='create a new branch off trunk for the patch')
1260 parser.add_option('-f', action='store_true', dest='force',
1261 help='with -b, clobber any existing branch')
1262 parser.add_option('--reject', action='store_true', dest='reject',
1263 help='allow failed patches and spew .rej files')
1264 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1265 help="don't commit after patch applies")
1266 (options, args) = parser.parse_args(args)
1267 if len(args) != 1:
1268 parser.print_help()
1269 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001270 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001272 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001273 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001274
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001275 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001276 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001277 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001278 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001280 # Assume it's a URL to the patch. Default to https.
1281 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001282 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001283 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001284 DieWithError('Must pass an issue ID or full URL for '
1285 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001286 issue = match.group(1)
1287 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001288
1289 if options.newbranch:
1290 if options.force:
1291 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001292 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001293 RunGit(['checkout', '-b', options.newbranch,
1294 Changelist().GetUpstreamBranch()])
1295
1296 # Switch up to the top-level directory, if necessary, in preparation for
1297 # applying the patch.
1298 top = RunGit(['rev-parse', '--show-cdup']).strip()
1299 if top:
1300 os.chdir(top)
1301
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001302 # Git patches have a/ at the beginning of source paths. We strip that out
1303 # with a sed script rather than the -p flag to patch so we can feed either
1304 # Git or svn-style patches into the same apply command.
1305 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001306 try:
1307 patch_data = subprocess2.check_output(
1308 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1309 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001310 DieWithError('Git patch mungling failed.')
1311 logging.info(patch_data)
1312 # We use "git apply" to apply the patch instead of "patch" so that we can
1313 # pick up file adds.
1314 # The --index flag means: also insert into the index (so we catch adds).
1315 cmd = ['git', 'apply', '--index', '-p0']
1316 if options.reject:
1317 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001318 try:
1319 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1320 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001321 DieWithError('Failed to apply the patch')
1322
1323 # If we had an issue, commit the current state and register the issue.
1324 if not options.nocommit:
1325 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1326 cl = Changelist()
1327 cl.SetIssue(issue)
1328 print "Committed patch."
1329 else:
1330 print "Patch applied to index."
1331 return 0
1332
1333
1334def CMDrebase(parser, args):
1335 """rebase current branch on top of svn repo"""
1336 # Provide a wrapper for git svn rebase to help avoid accidental
1337 # git svn dcommit.
1338 # It's the only command that doesn't use parser at all since we just defer
1339 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001340 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341
1342
1343def GetTreeStatus():
1344 """Fetches the tree status and returns either 'open', 'closed',
1345 'unknown' or 'unset'."""
1346 url = settings.GetTreeStatusUrl(error_ok=True)
1347 if url:
1348 status = urllib2.urlopen(url).read().lower()
1349 if status.find('closed') != -1 or status == '0':
1350 return 'closed'
1351 elif status.find('open') != -1 or status == '1':
1352 return 'open'
1353 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001354 return 'unset'
1355
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001356
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357def GetTreeStatusReason():
1358 """Fetches the tree status from a json url and returns the message
1359 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001360 url = settings.GetTreeStatusUrl()
1361 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001362 connection = urllib2.urlopen(json_url)
1363 status = json.loads(connection.read())
1364 connection.close()
1365 return status['message']
1366
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001367
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001368def CMDtree(parser, args):
1369 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001370 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371 status = GetTreeStatus()
1372 if 'unset' == status:
1373 print 'You must configure your tree status URL by running "git cl config".'
1374 return 2
1375
1376 print "The tree is %s" % status
1377 print
1378 print GetTreeStatusReason()
1379 if status != 'open':
1380 return 1
1381 return 0
1382
1383
1384def CMDupstream(parser, args):
1385 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001386 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001387 if args:
1388 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001389 cl = Changelist()
1390 print cl.GetUpstreamBranch()
1391 return 0
1392
1393
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001394def CMDset_commit(parser, args):
1395 """set the commit bit"""
1396 _, args = parser.parse_args(args)
1397 if args:
1398 parser.error('Unrecognized args: %s' % ' '.join(args))
1399 cl = Changelist()
1400 cl.SetFlag('commit', '1')
1401 return 0
1402
1403
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001404def Command(name):
1405 return getattr(sys.modules[__name__], 'CMD' + name, None)
1406
1407
1408def CMDhelp(parser, args):
1409 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001410 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001411 if len(args) == 1:
1412 return main(args + ['--help'])
1413 parser.print_help()
1414 return 0
1415
1416
1417def GenUsage(parser, command):
1418 """Modify an OptParse object with the function's documentation."""
1419 obj = Command(command)
1420 more = getattr(obj, 'usage_more', '')
1421 if command == 'help':
1422 command = '<command>'
1423 else:
1424 # OptParser.description prefer nicely non-formatted strings.
1425 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1426 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1427
1428
1429def main(argv):
1430 """Doesn't parse the arguments here, just find the right subcommand to
1431 execute."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001432 # Reload settings.
1433 global settings
1434 settings = Settings()
1435
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001436 # Do it late so all commands are listed.
1437 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1438 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1439 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1440
1441 # Create the option parse and add --verbose support.
1442 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001443 parser.add_option(
1444 '-v', '--verbose', action='count', default=0,
1445 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001446 old_parser_args = parser.parse_args
1447 def Parse(args):
1448 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001449 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001450 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001451 elif options.verbose:
1452 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 else:
1454 logging.basicConfig(level=logging.WARNING)
1455 return options, args
1456 parser.parse_args = Parse
1457
1458 if argv:
1459 command = Command(argv[0])
1460 if command:
1461 # "fix" the usage and the description now that we know the subcommand.
1462 GenUsage(parser, argv[0])
1463 try:
1464 return command(parser, argv[1:])
1465 except urllib2.HTTPError, e:
1466 if e.code != 500:
1467 raise
1468 DieWithError(
1469 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1470 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1471
1472 # Not a known command. Default to help.
1473 GenUsage(parser, 'help')
1474 return CMDhelp(parser, argv)
1475
1476
1477if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001478 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479 sys.exit(main(sys.argv[1:]))