blob: c967d8ff41538420118ff07b85ad1f5dcd7ad4bf [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
ukai@chromium.org78c4b982012-02-14 02:20:26 +000014import stat
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000015import sys
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import textwrap
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000017import urlparse
ukai@chromium.org78c4b982012-02-14 02:20:26 +000018import urllib
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000019import urllib2
20
21try:
maruel@chromium.orgc98c0c52011-04-06 13:39:43 +000022 import readline # pylint: disable=F0401,W0611
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000023except ImportError:
24 pass
25
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000026try:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000027 import simplejson as json # pylint: disable=F0401
dpranke@chromium.org20254fc2011-03-22 18:28:59 +000028except ImportError:
maruel@chromium.org2a74d372011-03-29 19:05:50 +000029 try:
maruel@chromium.org725f1c32011-04-01 20:24:54 +000030 import json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000031 except ImportError:
32 # Fall back to the packaged version.
maruel@chromium.orgb35c00c2011-03-31 00:43:35 +000033 sys.path.append(os.path.join(os.path.dirname(__file__), 'third_party'))
dpranke@chromium.orgfe79c312011-04-01 20:15:52 +000034 import simplejson as json # pylint: disable=F0401
maruel@chromium.org2a74d372011-03-29 19:05:50 +000035
36
37from third_party import upload
38import breakpad # pylint: disable=W0611
maruel@chromium.org6f09cd92011-04-01 16:38:12 +000039import fix_encoding
maruel@chromium.org0e0436a2011-10-25 13:32:41 +000040import gclient_utils
maruel@chromium.org2a74d372011-03-29 19:05:50 +000041import presubmit_support
maruel@chromium.orgcab38e92011-04-09 00:30:51 +000042import rietveld
maruel@chromium.org2a74d372011-03-29 19:05:50 +000043import scm
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000044import subprocess2
maruel@chromium.org2a74d372011-03-29 19:05:50 +000045import watchlists
46
47
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +000048DEFAULT_SERVER = 'https://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000049POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000050DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
51
maruel@chromium.org90541732011-04-01 17:54:18 +000052
maruel@chromium.orgddd59412011-11-30 14:20:38 +000053# Initialized in main()
54settings = None
55
56
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000057def DieWithError(message):
dpranke@chromium.org970c5222011-03-12 00:32:24 +000058 print >> sys.stderr, message
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000059 sys.exit(1)
60
61
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000062def RunCommand(args, error_ok=False, error_message=None, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000063 try:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000064 return subprocess2.check_output(args, shell=False, **kwargs)
65 except subprocess2.CalledProcessError, e:
66 if not error_ok:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000067 DieWithError(
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000068 'Command "%s" failed.\n%s' % (
69 ' '.join(args), error_message or e.stdout or ''))
70 return e.stdout
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000071
72
73def RunGit(args, **kwargs):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000074 """Returns stdout."""
75 return RunCommand(['git'] + args, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000076
77
78def RunGitWithCode(args):
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +000079 """Returns return code and stdout."""
80 out, code = subprocess2.communicate(['git'] + args, stdout=subprocess2.PIPE)
81 return code, out[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000082
83
84def usage(more):
85 def hook(fn):
86 fn.usage_more = more
87 return fn
88 return hook
89
90
maruel@chromium.org90541732011-04-01 17:54:18 +000091def ask_for_data(prompt):
92 try:
93 return raw_input(prompt)
94 except KeyboardInterrupt:
95 # Hide the exception.
96 sys.exit(1)
97
98
bauerb@chromium.org866276c2011-03-18 20:09:31 +000099def MatchSvnGlob(url, base_url, glob_spec, allow_wildcards):
100 """Return the corresponding git ref if |base_url| together with |glob_spec|
101 matches the full |url|.
102
103 If |allow_wildcards| is true, |glob_spec| can contain wildcards (see below).
104 """
105 fetch_suburl, as_ref = glob_spec.split(':')
106 if allow_wildcards:
107 glob_match = re.match('(.+/)?(\*|{[^/]*})(/.+)?', fetch_suburl)
108 if glob_match:
109 # Parse specs like "branches/*/src:refs/remotes/svn/*" or
110 # "branches/{472,597,648}/src:refs/remotes/svn/*".
111 branch_re = re.escape(base_url)
112 if glob_match.group(1):
113 branch_re += '/' + re.escape(glob_match.group(1))
114 wildcard = glob_match.group(2)
115 if wildcard == '*':
116 branch_re += '([^/]*)'
117 else:
118 # Escape and replace surrounding braces with parentheses and commas
119 # with pipe symbols.
120 wildcard = re.escape(wildcard)
121 wildcard = re.sub('^\\\\{', '(', wildcard)
122 wildcard = re.sub('\\\\,', '|', wildcard)
123 wildcard = re.sub('\\\\}$', ')', wildcard)
124 branch_re += wildcard
125 if glob_match.group(3):
126 branch_re += re.escape(glob_match.group(3))
127 match = re.match(branch_re, url)
128 if match:
129 return re.sub('\*$', match.group(1), as_ref)
130
131 # Parse specs like "trunk/src:refs/remotes/origin/trunk".
132 if fetch_suburl:
133 full_url = base_url + '/' + fetch_suburl
134 else:
135 full_url = base_url
136 if full_url == url:
137 return as_ref
138 return None
139
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000140
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000141class Settings(object):
142 def __init__(self):
143 self.default_server = None
144 self.cc = None
145 self.root = None
146 self.is_git_svn = None
147 self.svn_branch = None
148 self.tree_status_url = None
149 self.viewvc_url = None
150 self.updated = False
ukai@chromium.orge8077812012-02-03 03:41:46 +0000151 self.is_gerrit = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000152
153 def LazyUpdateIfNeeded(self):
154 """Updates the settings from a codereview.settings file, if available."""
155 if not self.updated:
156 cr_settings_file = FindCodereviewSettingsFile()
157 if cr_settings_file:
158 LoadCodereviewSettingsFromFile(cr_settings_file)
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000159 self.updated = True
160 DownloadHooks(False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000161 self.updated = True
162
163 def GetDefaultServerUrl(self, error_ok=False):
164 if not self.default_server:
165 self.LazyUpdateIfNeeded()
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000166 self.default_server = gclient_utils.UpgradeToHttps(
167 self._GetConfig('rietveld.server', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000168 if error_ok:
169 return self.default_server
170 if not self.default_server:
171 error_message = ('Could not find settings file. You must configure '
172 'your review setup by running "git cl config".')
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000173 self.default_server = gclient_utils.UpgradeToHttps(
174 self._GetConfig('rietveld.server', error_message=error_message))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000175 return self.default_server
176
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000177 def GetRoot(self):
178 if not self.root:
179 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
180 return self.root
181
182 def GetIsGitSvn(self):
183 """Return true if this repo looks like it's using git-svn."""
184 if self.is_git_svn is None:
185 # If you have any "svn-remote.*" config keys, we think you're using svn.
186 self.is_git_svn = RunGitWithCode(
187 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
188 return self.is_git_svn
189
190 def GetSVNBranch(self):
191 if self.svn_branch is None:
192 if not self.GetIsGitSvn():
193 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
194
195 # Try to figure out which remote branch we're based on.
196 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000197 # 1) iterate through our branch history and find the svn URL.
198 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000199
200 # regexp matching the git-svn line that contains the URL.
201 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
202
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000203 # We don't want to go through all of history, so read a line from the
204 # pipe at a time.
205 # The -100 is an arbitrary limit so we don't search forever.
206 cmd = ['git', 'log', '-100', '--pretty=medium']
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000207 proc = subprocess2.Popen(cmd, stdout=subprocess2.PIPE)
maruel@chromium.org740f9d72011-06-10 18:33:10 +0000208 url = None
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000209 for line in proc.stdout:
210 match = git_svn_re.match(line)
211 if match:
212 url = match.group(1)
213 proc.stdout.close() # Cut pipe.
214 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000215
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000216 if url:
217 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
218 remotes = RunGit(['config', '--get-regexp',
219 r'^svn-remote\..*\.url']).splitlines()
220 for remote in remotes:
221 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000222 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000223 remote = match.group(1)
224 base_url = match.group(2)
225 fetch_spec = RunGit(
bauerb@chromium.org866276c2011-03-18 20:09:31 +0000226 ['config', 'svn-remote.%s.fetch' % remote],
227 error_ok=True).strip()
228 if fetch_spec:
229 self.svn_branch = MatchSvnGlob(url, base_url, fetch_spec, False)
230 if self.svn_branch:
231 break
232 branch_spec = RunGit(
233 ['config', 'svn-remote.%s.branches' % remote],
234 error_ok=True).strip()
235 if branch_spec:
236 self.svn_branch = MatchSvnGlob(url, base_url, branch_spec, True)
237 if self.svn_branch:
238 break
239 tag_spec = RunGit(
240 ['config', 'svn-remote.%s.tags' % remote],
241 error_ok=True).strip()
242 if tag_spec:
243 self.svn_branch = MatchSvnGlob(url, base_url, tag_spec, True)
244 if self.svn_branch:
245 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000246
247 if not self.svn_branch:
248 DieWithError('Can\'t guess svn branch -- try specifying it on the '
249 'command line')
250
251 return self.svn_branch
252
253 def GetTreeStatusUrl(self, error_ok=False):
254 if not self.tree_status_url:
255 error_message = ('You must configure your tree status URL by running '
256 '"git cl config".')
257 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
258 error_ok=error_ok,
259 error_message=error_message)
260 return self.tree_status_url
261
262 def GetViewVCUrl(self):
263 if not self.viewvc_url:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000264 self.viewvc_url = gclient_utils.UpgradeToHttps(
265 self._GetConfig('rietveld.viewvc-url', error_ok=True))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000266 return self.viewvc_url
267
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000268 def GetDefaultCCList(self):
269 return self._GetConfig('rietveld.cc', error_ok=True)
270
ukai@chromium.orge8077812012-02-03 03:41:46 +0000271 def GetIsGerrit(self):
272 """Return true if this repo is assosiated with gerrit code review system."""
273 if self.is_gerrit is None:
274 self.is_gerrit = self._GetConfig('gerrit.host', error_ok=True)
275 return self.is_gerrit
276
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000277 def _GetConfig(self, param, **kwargs):
278 self.LazyUpdateIfNeeded()
279 return RunGit(['config', param], **kwargs).strip()
280
281
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000282def ShortBranchName(branch):
283 """Convert a name like 'refs/heads/foo' to just 'foo'."""
284 return branch.replace('refs/heads/', '')
285
286
287class Changelist(object):
288 def __init__(self, branchref=None):
289 # Poke settings so we get the "configure your server" message if necessary.
maruel@chromium.org379d07a2011-11-30 14:58:10 +0000290 global settings
291 if not settings:
292 # Happens when git_cl.py is used as a utility library.
293 settings = Settings()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000294 settings.GetDefaultServerUrl()
295 self.branchref = branchref
296 if self.branchref:
297 self.branch = ShortBranchName(self.branchref)
298 else:
299 self.branch = None
300 self.rietveld_server = None
301 self.upstream_branch = None
302 self.has_issue = False
303 self.issue = None
304 self.has_description = False
305 self.description = None
306 self.has_patchset = False
307 self.patchset = None
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000308 self._rpc_server = None
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000309 self.cc = None
310 self.watchers = ()
311
312 def GetCCList(self):
313 """Return the users cc'd on this CL.
314
315 Return is a string suitable for passing to gcl with the --cc flag.
316 """
317 if self.cc is None:
318 base_cc = settings .GetDefaultCCList()
319 more_cc = ','.join(self.watchers)
320 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
321 return self.cc
322
323 def SetWatchers(self, watchers):
324 """Set the list of email addresses that should be cc'd based on the changed
325 files in this CL.
326 """
327 self.watchers = watchers
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000328
329 def GetBranch(self):
330 """Returns the short branch name, e.g. 'master'."""
331 if not self.branch:
332 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
333 self.branch = ShortBranchName(self.branchref)
334 return self.branch
335
336 def GetBranchRef(self):
337 """Returns the full branch name, e.g. 'refs/heads/master'."""
338 self.GetBranch() # Poke the lazy loader.
339 return self.branchref
340
341 def FetchUpstreamTuple(self):
342 """Returns a tuple containg remote and remote ref,
343 e.g. 'origin', 'refs/heads/master'
344 """
345 remote = '.'
346 branch = self.GetBranch()
347 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
348 error_ok=True).strip()
349 if upstream_branch:
350 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
351 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000352 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
353 error_ok=True).strip()
354 if upstream_branch:
355 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000356 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000357 # Fall back on trying a git-svn upstream branch.
358 if settings.GetIsGitSvn():
359 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000360 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000361 # Else, try to guess the origin remote.
362 remote_branches = RunGit(['branch', '-r']).split()
363 if 'origin/master' in remote_branches:
364 # Fall back on origin/master if it exits.
365 remote = 'origin'
366 upstream_branch = 'refs/heads/master'
367 elif 'origin/trunk' in remote_branches:
368 # Fall back on origin/trunk if it exists. Generally a shared
369 # git-svn clone
370 remote = 'origin'
371 upstream_branch = 'refs/heads/trunk'
372 else:
373 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000374Either pass complete "git diff"-style arguments, like
375 git cl upload origin/master
376or verify this branch is set up to track another (via the --track argument to
377"git checkout -b ...").""")
378
379 return remote, upstream_branch
380
381 def GetUpstreamBranch(self):
382 if self.upstream_branch is None:
383 remote, upstream_branch = self.FetchUpstreamTuple()
384 if remote is not '.':
385 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
386 self.upstream_branch = upstream_branch
387 return self.upstream_branch
388
389 def GetRemoteUrl(self):
390 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
391
392 Returns None if there is no remote.
393 """
394 remote = self.FetchUpstreamTuple()[0]
395 if remote == '.':
396 return None
397 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
398
399 def GetIssue(self):
400 if not self.has_issue:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000401 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
402 if issue:
403 self.issue = issue
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000404 else:
405 self.issue = None
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000406 self.has_issue = True
407 return self.issue
408
409 def GetRietveldServer(self):
evan@chromium.org0af9b702012-02-11 00:42:16 +0000410 if not self.rietveld_server:
411 # If we're on a branch then get the server potentially associated
412 # with that branch.
413 if self.GetIssue():
414 self.rietveld_server = gclient_utils.UpgradeToHttps(RunGit(
415 ['config', self._RietveldServer()], error_ok=True).strip())
416 if not self.rietveld_server:
417 self.rietveld_server = settings.GetDefaultServerUrl()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000418 return self.rietveld_server
419
420 def GetIssueURL(self):
421 """Get the URL for a particular issue."""
422 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
423
424 def GetDescription(self, pretty=False):
425 if not self.has_description:
426 if self.GetIssue():
miket@chromium.org183df1a2012-01-04 19:44:55 +0000427 issue = int(self.GetIssue())
428 try:
429 self.description = self.RpcServer().get_description(issue).strip()
430 except urllib2.HTTPError, e:
431 if e.code == 404:
432 DieWithError(
433 ('\nWhile fetching the description for issue %d, received a '
434 '404 (not found)\n'
435 'error. It is likely that you deleted this '
436 'issue on the server. If this is the\n'
437 'case, please run\n\n'
438 ' git cl issue 0\n\n'
439 'to clear the association with the deleted issue. Then run '
440 'this command again.') % issue)
441 else:
442 DieWithError(
443 '\nFailed to fetch issue description. HTTP error ' + e.code)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000444 self.has_description = True
445 if pretty:
446 wrapper = textwrap.TextWrapper()
447 wrapper.initial_indent = wrapper.subsequent_indent = ' '
448 return wrapper.fill(self.description)
449 return self.description
450
451 def GetPatchset(self):
452 if not self.has_patchset:
453 patchset = RunGit(['config', self._PatchsetSetting()],
454 error_ok=True).strip()
455 if patchset:
456 self.patchset = patchset
457 else:
458 self.patchset = None
459 self.has_patchset = True
460 return self.patchset
461
462 def SetPatchset(self, patchset):
463 """Set this branch's patchset. If patchset=0, clears the patchset."""
464 if patchset:
465 RunGit(['config', self._PatchsetSetting(), str(patchset)])
466 else:
467 RunGit(['config', '--unset', self._PatchsetSetting()],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +0000468 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000469 self.has_patchset = False
470
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000471 def GetPatchSetDiff(self, issue):
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000472 patchset = self.RpcServer().get_issue_properties(
473 int(issue), False)['patchsets'][-1]
474 return self.RpcServer().get(
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000475 '/download/issue%s_%s.diff' % (issue, patchset))
476
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000477 def SetIssue(self, issue):
478 """Set this branch's issue. If issue=0, clears the issue."""
479 if issue:
480 RunGit(['config', self._IssueSetting(), str(issue)])
481 if self.rietveld_server:
482 RunGit(['config', self._RietveldServer(), self.rietveld_server])
483 else:
484 RunGit(['config', '--unset', self._IssueSetting()])
485 self.SetPatchset(0)
486 self.has_issue = False
487
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000488 def GetChange(self, upstream_branch, author):
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000489 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip() or '.'
490 absroot = os.path.abspath(root)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000491
492 # We use the sha1 of HEAD as a name of this change.
493 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
bauerb@chromium.org512f1ef2011-04-20 15:17:57 +0000494 # Need to pass a relative path for msysgit.
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000495 try:
maruel@chromium.org80a9ef12011-12-13 20:44:10 +0000496 files = scm.GIT.CaptureStatus([root], '.', upstream_branch)
maruel@chromium.org2b38e9c2011-10-19 00:04:35 +0000497 except subprocess2.CalledProcessError:
498 DieWithError(
499 ('\nFailed to diff against upstream branch %s!\n\n'
500 'This branch probably doesn\'t exist anymore. To reset the\n'
501 'tracking branch, please run\n'
502 ' git branch --set-upstream %s trunk\n'
503 'replacing trunk with origin/master or the relevant branch') %
504 (upstream_branch, self.GetBranch()))
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000505
506 issue = ConvertToInteger(self.GetIssue())
507 patchset = ConvertToInteger(self.GetPatchset())
508 if issue:
509 description = self.GetDescription()
510 else:
511 # If the change was never uploaded, use the log messages of all commits
512 # up to the branch point, as git cl upload will prefill the description
513 # with these log messages.
514 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
515 '%s...' % (upstream_branch)]).strip()
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000516
517 if not author:
maruel@chromium.org13f623c2011-07-22 16:02:23 +0000518 author = RunGit(['config', 'user.email']).strip() or None
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000519 return presubmit_support.GitChange(
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000520 name,
521 description,
522 absroot,
523 files,
524 issue,
525 patchset,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000526 author)
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000527
asvitkine@chromium.org15169952011-09-27 14:30:53 +0000528 def RunHook(self, committing, upstream_branch, may_prompt, verbose, author):
529 """Calls sys.exit() if the hook fails; returns a HookResults otherwise."""
530 change = self.GetChange(upstream_branch, author)
531
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000532 # Apply watchlists on upload.
533 if not committing:
534 watchlist = watchlists.Watchlists(change.RepositoryRoot())
535 files = [f.LocalPath() for f in change.AffectedFiles()]
536 self.SetWatchers(watchlist.GetWatchersForPaths(files))
537
538 try:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +0000539 return presubmit_support.DoPresubmitChecks(change, committing,
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000540 verbose=verbose, output_stream=sys.stdout, input_stream=sys.stdin,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000541 default_presubmit=None, may_prompt=may_prompt,
maruel@chromium.org239f4112011-06-03 20:08:23 +0000542 rietveld_obj=self.RpcServer())
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000543 except presubmit_support.PresubmitFailure, e:
544 DieWithError(
545 ('%s\nMaybe your depot_tools is out of date?\n'
546 'If all fails, contact maruel@') % e)
547
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000548 def CloseIssue(self):
maruel@chromium.org607bb1b2011-06-01 23:43:11 +0000549 """Updates the description and closes the issue."""
550 issue = int(self.GetIssue())
551 self.RpcServer().update_description(issue, self.description)
552 return self.RpcServer().close_issue(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000553
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000554 def SetFlag(self, flag, value):
555 """Patchset must match."""
556 if not self.GetPatchset():
557 DieWithError('The patchset needs to match. Send another patchset.')
558 try:
559 return self.RpcServer().set_flag(
560 int(self.GetIssue()), int(self.GetPatchset()), flag, value)
561 except urllib2.HTTPError, e:
562 if e.code == 404:
563 DieWithError('The issue %s doesn\'t exist.' % self.GetIssue())
564 if e.code == 403:
565 DieWithError(
566 ('Access denied to issue %s. Maybe the patchset %s doesn\'t '
567 'match?') % (self.GetIssue(), self.GetPatchset()))
568 raise
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000569
maruel@chromium.orgcab38e92011-04-09 00:30:51 +0000570 def RpcServer(self):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000571 """Returns an upload.RpcServer() to access this review's rietveld instance.
572 """
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000573 if not self._rpc_server:
evan@chromium.org0af9b702012-02-11 00:42:16 +0000574 self._rpc_server = rietveld.Rietveld(self.GetRietveldServer(),
575 None, None)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +0000576 return self._rpc_server
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000577
578 def _IssueSetting(self):
579 """Return the git setting that stores this change's issue."""
580 return 'branch.%s.rietveldissue' % self.GetBranch()
581
582 def _PatchsetSetting(self):
583 """Return the git setting that stores this change's most recent patchset."""
584 return 'branch.%s.rietveldpatchset' % self.GetBranch()
585
586 def _RietveldServer(self):
587 """Returns the git setting that stores this change's rietveld server."""
588 return 'branch.%s.rietveldserver' % self.GetBranch()
589
590
591def GetCodereviewSettingsInteractively():
592 """Prompt the user for settings."""
ukai@chromium.orge8077812012-02-03 03:41:46 +0000593 # TODO(ukai): ask code review system is rietveld or gerrit?
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000594 server = settings.GetDefaultServerUrl(error_ok=True)
595 prompt = 'Rietveld server (host[:port])'
596 prompt += ' [%s]' % (server or DEFAULT_SERVER)
maruel@chromium.org90541732011-04-01 17:54:18 +0000597 newserver = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000598 if not server and not newserver:
599 newserver = DEFAULT_SERVER
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000600 if newserver:
601 newserver = gclient_utils.UpgradeToHttps(newserver)
602 if newserver != server:
603 RunGit(['config', 'rietveld.server', newserver])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000604
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000605 def SetProperty(initial, caption, name, is_url):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000606 prompt = caption
607 if initial:
608 prompt += ' ("x" to clear) [%s]' % initial
maruel@chromium.org90541732011-04-01 17:54:18 +0000609 new_val = ask_for_data(prompt + ':')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000610 if new_val == 'x':
611 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000612 elif new_val:
613 if is_url:
614 new_val = gclient_utils.UpgradeToHttps(new_val)
615 if new_val != initial:
616 RunGit(['config', 'rietveld.' + name, new_val])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000617
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000618 SetProperty(settings.GetDefaultCCList(), 'CC list', 'cc', False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000619 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +0000620 'tree-status-url', False)
621 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url', True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000622
623 # TODO: configure a default branch to diff against, rather than this
624 # svn-based hackery.
625
626
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000627class ChangeDescription(object):
628 """Contains a parsed form of the change description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000629 def __init__(self, log_desc, reviewers):
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000630 self.log_desc = log_desc
631 self.reviewers = reviewers
632 self.description = self.log_desc
633
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000634 def Prompt(self):
635 content = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000636# This will displayed on the codereview site.
637# The first line will also be used as the subject of the review.
638"""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000639 content += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000640 if ('\nR=' not in self.description and
641 '\nTBR=' not in self.description and
642 self.reviewers):
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000643 content += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000644 if '\nBUG=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000645 content += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000646 if '\nTEST=' not in self.description:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000647 content += '\nTEST='
648 content = content.rstrip('\n') + '\n'
649 content = gclient_utils.RunEditor(content, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000650 if not content:
651 DieWithError('Running editor failed')
652 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000653 if not content.strip():
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000654 DieWithError('No CL description, aborting')
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000655 self.description = content
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000656
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000657 def ParseDescription(self):
jam@chromium.org31083642012-01-27 03:14:45 +0000658 """Updates the list of reviewers and subject from the description."""
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000659 self.description = self.description.strip('\n') + '\n'
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000660 # Retrieves all reviewer lines
661 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000662 reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000663 i.group(2).strip() for i in regexp.finditer(self.description))
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000664 if reviewers:
665 self.reviewers = reviewers
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']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000712
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000713 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
714 #should be of the form
715 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
716 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
717 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
718 keyvals['ORIGIN_URL_CONFIG']])
719
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000720
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000721def DownloadHooks(force):
722 """downloads hooks
723
724 Args:
725 force: True to update hooks. False to install hooks if not present.
726 """
727 if not settings.GetIsGerrit():
728 return
729 server_url = settings.GetDefaultServerUrl()
730 src = '%s/tools/hooks/commit-msg' % server_url
731 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
732 if not os.access(dst, os.X_OK):
733 if os.path.exists(dst):
734 if not force:
735 return
736 os.remove(dst)
737 try:
738 urllib.urlretrieve(src, dst)
739 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
740 except Exception:
741 if os.path.exists(dst):
742 os.remove(dst)
743 DieWithError('\nFailed to download hooks from %s' % src)
744
745
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000746@usage('[repo root containing codereview.settings]')
747def CMDconfig(parser, args):
748 """edit configuration for this tree"""
749
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000750 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000751 if len(args) == 0:
752 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000753 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000754 return 0
755
756 url = args[0]
757 if not url.endswith('codereview.settings'):
758 url = os.path.join(url, 'codereview.settings')
759
760 # Load code review settings and download hooks (if available).
761 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000762 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000763 return 0
764
765
766def CMDstatus(parser, args):
767 """show status of changelists"""
768 parser.add_option('--field',
769 help='print only specific field (desc|id|patch|url)')
770 (options, args) = parser.parse_args(args)
771
772 # TODO: maybe make show_branches a flag if necessary.
773 show_branches = not options.field
774
775 if show_branches:
776 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
777 if branches:
778 print 'Branches associated with reviews:'
779 for branch in sorted(branches.splitlines()):
780 cl = Changelist(branchref=branch)
781 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
782
783 cl = Changelist()
784 if options.field:
785 if options.field.startswith('desc'):
786 print cl.GetDescription()
787 elif options.field == 'id':
788 issueid = cl.GetIssue()
789 if issueid:
790 print issueid
791 elif options.field == 'patch':
792 patchset = cl.GetPatchset()
793 if patchset:
794 print patchset
795 elif options.field == 'url':
796 url = cl.GetIssueURL()
797 if url:
798 print url
799 else:
800 print
801 print 'Current branch:',
802 if not cl.GetIssue():
803 print 'no issue assigned.'
804 return 0
805 print cl.GetBranch()
806 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
807 print 'Issue description:'
808 print cl.GetDescription(pretty=True)
809 return 0
810
811
812@usage('[issue_number]')
813def CMDissue(parser, args):
814 """Set or display the current code review issue number.
815
816 Pass issue number 0 to clear the current issue.
817"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000818 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000819
820 cl = Changelist()
821 if len(args) > 0:
822 try:
823 issue = int(args[0])
824 except ValueError:
825 DieWithError('Pass a number to set the issue or none to list it.\n'
826 'Maybe you want to run git cl status?')
827 cl.SetIssue(issue)
828 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
829 return 0
830
831
832def CreateDescriptionFromLog(args):
833 """Pulls out the commit log to use as a base for the CL description."""
834 log_args = []
835 if len(args) == 1 and not args[0].endswith('.'):
836 log_args = [args[0] + '..']
837 elif len(args) == 1 and args[0].endswith('...'):
838 log_args = [args[0][:-1]]
839 elif len(args) == 2:
840 log_args = [args[0] + '..' + args[1]]
841 else:
842 log_args = args[:] # Hope for the best!
843 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
844
845
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000846def ConvertToInteger(inputval):
847 """Convert a string to integer, but returns either an int or None."""
848 try:
849 return int(inputval)
850 except (TypeError, ValueError):
851 return None
852
853
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000854def CMDpresubmit(parser, args):
855 """run presubmit tests on the current changelist"""
856 parser.add_option('--upload', action='store_true',
857 help='Run upload hook instead of the push/dcommit hook')
858 (options, args) = parser.parse_args(args)
859
860 # Make sure index is up-to-date before running diff-index.
861 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
862 if RunGit(['diff-index', 'HEAD']):
863 # TODO(maruel): Is this really necessary?
864 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
865 return 1
866
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000867 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000868 if args:
869 base_branch = args[0]
870 else:
871 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000872 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000873
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000874 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000875 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000876 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000877 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000878
879
ukai@chromium.orge8077812012-02-03 03:41:46 +0000880def GerritUpload(options, args, cl):
881 """upload the current branch to gerrit."""
882 # We assume the remote called "origin" is the one we want.
883 # It is probably not worthwhile to support different workflows.
884 remote = 'origin'
885 branch = 'master'
886 if options.target_branch:
887 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000888
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000889 log_desc = options.message or CreateDescriptionFromLog(args)
ukai@chromium.orge8077812012-02-03 03:41:46 +0000890 if options.reviewers:
891 log_desc += '\nR=' + options.reviewers
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000892 change_desc = ChangeDescription(log_desc, options.reviewers)
893 change_desc.ParseDescription()
ukai@chromium.orge8077812012-02-03 03:41:46 +0000894 if change_desc.IsEmpty():
895 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000896 return 1
897
ukai@chromium.orge8077812012-02-03 03:41:46 +0000898 receive_options = []
899 cc = cl.GetCCList().split(',')
900 if options.cc:
901 cc += options.cc.split(',')
902 cc = filter(None, cc)
903 if cc:
904 receive_options += ['--cc=' + email for email in cc]
905 if change_desc.reviewers:
906 reviewers = filter(None, change_desc.reviewers.split(','))
907 if reviewers:
908 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000909
ukai@chromium.orge8077812012-02-03 03:41:46 +0000910 git_command = ['push']
911 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000912 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000913 ' '.join(receive_options))
914 git_command += [remote, 'HEAD:refs/for/' + branch]
915 RunGit(git_command)
916 # TODO(ukai): parse Change-Id: and set issue number?
917 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000918
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000919
ukai@chromium.orge8077812012-02-03 03:41:46 +0000920def RietveldUpload(options, args, cl):
921 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000922 upload_args = ['--assume_yes'] # Don't ask about untracked files.
923 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924 if options.emulate_svn_auto_props:
925 upload_args.append('--emulate_svn_auto_props')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000926
927 change_desc = None
928
929 if cl.GetIssue():
930 if options.message:
931 upload_args.extend(['--message', options.message])
932 upload_args.extend(['--issue', cl.GetIssue()])
933 print ("This branch is associated with issue %s. "
934 "Adding patch to that issue." % cl.GetIssue())
935 else:
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000936 message = options.message or CreateDescriptionFromLog(args)
937 change_desc = ChangeDescription(message, options.reviewers)
938 if not options.force:
939 change_desc.Prompt()
940 change_desc.ParseDescription()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000941
942 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000943 print "Description is empty; aborting."
944 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000945
maruel@chromium.org71e12a92012-02-14 02:34:15 +0000946 upload_args.extend(['--message', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000947 if change_desc.reviewers:
948 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000949 if options.send_mail:
950 if not change_desc.reviewers:
951 DieWithError("Must specify reviewers to send email.")
952 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000953 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000954 if cc:
955 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000956
957 # Include the upstream repo's URL in the change -- this is useful for
958 # projects that have their source spread across multiple repos.
959 remote_url = None
960 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000961 # URL is dependent on the current directory.
962 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000963 if data:
964 keys = dict(line.split(': ', 1) for line in data.splitlines()
965 if ': ' in line)
966 remote_url = keys.get('URL', None)
967 else:
968 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
969 remote_url = (cl.GetRemoteUrl() + '@'
970 + cl.GetUpstreamBranch().split('/')[-1])
971 if remote_url:
972 upload_args.extend(['--base_url', remote_url])
973
974 try:
975 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000976 except KeyboardInterrupt:
977 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000978 except:
979 # If we got an exception after the user typed a description for their
980 # change, back up the description before re-raising.
981 if change_desc:
982 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
983 print '\nGot exception while uploading -- saving description to %s\n' \
984 % backup_path
985 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000986 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 backup_file.close()
988 raise
989
990 if not cl.GetIssue():
991 cl.SetIssue(issue)
992 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +0000993
994 if options.use_commit_queue:
995 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 return 0
997
998
ukai@chromium.orge8077812012-02-03 03:41:46 +0000999@usage('[args to "git diff"]')
1000def CMDupload(parser, args):
1001 """upload the current changelist to codereview"""
1002 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1003 help='bypass upload presubmit hook')
1004 parser.add_option('-f', action='store_true', dest='force',
1005 help="force yes to questions (don't prompt)")
1006 parser.add_option('-m', dest='message', help='message for patch')
1007 parser.add_option('-r', '--reviewers',
1008 help='reviewer email addresses')
1009 parser.add_option('--cc',
1010 help='cc email addresses')
1011 parser.add_option('--send-mail', action='store_true',
1012 help='send email to reviewer immediately')
1013 parser.add_option("--emulate_svn_auto_props", action="store_true",
1014 dest="emulate_svn_auto_props",
1015 help="Emulate Subversion's auto properties feature.")
1016 parser.add_option("--desc_from_logs", action="store_true",
1017 dest="from_logs",
1018 help="""Squashes git commit logs into change description and
1019 uses message as subject""")
1020 parser.add_option('-c', '--use-commit-queue', action='store_true',
1021 help='tell the commit queue to commit this patchset')
1022 if settings.GetIsGerrit():
1023 parser.add_option('--target_branch', dest='target_branch', default='master',
1024 help='target branch to upload')
1025 (options, args) = parser.parse_args(args)
1026
1027 # Make sure index is up-to-date before running diff-index.
1028 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1029 if RunGit(['diff-index', 'HEAD']):
1030 print 'Cannot upload with a dirty tree. You must commit locally first.'
1031 return 1
1032
1033 cl = Changelist()
1034 if args:
1035 # TODO(ukai): is it ok for gerrit case?
1036 base_branch = args[0]
1037 else:
1038 # Default to diffing against the "upstream" branch.
1039 base_branch = cl.GetUpstreamBranch()
1040 args = [base_branch + "..."]
1041
1042 if not options.bypass_hooks:
1043 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1044 may_prompt=not options.force,
1045 verbose=options.verbose,
1046 author=None)
1047 if not hook_results.should_continue():
1048 return 1
1049 if not options.reviewers and hook_results.reviewers:
1050 options.reviewers = hook_results.reviewers
1051
1052 # --no-ext-diff is broken in some versions of Git, so try to work around
1053 # this by overriding the environment (but there is still a problem if the
1054 # git config key "diff.external" is used).
1055 env = os.environ.copy()
1056 if 'GIT_EXTERNAL_DIFF' in env:
1057 del env['GIT_EXTERNAL_DIFF']
1058 subprocess2.call(
1059 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
1060
1061 if settings.GetIsGerrit():
1062 return GerritUpload(options, args, cl)
1063 return RietveldUpload(options, args, cl)
1064
1065
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001066def SendUpstream(parser, args, cmd):
1067 """Common code for CmdPush and CmdDCommit
1068
1069 Squashed commit into a single.
1070 Updates changelog with metadata (e.g. pointer to review).
1071 Pushes/dcommits the code upstream.
1072 Updates review and closes.
1073 """
1074 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1075 help='bypass upload presubmit hook')
1076 parser.add_option('-m', dest='message',
1077 help="override review description")
1078 parser.add_option('-f', action='store_true', dest='force',
1079 help="force yes to questions (don't prompt)")
1080 parser.add_option('-c', dest='contributor',
1081 help="external contributor for patch (appended to " +
1082 "description and used as author for git). Should be " +
1083 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001084 (options, args) = parser.parse_args(args)
1085 cl = Changelist()
1086
1087 if not args or cmd == 'push':
1088 # Default to merging against our best guess of the upstream branch.
1089 args = [cl.GetUpstreamBranch()]
1090
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001091 if options.contributor:
1092 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1093 print "Please provide contibutor as 'First Last <email@example.com>'"
1094 return 1
1095
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001096 base_branch = args[0]
1097
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001098 # Make sure index is up-to-date before running diff-index.
1099 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001100 if RunGit(['diff-index', 'HEAD']):
1101 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1102 return 1
1103
1104 # This rev-list syntax means "show all commits not in my branch that
1105 # are in base_branch".
1106 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1107 base_branch]).splitlines()
1108 if upstream_commits:
1109 print ('Base branch "%s" has %d commits '
1110 'not in this branch.' % (base_branch, len(upstream_commits)))
1111 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1112 return 1
1113
1114 if cmd == 'dcommit':
1115 # This is the revision `svn dcommit` will commit on top of.
1116 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1117 '--pretty=format:%H'])
1118 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1119 if extra_commits:
1120 print ('This branch has %d additional commits not upstreamed yet.'
1121 % len(extra_commits.splitlines()))
1122 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1123 'before attempting to %s.' % (base_branch, cmd))
1124 return 1
1125
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001126 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001127 author = None
1128 if options.contributor:
1129 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001130 hook_results = cl.RunHook(
1131 committing=True,
1132 upstream_branch=base_branch,
1133 may_prompt=not options.force,
1134 verbose=options.verbose,
1135 author=author)
1136 if not hook_results.should_continue():
1137 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001138
1139 if cmd == 'dcommit':
1140 # Check the tree status if the tree status URL is set.
1141 status = GetTreeStatus()
1142 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001143 print('The tree is closed. Please wait for it to reopen. Use '
1144 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001145 return 1
1146 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001147 print('Unable to determine tree status. Please verify manually and '
1148 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001149 else:
1150 breakpad.SendStack(
1151 'GitClHooksBypassedCommit',
1152 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001153 (cl.GetRietveldServer(), cl.GetIssue()),
1154 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001155
1156 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001157 if not description and cl.GetIssue():
1158 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001159
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001160 if not description:
1161 print 'No description set.'
1162 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1163 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001165 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001166 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001167
1168 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001169 description += "\nPatch from %s." % options.contributor
1170 print 'Description:', repr(description)
1171
1172 branches = [base_branch, cl.GetBranchRef()]
1173 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001174 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001175 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176
1177 # We want to squash all this branch's commits into one commit with the
1178 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001179 # We do this by doing a "reset --soft" to the base branch (which keeps
1180 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001181 MERGE_BRANCH = 'git-cl-commit'
1182 # Delete the merge branch if it already exists.
1183 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1184 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1185 RunGit(['branch', '-D', MERGE_BRANCH])
1186
1187 # We might be in a directory that's present in this branch but not in the
1188 # trunk. Move up to the top of the tree so that git commands that expect a
1189 # valid CWD won't fail after we check out the merge branch.
1190 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1191 if rel_base_path:
1192 os.chdir(rel_base_path)
1193
1194 # Stuff our change into the merge branch.
1195 # We wrap in a try...finally block so if anything goes wrong,
1196 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001197 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001198 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001199 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1200 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001201 if options.contributor:
1202 RunGit(['commit', '--author', options.contributor, '-m', description])
1203 else:
1204 RunGit(['commit', '-m', description])
1205 if cmd == 'push':
1206 # push the merge branch.
1207 remote, branch = cl.FetchUpstreamTuple()
1208 retcode, output = RunGitWithCode(
1209 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1210 logging.debug(output)
1211 else:
1212 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001213 retcode, output = RunGitWithCode(['svn', 'dcommit',
1214 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001215 finally:
1216 # And then swap back to the original branch and clean up.
1217 RunGit(['checkout', '-q', cl.GetBranch()])
1218 RunGit(['branch', '-D', MERGE_BRANCH])
1219
1220 if cl.GetIssue():
1221 if cmd == 'dcommit' and 'Committed r' in output:
1222 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1223 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001224 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1225 for l in output.splitlines(False))
1226 match = filter(None, match)
1227 if len(match) != 1:
1228 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1229 output)
1230 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001231 else:
1232 return 1
1233 viewvc_url = settings.GetViewVCUrl()
1234 if viewvc_url and revision:
1235 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1236 print ('Closing issue '
1237 '(you may be prompted for your codereview password)...')
1238 cl.CloseIssue()
1239 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001240
1241 if retcode == 0:
1242 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1243 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001244 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001245
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001246 return 0
1247
1248
1249@usage('[upstream branch to apply against]')
1250def CMDdcommit(parser, args):
1251 """commit the current changelist via git-svn"""
1252 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001253 message = """This doesn't appear to be an SVN repository.
1254If your project has a git mirror with an upstream SVN master, you probably need
1255to run 'git svn init', see your project's git mirror documentation.
1256If your project has a true writeable upstream repository, you probably want
1257to run 'git cl push' instead.
1258Choose wisely, if you get this wrong, your commit might appear to succeed but
1259will instead be silently ignored."""
1260 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001261 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001262 return SendUpstream(parser, args, 'dcommit')
1263
1264
1265@usage('[upstream branch to apply against]')
1266def CMDpush(parser, args):
1267 """commit the current changelist via git"""
1268 if settings.GetIsGitSvn():
1269 print('This appears to be an SVN repository.')
1270 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001271 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001272 return SendUpstream(parser, args, 'push')
1273
1274
1275@usage('<patch url or issue id>')
1276def CMDpatch(parser, args):
1277 """patch in a code review"""
1278 parser.add_option('-b', dest='newbranch',
1279 help='create a new branch off trunk for the patch')
1280 parser.add_option('-f', action='store_true', dest='force',
1281 help='with -b, clobber any existing branch')
1282 parser.add_option('--reject', action='store_true', dest='reject',
1283 help='allow failed patches and spew .rej files')
1284 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1285 help="don't commit after patch applies")
1286 (options, args) = parser.parse_args(args)
1287 if len(args) != 1:
1288 parser.print_help()
1289 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001290 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001291
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001292 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001293 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001294
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001295 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001296 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001297 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001298 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001299 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001300 # Assume it's a URL to the patch. Default to https.
1301 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001302 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001303 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001304 DieWithError('Must pass an issue ID or full URL for '
1305 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001306 issue = match.group(1)
1307 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308
1309 if options.newbranch:
1310 if options.force:
1311 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001312 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 RunGit(['checkout', '-b', options.newbranch,
1314 Changelist().GetUpstreamBranch()])
1315
1316 # Switch up to the top-level directory, if necessary, in preparation for
1317 # applying the patch.
1318 top = RunGit(['rev-parse', '--show-cdup']).strip()
1319 if top:
1320 os.chdir(top)
1321
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322 # Git patches have a/ at the beginning of source paths. We strip that out
1323 # with a sed script rather than the -p flag to patch so we can feed either
1324 # Git or svn-style patches into the same apply command.
1325 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001326 try:
1327 patch_data = subprocess2.check_output(
1328 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1329 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001330 DieWithError('Git patch mungling failed.')
1331 logging.info(patch_data)
1332 # We use "git apply" to apply the patch instead of "patch" so that we can
1333 # pick up file adds.
1334 # The --index flag means: also insert into the index (so we catch adds).
1335 cmd = ['git', 'apply', '--index', '-p0']
1336 if options.reject:
1337 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001338 try:
1339 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1340 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001341 DieWithError('Failed to apply the patch')
1342
1343 # If we had an issue, commit the current state and register the issue.
1344 if not options.nocommit:
1345 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1346 cl = Changelist()
1347 cl.SetIssue(issue)
1348 print "Committed patch."
1349 else:
1350 print "Patch applied to index."
1351 return 0
1352
1353
1354def CMDrebase(parser, args):
1355 """rebase current branch on top of svn repo"""
1356 # Provide a wrapper for git svn rebase to help avoid accidental
1357 # git svn dcommit.
1358 # It's the only command that doesn't use parser at all since we just defer
1359 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001360 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001361
1362
1363def GetTreeStatus():
1364 """Fetches the tree status and returns either 'open', 'closed',
1365 'unknown' or 'unset'."""
1366 url = settings.GetTreeStatusUrl(error_ok=True)
1367 if url:
1368 status = urllib2.urlopen(url).read().lower()
1369 if status.find('closed') != -1 or status == '0':
1370 return 'closed'
1371 elif status.find('open') != -1 or status == '1':
1372 return 'open'
1373 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001374 return 'unset'
1375
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001376
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001377def GetTreeStatusReason():
1378 """Fetches the tree status from a json url and returns the message
1379 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001380 url = settings.GetTreeStatusUrl()
1381 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001382 connection = urllib2.urlopen(json_url)
1383 status = json.loads(connection.read())
1384 connection.close()
1385 return status['message']
1386
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001387
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001388def CMDtree(parser, args):
1389 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001390 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001391 status = GetTreeStatus()
1392 if 'unset' == status:
1393 print 'You must configure your tree status URL by running "git cl config".'
1394 return 2
1395
1396 print "The tree is %s" % status
1397 print
1398 print GetTreeStatusReason()
1399 if status != 'open':
1400 return 1
1401 return 0
1402
1403
1404def CMDupstream(parser, args):
1405 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001406 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001407 if args:
1408 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001409 cl = Changelist()
1410 print cl.GetUpstreamBranch()
1411 return 0
1412
1413
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001414def CMDset_commit(parser, args):
1415 """set the commit bit"""
1416 _, args = parser.parse_args(args)
1417 if args:
1418 parser.error('Unrecognized args: %s' % ' '.join(args))
1419 cl = Changelist()
1420 cl.SetFlag('commit', '1')
1421 return 0
1422
1423
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001424def Command(name):
1425 return getattr(sys.modules[__name__], 'CMD' + name, None)
1426
1427
1428def CMDhelp(parser, args):
1429 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001430 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001431 if len(args) == 1:
1432 return main(args + ['--help'])
1433 parser.print_help()
1434 return 0
1435
1436
1437def GenUsage(parser, command):
1438 """Modify an OptParse object with the function's documentation."""
1439 obj = Command(command)
1440 more = getattr(obj, 'usage_more', '')
1441 if command == 'help':
1442 command = '<command>'
1443 else:
1444 # OptParser.description prefer nicely non-formatted strings.
1445 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1446 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1447
1448
1449def main(argv):
1450 """Doesn't parse the arguments here, just find the right subcommand to
1451 execute."""
maruel@chromium.org82798cb2012-02-23 18:16:12 +00001452 if sys.hexversion < 0x02060000:
1453 print >> sys.stderr, (
1454 '\nYour python version %s is unsupported, please upgrade.\n' %
1455 sys.version.split(' ', 1)[0])
1456 return 2
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001457 # Reload settings.
1458 global settings
1459 settings = Settings()
1460
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001461 # Do it late so all commands are listed.
1462 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1463 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1464 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1465
1466 # Create the option parse and add --verbose support.
1467 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001468 parser.add_option(
1469 '-v', '--verbose', action='count', default=0,
1470 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001471 old_parser_args = parser.parse_args
1472 def Parse(args):
1473 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001474 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001476 elif options.verbose:
1477 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001478 else:
1479 logging.basicConfig(level=logging.WARNING)
1480 return options, args
1481 parser.parse_args = Parse
1482
1483 if argv:
1484 command = Command(argv[0])
1485 if command:
1486 # "fix" the usage and the description now that we know the subcommand.
1487 GenUsage(parser, argv[0])
1488 try:
1489 return command(parser, argv[1:])
1490 except urllib2.HTTPError, e:
1491 if e.code != 500:
1492 raise
1493 DieWithError(
1494 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1495 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1496
1497 # Not a known command. Default to help.
1498 GenUsage(parser, 'help')
1499 return CMDhelp(parser, argv)
1500
1501
1502if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001503 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001504 sys.exit(main(sys.argv[1:]))