blob: be11cb3a7224e72411bd4d840d2e10ef72b9fc7d [file] [log] [blame]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001#!/usr/bin/python
2# git-cl -- a git-command for integrating reviews on Rietveld
3# Copyright (C) 2008 Evan Martin <martine@danga.com>
4
5import errno
6import logging
7import optparse
8import os
9import re
10import subprocess
11import sys
12import tempfile
13import textwrap
14import upload
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +000015import urlparse
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000016import urllib2
17
18try:
19 import readline
20except ImportError:
21 pass
22
23try:
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +000024 # TODO(dpranke): We wrap this in a try block for a limited form of
25 # backwards-compatibility with older versions of git-cl that weren't
26 # dependent on depot_tools. This version should still work outside of
27 # depot_tools as long as --bypass-hooks is used. We should remove this
28 # once this has baked for a while and things seem safe.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000029 depot_tools_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
30 sys.path.append(depot_tools_path)
31 import breakpad
32except ImportError:
33 pass
34
35DEFAULT_SERVER = 'http://codereview.appspot.com'
maruel@chromium.org0ba7f962011-01-11 22:13:58 +000036POSTUPSTREAM_HOOK_PATTERN = '.git/hooks/post-cl-%s'
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000037DESCRIPTION_BACKUP_FILE = '~/.git_cl_description_backup'
38
39def DieWithError(message):
40 print >>sys.stderr, message
41 sys.exit(1)
42
43
44def Popen(cmd, **kwargs):
45 """Wrapper for subprocess.Popen() that logs and watch for cygwin issues"""
46 logging.info('Popen: ' + ' '.join(cmd))
47 try:
48 return subprocess.Popen(cmd, **kwargs)
49 except OSError, e:
50 if e.errno == errno.EAGAIN and sys.platform == 'cygwin':
51 DieWithError(
52 'Visit '
53 'http://code.google.com/p/chromium/wiki/CygwinDllRemappingFailure to '
54 'learn how to fix this error; you need to rebase your cygwin dlls')
55 raise
56
57
58def RunCommand(cmd, error_ok=False, error_message=None,
maruel@chromium.orgb92e4802011-03-03 20:22:00 +000059 redirect_stdout=True, swallow_stderr=False, **kwargs):
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000060 if redirect_stdout:
61 stdout = subprocess.PIPE
62 else:
63 stdout = None
64 if swallow_stderr:
65 stderr = subprocess.PIPE
66 else:
67 stderr = None
maruel@chromium.orgb92e4802011-03-03 20:22:00 +000068 proc = Popen(cmd, stdout=stdout, stderr=stderr, **kwargs)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +000069 output = proc.communicate()[0]
70 if not error_ok and proc.returncode != 0:
71 DieWithError('Command "%s" failed.\n' % (' '.join(cmd)) +
72 (error_message or output or ''))
73 return output
74
75
76def RunGit(args, **kwargs):
77 cmd = ['git'] + args
78 return RunCommand(cmd, **kwargs)
79
80
81def RunGitWithCode(args):
82 proc = Popen(['git'] + args, stdout=subprocess.PIPE)
83 output = proc.communicate()[0]
84 return proc.returncode, output
85
86
87def usage(more):
88 def hook(fn):
89 fn.usage_more = more
90 return fn
91 return hook
92
93
94def FixUrl(server):
95 """Fix a server url to defaults protocol to http:// if none is specified."""
96 if not server:
97 return server
98 if not re.match(r'[a-z]+\://.*', server):
99 return 'http://' + server
100 return server
101
102
103class Settings(object):
104 def __init__(self):
105 self.default_server = None
106 self.cc = None
107 self.root = None
108 self.is_git_svn = None
109 self.svn_branch = None
110 self.tree_status_url = None
111 self.viewvc_url = None
112 self.updated = False
113
114 def LazyUpdateIfNeeded(self):
115 """Updates the settings from a codereview.settings file, if available."""
116 if not self.updated:
117 cr_settings_file = FindCodereviewSettingsFile()
118 if cr_settings_file:
119 LoadCodereviewSettingsFromFile(cr_settings_file)
120 self.updated = True
121
122 def GetDefaultServerUrl(self, error_ok=False):
123 if not self.default_server:
124 self.LazyUpdateIfNeeded()
125 self.default_server = FixUrl(self._GetConfig('rietveld.server',
126 error_ok=True))
127 if error_ok:
128 return self.default_server
129 if not self.default_server:
130 error_message = ('Could not find settings file. You must configure '
131 'your review setup by running "git cl config".')
132 self.default_server = FixUrl(self._GetConfig(
133 'rietveld.server', error_message=error_message))
134 return self.default_server
135
136 def GetCCList(self):
137 """Return the users cc'd on this CL.
138
139 Return is a string suitable for passing to gcl with the --cc flag.
140 """
141 if self.cc is None:
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000142 base_cc = self._GetConfig('rietveld.cc', error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000143 more_cc = self._GetConfig('rietveld.extracc', error_ok=True)
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000144 self.cc = ','.join(filter(None, (base_cc, more_cc))) or ''
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000145 return self.cc
146
147 def GetRoot(self):
148 if not self.root:
149 self.root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
150 return self.root
151
152 def GetIsGitSvn(self):
153 """Return true if this repo looks like it's using git-svn."""
154 if self.is_git_svn is None:
155 # If you have any "svn-remote.*" config keys, we think you're using svn.
156 self.is_git_svn = RunGitWithCode(
157 ['config', '--get-regexp', r'^svn-remote\.'])[0] == 0
158 return self.is_git_svn
159
160 def GetSVNBranch(self):
161 if self.svn_branch is None:
162 if not self.GetIsGitSvn():
163 DieWithError('Repo doesn\'t appear to be a git-svn repo.')
164
165 # Try to figure out which remote branch we're based on.
166 # Strategy:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000167 # 1) iterate through our branch history and find the svn URL.
168 # 2) find the svn-remote that fetches from the URL.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000169
170 # regexp matching the git-svn line that contains the URL.
171 git_svn_re = re.compile(r'^\s*git-svn-id: (\S+)@', re.MULTILINE)
172
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000173 # We don't want to go through all of history, so read a line from the
174 # pipe at a time.
175 # The -100 is an arbitrary limit so we don't search forever.
176 cmd = ['git', 'log', '-100', '--pretty=medium']
177 proc = Popen(cmd, stdout=subprocess.PIPE)
178 for line in proc.stdout:
179 match = git_svn_re.match(line)
180 if match:
181 url = match.group(1)
182 proc.stdout.close() # Cut pipe.
183 break
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000184
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000185 if url:
186 svn_remote_re = re.compile(r'^svn-remote\.([^.]+)\.url (.*)$')
187 remotes = RunGit(['config', '--get-regexp',
188 r'^svn-remote\..*\.url']).splitlines()
189 for remote in remotes:
190 match = svn_remote_re.match(remote)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000191 if match:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000192 remote = match.group(1)
193 base_url = match.group(2)
194 fetch_spec = RunGit(
195 ['config', 'svn-remote.'+remote+'.fetch']).strip().split(':')
196 if fetch_spec[0]:
197 full_url = base_url + '/' + fetch_spec[0]
198 else:
199 full_url = base_url
200 if full_url == url:
201 self.svn_branch = fetch_spec[1]
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000202 break
203
204 if not self.svn_branch:
205 DieWithError('Can\'t guess svn branch -- try specifying it on the '
206 'command line')
207
208 return self.svn_branch
209
210 def GetTreeStatusUrl(self, error_ok=False):
211 if not self.tree_status_url:
212 error_message = ('You must configure your tree status URL by running '
213 '"git cl config".')
214 self.tree_status_url = self._GetConfig('rietveld.tree-status-url',
215 error_ok=error_ok,
216 error_message=error_message)
217 return self.tree_status_url
218
219 def GetViewVCUrl(self):
220 if not self.viewvc_url:
221 self.viewvc_url = self._GetConfig('rietveld.viewvc-url', error_ok=True)
222 return self.viewvc_url
223
224 def _GetConfig(self, param, **kwargs):
225 self.LazyUpdateIfNeeded()
226 return RunGit(['config', param], **kwargs).strip()
227
228
229settings = Settings()
230
231
232did_migrate_check = False
233def CheckForMigration():
234 """Migrate from the old issue format, if found.
235
236 We used to store the branch<->issue mapping in a file in .git, but it's
237 better to store it in the .git/config, since deleting a branch deletes that
238 branch's entry there.
239 """
240
241 # Don't run more than once.
242 global did_migrate_check
243 if did_migrate_check:
244 return
245
246 gitdir = RunGit(['rev-parse', '--git-dir']).strip()
247 storepath = os.path.join(gitdir, 'cl-mapping')
248 if os.path.exists(storepath):
249 print "old-style git-cl mapping file (%s) found; migrating." % storepath
250 store = open(storepath, 'r')
251 for line in store:
252 branch, issue = line.strip().split()
253 RunGit(['config', 'branch.%s.rietveldissue' % ShortBranchName(branch),
254 issue])
255 store.close()
256 os.remove(storepath)
257 did_migrate_check = True
258
259
260def ShortBranchName(branch):
261 """Convert a name like 'refs/heads/foo' to just 'foo'."""
262 return branch.replace('refs/heads/', '')
263
264
265class Changelist(object):
266 def __init__(self, branchref=None):
267 # Poke settings so we get the "configure your server" message if necessary.
268 settings.GetDefaultServerUrl()
269 self.branchref = branchref
270 if self.branchref:
271 self.branch = ShortBranchName(self.branchref)
272 else:
273 self.branch = None
274 self.rietveld_server = None
275 self.upstream_branch = None
276 self.has_issue = False
277 self.issue = None
278 self.has_description = False
279 self.description = None
280 self.has_patchset = False
281 self.patchset = None
282
283 def GetBranch(self):
284 """Returns the short branch name, e.g. 'master'."""
285 if not self.branch:
286 self.branchref = RunGit(['symbolic-ref', 'HEAD']).strip()
287 self.branch = ShortBranchName(self.branchref)
288 return self.branch
289
290 def GetBranchRef(self):
291 """Returns the full branch name, e.g. 'refs/heads/master'."""
292 self.GetBranch() # Poke the lazy loader.
293 return self.branchref
294
295 def FetchUpstreamTuple(self):
296 """Returns a tuple containg remote and remote ref,
297 e.g. 'origin', 'refs/heads/master'
298 """
299 remote = '.'
300 branch = self.GetBranch()
301 upstream_branch = RunGit(['config', 'branch.%s.merge' % branch],
302 error_ok=True).strip()
303 if upstream_branch:
304 remote = RunGit(['config', 'branch.%s.remote' % branch]).strip()
305 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000306 upstream_branch = RunGit(['config', 'rietveld.upstream-branch'],
307 error_ok=True).strip()
308 if upstream_branch:
309 remote = RunGit(['config', 'rietveld.upstream-remote']).strip()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000310 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000311 # Fall back on trying a git-svn upstream branch.
312 if settings.GetIsGitSvn():
313 upstream_branch = settings.GetSVNBranch()
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000314 else:
bauerb@chromium.orgade368c2011-03-01 08:57:50 +0000315 # Else, try to guess the origin remote.
316 remote_branches = RunGit(['branch', '-r']).split()
317 if 'origin/master' in remote_branches:
318 # Fall back on origin/master if it exits.
319 remote = 'origin'
320 upstream_branch = 'refs/heads/master'
321 elif 'origin/trunk' in remote_branches:
322 # Fall back on origin/trunk if it exists. Generally a shared
323 # git-svn clone
324 remote = 'origin'
325 upstream_branch = 'refs/heads/trunk'
326 else:
327 DieWithError("""Unable to determine default branch to diff against.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000328Either pass complete "git diff"-style arguments, like
329 git cl upload origin/master
330or verify this branch is set up to track another (via the --track argument to
331"git checkout -b ...").""")
332
333 return remote, upstream_branch
334
335 def GetUpstreamBranch(self):
336 if self.upstream_branch is None:
337 remote, upstream_branch = self.FetchUpstreamTuple()
338 if remote is not '.':
339 upstream_branch = upstream_branch.replace('heads', 'remotes/' + remote)
340 self.upstream_branch = upstream_branch
341 return self.upstream_branch
342
343 def GetRemoteUrl(self):
344 """Return the configured remote URL, e.g. 'git://example.org/foo.git/'.
345
346 Returns None if there is no remote.
347 """
348 remote = self.FetchUpstreamTuple()[0]
349 if remote == '.':
350 return None
351 return RunGit(['config', 'remote.%s.url' % remote], error_ok=True).strip()
352
353 def GetIssue(self):
354 if not self.has_issue:
355 CheckForMigration()
356 issue = RunGit(['config', self._IssueSetting()], error_ok=True).strip()
357 if issue:
358 self.issue = issue
359 self.rietveld_server = FixUrl(RunGit(
360 ['config', self._RietveldServer()], error_ok=True).strip())
361 else:
362 self.issue = None
363 if not self.rietveld_server:
364 self.rietveld_server = settings.GetDefaultServerUrl()
365 self.has_issue = True
366 return self.issue
367
368 def GetRietveldServer(self):
369 self.GetIssue()
370 return self.rietveld_server
371
372 def GetIssueURL(self):
373 """Get the URL for a particular issue."""
374 return '%s/%s' % (self.GetRietveldServer(), self.GetIssue())
375
376 def GetDescription(self, pretty=False):
377 if not self.has_description:
378 if self.GetIssue():
379 path = '/' + self.GetIssue() + '/description'
380 rpc_server = self._RpcServer()
381 self.description = rpc_server.Send(path).strip()
382 self.has_description = True
383 if pretty:
384 wrapper = textwrap.TextWrapper()
385 wrapper.initial_indent = wrapper.subsequent_indent = ' '
386 return wrapper.fill(self.description)
387 return self.description
388
389 def GetPatchset(self):
390 if not self.has_patchset:
391 patchset = RunGit(['config', self._PatchsetSetting()],
392 error_ok=True).strip()
393 if patchset:
394 self.patchset = patchset
395 else:
396 self.patchset = None
397 self.has_patchset = True
398 return self.patchset
399
400 def SetPatchset(self, patchset):
401 """Set this branch's patchset. If patchset=0, clears the patchset."""
402 if patchset:
403 RunGit(['config', self._PatchsetSetting(), str(patchset)])
404 else:
405 RunGit(['config', '--unset', self._PatchsetSetting()],
406 swallow_stderr=True, error_ok=True)
407 self.has_patchset = False
408
409 def SetIssue(self, issue):
410 """Set this branch's issue. If issue=0, clears the issue."""
411 if issue:
412 RunGit(['config', self._IssueSetting(), str(issue)])
413 if self.rietveld_server:
414 RunGit(['config', self._RietveldServer(), self.rietveld_server])
415 else:
416 RunGit(['config', '--unset', self._IssueSetting()])
417 self.SetPatchset(0)
418 self.has_issue = False
419
420 def CloseIssue(self):
421 rpc_server = self._RpcServer()
422 # Newer versions of Rietveld require us to pass an XSRF token to POST, so
423 # we fetch it from the server. (The version used by Chromium has been
424 # modified so the token isn't required when closing an issue.)
425 xsrf_token = rpc_server.Send('/xsrf_token',
426 extra_headers={'X-Requesting-XSRF-Token': '1'})
427
428 # You cannot close an issue with a GET.
429 # We pass an empty string for the data so it is a POST rather than a GET.
430 data = [("description", self.description),
431 ("xsrf_token", xsrf_token)]
432 ctype, body = upload.EncodeMultipartFormData(data, [])
433 rpc_server.Send('/' + self.GetIssue() + '/close', body, ctype)
434
435 def _RpcServer(self):
436 """Returns an upload.RpcServer() to access this review's rietveld instance.
437 """
438 server = self.GetRietveldServer()
439 return upload.GetRpcServer(server, save_cookies=True)
440
441 def _IssueSetting(self):
442 """Return the git setting that stores this change's issue."""
443 return 'branch.%s.rietveldissue' % self.GetBranch()
444
445 def _PatchsetSetting(self):
446 """Return the git setting that stores this change's most recent patchset."""
447 return 'branch.%s.rietveldpatchset' % self.GetBranch()
448
449 def _RietveldServer(self):
450 """Returns the git setting that stores this change's rietveld server."""
451 return 'branch.%s.rietveldserver' % self.GetBranch()
452
453
454def GetCodereviewSettingsInteractively():
455 """Prompt the user for settings."""
456 server = settings.GetDefaultServerUrl(error_ok=True)
457 prompt = 'Rietveld server (host[:port])'
458 prompt += ' [%s]' % (server or DEFAULT_SERVER)
459 newserver = raw_input(prompt + ': ')
460 if not server and not newserver:
461 newserver = DEFAULT_SERVER
462 if newserver and newserver != server:
463 RunGit(['config', 'rietveld.server', newserver])
464
465 def SetProperty(initial, caption, name):
466 prompt = caption
467 if initial:
468 prompt += ' ("x" to clear) [%s]' % initial
469 new_val = raw_input(prompt + ': ')
470 if new_val == 'x':
471 RunGit(['config', '--unset-all', 'rietveld.' + name], error_ok=True)
472 elif new_val and new_val != initial:
473 RunGit(['config', 'rietveld.' + name, new_val])
474
475 SetProperty(settings.GetCCList(), 'CC list', 'cc')
476 SetProperty(settings.GetTreeStatusUrl(error_ok=True), 'Tree status URL',
477 'tree-status-url')
478 SetProperty(settings.GetViewVCUrl(), 'ViewVC URL', 'viewvc-url')
479
480 # TODO: configure a default branch to diff against, rather than this
481 # svn-based hackery.
482
483
484def FindCodereviewSettingsFile(filename='codereview.settings'):
485 """Finds the given file starting in the cwd and going up.
486
487 Only looks up to the top of the repository unless an
488 'inherit-review-settings-ok' file exists in the root of the repository.
489 """
490 inherit_ok_file = 'inherit-review-settings-ok'
491 cwd = os.getcwd()
492 root = os.path.abspath(RunGit(['rev-parse', '--show-cdup']).strip())
493 if os.path.isfile(os.path.join(root, inherit_ok_file)):
494 root = '/'
495 while True:
496 if filename in os.listdir(cwd):
497 if os.path.isfile(os.path.join(cwd, filename)):
498 return open(os.path.join(cwd, filename))
499 if cwd == root:
500 break
501 cwd = os.path.dirname(cwd)
502
503
504def LoadCodereviewSettingsFromFile(fileobj):
505 """Parse a codereview.settings file and updates hooks."""
506 def DownloadToFile(url, filename):
507 filename = os.path.join(settings.GetRoot(), filename)
508 contents = urllib2.urlopen(url).read()
509 fileobj = open(filename, 'w')
510 fileobj.write(contents)
511 fileobj.close()
512 os.chmod(filename, 0755)
513 return 0
514
515 keyvals = {}
516 for line in fileobj.read().splitlines():
517 if not line or line.startswith("#"):
518 continue
519 k, v = line.split(": ", 1)
520 keyvals[k] = v
521
522 def GetProperty(name):
523 return keyvals.get(name)
524
525 def SetProperty(name, setting, unset_error_ok=False):
526 fullname = 'rietveld.' + name
527 if setting in keyvals:
528 RunGit(['config', fullname, keyvals[setting]])
529 else:
530 RunGit(['config', '--unset-all', fullname], error_ok=unset_error_ok)
531
532 SetProperty('server', 'CODE_REVIEW_SERVER')
533 # Only server setting is required. Other settings can be absent.
534 # In that case, we ignore errors raised during option deletion attempt.
535 SetProperty('cc', 'CC_LIST', unset_error_ok=True)
536 SetProperty('tree-status-url', 'STATUS', unset_error_ok=True)
537 SetProperty('viewvc-url', 'VIEW_VC', unset_error_ok=True)
538
539 if 'PUSH_URL_CONFIG' in keyvals and 'ORIGIN_URL_CONFIG' in keyvals:
540 #should be of the form
541 #PUSH_URL_CONFIG: url.ssh://gitrw.chromium.org.pushinsteadof
542 #ORIGIN_URL_CONFIG: http://src.chromium.org/git
543 RunGit(['config', keyvals['PUSH_URL_CONFIG'],
544 keyvals['ORIGIN_URL_CONFIG']])
545
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000546
547@usage('[repo root containing codereview.settings]')
548def CMDconfig(parser, args):
549 """edit configuration for this tree"""
550
551 (options, args) = parser.parse_args(args)
552 if len(args) == 0:
553 GetCodereviewSettingsInteractively()
554 return 0
555
556 url = args[0]
557 if not url.endswith('codereview.settings'):
558 url = os.path.join(url, 'codereview.settings')
559
560 # Load code review settings and download hooks (if available).
561 LoadCodereviewSettingsFromFile(urllib2.urlopen(url))
562 return 0
563
564
565def CMDstatus(parser, args):
566 """show status of changelists"""
567 parser.add_option('--field',
568 help='print only specific field (desc|id|patch|url)')
569 (options, args) = parser.parse_args(args)
570
571 # TODO: maybe make show_branches a flag if necessary.
572 show_branches = not options.field
573
574 if show_branches:
575 branches = RunGit(['for-each-ref', '--format=%(refname)', 'refs/heads'])
576 if branches:
577 print 'Branches associated with reviews:'
578 for branch in sorted(branches.splitlines()):
579 cl = Changelist(branchref=branch)
580 print " %10s: %s" % (cl.GetBranch(), cl.GetIssue())
581
582 cl = Changelist()
583 if options.field:
584 if options.field.startswith('desc'):
585 print cl.GetDescription()
586 elif options.field == 'id':
587 issueid = cl.GetIssue()
588 if issueid:
589 print issueid
590 elif options.field == 'patch':
591 patchset = cl.GetPatchset()
592 if patchset:
593 print patchset
594 elif options.field == 'url':
595 url = cl.GetIssueURL()
596 if url:
597 print url
598 else:
599 print
600 print 'Current branch:',
601 if not cl.GetIssue():
602 print 'no issue assigned.'
603 return 0
604 print cl.GetBranch()
605 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
606 print 'Issue description:'
607 print cl.GetDescription(pretty=True)
608 return 0
609
610
611@usage('[issue_number]')
612def CMDissue(parser, args):
613 """Set or display the current code review issue number.
614
615 Pass issue number 0 to clear the current issue.
616"""
617 (options, args) = parser.parse_args(args)
618
619 cl = Changelist()
620 if len(args) > 0:
621 try:
622 issue = int(args[0])
623 except ValueError:
624 DieWithError('Pass a number to set the issue or none to list it.\n'
625 'Maybe you want to run git cl status?')
626 cl.SetIssue(issue)
627 print 'Issue number:', cl.GetIssue(), '(%s)' % cl.GetIssueURL()
628 return 0
629
630
631def CreateDescriptionFromLog(args):
632 """Pulls out the commit log to use as a base for the CL description."""
633 log_args = []
634 if len(args) == 1 and not args[0].endswith('.'):
635 log_args = [args[0] + '..']
636 elif len(args) == 1 and args[0].endswith('...'):
637 log_args = [args[0][:-1]]
638 elif len(args) == 2:
639 log_args = [args[0] + '..' + args[1]]
640 else:
641 log_args = args[:] # Hope for the best!
642 return RunGit(['log', '--pretty=format:%s\n\n%b'] + log_args)
643
644
645def UserEditedLog(starting_text):
646 """Given some starting text, let the user edit it and return the result."""
647 editor = os.getenv('EDITOR', 'vi')
648
649 (file_handle, filename) = tempfile.mkstemp()
650 fileobj = os.fdopen(file_handle, 'w')
651 fileobj.write(starting_text)
652 fileobj.close()
653
654 ret = subprocess.call(editor + ' ' + filename, shell=True)
655 if ret != 0:
656 os.remove(filename)
657 return
658
659 fileobj = open(filename)
660 text = fileobj.read()
661 fileobj.close()
662 os.remove(filename)
663 stripcomment_re = re.compile(r'^#.*$', re.MULTILINE)
664 return stripcomment_re.sub('', text).strip()
665
666
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000667def ConvertToInteger(inputval):
668 """Convert a string to integer, but returns either an int or None."""
669 try:
670 return int(inputval)
671 except (TypeError, ValueError):
672 return None
673
674
675def RunHook(committing, upstream_branch):
676 import presubmit_support
677 import scm
678 import watchlists
679
680 root = RunCommand(['git', 'rev-parse', '--show-cdup']).strip()
681 if not root:
682 root = "."
683 absroot = os.path.abspath(root)
684 if not root:
685 raise Exception("Could not get root directory.")
686
687 # We use the sha1 of HEAD as a name of this change.
688 name = RunCommand(['git', 'rev-parse', 'HEAD']).strip()
689 files = scm.GIT.CaptureStatus([root], upstream_branch)
690
691 cl = Changelist()
692 issue = ConvertToInteger(cl.GetIssue())
693 patchset = ConvertToInteger(cl.GetPatchset())
694 if issue:
695 description = cl.GetDescription()
696 else:
697 # If the change was never uploaded, use the log messages of all commits
698 # up to the branch point, as git cl upload will prefill the description
699 # with these log messages.
700 description = RunCommand(['git', 'log', '--pretty=format:%s%n%n%b',
701 '%s...' % (upstream_branch)]).strip()
702 change = presubmit_support.GitChange(name, description, absroot, files,
703 issue, patchset)
704
705 # Apply watchlists on upload.
706 if not committing:
707 watchlist = watchlists.Watchlists(change.RepositoryRoot())
708 files = [f.LocalPath() for f in change.AffectedFiles()]
709 watchers = watchlist.GetWatchersForPaths(files)
710 RunCommand(['git', 'config', '--replace-all',
711 'rietveld.extracc', ','.join(watchers)])
712
713 return presubmit_support.DoPresubmitChecks(change, committing,
714 verbose=None, output_stream=sys.stdout, input_stream=sys.stdin,
715 default_presubmit=None, may_prompt=None)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000716
717
718def CMDpresubmit(parser, args):
719 """run presubmit tests on the current changelist"""
720 parser.add_option('--upload', action='store_true',
721 help='Run upload hook instead of the push/dcommit hook')
722 (options, args) = parser.parse_args(args)
723
724 # Make sure index is up-to-date before running diff-index.
725 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
726 if RunGit(['diff-index', 'HEAD']):
727 # TODO(maruel): Is this really necessary?
728 print 'Cannot presubmit with a dirty tree. You must commit locally first.'
729 return 1
730
731 if args:
732 base_branch = args[0]
733 else:
734 # Default to diffing against the "upstream" branch.
735 base_branch = Changelist().GetUpstreamBranch()
736
737 if options.upload:
738 print '*** Presubmit checks for UPLOAD would report: ***'
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000739 return RunHook(committing=False, upstream_branch=base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000740 else:
741 print '*** Presubmit checks for DCOMMIT would report: ***'
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000742 return RunHook(committing=True, upstream_branch=base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000743
744
745@usage('[args to "git diff"]')
746def CMDupload(parser, args):
747 """upload the current changelist to codereview"""
748 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
749 help='bypass upload presubmit hook')
750 parser.add_option('-m', dest='message', help='message for patch')
751 parser.add_option('-r', '--reviewers',
752 help='reviewer email addresses')
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000753 parser.add_option('--cc',
754 help='cc email addresses')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000755 parser.add_option('--send-mail', action='store_true',
756 help='send email to reviewer immediately')
757 parser.add_option("--emulate_svn_auto_props", action="store_true",
758 dest="emulate_svn_auto_props",
759 help="Emulate Subversion's auto properties feature.")
760 parser.add_option("--desc_from_logs", action="store_true",
761 dest="from_logs",
762 help="""Squashes git commit logs into change description and
763 uses message as subject""")
764 (options, args) = parser.parse_args(args)
765
766 # Make sure index is up-to-date before running diff-index.
767 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
768 if RunGit(['diff-index', 'HEAD']):
769 print 'Cannot upload with a dirty tree. You must commit locally first.'
770 return 1
771
772 cl = Changelist()
773 if args:
774 base_branch = args[0]
775 else:
776 # Default to diffing against the "upstream" branch.
777 base_branch = cl.GetUpstreamBranch()
778 args = [base_branch + "..."]
779
780 if not options.bypass_hooks:
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000781 RunHook(committing=False, upstream_branch=base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000782
783 # --no-ext-diff is broken in some versions of Git, so try to work around
784 # this by overriding the environment (but there is still a problem if the
785 # git config key "diff.external" is used).
786 env = os.environ.copy()
787 if 'GIT_EXTERNAL_DIFF' in env:
788 del env['GIT_EXTERNAL_DIFF']
789 subprocess.call(['git', 'diff', '--no-ext-diff', '--stat', '-M'] + args,
790 env=env)
791
792 upload_args = ['--assume_yes'] # Don't ask about untracked files.
793 upload_args.extend(['--server', cl.GetRietveldServer()])
794 if options.reviewers:
795 upload_args.extend(['--reviewers', options.reviewers])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000796 if options.emulate_svn_auto_props:
797 upload_args.append('--emulate_svn_auto_props')
798 if options.send_mail:
799 if not options.reviewers:
800 DieWithError("Must specify reviewers to send email.")
801 upload_args.append('--send_mail')
802 if options.from_logs and not options.message:
803 print 'Must set message for subject line if using desc_from_logs'
804 return 1
805
806 change_desc = None
807
808 if cl.GetIssue():
809 if options.message:
810 upload_args.extend(['--message', options.message])
811 upload_args.extend(['--issue', cl.GetIssue()])
812 print ("This branch is associated with issue %s. "
813 "Adding patch to that issue." % cl.GetIssue())
814 else:
815 log_desc = CreateDescriptionFromLog(args)
816 if options.from_logs:
817 # Uses logs as description and message as subject.
818 subject = options.message
819 change_desc = subject + '\n\n' + log_desc
820 else:
821 initial_text = """# Enter a description of the change.
822# This will displayed on the codereview site.
823# The first line will also be used as the subject of the review.
824"""
825 if 'BUG=' not in log_desc:
826 log_desc += '\nBUG='
827 if 'TEST=' not in log_desc:
828 log_desc += '\nTEST='
829 change_desc = UserEditedLog(initial_text + log_desc)
830 subject = ''
831 if change_desc:
832 subject = change_desc.splitlines()[0]
833 if not change_desc:
834 print "Description is empty; aborting."
835 return 1
836 upload_args.extend(['--message', subject])
837 upload_args.extend(['--description', change_desc])
maruel@chromium.orgb2a7c332011-02-25 20:30:37 +0000838 cc = ','.join(filter(None, (settings.GetCCList(), options.cc)))
839 if cc:
840 upload_args.extend(['--cc', cc])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000841
842 # Include the upstream repo's URL in the change -- this is useful for
843 # projects that have their source spread across multiple repos.
844 remote_url = None
845 if settings.GetIsGitSvn():
maruel@chromium.orgb92e4802011-03-03 20:22:00 +0000846 # URL is dependent on the current directory.
847 data = RunGit(['svn', 'info'], cwd=settings.GetRoot())
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000848 if data:
849 keys = dict(line.split(': ', 1) for line in data.splitlines()
850 if ': ' in line)
851 remote_url = keys.get('URL', None)
852 else:
853 if cl.GetRemoteUrl() and '/' in cl.GetUpstreamBranch():
854 remote_url = (cl.GetRemoteUrl() + '@'
855 + cl.GetUpstreamBranch().split('/')[-1])
856 if remote_url:
857 upload_args.extend(['--base_url', remote_url])
858
859 try:
860 issue, patchset = upload.RealMain(['upload'] + upload_args + args)
861 except:
862 # If we got an exception after the user typed a description for their
863 # change, back up the description before re-raising.
864 if change_desc:
865 backup_path = os.path.expanduser(DESCRIPTION_BACKUP_FILE)
866 print '\nGot exception while uploading -- saving description to %s\n' \
867 % backup_path
868 backup_file = open(backup_path, 'w')
869 backup_file.write(change_desc)
870 backup_file.close()
871 raise
872
873 if not cl.GetIssue():
874 cl.SetIssue(issue)
875 cl.SetPatchset(patchset)
876 return 0
877
878
879def SendUpstream(parser, args, cmd):
880 """Common code for CmdPush and CmdDCommit
881
882 Squashed commit into a single.
883 Updates changelog with metadata (e.g. pointer to review).
884 Pushes/dcommits the code upstream.
885 Updates review and closes.
886 """
887 parser.add_option('--bypass-hooks', action='store_true', dest='bypass_hooks',
888 help='bypass upload presubmit hook')
889 parser.add_option('-m', dest='message',
890 help="override review description")
891 parser.add_option('-f', action='store_true', dest='force',
892 help="force yes to questions (don't prompt)")
893 parser.add_option('-c', dest='contributor',
894 help="external contributor for patch (appended to " +
895 "description and used as author for git). Should be " +
896 "formatted as 'First Last <email@example.com>'")
897 parser.add_option('--tbr', action='store_true', dest='tbr',
898 help="short for 'to be reviewed', commit branch " +
899 "even without uploading for review")
900 (options, args) = parser.parse_args(args)
901 cl = Changelist()
902
903 if not args or cmd == 'push':
904 # Default to merging against our best guess of the upstream branch.
905 args = [cl.GetUpstreamBranch()]
906
907 base_branch = args[0]
908
chase@chromium.orgc76e6752011-01-10 18:17:12 +0000909 # Make sure index is up-to-date before running diff-index.
910 RunGit(['update-index', '--refresh', '-q'], error_ok=True)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000911 if RunGit(['diff-index', 'HEAD']):
912 print 'Cannot %s with a dirty tree. You must commit locally first.' % cmd
913 return 1
914
915 # This rev-list syntax means "show all commits not in my branch that
916 # are in base_branch".
917 upstream_commits = RunGit(['rev-list', '^' + cl.GetBranchRef(),
918 base_branch]).splitlines()
919 if upstream_commits:
920 print ('Base branch "%s" has %d commits '
921 'not in this branch.' % (base_branch, len(upstream_commits)))
922 print 'Run "git merge %s" before attempting to %s.' % (base_branch, cmd)
923 return 1
924
925 if cmd == 'dcommit':
926 # This is the revision `svn dcommit` will commit on top of.
927 svn_head = RunGit(['log', '--grep=^git-svn-id:', '-1',
928 '--pretty=format:%H'])
929 extra_commits = RunGit(['rev-list', '^' + svn_head, base_branch])
930 if extra_commits:
931 print ('This branch has %d additional commits not upstreamed yet.'
932 % len(extra_commits.splitlines()))
933 print ('Upstream "%s" or rebase this branch on top of the upstream trunk '
934 'before attempting to %s.' % (base_branch, cmd))
935 return 1
936
937 if not options.force and not options.bypass_hooks:
dpranke@chromium.org23beb9e2011-03-11 23:20:54 +0000938 RunHook(committing=False, upstream_branch=base_branch)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +0000939
940 if cmd == 'dcommit':
941 # Check the tree status if the tree status URL is set.
942 status = GetTreeStatus()
943 if 'closed' == status:
944 print ('The tree is closed. Please wait for it to reopen. Use '
945 '"git cl dcommit -f" to commit on a closed tree.')
946 return 1
947 elif 'unknown' == status:
948 print ('Unable to determine tree status. Please verify manually and '
949 'use "git cl dcommit -f" to commit on a closed tree.')
950
951 description = options.message
952 if not options.tbr:
953 # It is important to have these checks early. Not only for user
954 # convenience, but also because the cl object then caches the correct values
955 # of these fields even as we're juggling branches for setting up the commit.
956 if not cl.GetIssue():
957 print 'Current issue unknown -- has this branch been uploaded?'
958 print 'Use --tbr to commit without review.'
959 return 1
960
961 if not description:
962 description = cl.GetDescription()
963
964 if not description:
965 print 'No description set.'
966 print 'Visit %s/edit to set it.' % (cl.GetIssueURL())
967 return 1
968
969 description += "\n\nReview URL: %s" % cl.GetIssueURL()
970 else:
971 if not description:
972 # Submitting TBR. See if there's already a description in Rietveld, else
973 # create a template description. Eitherway, give the user a chance to edit
974 # it to fill in the TBR= field.
975 if cl.GetIssue():
976 description = cl.GetDescription()
977
978 if not description:
979 description = """# Enter a description of the change.
980# This will be used as the change log for the commit.
981
982"""
983 description += CreateDescriptionFromLog(args)
984
985 description = UserEditedLog(description + '\nTBR=')
986
987 if not description:
988 print "Description empty; aborting."
989 return 1
990
991 if options.contributor:
992 if not re.match('^.*\s<\S+@\S+>$', options.contributor):
993 print "Please provide contibutor as 'First Last <email@example.com>'"
994 return 1
995 description += "\nPatch from %s." % options.contributor
996 print 'Description:', repr(description)
997
998 branches = [base_branch, cl.GetBranchRef()]
999 if not options.force:
1000 subprocess.call(['git', 'diff', '--stat'] + branches)
1001 raw_input("About to commit; enter to confirm.")
1002
1003 # We want to squash all this branch's commits into one commit with the
1004 # proper description.
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001005 # We do this by doing a "reset --soft" to the base branch (which keeps
1006 # the working copy the same), then dcommitting that.
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001007 MERGE_BRANCH = 'git-cl-commit'
1008 # Delete the merge branch if it already exists.
1009 if RunGitWithCode(['show-ref', '--quiet', '--verify',
1010 'refs/heads/' + MERGE_BRANCH])[0] == 0:
1011 RunGit(['branch', '-D', MERGE_BRANCH])
1012
1013 # We might be in a directory that's present in this branch but not in the
1014 # trunk. Move up to the top of the tree so that git commands that expect a
1015 # valid CWD won't fail after we check out the merge branch.
1016 rel_base_path = RunGit(['rev-parse', '--show-cdup']).strip()
1017 if rel_base_path:
1018 os.chdir(rel_base_path)
1019
1020 # Stuff our change into the merge branch.
1021 # We wrap in a try...finally block so if anything goes wrong,
1022 # we clean up the branches.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001023 retcode = -1
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001024 try:
bauerb@chromium.orgb4a75c42011-03-08 08:35:38 +00001025 RunGit(['checkout', '-q', '-b', MERGE_BRANCH])
1026 RunGit(['reset', '--soft', base_branch])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001027 if options.contributor:
1028 RunGit(['commit', '--author', options.contributor, '-m', description])
1029 else:
1030 RunGit(['commit', '-m', description])
1031 if cmd == 'push':
1032 # push the merge branch.
1033 remote, branch = cl.FetchUpstreamTuple()
1034 retcode, output = RunGitWithCode(
1035 ['push', '--porcelain', remote, 'HEAD:%s' % branch])
1036 logging.debug(output)
1037 else:
1038 # dcommit the merge branch.
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001039 retcode, output = RunGitWithCode(['svn', 'dcommit', '--no-rebase'])
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001040 finally:
1041 # And then swap back to the original branch and clean up.
1042 RunGit(['checkout', '-q', cl.GetBranch()])
1043 RunGit(['branch', '-D', MERGE_BRANCH])
1044
1045 if cl.GetIssue():
1046 if cmd == 'dcommit' and 'Committed r' in output:
1047 revision = re.match('.*?\nCommitted r(\\d+)', output, re.DOTALL).group(1)
1048 elif cmd == 'push' and retcode == 0:
maruel@chromium.orgdf947ea2011-01-12 20:44:54 +00001049 match = (re.match(r'.*?([a-f0-9]{7})\.\.([a-f0-9]{7})$', l)
1050 for l in output.splitlines(False))
1051 match = filter(None, match)
1052 if len(match) != 1:
1053 DieWithError("Couldn't parse ouput to extract the committed hash:\n%s" %
1054 output)
1055 revision = match[0].group(2)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001056 else:
1057 return 1
1058 viewvc_url = settings.GetViewVCUrl()
1059 if viewvc_url and revision:
1060 cl.description += ('\n\nCommitted: ' + viewvc_url + revision)
1061 print ('Closing issue '
1062 '(you may be prompted for your codereview password)...')
1063 cl.CloseIssue()
1064 cl.SetIssue(0)
maruel@chromium.org0ba7f962011-01-11 22:13:58 +00001065
1066 if retcode == 0:
1067 hook = POSTUPSTREAM_HOOK_PATTERN % cmd
1068 if os.path.isfile(hook):
1069 RunHook(hook, upstream_branch=base_branch, error_ok=True)
1070
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001071 return 0
1072
1073
1074@usage('[upstream branch to apply against]')
1075def CMDdcommit(parser, args):
1076 """commit the current changelist via git-svn"""
1077 if not settings.GetIsGitSvn():
thakis@chromium.orgcde3bb62011-01-20 01:16:14 +00001078 message = """This doesn't appear to be an SVN repository.
1079If your project has a git mirror with an upstream SVN master, you probably need
1080to run 'git svn init', see your project's git mirror documentation.
1081If your project has a true writeable upstream repository, you probably want
1082to run 'git cl push' instead.
1083Choose wisely, if you get this wrong, your commit might appear to succeed but
1084will instead be silently ignored."""
1085 print(message)
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001086 raw_input('[Press enter to dcommit or ctrl-C to quit]')
1087 return SendUpstream(parser, args, 'dcommit')
1088
1089
1090@usage('[upstream branch to apply against]')
1091def CMDpush(parser, args):
1092 """commit the current changelist via git"""
1093 if settings.GetIsGitSvn():
1094 print('This appears to be an SVN repository.')
1095 print('Are you sure you didn\'t mean \'git cl dcommit\'?')
1096 raw_input('[Press enter to push or ctrl-C to quit]')
1097 return SendUpstream(parser, args, 'push')
1098
1099
1100@usage('<patch url or issue id>')
1101def CMDpatch(parser, args):
1102 """patch in a code review"""
1103 parser.add_option('-b', dest='newbranch',
1104 help='create a new branch off trunk for the patch')
1105 parser.add_option('-f', action='store_true', dest='force',
1106 help='with -b, clobber any existing branch')
1107 parser.add_option('--reject', action='store_true', dest='reject',
1108 help='allow failed patches and spew .rej files')
1109 parser.add_option('-n', '--no-commit', action='store_true', dest='nocommit',
1110 help="don't commit after patch applies")
1111 (options, args) = parser.parse_args(args)
1112 if len(args) != 1:
1113 parser.print_help()
1114 return 1
1115 input = args[0]
1116
1117 if re.match(r'\d+', input):
1118 # Input is an issue id. Figure out the URL.
1119 issue = input
1120 server = settings.GetDefaultServerUrl()
1121 fetch = urllib2.urlopen('%s/%s' % (server, issue)).read()
1122 m = re.search(r'/download/issue[0-9]+_[0-9]+.diff', fetch)
1123 if not m:
1124 DieWithError('Must pass an issue ID or full URL for '
1125 '\'Download raw patch set\'')
1126 url = '%s%s' % (server, m.group(0).strip())
1127 else:
1128 # Assume it's a URL to the patch. Default to http.
1129 input = FixUrl(input)
1130 match = re.match(r'.*?/issue(\d+)_\d+.diff', input)
1131 if match:
1132 issue = match.group(1)
1133 url = input
1134 else:
1135 DieWithError('Must pass an issue ID or full URL for '
1136 '\'Download raw patch set\'')
1137
1138 if options.newbranch:
1139 if options.force:
1140 RunGit(['branch', '-D', options.newbranch],
1141 swallow_stderr=True, error_ok=True)
1142 RunGit(['checkout', '-b', options.newbranch,
1143 Changelist().GetUpstreamBranch()])
1144
1145 # Switch up to the top-level directory, if necessary, in preparation for
1146 # applying the patch.
1147 top = RunGit(['rev-parse', '--show-cdup']).strip()
1148 if top:
1149 os.chdir(top)
1150
1151 patch_data = urllib2.urlopen(url).read()
1152 # Git patches have a/ at the beginning of source paths. We strip that out
1153 # with a sed script rather than the -p flag to patch so we can feed either
1154 # Git or svn-style patches into the same apply command.
1155 # re.sub() should be used but flags=re.MULTILINE is only in python 2.7.
1156 sed_proc = Popen(['sed', '-e', 's|^--- a/|--- |; s|^+++ b/|+++ |'],
1157 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
1158 patch_data = sed_proc.communicate(patch_data)[0]
1159 if sed_proc.returncode:
1160 DieWithError('Git patch mungling failed.')
1161 logging.info(patch_data)
1162 # We use "git apply" to apply the patch instead of "patch" so that we can
1163 # pick up file adds.
1164 # The --index flag means: also insert into the index (so we catch adds).
1165 cmd = ['git', 'apply', '--index', '-p0']
1166 if options.reject:
1167 cmd.append('--reject')
1168 patch_proc = Popen(cmd, stdin=subprocess.PIPE)
1169 patch_proc.communicate(patch_data)
1170 if patch_proc.returncode:
1171 DieWithError('Failed to apply the patch')
1172
1173 # If we had an issue, commit the current state and register the issue.
1174 if not options.nocommit:
1175 RunGit(['commit', '-m', 'patch from issue %s' % issue])
1176 cl = Changelist()
1177 cl.SetIssue(issue)
1178 print "Committed patch."
1179 else:
1180 print "Patch applied to index."
1181 return 0
1182
1183
1184def CMDrebase(parser, args):
1185 """rebase current branch on top of svn repo"""
1186 # Provide a wrapper for git svn rebase to help avoid accidental
1187 # git svn dcommit.
1188 # It's the only command that doesn't use parser at all since we just defer
1189 # execution to git-svn.
1190 RunGit(['svn', 'rebase'] + args, redirect_stdout=False)
1191 return 0
1192
1193
1194def GetTreeStatus():
1195 """Fetches the tree status and returns either 'open', 'closed',
1196 'unknown' or 'unset'."""
1197 url = settings.GetTreeStatusUrl(error_ok=True)
1198 if url:
1199 status = urllib2.urlopen(url).read().lower()
1200 if status.find('closed') != -1 or status == '0':
1201 return 'closed'
1202 elif status.find('open') != -1 or status == '1':
1203 return 'open'
1204 return 'unknown'
1205
1206 return 'unset'
1207
1208def GetTreeStatusReason():
1209 """Fetches the tree status from a json url and returns the message
1210 with the reason for the tree to be opened or closed."""
1211 # Don't import it at file level since simplejson is not installed by default
1212 # on python 2.5 and it is only used for git-cl tree which isn't often used,
1213 # forcing everyone to install simplejson isn't efficient.
1214 try:
1215 import simplejson as json
1216 except ImportError:
1217 try:
1218 import json
1219 # Some versions of python2.5 have an incomplete json module. Check to make
1220 # sure loads exists.
1221 json.loads
1222 except (ImportError, AttributeError):
1223 print >> sys.stderr, 'Please install simplejson'
1224 sys.exit(1)
1225
msb@chromium.orgbf1a7ba2011-02-01 16:21:46 +00001226 url = settings.GetTreeStatusUrl()
1227 json_url = urlparse.urljoin(url, '/current?format=json')
chase@chromium.orgcc51cd02010-12-23 00:48:39 +00001228 connection = urllib2.urlopen(json_url)
1229 status = json.loads(connection.read())
1230 connection.close()
1231 return status['message']
1232
1233def CMDtree(parser, args):
1234 """show the status of the tree"""
1235 (options, args) = parser.parse_args(args)
1236 status = GetTreeStatus()
1237 if 'unset' == status:
1238 print 'You must configure your tree status URL by running "git cl config".'
1239 return 2
1240
1241 print "The tree is %s" % status
1242 print
1243 print GetTreeStatusReason()
1244 if status != 'open':
1245 return 1
1246 return 0
1247
1248
1249def CMDupstream(parser, args):
1250 """print the name of the upstream branch, if any"""
1251 (options, args) = parser.parse_args(args)
1252 cl = Changelist()
1253 print cl.GetUpstreamBranch()
1254 return 0
1255
1256
1257def Command(name):
1258 return getattr(sys.modules[__name__], 'CMD' + name, None)
1259
1260
1261def CMDhelp(parser, args):
1262 """print list of commands or help for a specific command"""
1263 (options, args) = parser.parse_args(args)
1264 if len(args) == 1:
1265 return main(args + ['--help'])
1266 parser.print_help()
1267 return 0
1268
1269
1270def GenUsage(parser, command):
1271 """Modify an OptParse object with the function's documentation."""
1272 obj = Command(command)
1273 more = getattr(obj, 'usage_more', '')
1274 if command == 'help':
1275 command = '<command>'
1276 else:
1277 # OptParser.description prefer nicely non-formatted strings.
1278 parser.description = re.sub('[\r\n ]{2,}', ' ', obj.__doc__)
1279 parser.set_usage('usage: %%prog %s [options] %s' % (command, more))
1280
1281
1282def main(argv):
1283 """Doesn't parse the arguments here, just find the right subcommand to
1284 execute."""
1285 # Do it late so all commands are listed.
1286 CMDhelp.usage_more = ('\n\nCommands are:\n' + '\n'.join([
1287 ' %-10s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n')[0].strip())
1288 for fn in dir(sys.modules[__name__]) if fn.startswith('CMD')]))
1289
1290 # Create the option parse and add --verbose support.
1291 parser = optparse.OptionParser()
1292 parser.add_option('-v', '--verbose', action='store_true')
1293 old_parser_args = parser.parse_args
1294 def Parse(args):
1295 options, args = old_parser_args(args)
1296 if options.verbose:
1297 logging.basicConfig(level=logging.DEBUG)
1298 else:
1299 logging.basicConfig(level=logging.WARNING)
1300 return options, args
1301 parser.parse_args = Parse
1302
1303 if argv:
1304 command = Command(argv[0])
1305 if command:
1306 # "fix" the usage and the description now that we know the subcommand.
1307 GenUsage(parser, argv[0])
1308 try:
1309 return command(parser, argv[1:])
1310 except urllib2.HTTPError, e:
1311 if e.code != 500:
1312 raise
1313 DieWithError(
1314 ('AppEngine is misbehaving and returned HTTP %d, again. Keep faith '
1315 'and retry or visit go/isgaeup.\n%s') % (e.code, str(e)))
1316
1317 # Not a known command. Default to help.
1318 GenUsage(parser, 'help')
1319 return CMDhelp(parser, argv)
1320
1321
1322if __name__ == '__main__':
1323 sys.exit(main(sys.argv[1:]))