blob: 56d86f2f024fc616369c2404ed81bcac1cab4d6f [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
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000400 else:
401 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000402 self.has_issue = True
403 return self.issue
404
405 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000406 if not self.rietveld_server:
407 # If we're on a branch then get the server potentially associated
408 # with that branch.
409 if self.GetIssue():
410 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
411 ['config', self._RietveldServer()], error_ok=True).strip())
412 if not self.rietveld_server:
413 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000414 return self.rietveld_server
415
416 def GetIssueURL(self):
417 """Get the URL for a particular issue."""
418 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
419
420 def GetDescription(self, pretty=False):
421 if not self.has_description:
422 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000423 issue = int(self.GetIssue())
424 try:
425 self.description = self.RpcServer().get_description(issue).strip()
426 except urllib2.HTTPError, e:
427 if e.code == 404:
428 DieWithError(
429 ('\nWhile fetching the description for issue %d, received a '
430 '404 (not found)\n'
431 'error. It is likely that you deleted this '
432 'issue on the server. If this is the\n'
433 'case, please run\n\n'
434 ' git cl issue 0\n\n'
435 'to clear the association with the deleted issue. Then run '
436 'this command again.') % issue)
437 else:
438 DieWithError(
439 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000440 self.has_description = True
441 if pretty:
442 wrapper = textwrap.TextWrapper()
443 wrapper.initial_indent = wrapper.subsequent_indent = ' '
444 return wrapper.fill(self.description)
445 return self.description
446
447 def GetPatchset(self):
448 if not self.has_patchset:
449 patchset = RunGit(['config', self._PatchsetSetting()],
450 error_ok=True).strip()
451 if patchset:
452 self.patchset = patchset
453 else:
454 self.patchset = None
455 self.has_patchset = True
456 return self.patchset
457
458 def SetPatchset(self, patchset):
459 """Set this branch's patchset. If patchset=0, clears the patchset."""
460 if patchset:
461 RunGit(['config', self._PatchsetSetting(), str(patchset)])
462 else:
463 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000464 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000465 self.has_patchset = False
466
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000467 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000468 patchset = self.RpcServer().get_issue_properties(
469 int(issue), False)['patchsets'][-1]
470 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000471 '/download/issue%s_%s.diff' % (issue, patchset))
472
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000473 def SetIssue(self, issue):
474 """Set this branch's issue. If issue=0, clears the issue."""
475 if issue:
476 RunGit(['config', self._IssueSetting(), str(issue)])
477 if self.rietveld_server:
478 RunGit(['config', self._RietveldServer(), self.rietveld_server])
479 else:
480 RunGit(['config', '--unset', self._IssueSetting()])
481 self.SetPatchset(0)
482 self.has_issue = False
483
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000484 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000485 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
486 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000487
488 # We use the sha1 of HEAD as a name of this change.
489 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000490 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000491 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000492 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000493 except subprocess2.CalledProcessError:
494 DieWithError(
495 ('\nFailed to diff against upstream branch %s!\n\n'
496 'This branch probably doesn\'t exist anymore. To reset the\n'
497 'tracking branch, please run\n'
498 ' git branch --set-upstream %s trunk\n'
499 'replacing trunk with origin/master or the relevant branch') %
500 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000501
502 issue = ConvertToInteger(self.GetIssue())
503 patchset = ConvertToInteger(self.GetPatchset())
504 if issue:
505 description = self.GetDescription()
506 else:
507 # If the change was never uploaded, use the log messages of all commits
508 # up to the branch point, as git cl upload will prefill the description
509 # with these log messages.
510 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
511 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000512
513 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000514 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000515 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000516 name,
517 description,
518 absroot,
519 files,
520 issue,
521 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000522 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000523
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000524 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
525 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
526 change = self.GetChange(upstream_branch, author)
527
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000528 # Apply watchlists on upload.
529 if not committing:
530 watchlist = watchlists.Watchlists(change.RepositoryRoot())
531 files = [f.LocalPath() for f in change.AffectedFiles()]
532 self.SetWatchers(watchlist.GetWatchersForPaths(files))
533
534 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000535 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000536 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000537 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000538 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000539 except presubmit_support.PresubmitFailure, e:
540 DieWithError(
541 ('%s\nMaybe your depot_tools is out of date?\n'
542 'If all fails, contact maruel@') % e)
543
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000544 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000545 """Updates the description and closes the issue."""
546 issue = int(self.GetIssue())
547 self.RpcServer().update_description(issue, self.description)
548 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000549
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000550 def SetFlag(self, flag, value):
551 """Patchset must match."""
552 if not self.GetPatchset():
553 DieWithError('The patchset needs to match. Send another patchset.')
554 try:
555 return self.RpcServer().set_flag(
556 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
557 except urllib2.HTTPError, e:
558 if e.code == 404:
559 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
560 if e.code == 403:
561 DieWithError(
562 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
563 'match?') % (self.GetIssue(), self.GetPatchset()))
564 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000565
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000566 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000567 """Returns an upload.RpcServer() to access this review's rietveld instance.
568 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000569 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000570 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
571 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000572 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000573
574 def _IssueSetting(self):
575 """Return the git setting that stores this change's issue."""
576 return 'branch.%s.rietveldissue' % self.GetBranch()
577
578 def _PatchsetSetting(self):
579 """Return the git setting that stores this change's most recent patchset."""
580 return 'branch.%s.rietveldpatchset' % self.GetBranch()
581
582 def _RietveldServer(self):
583 """Returns the git setting that stores this change's rietveld server."""
584 return 'branch.%s.rietveldserver' % self.GetBranch()
585
586
587def GetCodereviewSettingsInteractively():
588 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000589 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000590 server = settings.GetDefaultServerUrl(error_ok=True)
591 prompt = 'Rietveld server (host[:port])'
592 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000593 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000594 if not server and not newserver:
595 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000596 if newserver:
597 newserver = gclient_utils.UpgradeToHttps(newserver)
598 if newserver != server:
599 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000600
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000601 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000602 prompt = caption
603 if initial:
604 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000605 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 if new_val == 'x':
607 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000608 elif new_val:
609 if is_url:
610 new_val = gclient_utils.UpgradeToHttps(new_val)
611 if new_val != initial:
612 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000613
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000614 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000615 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000616 'tree-status-url', False)
617 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000618
619 # TODO: configure a default branch to diff against, rather than this
620 # svn-based hackery.
621
622
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000623class ChangeDescription(object):
624 """Contains a parsed form of the change description."""
jam@chromium.org31083642012-01-27 03:14:45 +0000625 def __init__(self, subject, log_desc, reviewers):
626 self.subject = subject
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000627 self.log_desc = log_desc
628 self.reviewers = reviewers
629 self.description = self.log_desc
630
jam@chromium.org31083642012-01-27 03:14:45 +0000631 def Update(self):
632 initial_text = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000633# This will displayed on the codereview site.
634# The first line will also be used as the subject of the review.
635"""
jam@chromium.org31083642012-01-27 03:14:45 +0000636 initial_text += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000637 if ('\nR=' not in self.description and
638 '\nTBR=' not in self.description and
639 self.reviewers):
jam@chromium.org31083642012-01-27 03:14:45 +0000640 initial_text += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000641 if '\nBUG=' not in self.description:
jam@chromium.org31083642012-01-27 03:14:45 +0000642 initial_text += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000643 if '\nTEST=' not in self.description:
jam@chromium.org31083642012-01-27 03:14:45 +0000644 initial_text += '\nTEST='
645 initial_text = initial_text.rstrip('\n') + '\n'
646 content = gclient_utils.RunEditor(initial_text, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000647 if not content:
648 DieWithError('Running editor failed')
649 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
650 if not content:
651 DieWithError('No CL description, aborting')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000652 self.ParseDescription(content)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000653
ukai@chromium.orge8077812012-02-03 03:41:46 +0000654 def ParseDescription(self, description):
jam@chromium.org31083642012-01-27 03:14:45 +0000655 """Updates the list of reviewers and subject from the description."""
656 if not description:
657 self.description = description
658 return
659
660 self.description = description.strip('\n') + '\n'
661 self.subject = description.split('\n', 1)[0]
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000662 # Retrieves all reviewer lines
663 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
jam@chromium.org31083642012-01-27 03:14:45 +0000664 self.reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000665 i.group(2).strip() for i in regexp.finditer(self.description))
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000666
667 def IsEmpty(self):
668 return not self.description
669
670
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000671def FindCodereviewSettingsFile(filename='codereview.settings'):
672 """Finds the given file starting in the cwd and going up.
673
674 Only looks up to the top of the repository unless an
675 'inherit-review-settings-ok' file exists in the root of the repository.
676 """
677 inherit_ok_file = 'inherit-review-settings-ok'
678 cwd = os.getcwd()
679 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
680 if os.path.isfile(os.path.join(root, inherit_ok_file)):
681 root = '/'
682 while True:
683 if filename in os.listdir(cwd):
684 if os.path.isfile(os.path.join(cwd, filename)):
685 return open(os.path.join(cwd, filename))
686 if cwd == root:
687 break
688 cwd = os.path.dirname(cwd)
689
690
691def LoadCodereviewSettingsFromFile(fileobj):
692 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000693 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000694
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000695 def SetProperty(name, setting, unset_error_ok=False):
696 fullname = 'rietveld.' + name
697 if setting in keyvals:
698 RunGit(['config', fullname, keyvals[setting]])
699 else:
700 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
701
702 SetProperty('server', 'CODE_REVIEW_SERVER')
703 # Only server setting is required. Other settings can be absent.
704 # In that case, we ignore errors raised during option deletion attempt.
705 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
706 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
707 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
708
ukai@chromium.orge8077812012-02-03 03:41:46 +0000709 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
710 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
711 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
712 # Install the standard commit-msg hook.
713 RunCommand(['scp', '-p', '-P', keyvals['GERRIT_PORT'],
714 '%s:hooks/commit-msg' % keyvals['GERRIT_HOST'],
715 os.path.join(settings.GetRoot(),
716 '.git', 'hooks', 'commit-msg')])
717
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000718 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
719 #should be of the form
720 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
721 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
722 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
723 keyvals['ORIGIN_URL_CONFIG']])
724
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000725
726@usage('[repo root containing codereview.settings]')
727def CMDconfig(parser, args):
728 """edit configuration for this tree"""
729
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000730 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000731 if len(args) == 0:
732 GetCodereviewSettingsInteractively()
733 return 0
734
735 url = args[0]
736 if not url.endswith('codereview.settings'):
737 url = os.path.join(url, 'codereview.settings')
738
739 # Load code review settings and download hooks (if available).
740 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
741 return 0
742
743
744def CMDstatus(parser, args):
745 """show status of changelists"""
746 parser.add_option('--field',
747 help='print only specific field (desc|id|patch|url)')
748 (options, args) = parser.parse_args(args)
749
750 # TODO: maybe make show_branches a flag if necessary.
751 show_branches = not options.field
752
753 if show_branches:
754 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
755 if branches:
756 print 'Branches associated with reviews:'
757 for branch in sorted(branches.splitlines()):
758 cl = Changelist(branchref=branch)
759 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
760
761 cl = Changelist()
762 if options.field:
763 if options.field.startswith('desc'):
764 print cl.GetDescription()
765 elif options.field == 'id':
766 issueid = cl.GetIssue()
767 if issueid:
768 print issueid
769 elif options.field == 'patch':
770 patchset = cl.GetPatchset()
771 if patchset:
772 print patchset
773 elif options.field == 'url':
774 url = cl.GetIssueURL()
775 if url:
776 print url
777 else:
778 print
779 print 'Current branch:',
780 if not cl.GetIssue():
781 print 'no issue assigned.'
782 return 0
783 print cl.GetBranch()
784 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
785 print 'Issue description:'
786 print cl.GetDescription(pretty=True)
787 return 0
788
789
790@usage('[issue_number]')
791def CMDissue(parser, args):
792 """Set or display the current code review issue number.
793
794 Pass issue number 0 to clear the current issue.
795"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000796 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000797
798 cl = Changelist()
799 if len(args) > 0:
800 try:
801 issue = int(args[0])
802 except ValueError:
803 DieWithError('Pass a number to set the issue or none to list it.\n'
804 'Maybe you want to run git cl status?')
805 cl.SetIssue(issue)
806 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
807 return 0
808
809
810def CreateDescriptionFromLog(args):
811 """Pulls out the commit log to use as a base for the CL description."""
812 log_args = []
813 if len(args) == 1 and not args[0].endswith('.'):
814 log_args = [args[0] + '..']
815 elif len(args) == 1 and args[0].endswith('...'):
816 log_args = [args[0][:-1]]
817 elif len(args) == 2:
818 log_args = [args[0] + '..' + args[1]]
819 else:
820 log_args = args[:] # Hope for the best!
821 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
822
823
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000824def ConvertToInteger(inputval):
825 """Convert a string to integer, but returns either an int or None."""
826 try:
827 return int(inputval)
828 except (TypeError, ValueError):
829 return None
830
831
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000832def CMDpresubmit(parser, args):
833 """run presubmit tests on the current changelist"""
834 parser.add_option('--upload', action='store_true',
835 help='Run upload hook instead of the push/dcommit hook')
836 (options, args) = parser.parse_args(args)
837
838 # Make sure index is up-to-date before running diff-index.
839 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
840 if RunGit(['diff-index', 'HEAD']):
841 # TODO(maruel): Is this really necessary?
842 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
843 return 1
844
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000845 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000846 if args:
847 base_branch = args[0]
848 else:
849 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000850 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000851
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000852 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000853 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000854 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000855 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000856
857
ukai@chromium.orge8077812012-02-03 03:41:46 +0000858def GerritUpload(options, args, cl):
859 """upload the current branch to gerrit."""
860 # We assume the remote called "origin" is the one we want.
861 # It is probably not worthwhile to support different workflows.
862 remote = 'origin'
863 branch = 'master'
864 if options.target_branch:
865 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000866
ukai@chromium.orge8077812012-02-03 03:41:46 +0000867 log_desc = CreateDescriptionFromLog(args)
868 if options.reviewers:
869 log_desc += '\nR=' + options.reviewers
870 change_desc = ChangeDescription(options.message, log_desc,
871 options.reviewers)
872 change_desc.ParseDescription(log_desc)
873 if change_desc.IsEmpty():
874 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000875 return 1
876
ukai@chromium.orge8077812012-02-03 03:41:46 +0000877 receive_options = []
878 cc = cl.GetCCList().split(',')
879 if options.cc:
880 cc += options.cc.split(',')
881 cc = filter(None, cc)
882 if cc:
883 receive_options += ['--cc=' + email for email in cc]
884 if change_desc.reviewers:
885 reviewers = filter(None, change_desc.reviewers.split(','))
886 if reviewers:
887 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888
ukai@chromium.orge8077812012-02-03 03:41:46 +0000889 git_command = ['push']
890 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000891 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000892 ' '.join(receive_options))
893 git_command += [remote, 'HEAD:refs/for/' + branch]
894 RunGit(git_command)
895 # TODO(ukai): parse Change-Id: and set issue number?
896 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000897
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000898
ukai@chromium.orge8077812012-02-03 03:41:46 +0000899def RietveldUpload(options, args, cl):
900 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000901 upload_args = ['--assume_yes'] # Don't ask about untracked files.
902 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000903 if options.emulate_svn_auto_props:
904 upload_args.append('--emulate_svn_auto_props')
jam@chromium.org31083642012-01-27 03:14:45 +0000905 if options.from_logs and not options.message:
906 print 'Must set message for subject line if using desc_from_logs'
907 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000908
909 change_desc = None
910
911 if cl.GetIssue():
912 if options.message:
913 upload_args.extend(['--message', options.message])
914 upload_args.extend(['--issue', cl.GetIssue()])
915 print ("This branch is associated with issue %s. "
916 "Adding patch to that issue." % cl.GetIssue())
917 else:
jam@chromium.org31083642012-01-27 03:14:45 +0000918 log_desc = CreateDescriptionFromLog(args)
919 change_desc = ChangeDescription(options.message, log_desc,
920 options.reviewers)
921 if not options.from_logs:
922 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000923
924 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000925 print "Description is empty; aborting."
926 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000927
jam@chromium.org31083642012-01-27 03:14:45 +0000928 upload_args.extend(['--message', change_desc.subject])
929 upload_args.extend(['--description', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000930 if change_desc.reviewers:
931 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000932 if options.send_mail:
933 if not change_desc.reviewers:
934 DieWithError("Must specify reviewers to send email.")
935 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000936 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000937 if cc:
938 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939
940 # Include the upstream repo's URL in the change -- this is useful for
941 # projects that have their source spread across multiple repos.
942 remote_url = None
943 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000944 # URL is dependent on the current directory.
945 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000946 if data:
947 keys = dict(line.split(': ', 1) for line in data.splitlines()
948 if ': ' in line)
949 remote_url = keys.get('URL', None)
950 else:
951 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
952 remote_url = (cl.GetRemoteUrl() + '@'
953 + cl.GetUpstreamBranch().split('/')[-1])
954 if remote_url:
955 upload_args.extend(['--base_url', remote_url])
956
957 try:
958 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000959 except KeyboardInterrupt:
960 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000961 except:
962 # If we got an exception after the user typed a description for their
963 # change, back up the description before re-raising.
964 if change_desc:
965 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
966 print '\nGot exception while uploading -- saving description to %s\n' \
967 % backup_path
968 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000969 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000970 backup_file.close()
971 raise
972
973 if not cl.GetIssue():
974 cl.SetIssue(issue)
975 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000976
977 if options.use_commit_queue:
978 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000979 return 0
980
981
ukai@chromium.orge8077812012-02-03 03:41:46 +0000982@usage('[args to "git diff"]')
983def CMDupload(parser, args):
984 """upload the current changelist to codereview"""
985 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
986 help='bypass upload presubmit hook')
987 parser.add_option('-f', action='store_true', dest='force',
988 help="force yes to questions (don't prompt)")
989 parser.add_option('-m', dest='message', help='message for patch')
990 parser.add_option('-r', '--reviewers',
991 help='reviewer email addresses')
992 parser.add_option('--cc',
993 help='cc email addresses')
994 parser.add_option('--send-mail', action='store_true',
995 help='send email to reviewer immediately')
996 parser.add_option("--emulate_svn_auto_props", action="store_true",
997 dest="emulate_svn_auto_props",
998 help="Emulate Subversion's auto properties feature.")
999 parser.add_option("--desc_from_logs", action="store_true",
1000 dest="from_logs",
1001 help="""Squashes git commit logs into change description and
1002 uses message as subject""")
1003 parser.add_option('-c', '--use-commit-queue', action='store_true',
1004 help='tell the commit queue to commit this patchset')
1005 if settings.GetIsGerrit():
1006 parser.add_option('--target_branch', dest='target_branch', default='master',
1007 help='target branch to upload')
1008 (options, args) = parser.parse_args(args)
1009
1010 # Make sure index is up-to-date before running diff-index.
1011 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1012 if RunGit(['diff-index', 'HEAD']):
1013 print 'Cannot upload with a dirty tree. You must commit locally first.'
1014 return 1
1015
1016 cl = Changelist()
1017 if args:
1018 # TODO(ukai): is it ok for gerrit case?
1019 base_branch = args[0]
1020 else:
1021 # Default to diffing against the "upstream" branch.
1022 base_branch = cl.GetUpstreamBranch()
1023 args = [base_branch + "..."]
1024
1025 if not options.bypass_hooks:
1026 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1027 may_prompt=not options.force,
1028 verbose=options.verbose,
1029 author=None)
1030 if not hook_results.should_continue():
1031 return 1
1032 if not options.reviewers and hook_results.reviewers:
1033 options.reviewers = hook_results.reviewers
1034
1035 # --no-ext-diff is broken in some versions of Git, so try to work around
1036 # this by overriding the environment (but there is still a problem if the
1037 # git config key "diff.external" is used).
1038 env = os.environ.copy()
1039 if 'GIT_EXTERNAL_DIFF' in env:
1040 del env['GIT_EXTERNAL_DIFF']
1041 subprocess2.call(
1042 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
1043
1044 if settings.GetIsGerrit():
1045 return GerritUpload(options, args, cl)
1046 return RietveldUpload(options, args, cl)
1047
1048
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001049def SendUpstream(parser, args, cmd):
1050 """Common code for CmdPush and CmdDCommit
1051
1052 Squashed commit into a single.
1053 Updates changelog with metadata (e.g. pointer to review).
1054 Pushes/dcommits the code upstream.
1055 Updates review and closes.
1056 """
1057 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1058 help='bypass upload presubmit hook')
1059 parser.add_option('-m', dest='message',
1060 help="override review description")
1061 parser.add_option('-f', action='store_true', dest='force',
1062 help="force yes to questions (don't prompt)")
1063 parser.add_option('-c', dest='contributor',
1064 help="external contributor for patch (appended to " +
1065 "description and used as author for git). Should be " +
1066 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001067 (options, args) = parser.parse_args(args)
1068 cl = Changelist()
1069
1070 if not args or cmd == 'push':
1071 # Default to merging against our best guess of the upstream branch.
1072 args = [cl.GetUpstreamBranch()]
1073
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001074 if options.contributor:
1075 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1076 print "Please provide contibutor as 'First Last <email@example.com>'"
1077 return 1
1078
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001079 base_branch = args[0]
1080
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001081 # Make sure index is up-to-date before running diff-index.
1082 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001083 if RunGit(['diff-index', 'HEAD']):
1084 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1085 return 1
1086
1087 # This rev-list syntax means "show all commits not in my branch that
1088 # are in base_branch".
1089 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1090 base_branch]).splitlines()
1091 if upstream_commits:
1092 print ('Base branch "%s" has %d commits '
1093 'not in this branch.' % (base_branch, len(upstream_commits)))
1094 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1095 return 1
1096
1097 if cmd == 'dcommit':
1098 # This is the revision `svn dcommit` will commit on top of.
1099 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1100 '--pretty=format:%H'])
1101 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1102 if extra_commits:
1103 print ('This branch has %d additional commits not upstreamed yet.'
1104 % len(extra_commits.splitlines()))
1105 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1106 'before attempting to %s.' % (base_branch, cmd))
1107 return 1
1108
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001109 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001110 author = None
1111 if options.contributor:
1112 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001113 hook_results = cl.RunHook(
1114 committing=True,
1115 upstream_branch=base_branch,
1116 may_prompt=not options.force,
1117 verbose=options.verbose,
1118 author=author)
1119 if not hook_results.should_continue():
1120 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001121
1122 if cmd == 'dcommit':
1123 # Check the tree status if the tree status URL is set.
1124 status = GetTreeStatus()
1125 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001126 print('The tree is closed. Please wait for it to reopen. Use '
1127 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001128 return 1
1129 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001130 print('Unable to determine tree status. Please verify manually and '
1131 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001132 else:
1133 breakpad.SendStack(
1134 'GitClHooksBypassedCommit',
1135 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001136 (cl.GetRietveldServer(), cl.GetIssue()),
1137 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138
1139 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001140 if not description and cl.GetIssue():
1141 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001142
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001143 if not description:
1144 print 'No description set.'
1145 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1146 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001148 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001149 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001150
1151 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001152 description += "\nPatch from %s." % options.contributor
1153 print 'Description:', repr(description)
1154
1155 branches = [base_branch, cl.GetBranchRef()]
1156 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001157 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001158 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159
1160 # We want to squash all this branch's commits into one commit with the
1161 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001162 # We do this by doing a "reset --soft" to the base branch (which keeps
1163 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164 MERGE_BRANCH = 'git-cl-commit'
1165 # Delete the merge branch if it already exists.
1166 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1167 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1168 RunGit(['branch', '-D', MERGE_BRANCH])
1169
1170 # We might be in a directory that's present in this branch but not in the
1171 # trunk. Move up to the top of the tree so that git commands that expect a
1172 # valid CWD won't fail after we check out the merge branch.
1173 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1174 if rel_base_path:
1175 os.chdir(rel_base_path)
1176
1177 # Stuff our change into the merge branch.
1178 # We wrap in a try...finally block so if anything goes wrong,
1179 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001180 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001182 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1183 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001184 if options.contributor:
1185 RunGit(['commit', '--author', options.contributor, '-m', description])
1186 else:
1187 RunGit(['commit', '-m', description])
1188 if cmd == 'push':
1189 # push the merge branch.
1190 remote, branch = cl.FetchUpstreamTuple()
1191 retcode, output = RunGitWithCode(
1192 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1193 logging.debug(output)
1194 else:
1195 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001196 retcode, output = RunGitWithCode(['svn', 'dcommit',
1197 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198 finally:
1199 # And then swap back to the original branch and clean up.
1200 RunGit(['checkout', '-q', cl.GetBranch()])
1201 RunGit(['branch', '-D', MERGE_BRANCH])
1202
1203 if cl.GetIssue():
1204 if cmd == 'dcommit' and 'Committed r' in output:
1205 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1206 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001207 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1208 for l in output.splitlines(False))
1209 match = filter(None, match)
1210 if len(match) != 1:
1211 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1212 output)
1213 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001214 else:
1215 return 1
1216 viewvc_url = settings.GetViewVCUrl()
1217 if viewvc_url and revision:
1218 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1219 print ('Closing issue '
1220 '(you may be prompted for your codereview password)...')
1221 cl.CloseIssue()
1222 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001223
1224 if retcode == 0:
1225 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1226 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001227 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001228
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001229 return 0
1230
1231
1232@usage('[upstream branch to apply against]')
1233def CMDdcommit(parser, args):
1234 """commit the current changelist via git-svn"""
1235 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001236 message = """This doesn't appear to be an SVN repository.
1237If your project has a git mirror with an upstream SVN master, you probably need
1238to run 'git svn init', see your project's git mirror documentation.
1239If your project has a true writeable upstream repository, you probably want
1240to run 'git cl push' instead.
1241Choose wisely, if you get this wrong, your commit might appear to succeed but
1242will instead be silently ignored."""
1243 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001244 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001245 return SendUpstream(parser, args, 'dcommit')
1246
1247
1248@usage('[upstream branch to apply against]')
1249def CMDpush(parser, args):
1250 """commit the current changelist via git"""
1251 if settings.GetIsGitSvn():
1252 print('This appears to be an SVN repository.')
1253 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001254 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 return SendUpstream(parser, args, 'push')
1256
1257
1258@usage('<patch url or issue id>')
1259def CMDpatch(parser, args):
1260 """patch in a code review"""
1261 parser.add_option('-b', dest='newbranch',
1262 help='create a new branch off trunk for the patch')
1263 parser.add_option('-f', action='store_true', dest='force',
1264 help='with -b, clobber any existing branch')
1265 parser.add_option('--reject', action='store_true', dest='reject',
1266 help='allow failed patches and spew .rej files')
1267 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1268 help="don't commit after patch applies")
1269 (options, args) = parser.parse_args(args)
1270 if len(args) != 1:
1271 parser.print_help()
1272 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001273 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001274
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001275 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001276 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001277
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001278 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001279 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001280 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001281 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001282 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001283 # Assume it's a URL to the patch. Default to https.
1284 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001285 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001286 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001287 DieWithError('Must pass an issue ID or full URL for '
1288 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001289 issue = match.group(1)
1290 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291
1292 if options.newbranch:
1293 if options.force:
1294 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001295 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296 RunGit(['checkout', '-b', options.newbranch,
1297 Changelist().GetUpstreamBranch()])
1298
1299 # Switch up to the top-level directory, if necessary, in preparation for
1300 # applying the patch.
1301 top = RunGit(['rev-parse', '--show-cdup']).strip()
1302 if top:
1303 os.chdir(top)
1304
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 # Git patches have a/ at the beginning of source paths. We strip that out
1306 # with a sed script rather than the -p flag to patch so we can feed either
1307 # Git or svn-style patches into the same apply command.
1308 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001309 try:
1310 patch_data = subprocess2.check_output(
1311 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1312 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 DieWithError('Git patch mungling failed.')
1314 logging.info(patch_data)
1315 # We use "git apply" to apply the patch instead of "patch" so that we can
1316 # pick up file adds.
1317 # The --index flag means: also insert into the index (so we catch adds).
1318 cmd = ['git', 'apply', '--index', '-p0']
1319 if options.reject:
1320 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001321 try:
1322 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1323 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001324 DieWithError('Failed to apply the patch')
1325
1326 # If we had an issue, commit the current state and register the issue.
1327 if not options.nocommit:
1328 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1329 cl = Changelist()
1330 cl.SetIssue(issue)
1331 print "Committed patch."
1332 else:
1333 print "Patch applied to index."
1334 return 0
1335
1336
1337def CMDrebase(parser, args):
1338 """rebase current branch on top of svn repo"""
1339 # Provide a wrapper for git svn rebase to help avoid accidental
1340 # git svn dcommit.
1341 # It's the only command that doesn't use parser at all since we just defer
1342 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001343 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001344
1345
1346def GetTreeStatus():
1347 """Fetches the tree status and returns either 'open', 'closed',
1348 'unknown' or 'unset'."""
1349 url = settings.GetTreeStatusUrl(error_ok=True)
1350 if url:
1351 status = urllib2.urlopen(url).read().lower()
1352 if status.find('closed') != -1 or status == '0':
1353 return 'closed'
1354 elif status.find('open') != -1 or status == '1':
1355 return 'open'
1356 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001357 return 'unset'
1358
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001359
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001360def GetTreeStatusReason():
1361 """Fetches the tree status from a json url and returns the message
1362 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001363 url = settings.GetTreeStatusUrl()
1364 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001365 connection = urllib2.urlopen(json_url)
1366 status = json.loads(connection.read())
1367 connection.close()
1368 return status['message']
1369
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001370
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001371def CMDtree(parser, args):
1372 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001373 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 status = GetTreeStatus()
1375 if 'unset' == status:
1376 print 'You must configure your tree status URL by running "git cl config".'
1377 return 2
1378
1379 print "The tree is %s" % status
1380 print
1381 print GetTreeStatusReason()
1382 if status != 'open':
1383 return 1
1384 return 0
1385
1386
1387def CMDupstream(parser, args):
1388 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001389 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001390 if args:
1391 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001392 cl = Changelist()
1393 print cl.GetUpstreamBranch()
1394 return 0
1395
1396
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001397def CMDset_commit(parser, args):
1398 """set the commit bit"""
1399 _, args = parser.parse_args(args)
1400 if args:
1401 parser.error('Unrecognized args: %s' % ' '.join(args))
1402 cl = Changelist()
1403 cl.SetFlag('commit', '1')
1404 return 0
1405
1406
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001407def Command(name):
1408 return getattr(sys.modules[__name__], 'CMD' + name, None)
1409
1410
1411def CMDhelp(parser, args):
1412 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001413 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001414 if len(args) == 1:
1415 return main(args + ['--help'])
1416 parser.print_help()
1417 return 0
1418
1419
1420def GenUsage(parser, command):
1421 """Modify an OptParse object with the function's documentation."""
1422 obj = Command(command)
1423 more = getattr(obj, 'usage_more', '')
1424 if command == 'help':
1425 command = '<command>'
1426 else:
1427 # OptParser.description prefer nicely non-formatted strings.
1428 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1429 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1430
1431
1432def main(argv):
1433 """Doesn't parse the arguments here, just find the right subcommand to
1434 execute."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001435 # Reload settings.
1436 global settings
1437 settings = Settings()
1438
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001439 # Do it late so all commands are listed.
1440 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1441 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1442 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1443
1444 # Create the option parse and add --verbose support.
1445 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001446 parser.add_option(
1447 '-v', '--verbose', action='count', default=0,
1448 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001449 old_parser_args = parser.parse_args
1450 def Parse(args):
1451 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001452 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001453 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001454 elif options.verbose:
1455 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001456 else:
1457 logging.basicConfig(level=logging.WARNING)
1458 return options, args
1459 parser.parse_args = Parse
1460
1461 if argv:
1462 command = Command(argv[0])
1463 if command:
1464 # "fix" the usage and the description now that we know the subcommand.
1465 GenUsage(parser, argv[0])
1466 try:
1467 return command(parser, argv[1:])
1468 except urllib2.HTTPError, e:
1469 if e.code != 500:
1470 raise
1471 DieWithError(
1472 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1473 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1474
1475 # Not a known command. Default to help.
1476 GenUsage(parser, 'help')
1477 return CMDhelp(parser, argv)
1478
1479
1480if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001481 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482 sys.exit(main(sys.argv[1:]))