blob: 72ad8f5892c0430e1d825bf335bf2067c28a82ad [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."""
jam@chromium.org31083642012-01-27 03:14:45 +0000629 def __init__(self, subject, log_desc, reviewers):
630 self.subject = subject
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000631 self.log_desc = log_desc
632 self.reviewers = reviewers
633 self.description = self.log_desc
634
jam@chromium.org31083642012-01-27 03:14:45 +0000635 def Update(self):
636 initial_text = """# Enter a description of the change.
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000637# This will displayed on the codereview site.
638# The first line will also be used as the subject of the review.
639"""
jam@chromium.org31083642012-01-27 03:14:45 +0000640 initial_text += self.description
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000641 if ('\nR=' not in self.description and
642 '\nTBR=' not in self.description and
643 self.reviewers):
jam@chromium.org31083642012-01-27 03:14:45 +0000644 initial_text += '\nR=' + self.reviewers
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000645 if '\nBUG=' not in self.description:
jam@chromium.org31083642012-01-27 03:14:45 +0000646 initial_text += '\nBUG='
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000647 if '\nTEST=' not in self.description:
jam@chromium.org31083642012-01-27 03:14:45 +0000648 initial_text += '\nTEST='
649 initial_text = initial_text.rstrip('\n') + '\n'
650 content = gclient_utils.RunEditor(initial_text, True)
maruel@chromium.org0e0436a2011-10-25 13:32:41 +0000651 if not content:
652 DieWithError('Running editor failed')
653 content = re.compile(r'^#.*$', re.MULTILINE).sub('', content).strip()
654 if not content:
655 DieWithError('No CL description, aborting')
ukai@chromium.orge8077812012-02-03 03:41:46 +0000656 self.ParseDescription(content)
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000657
ukai@chromium.orge8077812012-02-03 03:41:46 +0000658 def ParseDescription(self, description):
jam@chromium.org31083642012-01-27 03:14:45 +0000659 """Updates the list of reviewers and subject from the description."""
660 if not description:
661 self.description = description
662 return
663
664 self.description = description.strip('\n') + '\n'
665 self.subject = description.split('\n', 1)[0]
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000666 # Retrieves all reviewer lines
667 regexp = re.compile(r'^\s*(TBR|R)=(.+)$', re.MULTILINE)
jam@chromium.org31083642012-01-27 03:14:45 +0000668 self.reviewers = ','.join(
maruel@chromium.orgddd59412011-11-30 14:20:38 +0000669 i.group(2).strip() for i in regexp.finditer(self.description))
dpranke@chromium.org20254fc2011-03-22 18:28:59 +0000670
671 def IsEmpty(self):
672 return not self.description
673
674
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000675def FindCodereviewSettingsFile(filename='codereview.settings'):
676 """Finds the given file starting in the cwd and going up.
677
678 Only looks up to the top of the repository unless an
679 'inherit-review-settings-ok' file exists in the root of the repository.
680 """
681 inherit_ok_file = 'inherit-review-settings-ok'
682 cwd = os.getcwd()
683 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
684 if os.path.isfile(os.path.join(root, inherit_ok_file)):
685 root = '/'
686 while True:
687 if filename in os.listdir(cwd):
688 if os.path.isfile(os.path.join(cwd, filename)):
689 return open(os.path.join(cwd, filename))
690 if cwd == root:
691 break
692 cwd = os.path.dirname(cwd)
693
694
695def LoadCodereviewSettingsFromFile(fileobj):
696 """Parse a codereview.settings file and updates hooks."""
maruel@chromium.org99ac1c52012-01-16 14:52:12 +0000697 keyvals = gclient_utils.ParseCodereviewSettingsContent(fileobj.read())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000698
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000699 def SetProperty(name, setting, unset_error_ok=False):
700 fullname = 'rietveld.' + name
701 if setting in keyvals:
702 RunGit(['config', fullname, keyvals[setting]])
703 else:
704 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
705
706 SetProperty('server', 'CODE_REVIEW_SERVER')
707 # Only server setting is required. Other settings can be absent.
708 # In that case, we ignore errors raised during option deletion attempt.
709 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
710 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
711 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
712
ukai@chromium.orge8077812012-02-03 03:41:46 +0000713 if 'GERRIT_HOST' in keyvals and 'GERRIT_PORT' in keyvals:
714 RunGit(['config', 'gerrit.host', keyvals['GERRIT_HOST']])
715 RunGit(['config', 'gerrit.port', keyvals['GERRIT_PORT']])
ukai@chromium.orge8077812012-02-03 03:41:46 +0000716
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000717 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
718 #should be of the form
719 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
720 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
721 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
722 keyvals['ORIGIN_URL_CONFIG']])
723
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000724
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000725def DownloadHooks(force):
726 """downloads hooks
727
728 Args:
729 force: True to update hooks. False to install hooks if not present.
730 """
731 if not settings.GetIsGerrit():
732 return
733 server_url = settings.GetDefaultServerUrl()
734 src = '%s/tools/hooks/commit-msg' % server_url
735 dst = os.path.join(settings.GetRoot(), '.git', 'hooks', 'commit-msg')
736 if not os.access(dst, os.X_OK):
737 if os.path.exists(dst):
738 if not force:
739 return
740 os.remove(dst)
741 try:
742 urllib.urlretrieve(src, dst)
743 os.chmod(dst, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
744 except Exception:
745 if os.path.exists(dst):
746 os.remove(dst)
747 DieWithError('\nFailed to download hooks from %s' % src)
748
749
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000750@usage('[repo root containing codereview.settings]')
751def CMDconfig(parser, args):
752 """edit configuration for this tree"""
753
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000754 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 if len(args) == 0:
756 GetCodereviewSettingsInteractively()
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000757 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000758 return 0
759
760 url = args[0]
761 if not url.endswith('codereview.settings'):
762 url = os.path.join(url, 'codereview.settings')
763
764 # Load code review settings and download hooks (if available).
765 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
ukai@chromium.org78c4b982012-02-14 02:20:26 +0000766 DownloadHooks(True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000767 return 0
768
769
770def CMDstatus(parser, args):
771 """show status of changelists"""
772 parser.add_option('--field',
773 help='print only specific field (desc|id|patch|url)')
774 (options, args) = parser.parse_args(args)
775
776 # TODO: maybe make show_branches a flag if necessary.
777 show_branches = not options.field
778
779 if show_branches:
780 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
781 if branches:
782 print 'Branches associated with reviews:'
783 for branch in sorted(branches.splitlines()):
784 cl = Changelist(branchref=branch)
785 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
786
787 cl = Changelist()
788 if options.field:
789 if options.field.startswith('desc'):
790 print cl.GetDescription()
791 elif options.field == 'id':
792 issueid = cl.GetIssue()
793 if issueid:
794 print issueid
795 elif options.field == 'patch':
796 patchset = cl.GetPatchset()
797 if patchset:
798 print patchset
799 elif options.field == 'url':
800 url = cl.GetIssueURL()
801 if url:
802 print url
803 else:
804 print
805 print 'Current branch:',
806 if not cl.GetIssue():
807 print 'no issue assigned.'
808 return 0
809 print cl.GetBranch()
810 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
811 print 'Issue description:'
812 print cl.GetDescription(pretty=True)
813 return 0
814
815
816@usage('[issue_number]')
817def CMDissue(parser, args):
818 """Set or display the current code review issue number.
819
820 Pass issue number 0 to clear the current issue.
821"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +0000822 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000823
824 cl = Changelist()
825 if len(args) > 0:
826 try:
827 issue = int(args[0])
828 except ValueError:
829 DieWithError('Pass a number to set the issue or none to list it.\n'
830 'Maybe you want to run git cl status?')
831 cl.SetIssue(issue)
832 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
833 return 0
834
835
836def CreateDescriptionFromLog(args):
837 """Pulls out the commit log to use as a base for the CL description."""
838 log_args = []
839 if len(args) == 1 and not args[0].endswith('.'):
840 log_args = [args[0] + '..']
841 elif len(args) == 1 and args[0].endswith('...'):
842 log_args = [args[0][:-1]]
843 elif len(args) == 2:
844 log_args = [args[0] + '..' + args[1]]
845 else:
846 log_args = args[:] # Hope for the best!
847 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
848
849
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000850def ConvertToInteger(inputval):
851 """Convert a string to integer, but returns either an int or None."""
852 try:
853 return int(inputval)
854 except (TypeError, ValueError):
855 return None
856
857
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000858def CMDpresubmit(parser, args):
859 """run presubmit tests on the current changelist"""
860 parser.add_option('--upload', action='store_true',
861 help='Run upload hook instead of the push/dcommit hook')
862 (options, args) = parser.parse_args(args)
863
864 # Make sure index is up-to-date before running diff-index.
865 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
866 if RunGit(['diff-index', 'HEAD']):
867 # TODO(maruel): Is this really necessary?
868 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
869 return 1
870
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000871 cl = Changelist()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000872 if args:
873 base_branch = args[0]
874 else:
875 # Default to diffing against the "upstream" branch.
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000876 base_branch = cl.GetUpstreamBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000877
bauerb@chromium.org6fb99c62011-04-18 15:57:28 +0000878 cl.RunHook(committing=not options.upload, upstream_branch=base_branch,
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +0000879 may_prompt=False, verbose=options.verbose,
maruel@chromium.org03b3bdc2011-06-14 13:04:12 +0000880 author=None)
dpranke@chromium.org0a2bb372011-03-25 01:16:22 +0000881 return 0
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000882
883
ukai@chromium.orge8077812012-02-03 03:41:46 +0000884def GerritUpload(options, args, cl):
885 """upload the current branch to gerrit."""
886 # We assume the remote called "origin" is the one we want.
887 # It is probably not worthwhile to support different workflows.
888 remote = 'origin'
889 branch = 'master'
890 if options.target_branch:
891 branch = options.target_branch
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000892
ukai@chromium.orge8077812012-02-03 03:41:46 +0000893 log_desc = CreateDescriptionFromLog(args)
894 if options.reviewers:
895 log_desc += '\nR=' + options.reviewers
896 change_desc = ChangeDescription(options.message, log_desc,
897 options.reviewers)
898 change_desc.ParseDescription(log_desc)
899 if change_desc.IsEmpty():
900 print "Description is empty; aborting."
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000901 return 1
902
ukai@chromium.orge8077812012-02-03 03:41:46 +0000903 receive_options = []
904 cc = cl.GetCCList().split(',')
905 if options.cc:
906 cc += options.cc.split(',')
907 cc = filter(None, cc)
908 if cc:
909 receive_options += ['--cc=' + email for email in cc]
910 if change_desc.reviewers:
911 reviewers = filter(None, change_desc.reviewers.split(','))
912 if reviewers:
913 receive_options += ['--reviewer=' + email for email in reviewers]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000914
ukai@chromium.orge8077812012-02-03 03:41:46 +0000915 git_command = ['push']
916 if receive_options:
ukai@chromium.org19bbfa22012-02-03 16:18:11 +0000917 git_command.append('--receive-pack=git receive-pack %s' %
ukai@chromium.orge8077812012-02-03 03:41:46 +0000918 ' '.join(receive_options))
919 git_command += [remote, 'HEAD:refs/for/' + branch]
920 RunGit(git_command)
921 # TODO(ukai): parse Change-Id: and set issue number?
922 return 0
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000923
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000924
ukai@chromium.orge8077812012-02-03 03:41:46 +0000925def RietveldUpload(options, args, cl):
926 """upload the patch to rietveld."""
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000927 upload_args = ['--assume_yes'] # Don't ask about untracked files.
928 upload_args.extend(['--server', cl.GetRietveldServer()])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000929 if options.emulate_svn_auto_props:
930 upload_args.append('--emulate_svn_auto_props')
jam@chromium.org31083642012-01-27 03:14:45 +0000931 if options.from_logs and not options.message:
932 print 'Must set message for subject line if using desc_from_logs'
933 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000934
935 change_desc = None
936
937 if cl.GetIssue():
938 if options.message:
939 upload_args.extend(['--message', options.message])
940 upload_args.extend(['--issue', cl.GetIssue()])
941 print ("This branch is associated with issue %s. "
942 "Adding patch to that issue." % cl.GetIssue())
943 else:
jam@chromium.org31083642012-01-27 03:14:45 +0000944 log_desc = CreateDescriptionFromLog(args)
945 change_desc = ChangeDescription(options.message, log_desc,
946 options.reviewers)
947 if not options.from_logs:
948 change_desc.Update()
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000949
950 if change_desc.IsEmpty():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000951 print "Description is empty; aborting."
952 return 1
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000953
jam@chromium.org31083642012-01-27 03:14:45 +0000954 upload_args.extend(['--message', change_desc.subject])
955 upload_args.extend(['--description', change_desc.description])
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000956 if change_desc.reviewers:
957 upload_args.extend(['--reviewers', change_desc.reviewers])
maruel@chromium.orga3353652011-11-30 14:26:57 +0000958 if options.send_mail:
959 if not change_desc.reviewers:
960 DieWithError("Must specify reviewers to send email.")
961 upload_args.append('--send_mail')
bauerb@chromium.orgae6df352011-04-06 17:40:39 +0000962 cc = ','.join(filter(None, (cl.GetCCList(), options.cc)))
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000963 if cc:
964 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000965
966 # Include the upstream repo's URL in the change -- this is useful for
967 # projects that have their source spread across multiple repos.
968 remote_url = None
969 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000970 # URL is dependent on the current directory.
971 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000972 if data:
973 keys = dict(line.split(': ', 1) for line in data.splitlines()
974 if ': ' in line)
975 remote_url = keys.get('URL', None)
976 else:
977 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
978 remote_url = (cl.GetRemoteUrl() + '@'
979 + cl.GetUpstreamBranch().split('/')[-1])
980 if remote_url:
981 upload_args.extend(['--base_url', remote_url])
982
983 try:
984 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
maruel@chromium.org9ce0dff2011-04-04 17:56:50 +0000985 except KeyboardInterrupt:
986 sys.exit(1)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000987 except:
988 # If we got an exception after the user typed a description for their
989 # change, back up the description before re-raising.
990 if change_desc:
991 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
992 print '\nGot exception while uploading -- saving description to %s\n' \
993 % backup_path
994 backup_file = open(backup_path, 'w')
dpranke@chromium.org970c5222011-03-12 00:32:24 +0000995 backup_file.write(change_desc.description)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000996 backup_file.close()
997 raise
998
999 if not cl.GetIssue():
1000 cl.SetIssue(issue)
1001 cl.SetPatchset(patchset)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001002
1003 if options.use_commit_queue:
1004 cl.SetFlag('commit', '1')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001005 return 0
1006
1007
ukai@chromium.orge8077812012-02-03 03:41:46 +00001008@usage('[args to "git diff"]')
1009def CMDupload(parser, args):
1010 """upload the current changelist to codereview"""
1011 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1012 help='bypass upload presubmit hook')
1013 parser.add_option('-f', action='store_true', dest='force',
1014 help="force yes to questions (don't prompt)")
1015 parser.add_option('-m', dest='message', help='message for patch')
1016 parser.add_option('-r', '--reviewers',
1017 help='reviewer email addresses')
1018 parser.add_option('--cc',
1019 help='cc email addresses')
1020 parser.add_option('--send-mail', action='store_true',
1021 help='send email to reviewer immediately')
1022 parser.add_option("--emulate_svn_auto_props", action="store_true",
1023 dest="emulate_svn_auto_props",
1024 help="Emulate Subversion's auto properties feature.")
1025 parser.add_option("--desc_from_logs", action="store_true",
1026 dest="from_logs",
1027 help="""Squashes git commit logs into change description and
1028 uses message as subject""")
1029 parser.add_option('-c', '--use-commit-queue', action='store_true',
1030 help='tell the commit queue to commit this patchset')
1031 if settings.GetIsGerrit():
1032 parser.add_option('--target_branch', dest='target_branch', default='master',
1033 help='target branch to upload')
1034 (options, args) = parser.parse_args(args)
1035
1036 # Make sure index is up-to-date before running diff-index.
1037 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
1038 if RunGit(['diff-index', 'HEAD']):
1039 print 'Cannot upload with a dirty tree. You must commit locally first.'
1040 return 1
1041
1042 cl = Changelist()
1043 if args:
1044 # TODO(ukai): is it ok for gerrit case?
1045 base_branch = args[0]
1046 else:
1047 # Default to diffing against the "upstream" branch.
1048 base_branch = cl.GetUpstreamBranch()
1049 args = [base_branch + "..."]
1050
1051 if not options.bypass_hooks:
1052 hook_results = cl.RunHook(committing=False, upstream_branch=base_branch,
1053 may_prompt=not options.force,
1054 verbose=options.verbose,
1055 author=None)
1056 if not hook_results.should_continue():
1057 return 1
1058 if not options.reviewers and hook_results.reviewers:
1059 options.reviewers = hook_results.reviewers
1060
1061 # --no-ext-diff is broken in some versions of Git, so try to work around
1062 # this by overriding the environment (but there is still a problem if the
1063 # git config key "diff.external" is used).
1064 env = os.environ.copy()
1065 if 'GIT_EXTERNAL_DIFF' in env:
1066 del env['GIT_EXTERNAL_DIFF']
1067 subprocess2.call(
1068 ['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args, env=env)
1069
1070 if settings.GetIsGerrit():
1071 return GerritUpload(options, args, cl)
1072 return RietveldUpload(options, args, cl)
1073
1074
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001075def SendUpstream(parser, args, cmd):
1076 """Common code for CmdPush and CmdDCommit
1077
1078 Squashed commit into a single.
1079 Updates changelog with metadata (e.g. pointer to review).
1080 Pushes/dcommits the code upstream.
1081 Updates review and closes.
1082 """
1083 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
1084 help='bypass upload presubmit hook')
1085 parser.add_option('-m', dest='message',
1086 help="override review description")
1087 parser.add_option('-f', action='store_true', dest='force',
1088 help="force yes to questions (don't prompt)")
1089 parser.add_option('-c', dest='contributor',
1090 help="external contributor for patch (appended to " +
1091 "description and used as author for git). Should be " +
1092 "formatted as 'First Last <email@example.com>'")
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001093 (options, args) = parser.parse_args(args)
1094 cl = Changelist()
1095
1096 if not args or cmd == 'push':
1097 # Default to merging against our best guess of the upstream branch.
1098 args = [cl.GetUpstreamBranch()]
1099
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001100 if options.contributor:
1101 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
1102 print "Please provide contibutor as 'First Last <email@example.com>'"
1103 return 1
1104
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001105 base_branch = args[0]
1106
chase@chromium.orgc76e6752011-01-10 18:17:12 +00001107 # Make sure index is up-to-date before running diff-index.
1108 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001109 if RunGit(['diff-index', 'HEAD']):
1110 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
1111 return 1
1112
1113 # This rev-list syntax means "show all commits not in my branch that
1114 # are in base_branch".
1115 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
1116 base_branch]).splitlines()
1117 if upstream_commits:
1118 print ('Base branch "%s" has %d commits '
1119 'not in this branch.' % (base_branch, len(upstream_commits)))
1120 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
1121 return 1
1122
1123 if cmd == 'dcommit':
1124 # This is the revision `svn dcommit` will commit on top of.
1125 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
1126 '--pretty=format:%H'])
1127 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
1128 if extra_commits:
1129 print ('This branch has %d additional commits not upstreamed yet.'
1130 % len(extra_commits.splitlines()))
1131 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
1132 'before attempting to %s.' % (base_branch, cmd))
1133 return 1
1134
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001135 if not options.bypass_hooks:
maruel@chromium.org13f623c2011-07-22 16:02:23 +00001136 author = None
1137 if options.contributor:
1138 author = re.search(r'\<(.*)\>', options.contributor).group(1)
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001139 hook_results = cl.RunHook(
1140 committing=True,
1141 upstream_branch=base_branch,
1142 may_prompt=not options.force,
1143 verbose=options.verbose,
1144 author=author)
1145 if not hook_results.should_continue():
1146 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001147
1148 if cmd == 'dcommit':
1149 # Check the tree status if the tree status URL is set.
1150 status = GetTreeStatus()
1151 if 'closed' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001152 print('The tree is closed. Please wait for it to reopen. Use '
1153 '"git cl dcommit --bypass-hooks" to commit on a closed tree.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001154 return 1
1155 elif 'unknown' == status:
maruel@chromium.orgb0a63912012-01-17 18:10:16 +00001156 print('Unable to determine tree status. Please verify manually and '
1157 'use "git cl dcommit --bypass-hooks" to commit on a closed tree.')
maruel@chromium.orgac637152012-01-16 14:19:54 +00001158 else:
1159 breakpad.SendStack(
1160 'GitClHooksBypassedCommit',
1161 'Issue %s/%s bypassed hook when committing' %
maruel@chromium.org2e72bb12012-01-17 15:18:35 +00001162 (cl.GetRietveldServer(), cl.GetIssue()),
1163 verbose=False)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001164
1165 description = options.message
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001166 if not description and cl.GetIssue():
1167 description = cl.GetDescription()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001168
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001169 if not description:
1170 print 'No description set.'
1171 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
1172 return 1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001173
maruel@chromium.orgcc73ad62011-07-06 17:39:26 +00001174 if cl.GetIssue():
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001175 description += "\n\nReview URL: %s" % cl.GetIssueURL()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001176
1177 if options.contributor:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001178 description += "\nPatch from %s." % options.contributor
1179 print 'Description:', repr(description)
1180
1181 branches = [base_branch, cl.GetBranchRef()]
1182 if not options.force:
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001183 subprocess2.call(['git', 'diff', '--stat'] + branches)
maruel@chromium.org90541732011-04-01 17:54:18 +00001184 ask_for_data('About to commit; enter to confirm.')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001185
1186 # We want to squash all this branch's commits into one commit with the
1187 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001188 # We do this by doing a "reset --soft" to the base branch (which keeps
1189 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001190 MERGE_BRANCH = 'git-cl-commit'
1191 # Delete the merge branch if it already exists.
1192 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1193 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1194 RunGit(['branch', '-D', MERGE_BRANCH])
1195
1196 # We might be in a directory that's present in this branch but not in the
1197 # trunk. Move up to the top of the tree so that git commands that expect a
1198 # valid CWD won't fail after we check out the merge branch.
1199 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1200 if rel_base_path:
1201 os.chdir(rel_base_path)
1202
1203 # Stuff our change into the merge branch.
1204 # We wrap in a try...finally block so if anything goes wrong,
1205 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001206 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001207 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001208 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1209 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001210 if options.contributor:
1211 RunGit(['commit', '--author', options.contributor, '-m', description])
1212 else:
1213 RunGit(['commit', '-m', description])
1214 if cmd == 'push':
1215 # push the merge branch.
1216 remote, branch = cl.FetchUpstreamTuple()
1217 retcode, output = RunGitWithCode(
1218 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1219 logging.debug(output)
1220 else:
1221 # dcommit the merge branch.
bauerb@chromium.org2e64fa12011-05-05 11:13:44 +00001222 retcode, output = RunGitWithCode(['svn', 'dcommit',
1223 '--no-rebase', '--rmdir'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001224 finally:
1225 # And then swap back to the original branch and clean up.
1226 RunGit(['checkout', '-q', cl.GetBranch()])
1227 RunGit(['branch', '-D', MERGE_BRANCH])
1228
1229 if cl.GetIssue():
1230 if cmd == 'dcommit' and 'Committed r' in output:
1231 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1232 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001233 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1234 for l in output.splitlines(False))
1235 match = filter(None, match)
1236 if len(match) != 1:
1237 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1238 output)
1239 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001240 else:
1241 return 1
1242 viewvc_url = settings.GetViewVCUrl()
1243 if viewvc_url and revision:
1244 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1245 print ('Closing issue '
1246 '(you may be prompted for your codereview password)...')
1247 cl.CloseIssue()
1248 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001249
1250 if retcode == 0:
1251 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1252 if os.path.isfile(hook):
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001253 RunCommand([hook, base_branch], error_ok=True)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001254
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001255 return 0
1256
1257
1258@usage('[upstream branch to apply against]')
1259def CMDdcommit(parser, args):
1260 """commit the current changelist via git-svn"""
1261 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001262 message = """This doesn't appear to be an SVN repository.
1263If your project has a git mirror with an upstream SVN master, you probably need
1264to run 'git svn init', see your project's git mirror documentation.
1265If your project has a true writeable upstream repository, you probably want
1266to run 'git cl push' instead.
1267Choose wisely, if you get this wrong, your commit might appear to succeed but
1268will instead be silently ignored."""
1269 print(message)
maruel@chromium.org90541732011-04-01 17:54:18 +00001270 ask_for_data('[Press enter to dcommit or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001271 return SendUpstream(parser, args, 'dcommit')
1272
1273
1274@usage('[upstream branch to apply against]')
1275def CMDpush(parser, args):
1276 """commit the current changelist via git"""
1277 if settings.GetIsGitSvn():
1278 print('This appears to be an SVN repository.')
1279 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
maruel@chromium.org90541732011-04-01 17:54:18 +00001280 ask_for_data('[Press enter to push or ctrl-C to quit]')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001281 return SendUpstream(parser, args, 'push')
1282
1283
1284@usage('<patch url or issue id>')
1285def CMDpatch(parser, args):
1286 """patch in a code review"""
1287 parser.add_option('-b', dest='newbranch',
1288 help='create a new branch off trunk for the patch')
1289 parser.add_option('-f', action='store_true', dest='force',
1290 help='with -b, clobber any existing branch')
1291 parser.add_option('--reject', action='store_true', dest='reject',
1292 help='allow failed patches and spew .rej files')
1293 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1294 help="don't commit after patch applies")
1295 (options, args) = parser.parse_args(args)
1296 if len(args) != 1:
1297 parser.print_help()
1298 return 1
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001299 issue_arg = args[0]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001300
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001301 # TODO(maruel): Use apply_issue.py
ukai@chromium.orge8077812012-02-03 03:41:46 +00001302 # TODO(ukai): use gerrit-cherry-pick for gerrit repository?
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001303
dpranke@chromium.org6a2d0832011-03-18 05:28:42 +00001304 if re.match(r'\d+', issue_arg):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001305 # Input is an issue id. Figure out the URL.
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001306 issue = issue_arg
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001307 patch_data = Changelist().GetPatchSetDiff(issue)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001308 else:
maruel@chromium.orgeb5edbc2012-01-16 17:03:28 +00001309 # Assume it's a URL to the patch. Default to https.
1310 issue_url = gclient_utils.UpgradeToHttps(issue_arg)
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001311 match = re.match(r'.*?/issue(\d+)_\d+.diff', issue_url)
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001312 if not match:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001313 DieWithError('Must pass an issue ID or full URL for '
1314 '\'Download raw patch set\'')
maruel@chromium.orge77ebbf2011-03-29 20:35:38 +00001315 issue = match.group(1)
1316 patch_data = urllib2.urlopen(issue_arg).read()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001317
1318 if options.newbranch:
1319 if options.force:
1320 RunGit(['branch', '-D', options.newbranch],
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001321 stderr=subprocess2.PIPE, error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001322 RunGit(['checkout', '-b', options.newbranch,
1323 Changelist().GetUpstreamBranch()])
1324
1325 # Switch up to the top-level directory, if necessary, in preparation for
1326 # applying the patch.
1327 top = RunGit(['rev-parse', '--show-cdup']).strip()
1328 if top:
1329 os.chdir(top)
1330
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001331 # Git patches have a/ at the beginning of source paths. We strip that out
1332 # with a sed script rather than the -p flag to patch so we can feed either
1333 # Git or svn-style patches into the same apply command.
1334 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001335 try:
1336 patch_data = subprocess2.check_output(
1337 ['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'], stdin=patch_data)
1338 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001339 DieWithError('Git patch mungling failed.')
1340 logging.info(patch_data)
1341 # We use "git apply" to apply the patch instead of "patch" so that we can
1342 # pick up file adds.
1343 # The --index flag means: also insert into the index (so we catch adds).
1344 cmd = ['git', 'apply', '--index', '-p0']
1345 if options.reject:
1346 cmd.append('--reject')
maruel@chromium.org32f9f5e2011-09-14 13:41:47 +00001347 try:
1348 subprocess2.check_call(cmd, stdin=patch_data, stdout=subprocess2.VOID)
1349 except subprocess2.CalledProcessError:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001350 DieWithError('Failed to apply the patch')
1351
1352 # If we had an issue, commit the current state and register the issue.
1353 if not options.nocommit:
1354 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1355 cl = Changelist()
1356 cl.SetIssue(issue)
1357 print "Committed patch."
1358 else:
1359 print "Patch applied to index."
1360 return 0
1361
1362
1363def CMDrebase(parser, args):
1364 """rebase current branch on top of svn repo"""
1365 # Provide a wrapper for git svn rebase to help avoid accidental
1366 # git svn dcommit.
1367 # It's the only command that doesn't use parser at all since we just defer
1368 # execution to git-svn.
maruel@chromium.org75075572011-10-10 19:55:28 +00001369 return subprocess2.call(['git', 'svn', 'rebase'] + args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001370
1371
1372def GetTreeStatus():
1373 """Fetches the tree status and returns either 'open', 'closed',
1374 'unknown' or 'unset'."""
1375 url = settings.GetTreeStatusUrl(error_ok=True)
1376 if url:
1377 status = urllib2.urlopen(url).read().lower()
1378 if status.find('closed') != -1 or status == '0':
1379 return 'closed'
1380 elif status.find('open') != -1 or status == '1':
1381 return 'open'
1382 return 'unknown'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001383 return 'unset'
1384
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001385
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001386def GetTreeStatusReason():
1387 """Fetches the tree status from a json url and returns the message
1388 with the reason for the tree to be opened or closed."""
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001389 url = settings.GetTreeStatusUrl()
1390 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001391 connection = urllib2.urlopen(json_url)
1392 status = json.loads(connection.read())
1393 connection.close()
1394 return status['message']
1395
dpranke@chromium.org970c5222011-03-12 00:32:24 +00001396
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001397def CMDtree(parser, args):
1398 """show the status of the tree"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001399 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001400 status = GetTreeStatus()
1401 if 'unset' == status:
1402 print 'You must configure your tree status URL by running "git cl config".'
1403 return 2
1404
1405 print "The tree is %s" % status
1406 print
1407 print GetTreeStatusReason()
1408 if status != 'open':
1409 return 1
1410 return 0
1411
1412
1413def CMDupstream(parser, args):
1414 """print the name of the upstream branch, if any"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001415 _, args = parser.parse_args(args)
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001416 if args:
1417 parser.error('Unrecognized args: %s' % ' '.join(args))
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001418 cl = Changelist()
1419 print cl.GetUpstreamBranch()
1420 return 0
1421
1422
maruel@chromium.org27bb3872011-05-30 20:33:19 +00001423def CMDset_commit(parser, args):
1424 """set the commit bit"""
1425 _, args = parser.parse_args(args)
1426 if args:
1427 parser.error('Unrecognized args: %s' % ' '.join(args))
1428 cl = Changelist()
1429 cl.SetFlag('commit', '1')
1430 return 0
1431
1432
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001433def Command(name):
1434 return getattr(sys.modules[__name__], 'CMD' + name, None)
1435
1436
1437def CMDhelp(parser, args):
1438 """print list of commands or help for a specific command"""
dpranke@chromium.org97ae58e2011-03-18 00:29:20 +00001439 _, args = parser.parse_args(args)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001440 if len(args) == 1:
1441 return main(args + ['--help'])
1442 parser.print_help()
1443 return 0
1444
1445
1446def GenUsage(parser, command):
1447 """Modify an OptParse object with the function's documentation."""
1448 obj = Command(command)
1449 more = getattr(obj, 'usage_more', '')
1450 if command == 'help':
1451 command = '<command>'
1452 else:
1453 # OptParser.description prefer nicely non-formatted strings.
1454 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1455 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1456
1457
1458def main(argv):
1459 """Doesn't parse the arguments here, just find the right subcommand to
1460 execute."""
maruel@chromium.orgddd59412011-11-30 14:20:38 +00001461 # Reload settings.
1462 global settings
1463 settings = Settings()
1464
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001465 # Do it late so all commands are listed.
1466 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1467 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1468 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1469
1470 # Create the option parse and add --verbose support.
1471 parser = optparse.OptionParser()
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001472 parser.add_option(
1473 '-v', '--verbose', action='count', default=0,
1474 help='Use 2 times for more debugging info')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001475 old_parser_args = parser.parse_args
1476 def Parse(args):
1477 options, args = old_parser_args(args)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001478 if options.verbose >= 2:
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001479 logging.basicConfig(level=logging.DEBUG)
maruel@chromium.org899e1c12011-04-07 17:03:18 +00001480 elif options.verbose:
1481 logging.basicConfig(level=logging.INFO)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001482 else:
1483 logging.basicConfig(level=logging.WARNING)
1484 return options, args
1485 parser.parse_args = Parse
1486
1487 if argv:
1488 command = Command(argv[0])
1489 if command:
1490 # "fix" the usage and the description now that we know the subcommand.
1491 GenUsage(parser, argv[0])
1492 try:
1493 return command(parser, argv[1:])
1494 except urllib2.HTTPError, e:
1495 if e.code != 500:
1496 raise
1497 DieWithError(
1498 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1499 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1500
1501 # Not a known command. Default to help.
1502 GenUsage(parser, 'help')
1503 return CMDhelp(parser, argv)
1504
1505
1506if __name__ == '__main__':
maruel@chromium.org6f09cd92011-04-01 16:38:12 +00001507 fix_encoding.fix_encoding()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001508 sys.exit(main(sys.argv[1:]))