Changeset - d6ac3baaa15a
CONTRIBUTORS
Show inline comments
 
List of contributors to RhodeCode project:
 
    Marcin Kuźmiński <marcin@python-works.com>
 
    Lukasz Balcerzak <lukaszbalcerzak@gmail.com>
 
    Jason Harris <jason@jasonfharris.com>
 
    Thayne Harbaugh  <thayne@fusionio.com>
 
    cejones <>
 
    Thomas Waldmann <tw-public@gmx.de>
 
    Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
 
    Dmitri Kuznetsov <>
 
    Jared Bunting <jared.bunting@peachjean.com>
 
    Steve Romanow <slestak989@gmail.com>
 
    Augosto Hermann <augusto.herrmann@planejamento.gov.br>    
 
    Ankit Solanki <ankit.solanki@gmail.com>    
 
    Liad Shani <liadff@gmail.com>
 
    Les Peabody <lpeabody@gmail.com>
 
    Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
 
    Matt Zuba <matt.zuba@goodwillaz.org>
 
    Aras Pranckevicius <aras@unity3d.com>
 
    Tony Bussieres <t.bussieres@gmail.com>
 
    Erwin Kroon <e.kroon@smartmetersolutions.nl>
 
    nansenat16 <nansenat16@null.tw>
 
    Vincent Duvert <vincent@duvert.net>
 
    Takumi IINO <trot.thunder@gmail.com>
 
    Indra Talip <indra.talip@gmail.com>
 
    James Rhodes <jrhodes@redpointsoftware.com.au>
 
    Dominik Ruf <dominikruf@gmail.com>
 
    xpol <xpolife@gmail.com>
 
\ No newline at end of file
 
    xpol <xpolife@gmail.com>
 
    Vincent Caron <vcaron@bearstech.com>
 
    Zachary Auclair <zach101@gmail.com>
 
    Stefan Engel <mail@engel-stefan.de>
development.ini
Show inline comments
 
@@ -63,96 +63,106 @@ cut_off_limit = 256000
 
force_https = false
 
commit_parse_limit = 25
 
use_gravatar = true
 

	
 
## alternative_gravatar_url allows you to use your own avatar server application
 
## the following parts of the URL will be replaced
 
## {email}        user email
 
## {md5email}     md5 hash of the user email (like at gravatar.com)
 
## {size}         size of the image that is expected from the server application
 
## {scheme}       http/https from RhodeCode server
 
## {netloc}       network location from RhodeCode server
 
#alternative_gravatar_url = http://myavatarserver.com/getbyemail/{email}/{size}
 
#alternative_gravatar_url = http://myavatarserver.com/getbymd5/{md5email}?s={size}
 

	
 
container_auth_enabled = false
 
proxypass_auth_enabled = false
 
default_encoding = utf8
 

	
 
## overwrite schema of clone url
 
## available vars:
 
## scheme - http/https
 
## user - current user
 
## pass - password 
 
## netloc - network location
 
## path - usually repo_name
 

	
 
#clone_uri = {scheme}://{user}{pass}{netloc}{path}
 

	
 
## issue tracking mapping for commits messages
 
## comment out issue_pat, issue_server, issue_prefix to enable
 

	
 
## pattern to get the issues from commit messages
 
## default one used here is #<numbers> with a regex passive group for `#`
 
## {id} will be all groups matched from this pattern
 

	
 
issue_pat = (?:\s*#)(\d+)
 

	
 
## server url to the issue, each {id} will be replaced with match
 
## fetched from the regex and {repo} is replaced with full repository name
 
## including groups {repo_name} is replaced with just name of repo
 

	
 
issue_server_link = https://myissueserver.com/{repo}/issue/{id}
 

	
 
## prefix to add to link to indicate it's an url
 
## #314 will be replaced by <issue_prefix><id>
 

	
 
issue_prefix = #
 

	
 
## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
 
## multiple patterns, to other issues server, wiki or others
 
## below an example how to create a wiki pattern 
 
#  #wiki-some-id -> https://mywiki.com/some-id
 

	
 
#issue_pat_wiki = (?:wiki-)(.+)
 
#issue_server_link_wiki = https://mywiki.com/{id}
 
#issue_prefix_wiki = WIKI-
 

	
 

	
 
## instance-id prefix
 
## a prefix key for this instance used for cache invalidation when running 
 
## multiple instances of rhodecode, make sure it's globally unique for 
 
## all running rhodecode instances. Leave empty if you don't use it
 
instance_id = 
 

	
 
## alternative return HTTP header for failed authentication. Default HTTP
 
## response is 401 HTTPUnauthorized. Currently HG clients have troubles with 
 
## handling that. Set this variable to 403 to return HTTPForbidden
 
auth_ret_code =
 

	
 
####################################
 
###        CELERY CONFIG        ####
 
####################################
 
use_celery = false
 
broker.host = localhost
 
broker.vhost = rabbitmqhost
 
broker.port = 5672
 
broker.user = rabbitmq
 
broker.password = qweqwe
 

	
 
celery.imports = rhodecode.lib.celerylib.tasks
 

	
 
celery.result.backend = amqp
 
celery.result.dburi = amqp://
 
celery.result.serialier = json
 

	
 
#celery.send.task.error.emails = true
 
#celery.amqp.task.result.expires = 18000
 

	
 
celeryd.concurrency = 2
 
#celeryd.log.file = celeryd.log
 
celeryd.log.level = debug
 
celeryd.max.tasks.per.child = 1
 

	
 
#tasks will never be sent to the queue, but executed locally instead.
 
celery.always.eager = false
 

	
 
####################################
 
###         BEAKER CACHE        ####
 
####################################
 
beaker.cache.data_dir=%(here)s/data/cache/data
 
beaker.cache.lock_dir=%(here)s/data/cache/lock
 

	
 
beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
 

	
 
beaker.cache.super_short_term.type=memory
 
beaker.cache.super_short_term.expire=10
docs/changelog.rst
Show inline comments
 
.. _changelog:
 

	
 
=========
 
Changelog
 
=========
 

	
 

	
 
1.4.3 (**2012-09-28**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- #558 Added config file to hooks extra data
 
- bumped mercurial version to 2.3.1
 
- #518 added possibility of specifying multiple patterns for issues
 

	
 
fixes
 
+++++
 

	
 
- fixed #570 explicit users group permissions can overwrite owner permissions
 
- fixed #578 set proper PATH with current Python for Git
 
  hooks to execute within same Python as RhodeCode 
 
- fixed issue with Git bare repos that ends with .git in name
 

	
 
1.4.2 (**2012-09-12**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- added option to menu to quick lock/unlock repository for users that have
 
  write access to
 
- Implemented permissions for writing to repo
 
  groups. Now only write access to group allows to create a repostiory
 
  within that group
 
- #565 Add support for {netloc} and {scheme} to alternative_gravatar_url
 
- updated translation for zh_CN 
 

	
 
fixes
 
+++++
 

	
 
- fixed visual permissions check on repos groups inside groups
 
- fixed issues with non-ascii search terms in search, and indexers
 
- fixed parsing of page number in GET parameters
 
- fixed issues with generating pull-request overview for repos with
 
  bookmarks and tags, also preview doesn't loose chosen revision from
 
  select dropdown
 

	
 
1.4.1 (**2012-09-07**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- always put a comment about code-review status change even if user send
 
  empty data 
 
- modified_on column saves repository update and it's going to be used
 
  later for light version of main page ref #500
 
- pull request notifications send much nicer emails with details about pull
 
  request
 
- #551 show breadcrumbs in summary view for repositories inside a group
 

	
 
fixes
 
+++++
 

	
 
- fixed migrations of permissions that can lead to inconsistency.
 
  Some users sent feedback that after upgrading from older versions issues 
 
  with updating default permissions occurred. RhodeCode detects that now and
 
  resets default user permission to initial state if there is a need for that.
 
  Also forces users to set the default value for new forking permission. 
 
- #535 improved apache wsgi example configuration in docs
 
- fixes #550 mercurial repositories comparision failed when origin repo had
production.ini
Show inline comments
 
@@ -63,96 +63,106 @@ cut_off_limit = 256000
 
force_https = false
 
commit_parse_limit = 50
 
use_gravatar = true
 

	
 
## alternative_gravatar_url allows you to use your own avatar server application
 
## the following parts of the URL will be replaced
 
## {email}        user email
 
## {md5email}     md5 hash of the user email (like at gravatar.com)
 
## {size}         size of the image that is expected from the server application
 
## {scheme}       http/https from RhodeCode server
 
## {netloc}       network location from RhodeCode server
 
#alternative_gravatar_url = http://myavatarserver.com/getbyemail/{email}/{size}
 
#alternative_gravatar_url = http://myavatarserver.com/getbymd5/{md5email}?s={size}
 

	
 
container_auth_enabled = false
 
proxypass_auth_enabled = false
 
default_encoding = utf8
 

	
 
## overwrite schema of clone url
 
## available vars:
 
## scheme - http/https
 
## user - current user
 
## pass - password 
 
## netloc - network location
 
## path - usually repo_name
 

	
 
#clone_uri = {scheme}://{user}{pass}{netloc}{path}
 

	
 
## issue tracking mapping for commits messages
 
## comment out issue_pat, issue_server, issue_prefix to enable
 

	
 
## pattern to get the issues from commit messages
 
## default one used here is #<numbers> with a regex passive group for `#`
 
## {id} will be all groups matched from this pattern
 

	
 
issue_pat = (?:\s*#)(\d+)
 

	
 
## server url to the issue, each {id} will be replaced with match
 
## fetched from the regex and {repo} is replaced with full repository name
 
## including groups {repo_name} is replaced with just name of repo
 

	
 
issue_server_link = https://myissueserver.com/{repo}/issue/{id}
 

	
 
## prefix to add to link to indicate it's an url
 
## #314 will be replaced by <issue_prefix><id>
 

	
 
issue_prefix = #
 

	
 
## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
 
## multiple patterns, to other issues server, wiki or others
 
## below an example how to create a wiki pattern 
 
#  #wiki-some-id -> https://mywiki.com/some-id
 

	
 
#issue_pat_wiki = (?:wiki-)(.+)
 
#issue_server_link_wiki = https://mywiki.com/{id}
 
#issue_prefix_wiki = WIKI-
 

	
 

	
 
## instance-id prefix
 
## a prefix key for this instance used for cache invalidation when running 
 
## multiple instances of rhodecode, make sure it's globally unique for 
 
## all running rhodecode instances. Leave empty if you don't use it
 
instance_id = 
 

	
 
## alternative return HTTP header for failed authentication. Default HTTP
 
## response is 401 HTTPUnauthorized. Currently HG clients have troubles with 
 
## handling that. Set this variable to 403 to return HTTPForbidden
 
auth_ret_code =
 

	
 
####################################
 
###        CELERY CONFIG        ####
 
####################################
 
use_celery = false
 
broker.host = localhost
 
broker.vhost = rabbitmqhost
 
broker.port = 5672
 
broker.user = rabbitmq
 
broker.password = qweqwe
 

	
 
celery.imports = rhodecode.lib.celerylib.tasks
 

	
 
celery.result.backend = amqp
 
celery.result.dburi = amqp://
 
celery.result.serialier = json
 

	
 
#celery.send.task.error.emails = true
 
#celery.amqp.task.result.expires = 18000
 

	
 
celeryd.concurrency = 2
 
#celeryd.log.file = celeryd.log
 
celeryd.log.level = debug
 
celeryd.max.tasks.per.child = 1
 

	
 
#tasks will never be sent to the queue, but executed locally instead.
 
celery.always.eager = false
 

	
 
####################################
 
###         BEAKER CACHE        ####
 
####################################
 
beaker.cache.data_dir=%(here)s/data/cache/data
 
beaker.cache.lock_dir=%(here)s/data/cache/lock
 

	
 
beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
 

	
 
beaker.cache.super_short_term.type=memory
 
beaker.cache.super_short_term.expire=10
rhodecode/__init__.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.__init__
 
    ~~~~~~~~~~~~~~~~~~
 

	
 
    RhodeCode, a web based repository management based on pylons
 
    versioning implementation: http://www.python.org/dev/peps/pep-0386/
 

	
 
    :created_on: Apr 9, 2010
 
    :author: marcink
 
    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
 
    :license: GPLv3, see COPYING for more details.
 
"""
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
import sys
 
import platform
 

	
 
VERSION = (1, 4, 2)
 
VERSION = (1, 4, 3)
 

	
 
try:
 
    from rhodecode.lib import get_current_revision
 
    _rev = get_current_revision(quiet=True)
 
    if _rev and len(VERSION) > 3:
 
        VERSION += ('dev%s' % _rev[0],)
 
except ImportError:
 
    pass
 

	
 
__version__ = ('.'.join((str(each) for each in VERSION[:3])) +
 
               '.'.join(VERSION[3:]))
 
__dbversion__ = 7  # defines current db version for migrations
 
__platform__ = platform.system()
 
__license__ = 'GPLv3'
 
__py_version__ = sys.version_info
 
__author__ = 'Marcin Kuzminski'
 
__url__ = 'http://rhodecode.org'
 

	
 
PLATFORM_WIN = ('Windows')
 
PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS') #depracated
 

	
 
is_windows = __platform__ in PLATFORM_WIN
 
is_unix = not is_windows
 

	
 

	
 
BACKENDS = {
 
    'hg': 'Mercurial repository',
 
    'git': 'Git repository',
 
}
 

	
 
CELERY_ON = False
 
CELERY_EAGER = False
 

	
 
# link to config for pylons
 
CONFIG = {}
 

	
 
# Linked module for extensions
 
EXTENSIONS = {}
rhodecode/config/deployment.ini_tmpl
Show inline comments
 
@@ -63,96 +63,106 @@ cut_off_limit = 256000
 
force_https = false
 
commit_parse_limit = 50
 
use_gravatar = true
 

	
 
## alternative_gravatar_url allows you to use your own avatar server application
 
## the following parts of the URL will be replaced
 
## {email}        user email
 
## {md5email}     md5 hash of the user email (like at gravatar.com)
 
## {size}         size of the image that is expected from the server application
 
## {scheme}       http/https from RhodeCode server
 
## {netloc}       network location from RhodeCode server
 
#alternative_gravatar_url = http://myavatarserver.com/getbyemail/{email}/{size}
 
#alternative_gravatar_url = http://myavatarserver.com/getbymd5/{md5email}?s={size}
 

	
 
container_auth_enabled = false
 
proxypass_auth_enabled = false
 
default_encoding = utf8
 

	
 
## overwrite schema of clone url
 
## available vars:
 
## scheme - http/https
 
## user - current user
 
## pass - password 
 
## netloc - network location
 
## path - usually repo_name
 

	
 
#clone_uri = {scheme}://{user}{pass}{netloc}{path}
 

	
 
## issue tracking mapping for commits messages
 
## comment out issue_pat, issue_server, issue_prefix to enable
 

	
 
## pattern to get the issues from commit messages
 
## default one used here is #<numbers> with a regex passive group for `#`
 
## {id} will be all groups matched from this pattern
 

	
 
issue_pat = (?:\s*#)(\d+)
 

	
 
## server url to the issue, each {id} will be replaced with match
 
## fetched from the regex and {repo} is replaced with full repository name
 
## including groups {repo_name} is replaced with just name of repo
 

	
 
issue_server_link = https://myissueserver.com/{repo}/issue/{id}
 

	
 
## prefix to add to link to indicate it's an url
 
## #314 will be replaced by <issue_prefix><id>
 

	
 
issue_prefix = #
 

	
 
## issue_pat, issue_server_link, issue_prefix can have suffixes to specify
 
## multiple patterns, to other issues server, wiki or others
 
## below an example how to create a wiki pattern 
 
#  #wiki-some-id -> https://mywiki.com/some-id
 

	
 
#issue_pat_wiki = (?:wiki-)(.+)
 
#issue_server_link_wiki = https://mywiki.com/{id}
 
#issue_prefix_wiki = WIKI-
 

	
 

	
 
## instance-id prefix
 
## a prefix key for this instance used for cache invalidation when running 
 
## multiple instances of rhodecode, make sure it's globally unique for 
 
## all running rhodecode instances. Leave empty if you don't use it
 
instance_id = 
 

	
 
## alternative return HTTP header for failed authentication. Default HTTP
 
## response is 401 HTTPUnauthorized. Currently HG clients have troubles with 
 
## handling that. Set this variable to 403 to return HTTPForbidden
 
auth_ret_code =
 

	
 
####################################
 
###        CELERY CONFIG        ####
 
####################################
 
use_celery = false
 
broker.host = localhost
 
broker.vhost = rabbitmqhost
 
broker.port = 5672
 
broker.user = rabbitmq
 
broker.password = qweqwe
 

	
 
celery.imports = rhodecode.lib.celerylib.tasks
 

	
 
celery.result.backend = amqp
 
celery.result.dburi = amqp://
 
celery.result.serialier = json
 

	
 
#celery.send.task.error.emails = true
 
#celery.amqp.task.result.expires = 18000
 

	
 
celeryd.concurrency = 2
 
#celeryd.log.file = celeryd.log
 
celeryd.log.level = debug
 
celeryd.max.tasks.per.child = 1
 

	
 
#tasks will never be sent to the queue, but executed locally instead.
 
celery.always.eager = false
 

	
 
####################################
 
###         BEAKER CACHE        ####
 
####################################
 
beaker.cache.data_dir=%(here)s/data/cache/data
 
beaker.cache.lock_dir=%(here)s/data/cache/lock
 

	
 
beaker.cache.regions=super_short_term,short_term,long_term,sql_cache_short,sql_cache_med,sql_cache_long
 

	
 
beaker.cache.super_short_term.type=memory
 
beaker.cache.super_short_term.expire=10
rhodecode/controllers/pullrequests.py
Show inline comments
 
@@ -5,153 +5,161 @@
 

	
 
    pull requests controller for rhodecode for initializing pull requests
 

	
 
    :created_on: May 7, 2012
 
    :author: marcink
 
    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
 
    :license: GPLv3, see COPYING for more details.
 
"""
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
import logging
 
import traceback
 
import formencode
 

	
 
from webob.exc import HTTPNotFound, HTTPForbidden
 
from collections import defaultdict
 
from itertools import groupby
 

	
 
from pylons import request, response, session, tmpl_context as c, url
 
from pylons.controllers.util import abort, redirect
 
from pylons.i18n.translation import _
 
from pylons.decorators import jsonify
 

	
 
from rhodecode.lib.compat import json
 
from rhodecode.lib.base import BaseRepoController, render
 
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator,\
 
    NotAnonymous
 
from rhodecode.lib import helpers as h
 
from rhodecode.lib import diffs
 
from rhodecode.lib.utils import action_logger
 
from rhodecode.model.db import User, PullRequest, ChangesetStatus,\
 
    ChangesetComment
 
from rhodecode.model.pull_request import PullRequestModel
 
from rhodecode.model.meta import Session
 
from rhodecode.model.repo import RepoModel
 
from rhodecode.model.comment import ChangesetCommentsModel
 
from rhodecode.model.changeset_status import ChangesetStatusModel
 
from rhodecode.model.forms import PullRequestForm
 
from rhodecode.lib.vcs.exceptions import EmptyRepositoryError
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class PullrequestsController(BaseRepoController):
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def __before__(self):
 
        super(PullrequestsController, self).__before__()
 
        repo_model = RepoModel()
 
        c.users_array = repo_model.get_users_js()
 
        c.users_groups_array = repo_model.get_users_groups_js()
 

	
 
    def _get_repo_refs(self, repo):
 
        hist_l = []
 

	
 
        branches_group = ([('branch:%s:%s' % (k, v), k) for
 
                         k, v in repo.branches.iteritems()], _("Branches"))
 
        bookmarks_group = ([('book:%s:%s' % (k, v), k) for
 
                         k, v in repo.bookmarks.iteritems()], _("Bookmarks"))
 
        tags_group = ([('tag:%s:%s' % (k, v), k) for
 
                         k, v in repo.tags.iteritems()], _("Tags"))
 

	
 
        hist_l.append(bookmarks_group)
 
        hist_l.append(branches_group)
 
        hist_l.append(tags_group)
 

	
 
        return hist_l
 

	
 
    def _get_default_rev(self, repo):
 
        """
 
        Get's default revision to do compare on pull request
 

	
 
        :param repo:
 
        """
 
        repo = repo.scm_instance
 
        if 'default' in repo.branches:
 
            return 'default'
 
        else:
 
            #if repo doesn't have default branch return first found
 
            return repo.branches.keys()[0]
 

	
 
    def show_all(self, repo_name):
 
        c.pull_requests = PullRequestModel().get_all(repo_name)
 
        c.repo_name = repo_name
 
        return render('/pullrequests/pullrequest_show_all.html')
 

	
 
    @NotAnonymous()
 
    def index(self):
 
        org_repo = c.rhodecode_db_repo
 

	
 
        if org_repo.scm_instance.alias != 'hg':
 
            log.error('Review not available for GIT REPOS')
 
            raise HTTPNotFound
 

	
 
        try:
 
            org_repo.scm_instance.get_changeset()
 
        except EmptyRepositoryError, e:
 
            h.flash(h.literal(_('There are no changesets yet')),
 
                    category='warning')
 
            redirect(url('summary_home', repo_name=org_repo.repo_name))
 

	
 
        other_repos_info = {}
 

	
 
        c.org_refs = self._get_repo_refs(c.rhodecode_repo)
 
        c.org_repos = []
 
        c.other_repos = []
 
        c.org_repos.append((org_repo.repo_name, '%s/%s' % (
 
                                org_repo.user.username, c.repo_name))
 
                           )
 

	
 
        # add org repo to other so we can open pull request agains itself
 
        c.other_repos.extend(c.org_repos)
 

	
 
        c.default_pull_request = org_repo.repo_name  # repo name pre-selected
 
        c.default_pull_request_rev = self._get_default_rev(org_repo)  # revision pre-selected
 
        c.default_revs = self._get_repo_refs(org_repo.scm_instance)
 
        #add orginal repo
 
        other_repos_info[org_repo.repo_name] = {
 
            'gravatar': h.gravatar_url(org_repo.user.email, 24),
 
            'description': org_repo.description,
 
            'revs': h.select('other_ref', '', c.default_revs, class_='refs')
 
        }
 

	
 
        #gather forks and add to this list
 
        for fork in org_repo.forks:
 
            c.other_repos.append((fork.repo_name, '%s/%s' % (
 
                                    fork.user.username, fork.repo_name))
 
                                 )
 
            other_repos_info[fork.repo_name] = {
 
                'gravatar': h.gravatar_url(fork.user.email, 24),
 
                'description': fork.description,
 
                'revs': h.select('other_ref', '',
 
                                 self._get_repo_refs(fork.scm_instance),
 
                                 class_='refs')
 
            }
 
        #add parents of this fork also
 
        if org_repo.parent:
 
            c.default_pull_request = org_repo.parent.repo_name
 
            c.default_pull_request_rev = self._get_default_rev(org_repo.parent)
 
            c.default_revs = self._get_repo_refs(org_repo.parent.scm_instance)
 
            c.other_repos.append((org_repo.parent.repo_name, '%s/%s' % (
 
                                        org_repo.parent.user.username,
 
                                        org_repo.parent.repo_name))
 
                                     )
 
            other_repos_info[org_repo.parent.repo_name] = {
 
                'gravatar': h.gravatar_url(org_repo.parent.user.email, 24),
 
                'description': org_repo.parent.description,
 
                'revs': h.select('other_ref', '',
 
                                 self._get_repo_refs(org_repo.parent.scm_instance),
rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.mo
Show inline comments
 
binary diff not shown
rhodecode/i18n/zh_CN/LC_MESSAGES/rhodecode.po
Show inline comments
 
# Chinese (China) translations for RhodeCode.
 
# Copyright (C) 2011 ORGANIZATION
 
# This file is distributed under the same license as the RhodeCode project.
 
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
 
# mikespook <mikespook@gmail.com>, 2012.
 
# xpol <xpolife@gmail.com>, 2012.
 
msgid ""
 
msgstr ""
 
"Project-Id-Version: RhodeCode 1.2.0\n"
 
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
 
"POT-Creation-Date: 2012-09-02 20:30+0200\n"
 
"PO-Revision-Date: 2012-09-19 13:27+0800\n"
 
"POT-Creation-Date: 2012-09-21 14:39+0800\n"
 
"PO-Revision-Date: 2012-09-21 15:20+0800\n"
 
"Last-Translator: xpol <xpolife@gmail.com>\n"
 
"Language-Team: mikespook\n"
 
"Plural-Forms: nplurals=1; plural=0;\n"
 
"MIME-Version: 1.0\n"
 
"Content-Type: text/plain; charset=UTF-8\n"
 
"Content-Transfer-Encoding: 8bit\n"
 
"Generated-By: Babel 0.9.6\n"
 
"X-Generator: Poedit 1.5.3\n"
 
"X-Poedit-Basepath: E:\\home\\rhodecode\n"
 
"X-Poedit-SourceCharset: UTF-8\n"
 

	
 
#: rhodecode/controllers/changelog.py:94
 

	
 
#: rhodecode/controllers/changelog.py:95
 
msgid "All Branches"
 
msgstr "所有分支"
 

	
 
#: rhodecode/controllers/changeset.py:83
 
msgid "show white space"
 
msgstr "显示空白字符"
 

	
 
#: rhodecode/controllers/changeset.py:90 rhodecode/controllers/changeset.py:97
 
msgid "ignore white space"
 
msgstr "忽略空白字符"
 

	
 
#: rhodecode/controllers/changeset.py:157
 
#, python-format
 
msgid "%s line context"
 
msgstr "%s 行上下文"
 

	
 
#: rhodecode/controllers/changeset.py:333
 
#: rhodecode/controllers/changeset.py:348 rhodecode/lib/diffs.py:70
 
#: rhodecode/controllers/changeset.py:348 rhodecode/lib/diffs.py:71
 
msgid "binary file"
 
msgstr "二进制文件"
 

	
 
#: rhodecode/controllers/changeset.py:408
 
#: rhodecode/controllers/changeset.py:381
 
#: rhodecode/controllers/pullrequests.py:368
 
#, python-format
 
#| msgid "Last change"
 
msgid "Status change -> %s"
 
msgstr "状态改变 -> %s"
 

	
 
#: rhodecode/controllers/changeset.py:412
 
msgid ""
 
"Changing status on a changeset associated witha closed pull request is not "
 
"allowed"
 
msgstr "不允许修改已关闭拉取请求的修订集状态"
 

	
 
#: rhodecode/controllers/compare.py:69
 
#: rhodecode/controllers/compare.py:72
 
msgid "There are no changesets yet"
 
msgstr "还没有修订集"
 

	
 
#: rhodecode/controllers/error.py:69
 
msgid "Home page"
 
msgstr "主页"
 

	
 
#: rhodecode/controllers/error.py:98
 
msgid ""
 
"The request could not be understood by the server due to malformed syntax."
 
msgstr "由于错误的语法,服务器无法对请求进行响应。"
 

	
 
#: rhodecode/controllers/error.py:101
 
msgid "Unauthorized access to resource"
 
msgstr "未授权的资源访问"
 

	
 
#: rhodecode/controllers/error.py:103
 
msgid "You don't have permission to view this page"
 
msgstr "无权访问该页面"
 

	
 
#: rhodecode/controllers/error.py:105
 
msgid "The resource could not be found"
 
msgstr "资源未找到"
 

	
 
#: rhodecode/controllers/error.py:107
 
msgid ""
 
"The server encountered an unexpected condition which prevented it from "
 
"fulfilling the request."
 
msgstr "服务进入非预期的混乱状态,这会阻止它对请求进行响应。"
 

	
 
#: rhodecode/controllers/feed.py:49
 
#, python-format
 
msgid "Changes on %s repository"
 
msgstr "%s 库的修改"
 

	
 
#: rhodecode/controllers/feed.py:50
 
#, python-format
 
msgid "%s %s feed"
 
msgstr "%s %s 订阅"
 

	
 
#: rhodecode/controllers/feed.py:75
 
#: rhodecode/controllers/feed.py:67
 
#: rhodecode/templates/changeset/changeset.html:119
 
msgid "Changeset was too big and was cut off..."
 
msgstr "修订集太大已被截断......"
 

	
 
#: rhodecode/controllers/feed.py:81
 
msgid "commited on"
 
msgstr "提交于"
 

	
 
#: rhodecode/controllers/files.py:84
 
msgid "click here to add new file"
 
msgstr "点击此处添加新文件"
 

	
 
#: rhodecode/controllers/files.py:85
 
#, python-format
 
msgid "There are no files yet %s"
 
msgstr "还没有文件 %s"
 

	
 
#: rhodecode/controllers/files.py:239 rhodecode/controllers/files.py:299
 
#, python-format
 
msgid "This repository is has been locked by %s on %s"
 
msgstr "版本库由 %s 于 %s 锁定"
 

	
 
#: rhodecode/controllers/files.py:266
 
#, python-format
 
msgid "Edited %s via RhodeCode"
 
msgstr "通过 RhodeCode 修改了 %s"
 

	
 
#: rhodecode/controllers/files.py:271
 
msgid "No changes"
 
msgstr "无变更"
 

	
 
#: rhodecode/controllers/files.py:282 rhodecode/controllers/files.py:346
 
#, python-format
 
msgid "Successfully committed to %s"
 
msgstr "成功提交到 %s"
 

	
 
#: rhodecode/controllers/files.py:287 rhodecode/controllers/files.py:352
 
msgid "Error occurred during commit"
 
msgstr "提交时发生错误"
 

	
 
#: rhodecode/controllers/files.py:318
 
#, python-format
 
msgid "Added %s via RhodeCode"
 
msgstr "通过 RhodeCode 添加了 %s"
 

	
 
#: rhodecode/controllers/files.py:332
 
msgid "No content"
 
msgstr "无内容"
 

	
 
#: rhodecode/controllers/files.py:336
 
msgid "No filename"
 
msgstr "无文件名"
 

	
 
#: rhodecode/controllers/files.py:378
 
msgid "downloads disabled"
 
msgstr "禁止下载"
 

	
 
#: rhodecode/controllers/files.py:389
 
#, python-format
 
msgid "Unknown revision %s"
 
msgstr "未知版本 %s"
 

	
 
#: rhodecode/controllers/files.py:391
 
msgid "Empty repository"
 
msgstr "空版本库"
 

	
 
#: rhodecode/controllers/files.py:393
 
msgid "Unknown archive type"
 
msgstr "未知包类型"
 

	
 
#: rhodecode/controllers/files.py:494
 
#: rhodecode/templates/changeset/changeset_range.html:13
 
#: rhodecode/templates/changeset/changeset_range.html:31
 
msgid "Changesets"
 
msgstr "修订集"
 

	
 
#: rhodecode/controllers/files.py:495 rhodecode/controllers/pullrequests.py:72
 
#: rhodecode/controllers/summary.py:232 rhodecode/model/scm.py:543
 
#: rhodecode/controllers/summary.py:234 rhodecode/model/scm.py:543
 
msgid "Branches"
 
msgstr "分支"
 

	
 
#: rhodecode/controllers/files.py:496 rhodecode/controllers/pullrequests.py:76
 
#: rhodecode/controllers/summary.py:233 rhodecode/model/scm.py:554
 
#: rhodecode/controllers/summary.py:235 rhodecode/model/scm.py:554
 
msgid "Tags"
 
msgstr "标签"
 

	
 
#: rhodecode/controllers/forks.py:73 rhodecode/controllers/admin/repos.py:90
 
#: rhodecode/controllers/forks.py:74 rhodecode/controllers/admin/repos.py:90
 
#, python-format
 
msgid ""
 
"%s repository is not mapped to db perhaps it was created or renamed from the "
 
"filesystem please run the application again in order to rescan repositories"
 
msgstr ""
 
"版本库 %s 没有映射到数据库,可能是从文件系统创建或者重命名,请重启 RhodeCode "
 
"以重新扫描版本库"
 

	
 
#: rhodecode/controllers/forks.py:133 rhodecode/controllers/settings.py:72
 
#: rhodecode/controllers/forks.py:134 rhodecode/controllers/settings.py:73
 
#, python-format
 
msgid ""
 
"%s repository is not mapped to db perhaps it was created or renamed from the "
 
"file system please run the application again in order to rescan repositories"
 
msgstr ""
 
" 版本库 %s 没有映射到数据库,可能是从文件系统创建或者重命名,请重启 "
 
"RhodeCode 以重新扫描版本库"
 

	
 
#: rhodecode/controllers/forks.py:167
 
#: rhodecode/controllers/forks.py:168
 
#, python-format
 
msgid "forked %s repository as %s"
 
msgstr "版本库 %s 被分支到 %s"
 

	
 
#: rhodecode/controllers/forks.py:181
 
#: rhodecode/controllers/forks.py:182
 
#, python-format
 
msgid "An error occurred during repository forking %s"
 
msgstr "在分支版本库 %s 的时候发生错误"
 

	
 
#: rhodecode/controllers/journal.py:202 rhodecode/controllers/journal.py:239
 
#: rhodecode/controllers/journal.py:203 rhodecode/controllers/journal.py:240
 
msgid "public journal"
 
msgstr "公共日志"
 

	
 
#: rhodecode/controllers/journal.py:206 rhodecode/controllers/journal.py:243
 
#: rhodecode/templates/base/base.html:220
 
#: rhodecode/controllers/journal.py:207 rhodecode/controllers/journal.py:244
 
#: rhodecode/templates/base/base.html:229
 
msgid "journal"
 
msgstr "日志"
 

	
 
#: rhodecode/controllers/login.py:143
 
msgid "You have successfully registered into rhodecode"
 
msgstr "成功注册到 rhodecode"
 

	
 
#: rhodecode/controllers/login.py:164
 
msgid "Your password reset link was sent"
 
msgstr "密码重置链接已经发送"
 

	
 
#: rhodecode/controllers/login.py:184
 
msgid ""
 
"Your password reset was successful, new password has been sent to your email"
 
msgstr "密码已经成功重置,新密码已经发送到你的邮箱"
 

	
 
#: rhodecode/controllers/pullrequests.py:74 rhodecode/model/scm.py:549
 
msgid "Bookmarks"
 
msgstr "书签"
 

	
 
#: rhodecode/controllers/pullrequests.py:158
 
#: rhodecode/controllers/pullrequests.py:174
 
msgid "Pull request requires a title with min. 3 chars"
 
msgstr "拉取请求的标题至少 3 个字符"
 

	
 
#: rhodecode/controllers/pullrequests.py:160
 
#: rhodecode/controllers/pullrequests.py:176
 
msgid "error during creation of pull request"
 
msgstr "提交拉取请求时发生错误"
 

	
 
#: rhodecode/controllers/pullrequests.py:181
 
#: rhodecode/controllers/pullrequests.py:197
 
msgid "Successfully opened new pull request"
 
msgstr "成功提交拉取请求"
 

	
 
#: rhodecode/controllers/pullrequests.py:184
 
#: rhodecode/controllers/pullrequests.py:200
 
msgid "Error occurred during sending pull request"
 
msgstr "提交拉取请求时发生错误"
 

	
 
#: rhodecode/controllers/pullrequests.py:217
 
#: rhodecode/controllers/pullrequests.py:233
 
msgid "Successfully deleted pull request"
 
msgstr "成功删除拉取请求"
 

	
 
#: rhodecode/controllers/search.py:131
 
#: rhodecode/controllers/search.py:132
 
msgid "Invalid search query. Try quoting it."
 
msgstr "错误的搜索。请尝试用引号包含它。"
 

	
 
#: rhodecode/controllers/search.py:136
 
#: rhodecode/controllers/search.py:137
 
msgid "There is no index to search in. Please run whoosh indexer"
 
msgstr "没有索引用于搜索。请运行 whoosh 索引器"
 

	
 
#: rhodecode/controllers/search.py:140
 
#: rhodecode/controllers/search.py:141
 
msgid "An error occurred during this search operation"
 
msgstr "在搜索操作中发生异常"
 

	
 
#: rhodecode/controllers/settings.py:107
 
#: rhodecode/controllers/settings.py:108
 
#: rhodecode/controllers/admin/repos.py:266
 
#, python-format
 
msgid "Repository %s updated successfully"
 
msgstr "版本库 %s 成功更新"
 

	
 
#: rhodecode/controllers/settings.py:125
 
#: rhodecode/controllers/settings.py:126
 
#: rhodecode/controllers/admin/repos.py:284
 
#, python-format
 
msgid "error occurred during update of repository %s"
 
msgstr "在更新版本库 %s 的时候发生错误"
 

	
 
#: rhodecode/controllers/settings.py:143
 
#: rhodecode/controllers/settings.py:144
 
#: rhodecode/controllers/admin/repos.py:302
 
#, python-format
 
msgid ""
 
"%s repository is not mapped to db perhaps it was moved or renamed  from the "
 
"filesystem please run the application again in order to rescan repositories"
 
msgstr ""
 
"版本库 %s 没有映射到数据库,可能是从文件系统创建或者重命名,请重启 RhodeCode "
 
"以重新扫描版本库"
 

	
 
#: rhodecode/controllers/settings.py:155
 
#: rhodecode/controllers/settings.py:156
 
#: rhodecode/controllers/admin/repos.py:314
 
#, python-format
 
msgid "deleted repository %s"
 
msgstr "已经删除版本库 %s"
 

	
 
#: rhodecode/controllers/settings.py:159
 
#: rhodecode/controllers/settings.py:160
 
#: rhodecode/controllers/admin/repos.py:324
 
#: rhodecode/controllers/admin/repos.py:330
 
#, python-format
 
msgid "An error occurred during deletion of %s"
 
msgstr "在删除 %s 的时候发生错误"
 

	
 
#: rhodecode/controllers/settings.py:179
 
#| msgid "unlock"
 
msgid "unlocked"
 
msgstr "未锁"
 

	
 
#: rhodecode/controllers/settings.py:182
 
#| msgid "unlock"
 
msgid "locked"
 
msgstr "已锁"
 

	
 
#: rhodecode/controllers/settings.py:184
 
#, python-format
 
#| msgid "forked %s repository as %s"
 
msgid "Repository has been %s"
 
msgstr "版本库已经 %s"
 

	
 
#: rhodecode/controllers/settings.py:188
 
#: rhodecode/controllers/admin/repos.py:422
 
msgid "An error occurred during unlocking"
 
msgstr "解锁时发生错误"
 

	
 
#: rhodecode/controllers/summary.py:138
 
msgid "No data loaded yet"
 
msgstr "数据未加载"
 

	
 
#: rhodecode/controllers/summary.py:142
 
#: rhodecode/templates/summary/summary.html:148
 
msgid "Statistics are disabled for this repository"
 
msgstr "该版本库统计功能已经禁用"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:50
 
msgid "BASE"
 
msgstr "BASE"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:51
 
msgid "ONELEVEL"
 
msgstr "ONELEVEL"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:52
 
msgid "SUBTREE"
 
msgstr "SUBTREE"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:56
 
msgid "NEVER"
 
msgstr "NEVER"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:57
 
msgid "ALLOW"
 
msgstr "ALLOW"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:58
 
msgid "TRY"
 
msgstr "TRY"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:59
 
msgid "DEMAND"
 
msgstr "DEMAND"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:60
 
msgid "HARD"
 
msgstr "HARD"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:64
 
msgid "No encryption"
 
msgstr "未加密"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:65
 
msgid "LDAPS connection"
 
msgstr "LDAPS 连接"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:66
 
msgid "START_TLS on LDAP connection"
 
msgstr "LDAP 连接上的 START_TLS"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:126
 
msgid "Ldap settings updated successfully"
 
msgstr "LDAP 设置已经成功更新"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:130
 
msgid "Unable to activate ldap. The \"python-ldap\" library is missing."
 
msgstr "无法启用 LDAP。缺失“python-ldap”库。"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:147
 
msgid "error occurred during update of ldap settings"
 
msgstr "更新 LDAP 设置时发生错误"
 

	
 
#: rhodecode/controllers/admin/permissions.py:59
 
msgid "None"
 
msgstr "无"
 

	
 
#: rhodecode/controllers/admin/permissions.py:60
 
msgid "Read"
 
msgstr "读"
 

	
 
#: rhodecode/controllers/admin/permissions.py:61
 
msgid "Write"
 
msgstr "写"
 

	
 
#: rhodecode/controllers/admin/permissions.py:62
 
#: rhodecode/templates/admin/ldap/ldap.html:9
 
#: rhodecode/templates/admin/permissions/permissions.html:9
 
#: rhodecode/templates/admin/repos/repo_add.html:9
 
#: rhodecode/templates/admin/repos/repo_edit.html:9
 
#: rhodecode/templates/admin/repos/repos.html:9
 
#: rhodecode/templates/admin/repos_groups/repos_groups_add.html:8
 
#: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:8
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:10
 
#: rhodecode/templates/admin/settings/hooks.html:9
 
#: rhodecode/templates/admin/settings/settings.html:9
 
#: rhodecode/templates/admin/users/user_add.html:8
 
#: rhodecode/templates/admin/users/user_edit.html:9
 
#: rhodecode/templates/admin/users/user_edit.html:122
 
#: rhodecode/templates/admin/users/users.html:9
 
#: rhodecode/templates/admin/users_groups/users_group_add.html:8
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:9
 
#: rhodecode/templates/admin/users_groups/users_groups.html:9
 
#: rhodecode/templates/base/base.html:197
 
#: rhodecode/templates/base/base.html:337
 
#: rhodecode/templates/base/base.html:339
 
#: rhodecode/templates/base/base.html:341
 
#: rhodecode/templates/base/base.html:346
 
#: rhodecode/templates/base/base.html:348
 
#: rhodecode/templates/base/base.html:350
 
msgid "Admin"
 
msgstr "管理"
 

	
 
#: rhodecode/controllers/admin/permissions.py:65
 
msgid "disabled"
 
msgstr "禁用"
 

	
 
#: rhodecode/controllers/admin/permissions.py:67
 
msgid "allowed with manual account activation"
 
msgstr "允许手工启用帐号"
 

	
 
#: rhodecode/controllers/admin/permissions.py:69
 
msgid "allowed with automatic account activation"
 
msgstr "允许自动启用帐号"
 

	
 
#: rhodecode/controllers/admin/permissions.py:71
 
#: rhodecode/controllers/admin/permissions.py:74
 
msgid "Disabled"
 
msgstr "停用"
 

	
 
#: rhodecode/controllers/admin/permissions.py:72
 
#: rhodecode/controllers/admin/permissions.py:75
 
msgid "Enabled"
 
msgstr "启用"
 

	
 
#: rhodecode/controllers/admin/permissions.py:116
 
msgid "Default permissions updated successfully"
 
msgstr "成功更新默认权限"
 

	
 
#: rhodecode/controllers/admin/permissions.py:130
 
msgid "error occurred during update of permissions"
 
msgstr "更新权限时发生错误"
 

	
 
#: rhodecode/controllers/admin/repos.py:123
 
msgid "--REMOVE FORK--"
 
msgstr "-- 移除分支 --"
 

	
 
#: rhodecode/controllers/admin/repos.py:192
 
#, python-format
 
msgid "created repository %s from %s"
 
msgstr "新版本库 %s 基于 %s 建立。"
 

	
 
#: rhodecode/controllers/admin/repos.py:196
 
#, python-format
 
msgid "created repository %s"
 
msgstr "建立版本库 %s"
 

	
 
#: rhodecode/controllers/admin/repos.py:227
 
#, python-format
 
msgid "error occurred during creation of repository %s"
 
msgstr "创建版本库时发生错误 %s"
 

	
 
#: rhodecode/controllers/admin/repos.py:319
 
#, python-format
 
msgid "Cannot delete %s it still contains attached forks"
 
msgstr "无法删除 %s 因为它还有其他分支版本库"
 

	
 
#: rhodecode/controllers/admin/repos.py:348
 
msgid "An error occurred during deletion of repository user"
 
msgstr "删除版本库用户时发生错误"
 

	
 
#: rhodecode/controllers/admin/repos.py:367
 
msgid "An error occurred during deletion of repository users groups"
 
msgstr "删除版本库用户组时发生错误"
 

	
 
#: rhodecode/controllers/admin/repos.py:385
 
msgid "An error occurred during deletion of repository stats"
 
msgstr "删除版本库统计时发生错误"
 

	
 
#: rhodecode/controllers/admin/repos.py:402
 
msgid "An error occurred during cache invalidation"
 
msgstr "清除缓存时发生错误"
 

	
 
#: rhodecode/controllers/admin/repos.py:422
 
msgid "An error occurred during unlocking"
 
msgstr "解锁时发生错误"
 

	
 
#: rhodecode/controllers/admin/repos.py:442
 
msgid "Updated repository visibility in public journal"
 
msgstr "成功更新在公共日志中的可见性"
 

	
 
#: rhodecode/controllers/admin/repos.py:446
 
msgid "An error occurred during setting this repository in public journal"
 
msgstr "设置版本库到公共日志时发生错误"
 

	
 
#: rhodecode/controllers/admin/repos.py:451 rhodecode/model/validators.py:299
 
#: rhodecode/controllers/admin/repos.py:451 rhodecode/model/validators.py:300
 
msgid "Token mismatch"
 
msgstr "令牌不匹配"
 

	
 
#: rhodecode/controllers/admin/repos.py:464
 
msgid "Pulled from remote location"
 
msgstr "成功拉取自远程路径"
 

	
 
#: rhodecode/controllers/admin/repos.py:466
 
msgid "An error occurred during pull from remote location"
 
msgstr "从远程路径拉取时发生错误"
 

	
 
#: rhodecode/controllers/admin/repos.py:482
 
msgid "Nothing"
 
msgstr "无"
 

	
 
#: rhodecode/controllers/admin/repos.py:484
 
#, python-format
 
msgid "Marked repo %s as fork of %s"
 
msgstr "成功将版本库 %s 标记为从 %s 分支"
 

	
 
#: rhodecode/controllers/admin/repos.py:488
 
msgid "An error occurred during this operation"
 
msgstr "在搜索操作中发生错误"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:116
 
#: rhodecode/controllers/admin/repos_groups.py:117
 
#, python-format
 
msgid "created repos group %s"
 
msgstr "建立版本库组 %s"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:129
 
#: rhodecode/controllers/admin/repos_groups.py:130
 
#, python-format
 
msgid "error occurred during creation of repos group %s"
 
msgstr "创建版本库组时发生错误 %s"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:163
 
#: rhodecode/controllers/admin/repos_groups.py:164
 
#, python-format
 
msgid "updated repos group %s"
 
msgstr "更新版本库组 %s"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:176
 
#: rhodecode/controllers/admin/repos_groups.py:177
 
#, python-format
 
msgid "error occurred during update of repos group %s"
 
msgstr "更新版本库组时发生错误 %s"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:194
 
#: rhodecode/controllers/admin/repos_groups.py:195
 
#, python-format
 
msgid "This group contains %s repositores and cannot be deleted"
 
msgstr "这个组内有 %s 个版本库因而无法删除"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:202
 
#: rhodecode/controllers/admin/repos_groups.py:203
 
#, python-format
 
msgid "removed repos group %s"
 
msgstr "移除版本库组 %s"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:208
 
#: rhodecode/controllers/admin/repos_groups.py:209
 
msgid "Cannot delete this group it still contains subgroups"
 
msgstr "不能删除包含子组的组"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:213
 
#: rhodecode/controllers/admin/repos_groups.py:218
 
#: rhodecode/controllers/admin/repos_groups.py:214
 
#: rhodecode/controllers/admin/repos_groups.py:219
 
#, python-format
 
msgid "error occurred during deletion of repos group %s"
 
msgstr "删除版本库组时发生错误 %s"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:238
 
#: rhodecode/controllers/admin/repos_groups.py:240
 
msgid "An error occurred during deletion of group user"
 
msgstr "删除组用户时发生错误"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:258
 
#: rhodecode/controllers/admin/repos_groups.py:261
 
msgid "An error occurred during deletion of group users groups"
 
msgstr "删除版本库组的用户组时发生错误"
 

	
 
#: rhodecode/controllers/admin/settings.py:121
 
#: rhodecode/controllers/admin/settings.py:122
 
#, python-format
 
msgid "Repositories successfully rescanned added: %s,removed: %s"
 
msgstr "重新扫描版本库成功,增加 %s, 移除 %s"
 

	
 
#: rhodecode/controllers/admin/settings.py:129
 
#: rhodecode/controllers/admin/settings.py:130
 
msgid "Whoosh reindex task scheduled"
 
msgstr "Whoosh 重新索引任务调度"
 

	
 
#: rhodecode/controllers/admin/settings.py:160
 
#: rhodecode/controllers/admin/settings.py:161
 
msgid "Updated application settings"
 
msgstr "更新应用设置"
 

	
 
#: rhodecode/controllers/admin/settings.py:164
 
#: rhodecode/controllers/admin/settings.py:275
 
#: rhodecode/controllers/admin/settings.py:165
 
#: rhodecode/controllers/admin/settings.py:293
 
msgid "error occurred during updating application settings"
 
msgstr "更新设置时发生错误"
 

	
 
#: rhodecode/controllers/admin/settings.py:200
 
#: rhodecode/controllers/admin/settings.py:201
 
msgid "Updated visualisation settings"
 
msgstr "成功更新可视化设置"
 

	
 
#: rhodecode/controllers/admin/settings.py:205
 
#: rhodecode/controllers/admin/settings.py:206
 
msgid "error occurred during updating visualisation settings"
 
msgstr "更新可视化设置时发生错误"
 

	
 
#: rhodecode/controllers/admin/settings.py:271
 
#: rhodecode/controllers/admin/settings.py:289
 
msgid "Updated VCS settings"
 
msgstr "成功更新版本控制系统设置"
 

	
 
#: rhodecode/controllers/admin/settings.py:285
 
#: rhodecode/controllers/admin/settings.py:303
 
msgid "Added new hook"
 
msgstr "新建钩子"
 

	
 
#: rhodecode/controllers/admin/settings.py:297
 
#: rhodecode/controllers/admin/settings.py:315
 
msgid "Updated hooks"
 
msgstr "更新钩子"
 

	
 
#: rhodecode/controllers/admin/settings.py:301
 
#: rhodecode/controllers/admin/settings.py:319
 
msgid "error occurred during hook creation"
 
msgstr "创建钩子时发生错误"
 

	
 
#: rhodecode/controllers/admin/settings.py:320
 
#: rhodecode/controllers/admin/settings.py:338
 
msgid "Email task created"
 
msgstr "已创建电子邮件任务"
 

	
 
#: rhodecode/controllers/admin/settings.py:375
 
#: rhodecode/controllers/admin/settings.py:393
 
msgid "You can't edit this user since it's crucial for entire application"
 
msgstr "由于是系统帐号,无法编辑该用户"
 

	
 
#: rhodecode/controllers/admin/settings.py:406
 
#: rhodecode/controllers/admin/settings.py:424
 
msgid "Your account was updated successfully"
 
msgstr "你的帐号已经更新完成"
 

	
 
#: rhodecode/controllers/admin/settings.py:421
 
#: rhodecode/controllers/admin/settings.py:439
 
#: rhodecode/controllers/admin/users.py:191
 
#, python-format
 
msgid "error occurred during update of user %s"
 
msgstr "更新用户 %s 时发生错误"
 

	
 
#: rhodecode/controllers/admin/users.py:130
 
#, python-format
 
msgid "created user %s"
 
msgstr "创建用户 %s"
 

	
 
#: rhodecode/controllers/admin/users.py:142
 
#, python-format
 
msgid "error occurred during creation of user %s"
 
msgstr "创建用户 %s 时发生错误"
 

	
 
#: rhodecode/controllers/admin/users.py:171
 
msgid "User updated successfully"
 
msgstr "用户更新成功"
 

	
 
#: rhodecode/controllers/admin/users.py:207
 
msgid "successfully deleted user"
 
msgstr "用户删除成功"
 

	
 
#: rhodecode/controllers/admin/users.py:212
 
msgid "An error occurred during deletion of user"
 
msgstr "删除用户时发生错误"
 

	
 
#: rhodecode/controllers/admin/users.py:226
 
msgid "You can't edit this user"
 
msgstr "无法编辑该用户"
 

	
 
#: rhodecode/controllers/admin/users.py:266
 
msgid "Granted 'repository create' permission to user"
 
msgstr "已授予用户‘创建版本库’的权限"
 

	
 
#: rhodecode/controllers/admin/users.py:271
 
msgid "Revoked 'repository create' permission to user"
 
msgstr "已撤销用户‘创建版本库’的权限"
 

	
 
#: rhodecode/controllers/admin/users.py:277
 
msgid "Granted 'repository fork' permission to user"
 
msgstr "成功授予了用户“分支版本库”权限"
 

	
 
#: rhodecode/controllers/admin/users.py:282
 
msgid "Revoked 'repository fork' permission to user"
 
msgstr "成功撤销用户“分支版本库”权限"
 

	
 
#: rhodecode/controllers/admin/users.py:288
 
@@ -677,614 +705,619 @@ msgstr "建立用户组 %s"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:95
 
#, python-format
 
msgid "error occurred during creation of users group %s"
 
msgstr "创建用户组 %s 时发生错误"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:135
 
#, python-format
 
msgid "updated users group %s"
 
msgstr "更新用户组 %s"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:157
 
#, python-format
 
msgid "error occurred during update of users group %s"
 
msgstr "更新用户组 %s 时发生错误"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:174
 
msgid "successfully deleted users group"
 
msgstr "删除用户组成功"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:179
 
msgid "An error occurred during deletion of users group"
 
msgstr "删除用户组时发生错误"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:233
 
msgid "Granted 'repository create' permission to users group"
 
msgstr "已授予用户组‘创建版本库’的权限"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:238
 
msgid "Revoked 'repository create' permission to users group"
 
msgstr "已撤销用户组‘创建版本库’的权限"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:244
 
msgid "Granted 'repository fork' permission to users group"
 
msgstr "已授予用户组‘分支版本库’的权限"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:249
 
msgid "Revoked 'repository fork' permission to users group"
 
msgstr "已撤销用户组‘分支版本库’的权限"
 

	
 
#: rhodecode/lib/auth.py:499
 
msgid "You need to be a registered user to perform this action"
 
msgstr "必须是注册用户才能进行此操作"
 

	
 
#: rhodecode/lib/auth.py:540
 
msgid "You need to be a signed in to view this page"
 
msgstr "必须登录才能访问该页面"
 

	
 
#: rhodecode/lib/diffs.py:86
 
#: rhodecode/lib/diffs.py:87
 
msgid ""
 
"Changeset was too big and was cut off, use diff menu to display this diff"
 
msgstr "修订集因过大而被截断,可查看原始修订集作为替代"
 

	
 
#: rhodecode/lib/diffs.py:96
 
#: rhodecode/lib/diffs.py:97
 
msgid "No changes detected"
 
msgstr "未发现差异"
 

	
 
#: rhodecode/lib/helpers.py:372
 
#: rhodecode/lib/helpers.py:373
 
#, python-format
 
msgid "%a, %d %b %Y %H:%M:%S"
 
msgstr "%Y/%b/%d %H:%M:%S %a"
 

	
 
#: rhodecode/lib/helpers.py:484
 
#: rhodecode/lib/helpers.py:485
 
msgid "True"
 
msgstr "是"
 

	
 
#: rhodecode/lib/helpers.py:488
 
#: rhodecode/lib/helpers.py:489
 
msgid "False"
 
msgstr "否"
 

	
 
#: rhodecode/lib/helpers.py:532
 
#: rhodecode/lib/helpers.py:533
 
msgid "Changeset not found"
 
msgstr "未找到修订集"
 

	
 
#: rhodecode/lib/helpers.py:555
 
#: rhodecode/lib/helpers.py:556
 
#, python-format
 
msgid "Show all combined changesets %s->%s"
 
msgstr "显示合并的修订集 %s->%s"
 

	
 
#: rhodecode/lib/helpers.py:561
 
#: rhodecode/lib/helpers.py:562
 
msgid "compare view"
 
msgstr "比较显示"
 

	
 
#: rhodecode/lib/helpers.py:581
 
#: rhodecode/lib/helpers.py:582
 
msgid "and"
 
msgstr "还有"
 

	
 
#: rhodecode/lib/helpers.py:582
 
#: rhodecode/lib/helpers.py:583
 
#, python-format
 
msgid "%s more"
 
msgstr "%s 个"
 

	
 
#: rhodecode/lib/helpers.py:583
 
#: rhodecode/lib/helpers.py:584
 
#: rhodecode/templates/changelog/changelog.html:48
 
msgid "revisions"
 
msgstr "修订"
 

	
 
#: rhodecode/lib/helpers.py:606
 
#: rhodecode/lib/helpers.py:607
 
msgid "fork name "
 
msgstr "分支名称"
 

	
 
#: rhodecode/lib/helpers.py:620
 
#: rhodecode/lib/helpers.py:621
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:4
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:12
 
#, python-format
 
msgid "Pull request #%s"
 
msgstr "拉取请求 #%s"
 

	
 
#: rhodecode/lib/helpers.py:626
 
#: rhodecode/lib/helpers.py:627
 
msgid "[deleted] repository"
 
msgstr "[删除] 版本库"
 

	
 
#: rhodecode/lib/helpers.py:628 rhodecode/lib/helpers.py:638
 
#: rhodecode/lib/helpers.py:629 rhodecode/lib/helpers.py:639
 
msgid "[created] repository"
 
msgstr "[创建] 版本库"
 

	
 
#: rhodecode/lib/helpers.py:630
 
#: rhodecode/lib/helpers.py:631
 
msgid "[created] repository as fork"
 
msgstr "[创建] 分支版本库"
 

	
 
#: rhodecode/lib/helpers.py:632 rhodecode/lib/helpers.py:640
 
#: rhodecode/lib/helpers.py:633 rhodecode/lib/helpers.py:641
 
msgid "[forked] repository"
 
msgstr "[分支] 版本库"
 

	
 
#: rhodecode/lib/helpers.py:634 rhodecode/lib/helpers.py:642
 
#: rhodecode/lib/helpers.py:635 rhodecode/lib/helpers.py:643
 
msgid "[updated] repository"
 
msgstr "[更新] 版本库"
 

	
 
#: rhodecode/lib/helpers.py:636
 
#: rhodecode/lib/helpers.py:637
 
msgid "[delete] repository"
 
msgstr "[删除] 版本库"
 

	
 
#: rhodecode/lib/helpers.py:644
 
#: rhodecode/lib/helpers.py:645
 
msgid "[created] user"
 
msgstr "[创建] 用户"
 

	
 
#: rhodecode/lib/helpers.py:646
 
#: rhodecode/lib/helpers.py:647
 
msgid "[updated] user"
 
msgstr "[更新] 用户"
 

	
 
#: rhodecode/lib/helpers.py:648
 
#: rhodecode/lib/helpers.py:649
 
msgid "[created] users group"
 
msgstr "[创建] 用户组"
 

	
 
#: rhodecode/lib/helpers.py:650
 
#: rhodecode/lib/helpers.py:651
 
msgid "[updated] users group"
 
msgstr "[更新] 用户组"
 

	
 
#: rhodecode/lib/helpers.py:652
 
#: rhodecode/lib/helpers.py:653
 
msgid "[commented] on revision in repository"
 
msgstr "[评论] 了版本库中的修订"
 

	
 
#: rhodecode/lib/helpers.py:654
 
#: rhodecode/lib/helpers.py:655
 
msgid "[commented] on pull request for"
 
msgstr "[评论] 拉取请求"
 

	
 
#: rhodecode/lib/helpers.py:656
 
#: rhodecode/lib/helpers.py:657
 
msgid "[closed] pull request for"
 
msgstr "[关闭] 拉取请求"
 

	
 
#: rhodecode/lib/helpers.py:658
 
#: rhodecode/lib/helpers.py:659
 
msgid "[pushed] into"
 
msgstr "[推送] 到"
 

	
 
#: rhodecode/lib/helpers.py:660
 
#: rhodecode/lib/helpers.py:661
 
msgid "[committed via RhodeCode] into repository"
 
msgstr "[通过 RhodeCode 提交] 到版本库"
 

	
 
#: rhodecode/lib/helpers.py:662
 
#: rhodecode/lib/helpers.py:663
 
msgid "[pulled from remote] into repository"
 
msgstr "[远程拉取] 到版本库"
 

	
 
#: rhodecode/lib/helpers.py:664
 
#: rhodecode/lib/helpers.py:665
 
msgid "[pulled] from"
 
msgstr "[拉取] 自"
 

	
 
#: rhodecode/lib/helpers.py:666
 
#: rhodecode/lib/helpers.py:667
 
msgid "[started following] repository"
 
msgstr "[开始关注] 版本库"
 

	
 
#: rhodecode/lib/helpers.py:668
 
#: rhodecode/lib/helpers.py:669
 
msgid "[stopped following] repository"
 
msgstr "[停止关注] 版本库"
 

	
 
#: rhodecode/lib/helpers.py:840
 
#: rhodecode/lib/helpers.py:845
 
#, python-format
 
msgid " and %s more"
 
msgstr "还有 %s 个"
 

	
 
#: rhodecode/lib/helpers.py:844
 
#: rhodecode/lib/helpers.py:849
 
msgid "No Files"
 
msgstr "没有文件"
 

	
 
#: rhodecode/lib/utils2.py:335
 
#: rhodecode/lib/utils2.py:352
 
#, python-format
 
msgid "%d year"
 
msgid_plural "%d years"
 
msgstr[0] "%d 年"
 

	
 
#: rhodecode/lib/utils2.py:336
 
#: rhodecode/lib/utils2.py:353
 
#, python-format
 
msgid "%d month"
 
msgid_plural "%d months"
 
msgstr[0] "%d 月"
 

	
 
#: rhodecode/lib/utils2.py:337
 
#: rhodecode/lib/utils2.py:354
 
#, python-format
 
msgid "%d day"
 
msgid_plural "%d days"
 
msgstr[0] "%d 天"
 

	
 
#: rhodecode/lib/utils2.py:338
 
#: rhodecode/lib/utils2.py:355
 
#, python-format
 
msgid "%d hour"
 
msgid_plural "%d hours"
 
msgstr[0] "%d 小时"
 

	
 
#: rhodecode/lib/utils2.py:339
 
#: rhodecode/lib/utils2.py:356
 
#, python-format
 
msgid "%d minute"
 
msgid_plural "%d minutes"
 
msgstr[0] "%d 分钟"
 

	
 
#: rhodecode/lib/utils2.py:340
 
#: rhodecode/lib/utils2.py:357
 
#, python-format
 
msgid "%d second"
 
msgid_plural "%d seconds"
 
msgstr[0] "%d 秒"
 

	
 
#: rhodecode/lib/utils2.py:355
 
#: rhodecode/lib/utils2.py:372
 
#, python-format
 
msgid "%s ago"
 
msgstr "%s 之前"
 

	
 
#: rhodecode/lib/utils2.py:357
 
#: rhodecode/lib/utils2.py:374
 
#, python-format
 
msgid "%s and %s ago"
 
msgstr "%s 零 %s 之前"
 

	
 
#: rhodecode/lib/utils2.py:360
 
#: rhodecode/lib/utils2.py:377
 
msgid "just now"
 
msgstr "刚才"
 

	
 
#: rhodecode/lib/celerylib/tasks.py:269
 
msgid "password reset link"
 
msgstr "密码重置链接"
 

	
 
#: rhodecode/model/comment.py:110
 
#, python-format
 
msgid "on line %s"
 
msgstr "在 %s 行"
 

	
 
#: rhodecode/model/comment.py:157
 
#: rhodecode/model/comment.py:173
 
msgid "[Mention]"
 
msgstr "[提及]"
 

	
 
#: rhodecode/model/db.py:1140
 
#: rhodecode/model/db.py:1164
 
msgid "Repository no access"
 
msgstr "无版本库访问权限"
 

	
 
#: rhodecode/model/db.py:1141
 
#: rhodecode/model/db.py:1165
 
msgid "Repository read access"
 
msgstr "版本库读取权限"
 

	
 
#: rhodecode/model/db.py:1142
 
#: rhodecode/model/db.py:1166
 
msgid "Repository write access"
 
msgstr "版本库写入权限"
 

	
 
#: rhodecode/model/db.py:1143
 
#: rhodecode/model/db.py:1167
 
msgid "Repository admin access"
 
msgstr "版本库管理权限"
 

	
 
#: rhodecode/model/db.py:1145
 
#: rhodecode/model/db.py:1169
 
msgid "Repositories Group no access"
 
msgstr "无版本库组访问权限"
 

	
 
#: rhodecode/model/db.py:1146
 
#: rhodecode/model/db.py:1170
 
msgid "Repositories Group read access"
 
msgstr "版本库组读取权限"
 

	
 
#: rhodecode/model/db.py:1147
 
#: rhodecode/model/db.py:1171
 
msgid "Repositories Group write access"
 
msgstr "版本库组写入"
 

	
 
#: rhodecode/model/db.py:1148
 
#: rhodecode/model/db.py:1172
 
msgid "Repositories Group admin access"
 
msgstr "版本库组管理权限"
 

	
 
#: rhodecode/model/db.py:1150
 
#: rhodecode/model/db.py:1174
 
msgid "RhodeCode Administrator"
 
msgstr "RhodeCode 管理员"
 

	
 
#: rhodecode/model/db.py:1151
 
#: rhodecode/model/db.py:1175
 
msgid "Repository creation disabled"
 
msgstr "禁用创建版本库"
 

	
 
#: rhodecode/model/db.py:1152
 
#: rhodecode/model/db.py:1176
 
msgid "Repository creation enabled"
 
msgstr "允许创建版本库"
 

	
 
#: rhodecode/model/db.py:1153
 
#: rhodecode/model/db.py:1177
 
msgid "Repository forking disabled"
 
msgstr "禁用分支 版本库"
 

	
 
#: rhodecode/model/db.py:1154
 
#: rhodecode/model/db.py:1178
 
msgid "Repository forking enabled"
 
msgstr "允许分支版本库"
 

	
 
#: rhodecode/model/db.py:1155
 
#: rhodecode/model/db.py:1179
 
msgid "Register disabled"
 
msgstr "禁用注册"
 

	
 
#: rhodecode/model/db.py:1156
 
#: rhodecode/model/db.py:1180
 
msgid "Register new user with RhodeCode with manual activation"
 
msgstr "用手动激活注册新用户"
 

	
 
#: rhodecode/model/db.py:1159
 
#: rhodecode/model/db.py:1183
 
msgid "Register new user with RhodeCode with auto activation"
 
msgstr "用自动激活注册新用户"
 

	
 
#: rhodecode/model/db.py:1579
 
#: rhodecode/model/db.py:1611
 
msgid "Not Reviewed"
 
msgstr "未检视"
 

	
 
#: rhodecode/model/db.py:1580
 
#: rhodecode/model/db.py:1612
 
msgid "Approved"
 
msgstr "已批准"
 

	
 
#: rhodecode/model/db.py:1581
 
#: rhodecode/model/db.py:1613
 
msgid "Rejected"
 
msgstr "驳回"
 

	
 
#: rhodecode/model/db.py:1582
 
#: rhodecode/model/db.py:1614
 
msgid "Under Review"
 
msgstr "检视中"
 

	
 
#: rhodecode/model/forms.py:43
 
msgid "Please enter a login"
 
msgstr "请登录"
 

	
 
#: rhodecode/model/forms.py:44
 
#, python-format
 
msgid "Enter a value %(min)i characters long or more"
 
msgstr "输入一个不少于 %(min)i 个字符的值"
 

	
 
#: rhodecode/model/forms.py:52
 
msgid "Please enter a password"
 
msgstr "请输入密码"
 

	
 
#: rhodecode/model/forms.py:53
 
#, python-format
 
msgid "Enter %(min)i characters or more"
 
msgstr "输入少于 %(min)i 个字符"
 

	
 
#: rhodecode/model/notification.py:220
 
msgid "commented on commit"
 
msgstr "评论了评论"
 

	
 
#: rhodecode/model/notification.py:221
 
msgid "sent message"
 
msgstr "发送信息"
 

	
 
#: rhodecode/model/notification.py:222
 
msgid "mentioned you"
 
msgstr "提到了你"
 

	
 
#: rhodecode/model/notification.py:223
 
msgid "registered in RhodeCode"
 
msgstr "注册到 RhodeCode"
 

	
 
#: rhodecode/model/notification.py:224
 
msgid "opened new pull request"
 
msgstr "创建新的拉取请求"
 

	
 
#: rhodecode/model/notification.py:225
 
msgid "commented on pull request"
 
msgstr "评论了拉取请求"
 

	
 
#: rhodecode/model/pull_request.py:84
 
#: rhodecode/model/pull_request.py:89
 
#, python-format
 
msgid "%(user)s wants you to review pull request #%(pr_id)s"
 
msgstr "%(user)s 想要你检视拉取请求 #%(pr_id)s"
 

	
 
#: rhodecode/model/scm.py:535
 
msgid "latest tip"
 
msgstr "最后 tip 版本"
 

	
 
#: rhodecode/model/user.py:230
 
msgid "new user registration"
 
msgstr "[RhodeCode] 新用户注册"
 

	
 
#: rhodecode/model/user.py:255 rhodecode/model/user.py:277
 
#: rhodecode/model/user.py:299
 
msgid "You can't Edit this user since it's crucial for entire application"
 
msgstr "由于是系统帐号,无法编辑该用户"
 

	
 
#: rhodecode/model/user.py:323
 
msgid "You can't remove this user since it's crucial for entire application"
 
msgstr "由于是系统帐号,无法删除该用户"
 

	
 
#: rhodecode/model/user.py:329
 
#, python-format
 
msgid ""
 
"user \"%s\" still owns %s repositories and cannot be removed. Switch owners "
 
"or remove those repositories. %s"
 
msgstr ""
 
"由于用户 \"%s\" 拥有版本库 %s 因而无法删除,请修改版本库所有者或删除版本"
 
"库。%s"
 

	
 
#: rhodecode/model/validators.py:35 rhodecode/model/validators.py:36
 
#: rhodecode/model/validators.py:36 rhodecode/model/validators.py:37
 
msgid "Value cannot be an empty list"
 
msgstr "值不能为空"
 

	
 
#: rhodecode/model/validators.py:82
 
#: rhodecode/model/validators.py:83
 
#, python-format
 
msgid "Username \"%(username)s\" already exists"
 
msgstr "用户名称 %(username)s 已经存在"
 

	
 
#: rhodecode/model/validators.py:84
 
#: rhodecode/model/validators.py:85
 
#, python-format
 
msgid "Username \"%(username)s\" is forbidden"
 
msgstr "不允许用户名 \"%(username)s\""
 

	
 
#: rhodecode/model/validators.py:86
 
#: rhodecode/model/validators.py:87
 
msgid ""
 
"Username may only contain alphanumeric characters underscores, periods or "
 
"dashes and must begin with alphanumeric character"
 
msgstr ""
 
"只能使用字母、数字、下划线、小数点或减号作为用户名,且必须由数字或字母开头"
 

	
 
#: rhodecode/model/validators.py:114
 
#: rhodecode/model/validators.py:115
 
#, python-format
 
msgid "Username %(username)s is not valid"
 
msgstr "用户名称 %(username)s 无效"
 

	
 
#: rhodecode/model/validators.py:133
 
#: rhodecode/model/validators.py:134
 
msgid "Invalid users group name"
 
msgstr "无效的用户组名"
 

	
 
#: rhodecode/model/validators.py:134
 
#: rhodecode/model/validators.py:135
 
#, python-format
 
msgid "Users group \"%(usersgroup)s\" already exists"
 
msgstr "用户组 \"%(usersgroup)s\" 已经存在"
 

	
 
#: rhodecode/model/validators.py:136
 
#: rhodecode/model/validators.py:137
 
msgid ""
 
"users group name may only contain  alphanumeric characters underscores, "
 
"periods or dashes and must begin with alphanumeric character"
 
msgstr ""
 
"只能使用字母、数字、下划线、小数点或减号作为用户组名,且必须由数字或字母开头"
 

	
 
#: rhodecode/model/validators.py:174
 
#: rhodecode/model/validators.py:175
 
msgid "Cannot assign this group as parent"
 
msgstr "不能将这个组作为 parent"
 

	
 
#: rhodecode/model/validators.py:175
 
#: rhodecode/model/validators.py:176
 
#, python-format
 
msgid "Group \"%(group_name)s\" already exists"
 
msgstr "组 \"%(group_name)s\" 已经存在"
 

	
 
#: rhodecode/model/validators.py:177
 
#: rhodecode/model/validators.py:178
 
#, python-format
 
msgid "Repository with name \"%(group_name)s\" already exists"
 
msgstr "已经存在名为 \"%(group_name)s\" 的版本库"
 

	
 
#: rhodecode/model/validators.py:235
 
#: rhodecode/model/validators.py:236
 
msgid "Invalid characters (non-ascii) in password"
 
msgstr "密码含有无效(非ASCII)字符"
 

	
 
#: rhodecode/model/validators.py:250
 
#: rhodecode/model/validators.py:251
 
msgid "Passwords do not match"
 
msgstr "密码不符"
 

	
 
#: rhodecode/model/validators.py:267
 
#: rhodecode/model/validators.py:268
 
msgid "invalid password"
 
msgstr "无效密码"
 

	
 
#: rhodecode/model/validators.py:268
 
#: rhodecode/model/validators.py:269
 
msgid "invalid user name"
 
msgstr "无效用户名"
 

	
 
#: rhodecode/model/validators.py:269
 
#: rhodecode/model/validators.py:270
 
msgid "Your account is disabled"
 
msgstr "该帐号已被禁用"
 

	
 
#: rhodecode/model/validators.py:313
 
#: rhodecode/model/validators.py:314
 
#, python-format
 
msgid "Repository name %(repo)s is disallowed"
 
msgstr "版本库名称不能为 %(repo)s"
 

	
 
#: rhodecode/model/validators.py:315
 
#: rhodecode/model/validators.py:316
 
#, python-format
 
msgid "Repository named %(repo)s already exists"
 
msgstr "已经存在版本库 %(repo)s"
 

	
 
#: rhodecode/model/validators.py:316
 
#: rhodecode/model/validators.py:317
 
#, python-format
 
msgid "Repository \"%(repo)s\" already exists in group \"%(group)s\""
 
msgstr "版本库组 \"%(group)s\" 中已经存在版本库 \"%(repo)s\""
 

	
 
#: rhodecode/model/validators.py:318
 
#: rhodecode/model/validators.py:319
 
#, python-format
 
msgid "Repositories group with name \"%(repo)s\" already exists"
 
msgstr "已经存在名为 \"%(repo)s\" 的版本库组"
 

	
 
#: rhodecode/model/validators.py:431
 
#: rhodecode/model/validators.py:432
 
msgid "invalid clone url"
 
msgstr "无效的克隆地址"
 

	
 
#: rhodecode/model/validators.py:432
 
#: rhodecode/model/validators.py:433
 
msgid "Invalid clone url, provide a valid clone http(s)/svn+http(s) url"
 
msgstr "无效的克隆地址,提供一个有效的克隆 http(s) 或 svn+http(s) 地址"
 

	
 
#: rhodecode/model/validators.py:457
 
#: rhodecode/model/validators.py:458
 
msgid "Fork have to be the same type as parent"
 
msgstr "分支必须使用和父版本库相同的类型"
 

	
 
#: rhodecode/model/validators.py:478
 
#: rhodecode/model/validators.py:473
 
#| msgid "You don't have permission to view this page"
 
msgid "You don't have permissions to create repository in this group"
 
msgstr "没有在这个组里面创建版本库的权限"
 

	
 
#: rhodecode/model/validators.py:498
 
msgid "This username or users group name is not valid"
 
msgstr "用户或用户组名称无效"
 

	
 
#: rhodecode/model/validators.py:562
 
#: rhodecode/model/validators.py:582
 
msgid "This is not a valid path"
 
msgstr "不是一个合法的路径"
 

	
 
#: rhodecode/model/validators.py:577
 
#: rhodecode/model/validators.py:597
 
msgid "This e-mail address is already taken"
 
msgstr "该邮件地址已被使用"
 

	
 
#: rhodecode/model/validators.py:597
 
#: rhodecode/model/validators.py:617
 
#, python-format
 
msgid "e-mail \"%(email)s\" does not exist."
 
msgstr "邮件地址  \"%(email)s\" 不存在"
 

	
 
#: rhodecode/model/validators.py:634
 
#: rhodecode/model/validators.py:654
 
msgid ""
 
"The LDAP Login attribute of the CN must be specified - this is the name of "
 
"the attribute that is equivalent to \"username\""
 
msgstr "LDAP 登陆属性的 CN 必须指定 - 这个名字作为用户名"
 

	
 
#: rhodecode/model/validators.py:653
 
#: rhodecode/model/validators.py:673
 
#, python-format
 
msgid "Revisions %(revs)s are already part of pull request or have set status"
 
msgstr "修订 %(revs)s 已经包含在拉取请求中或者或者已经设置状态"
 

	
 
#: rhodecode/templates/index.html:3
 
msgid "Dashboard"
 
msgstr "控制面板"
 

	
 
#: rhodecode/templates/index_base.html:6
 
#: rhodecode/templates/repo_switcher_list.html:4
 
#: rhodecode/templates/admin/repos/repos.html:9
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:31
 
#: rhodecode/templates/admin/users/users.html:9
 
#: rhodecode/templates/bookmarks/bookmarks.html:10
 
#: rhodecode/templates/branches/branches.html:9
 
#: rhodecode/templates/journal/journal.html:40
 
#: rhodecode/templates/tags/tags.html:10
 
msgid "quick filter..."
 
msgstr "快速过滤..."
 

	
 
#: rhodecode/templates/index_base.html:6
 
#: rhodecode/templates/admin/repos/repos.html:9
 
#: rhodecode/templates/base/base.html:221
 
#: rhodecode/templates/base/base.html:230
 
msgid "repositories"
 
msgstr "个版本库"
 
msgstr "版本库"
 

	
 
#: rhodecode/templates/index_base.html:13
 
#: rhodecode/templates/index_base.html:15
 
#: rhodecode/templates/admin/repos/repos.html:21
 
msgid "ADD REPOSITORY"
 
msgstr "新建版本库"
 

	
 
#: rhodecode/templates/index_base.html:29
 
#: rhodecode/templates/admin/repos_groups/repos_groups_add.html:32
 
#: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:32
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:33
 
#: rhodecode/templates/admin/users_groups/users_group_add.html:32
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:33
 
msgid "Group name"
 
msgstr "组名"
 

	
 
#: rhodecode/templates/index_base.html:30
 
#: rhodecode/templates/index_base.html:71
 
#: rhodecode/templates/index_base.html:142
 
#: rhodecode/templates/index_base.html:168
 
#: rhodecode/templates/admin/repos/repo_add_base.html:56
 
#: rhodecode/templates/admin/repos/repo_edit.html:75
 
#: rhodecode/templates/admin/repos/repos.html:72
 
#: rhodecode/templates/admin/repos_groups/repos_groups_add.html:41
 
#: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:41
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:34
 
#: rhodecode/templates/forks/fork.html:59
 
#: rhodecode/templates/settings/repo_settings.html:66
 
#: rhodecode/templates/summary/summary.html:105
 
msgid "Description"
 
msgstr "描述"
 

	
 
#: rhodecode/templates/index_base.html:40
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:46
 
msgid "Repositories group"
 
msgstr "版本库组"
 

	
 
#: rhodecode/templates/index_base.html:70
 
#: rhodecode/templates/index_base.html:166
 
#: rhodecode/templates/admin/repos/repo_add_base.html:9
 
#: rhodecode/templates/admin/repos/repo_edit.html:32
 
#: rhodecode/templates/admin/repos/repos.html:70
 
#: rhodecode/templates/admin/users/user_edit.html:192
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:59
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:157
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:193
 
#: rhodecode/templates/admin/users/user_edit_my_account_repos.html:6
 
#: rhodecode/templates/bookmarks/bookmarks.html:36
 
@@ -1506,97 +1539,97 @@ msgstr "注册后,帐号将启用"
 
msgid "Your account must wait for activation by administrator"
 
msgstr "管理员审核后,你注册的帐号将被启用"
 

	
 
#: rhodecode/templates/repo_switcher_list.html:11
 
#: rhodecode/templates/admin/repos/repo_add_base.html:65
 
#: rhodecode/templates/admin/repos/repo_edit.html:85
 
#: rhodecode/templates/settings/repo_settings.html:76
 
msgid "Private repository"
 
msgstr "私有版本库"
 

	
 
#: rhodecode/templates/repo_switcher_list.html:16
 
msgid "Public repository"
 
msgstr "公共版本库"
 

	
 
#: rhodecode/templates/switch_to_list.html:3
 
#: rhodecode/templates/branches/branches.html:14
 
msgid "branches"
 
msgstr "分支"
 

	
 
#: rhodecode/templates/switch_to_list.html:10
 
#: rhodecode/templates/branches/branches_data.html:57
 
msgid "There are no branches yet"
 
msgstr "没有任何分支"
 

	
 
#: rhodecode/templates/switch_to_list.html:15
 
#: rhodecode/templates/shortlog/shortlog_data.html:10
 
#: rhodecode/templates/tags/tags.html:15
 
msgid "tags"
 
msgstr "标签"
 

	
 
#: rhodecode/templates/switch_to_list.html:22
 
#: rhodecode/templates/tags/tags_data.html:33
 
msgid "There are no tags yet"
 
msgstr "没有任何标签"
 

	
 
#: rhodecode/templates/switch_to_list.html:28
 
#: rhodecode/templates/bookmarks/bookmarks.html:15
 
msgid "bookmarks"
 
msgstr "书签"
 

	
 
#: rhodecode/templates/switch_to_list.html:35
 
#: rhodecode/templates/bookmarks/bookmarks_data.html:32
 
msgid "There are no bookmarks yet"
 
msgstr "无书签"
 

	
 
#: rhodecode/templates/admin/admin.html:5
 
#: rhodecode/templates/admin/admin.html:9
 
msgid "Admin journal"
 
msgstr "管理员日志"
 
msgstr "系统日志"
 

	
 
#: rhodecode/templates/admin/admin_log.html:6
 
#: rhodecode/templates/admin/repos/repos.html:74
 
#: rhodecode/templates/admin/users/user_edit_my_account_repos.html:8
 
#: rhodecode/templates/admin/users/user_edit_my_account_repos.html:9
 
#: rhodecode/templates/journal/journal.html:61
 
#: rhodecode/templates/journal/journal.html:62
 
msgid "Action"
 
msgstr "操作"
 

	
 
#: rhodecode/templates/admin/admin_log.html:7
 
msgid "Repository"
 
msgstr "版本库"
 

	
 
#: rhodecode/templates/admin/admin_log.html:8
 
#: rhodecode/templates/bookmarks/bookmarks.html:37
 
#: rhodecode/templates/bookmarks/bookmarks_data.html:7
 
#: rhodecode/templates/branches/branches.html:52
 
#: rhodecode/templates/tags/tags.html:37
 
#: rhodecode/templates/tags/tags_data.html:7
 
msgid "Date"
 
msgstr "日期"
 

	
 
#: rhodecode/templates/admin/admin_log.html:9
 
msgid "From IP"
 
msgstr "来源 IP"
 

	
 
#: rhodecode/templates/admin/admin_log.html:53
 
msgid "No actions yet"
 
msgstr "无操作"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:5
 
msgid "LDAP administration"
 
msgstr "LDAP 管理员"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:11
 
msgid "Ldap"
 
msgstr "LDAP"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:28
 
msgid "Connection settings"
 
msgstr "连接设置"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:30
 
msgid "Enable LDAP"
 
msgstr "启用 LDAP"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:34
 
@@ -1636,251 +1669,251 @@ msgid "LDAP Search Scope"
 
msgstr "LDAP 搜索范围"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:70
 
msgid "Attribute mappings"
 
msgstr "属性映射"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:72
 
msgid "Login Attribute"
 
msgstr "登录属性"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:76
 
msgid "First Name Attribute"
 
msgstr "名"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:80
 
msgid "Last Name Attribute"
 
msgstr "姓"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:84
 
msgid "E-mail Attribute"
 
msgstr "电子邮件属性"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:89
 
#: rhodecode/templates/admin/repos/repo_edit.html:141
 
#: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:74
 
#: rhodecode/templates/admin/settings/hooks.html:73
 
#: rhodecode/templates/admin/users/user_edit.html:129
 
#: rhodecode/templates/admin/users/user_edit.html:174
 
#: rhodecode/templates/admin/users/user_edit_my_account_form.html:79
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:135
 
#: rhodecode/templates/settings/repo_settings.html:93
 
msgid "Save"
 
msgstr "保存"
 

	
 
#: rhodecode/templates/admin/notifications/notifications.html:5
 
#: rhodecode/templates/admin/notifications/notifications.html:9
 
msgid "My Notifications"
 
msgstr "我的通知"
 

	
 
#: rhodecode/templates/admin/notifications/notifications.html:29
 
msgid "All"
 
msgstr "全部"
 

	
 
#: rhodecode/templates/admin/notifications/notifications.html:30
 
msgid "Comments"
 
msgstr "评论"
 

	
 
#: rhodecode/templates/admin/notifications/notifications.html:31
 
#: rhodecode/templates/base/base.html:254
 
#: rhodecode/templates/base/base.html:256
 
#: rhodecode/templates/base/base.html:263
 
#: rhodecode/templates/base/base.html:265
 
msgid "Pull requests"
 
msgstr "拉取请求"
 

	
 
#: rhodecode/templates/admin/notifications/notifications.html:35
 
msgid "Mark all read"
 
msgstr "全部标记为已读"
 

	
 
#: rhodecode/templates/admin/notifications/notifications_data.html:39
 
msgid "No notifications here yet"
 
msgstr "无通知"
 

	
 
#: rhodecode/templates/admin/notifications/show_notification.html:5
 
#: rhodecode/templates/admin/notifications/show_notification.html:11
 
msgid "Show notification"
 
msgstr "显示通知"
 

	
 
#: rhodecode/templates/admin/notifications/show_notification.html:9
 
msgid "Notifications"
 
msgstr "通知"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:5
 
msgid "Permissions administration"
 
msgstr "权限管理"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:11
 
#: rhodecode/templates/admin/repos/repo_edit.html:134
 
#: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:58
 
#: rhodecode/templates/admin/users/user_edit.html:139
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:100
 
#: rhodecode/templates/settings/repo_settings.html:86
 
msgid "Permissions"
 
msgstr "权限"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:24
 
msgid "Default permissions"
 
msgstr "默认权限"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:31
 
msgid "Anonymous access"
 
msgstr "匿名访问"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:41
 
msgid "Repository permission"
 
msgstr "版本库权限"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:49
 
msgid ""
 
"All default permissions on each repository will be reset to choosen "
 
"permission, note that all custom default permission on repositories will be "
 
"lost"
 
msgstr ""
 
"所有版本库的默认权限将被重置到选择的权限,所有版本库的自定义权限将被丢弃"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:50
 
msgid "overwrite existing settings"
 
msgstr "覆盖已有设置"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:55
 
msgid "Registration"
 
msgstr "注册"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:63
 
msgid "Repository creation"
 
msgstr "建立版本库"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:71
 
msgid "Repository forking"
 
msgstr "版本库分支"
 

	
 
#: rhodecode/templates/admin/permissions/permissions.html:78
 
#: rhodecode/templates/admin/repos/repo_edit.html:241
 
#: rhodecode/templates/admin/repos/repo_edit.html:255
 
msgid "set"
 
msgstr "设置"
 

	
 
#: rhodecode/templates/admin/repos/repo_add.html:5
 
#: rhodecode/templates/admin/repos/repo_add_create_repository.html:5
 
msgid "Add repository"
 
msgstr "添加版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_add.html:11
 
#: rhodecode/templates/admin/repos/repo_edit.html:11
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:10
 
msgid "Repositories"
 
msgstr "版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_add.html:13
 
msgid "add new"
 
msgstr "新建"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:20
 
#: rhodecode/templates/summary/summary.html:95
 
#: rhodecode/templates/summary/summary.html:96
 
msgid "Clone from"
 
msgstr "克隆自"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:24
 
#: rhodecode/templates/admin/repos/repo_edit.html:44
 
#: rhodecode/templates/settings/repo_settings.html:43
 
msgid "Optional http[s] url from which repository should be cloned."
 
msgstr "可选的,指定版本库应该从哪个 http[s] 地址克隆。"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:29
 
#: rhodecode/templates/admin/repos/repo_edit.html:49
 
#: rhodecode/templates/admin/repos_groups/repos_groups.html:4
 
#: rhodecode/templates/forks/fork.html:50
 
#: rhodecode/templates/settings/repo_settings.html:48
 
msgid "Repository group"
 
msgstr "版本库组"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:33
 
#: rhodecode/templates/forks/fork.html:54
 
msgid "Optionaly select a group to put this repository into."
 
msgstr "可选的,选择一个组将版本库放到其中"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:38
 
#: rhodecode/templates/admin/repos/repo_edit.html:58
 
msgid "Type"
 
msgstr "类型"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:42
 
msgid "Type of repository to create."
 
msgstr "要创建的版本库类型"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:47
 
#: rhodecode/templates/admin/repos/repo_edit.html:66
 
#: rhodecode/templates/forks/fork.html:41
 
#: rhodecode/templates/settings/repo_settings.html:57
 
msgid "Landing revision"
 
msgstr "默认修订"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:51
 
#: rhodecode/templates/admin/repos/repo_edit.html:70
 
#: rhodecode/templates/forks/fork.html:45
 
#: rhodecode/templates/settings/repo_settings.html:61
 
msgid "Default revision for files page, downloads, whoosh and readme"
 
msgstr "文件浏览、下载、whoosh和readme的默认修订版本"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:60
 
#: rhodecode/templates/admin/repos/repo_edit.html:79
 
#: rhodecode/templates/forks/fork.html:63
 
#: rhodecode/templates/settings/repo_settings.html:70
 
msgid ""
 
"Keep it short and to the point. Use a README file for longer descriptions."
 
msgstr "保持简短。用 README 文件来写更长的描述。"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:69
 
#: rhodecode/templates/admin/repos/repo_edit.html:89
 
#: rhodecode/templates/forks/fork.html:72
 
#: rhodecode/templates/settings/repo_settings.html:80
 
msgid ""
 
"Private repositories are only visible to people explicitly added as "
 
"collaborators."
 
msgstr "私有版本库只对显示添加的合作者可见。"
 
msgstr "私有版本库只对成员可见。"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:73
 
msgid "add"
 
msgstr "新建"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_create_repository.html:9
 
msgid "add new repository"
 
msgstr "新建版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:5
 
msgid "Edit repository"
 
msgstr "编辑版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:13
 
#: rhodecode/templates/admin/users/user_edit.html:13
 
#: rhodecode/templates/admin/users/user_edit.html:224
 
#: rhodecode/templates/admin/users/user_edit.html:226
 
#: rhodecode/templates/admin/users/user_edit_my_account_repos.html:28
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:13
 
#: rhodecode/templates/files/files_source.html:44
 
#: rhodecode/templates/journal/journal.html:81
 
msgid "edit"
 
msgstr "编辑"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:40
 
#: rhodecode/templates/settings/repo_settings.html:39
 
msgid "Clone uri"
 
msgstr "克隆地址"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:53
 
#: rhodecode/templates/settings/repo_settings.html:52
 
msgid "Optional select a group to put this repository into."
 
msgstr "可选的,选择一个组将版本库放到其中"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:94
 
msgid "Enable statistics"
 
msgstr "启用统计"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:98
 
msgid "Enable statistics window on summary page."
 
msgstr "启用概况页的统计窗口"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:103
 
msgid "Enable downloads"
 
msgstr "启用下载"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:107
 
msgid "Enable download menu on summary page."
 
@@ -1919,331 +1952,375 @@ msgstr "重置"
 
#: rhodecode/templates/admin/repos/repo_edit.html:152
 
msgid "Administration"
 
msgstr "管理"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:155
 
msgid "Statistics"
 
msgstr "统计"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:159
 
msgid "Reset current statistics"
 
msgstr "重置统计"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:159
 
msgid "Confirm to remove current statistics"
 
msgstr "确认移除当前统计"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:162
 
msgid "Fetched to rev"
 
msgstr "获取到修订"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:163
 
msgid "Stats gathered"
 
msgstr "已收集的统计"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:171
 
msgid "Remote"
 
msgstr "远程"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:175
 
msgid "Pull changes from remote location"
 
msgstr "从远程路径拉取修订集"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:175
 
msgid "Confirm to pull changes from remote side"
 
msgstr "确认从远程拉取修订集"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:186
 
msgid "Cache"
 
msgstr "缓存"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:190
 
msgid "Invalidate repository cache"
 
msgstr "清除版本库缓存"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:190
 
msgid "Confirm to invalidate repository cache"
 
msgstr "确认清除版本库缓存"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:195
 
#: rhodecode/templates/base/base.html:318
 
#: rhodecode/templates/base/base.html:320
 
#: rhodecode/templates/base/base.html:322
 
#: rhodecode/templates/admin/repos/repo_edit.html:193
 
msgid ""
 
"Manually invalidate cache for this repository. On first access repository "
 
"will be cached again"
 
msgstr "手动清除版本库缓存。之后第一次访问的时候将重建缓存"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:198
 
msgid "List of cached values"
 
msgstr "缓存数据列表"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:209
 
#: rhodecode/templates/base/base.html:327
 
#: rhodecode/templates/base/base.html:329
 
#: rhodecode/templates/base/base.html:331
 
msgid "Public journal"
 
msgstr "公共日志"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:201
 
#: rhodecode/templates/admin/repos/repo_edit.html:215
 
msgid "Remove from public journal"
 
msgstr "从公共日志删除"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:203
 
#: rhodecode/templates/admin/repos/repo_edit.html:217
 
msgid "Add to public journal"
 
msgstr "添加到公共日志"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:208
 
#: rhodecode/templates/admin/repos/repo_edit.html:222
 
msgid ""
 
"All actions made on this repository will be accessible to everyone in public "
 
"journal"
 
msgstr "任何人都可以在公共日志上看到这个版本库上的所有动作"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:215
 
#: rhodecode/templates/admin/repos/repo_edit.html:229
 
msgid "Locking"
 
msgstr "锁定"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:220
 
#: rhodecode/templates/admin/repos/repo_edit.html:234
 
msgid "Unlock locked repo"
 
msgstr "解锁版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:220
 
#: rhodecode/templates/admin/repos/repo_edit.html:234
 
msgid "Confirm to unlock repository"
 
msgstr "确认解锁版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:223
 
#: rhodecode/templates/admin/repos/repo_edit.html:237
 
msgid "lock repo"
 
msgstr "锁定版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:223
 
#: rhodecode/templates/admin/repos/repo_edit.html:237
 
msgid "Confirm to lock repository"
 
msgstr "确认锁定版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:224
 
#: rhodecode/templates/admin/repos/repo_edit.html:238
 
msgid "Repository is not locked"
 
msgstr "版本库未锁定"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:229
 
#: rhodecode/templates/admin/repos/repo_edit.html:243
 
msgid ""
 
"Force locking on repository. Works only when anonymous access is disabled"
 
msgstr "强制锁定版本库。只有在禁止匿名访问时候才有效"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:236
 
#: rhodecode/templates/admin/repos/repo_edit.html:250
 
msgid "Set as fork of"
 
msgstr "设置 fork 自"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:245
 
#: rhodecode/templates/admin/repos/repo_edit.html:259
 
msgid "Manually set this repository as a fork of another from the list"
 
msgstr "从列表中手动设置这个版本库 fork 自另一版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:251
 
#: rhodecode/templates/admin/repos/repo_edit.html:265
 
#: rhodecode/templates/changeset/changeset_file_comment.html:26
 
msgid "Delete"
 
msgstr "删除"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:255
 
#: rhodecode/templates/admin/repos/repo_edit.html:269
 
msgid "Remove this repository"
 
msgstr "删除版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:255
 
#: rhodecode/templates/admin/repos/repo_edit.html:269
 
#: rhodecode/templates/journal/journal.html:84
 
msgid "Confirm to delete this repository"
 
msgstr "确认删除版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:259
 
#: rhodecode/templates/admin/repos/repo_edit.html:273
 
msgid ""
 
"This repository will be renamed in a special way in order to be unaccesible "
 
"for RhodeCode and VCS systems.\n"
 
"                         If you need fully delete it from filesystem please "
 
"do it manually"
 
msgstr ""
 
"这个版本库将以特殊的方式重命名这样 RhodeCode 和版本控制系统将不能访问它。\n"
 
"                         如果需要从文件系统完全删除,你需要手动操作"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:3
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:3
 
msgid "none"
 
msgstr "无"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:4
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:4
 
msgid "read"
 
msgstr "读"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:5
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:5
 
msgid "write"
 
msgstr "写"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:6
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:6
 
#: rhodecode/templates/admin/users/users.html:85
 
#: rhodecode/templates/base/base.html:217
 
#: rhodecode/templates/base/base.html:226
 
msgid "admin"
 
msgstr "管理员"
 
msgstr "管理"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:7
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:7
 
msgid "member"
 
msgstr "成员"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:16
 
#: rhodecode/templates/data_table/_dt_elements.html:67
 
#: rhodecode/templates/journal/journal.html:132
 
#: rhodecode/templates/summary/summary.html:76
 
msgid "private repository"
 
msgstr "私有版本库"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:19
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:28
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:18
 
msgid "default"
 
msgstr "默认"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:33
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:58
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:23
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:42
 
msgid "revoke"
 
msgstr "移除"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:83
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:67
 
msgid "Add another member"
 
msgstr "添加成员"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:97
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:81
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:87
 
msgid "Failed to remove user"
 
msgstr "删除用户失败"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:112
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:96
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:103
 
msgid "Failed to remove users group"
 
msgstr "删除用户组失败"
 

	
 
#: rhodecode/templates/admin/repos/repos.html:5
 
msgid "Repositories administration"
 
msgstr "版本库管理员"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups.html:8
 
msgid "Groups"
 
msgstr "组"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups.html:12
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:73
 
msgid "apply to children"
 
msgstr "应用到成员"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html:74
 
msgid ""
 
"Set or revoke permission to all children of that group, including "
 
"repositories and other groups"
 
msgstr "授予或者撤销权限所有组成员,包括子组和子版本库"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups.html:9
 
#: rhodecode/templates/base/base.html:122
 
#: rhodecode/templates/base/base.html:309
 
#: rhodecode/templates/base/base.html:311
 
#: rhodecode/templates/base/base.html:313
 
#: rhodecode/templates/bookmarks/bookmarks.html:11
 
#: rhodecode/templates/branches/branches.html:10
 
#: rhodecode/templates/changelog/changelog.html:10
 
#: rhodecode/templates/changeset/changeset.html:10
 
#: rhodecode/templates/changeset/changeset_range.html:9
 
#: rhodecode/templates/compare/compare_diff.html:9
 
#: rhodecode/templates/files/file_diff.html:8
 
#: rhodecode/templates/files/files.html:8
 
#: rhodecode/templates/files/files_add.html:15
 
#: rhodecode/templates/files/files_edit.html:15
 
#: rhodecode/templates/followers/followers.html:9
 
#: rhodecode/templates/forks/fork.html:9
 
#: rhodecode/templates/forks/forks.html:9
 
#: rhodecode/templates/pullrequests/pullrequest.html:8
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:8
 
#: rhodecode/templates/pullrequests/pullrequest_show_all.html:8
 
#: rhodecode/templates/settings/repo_settings.html:9
 
#: rhodecode/templates/shortlog/shortlog.html:10
 
#: rhodecode/templates/summary/summary.html:8
 
#: rhodecode/templates/tags/tags.html:11
 
msgid "Home"
 
msgstr "首页"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups.html:13
 
msgid "with"
 
msgstr "有"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_add.html:5
 
msgid "Add repos group"
 
msgstr "添加版本库组"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_add.html:10
 
#: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:10
 
msgid "Repos groups"
 
msgstr "版本库组"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_add.html:12
 
msgid "add new repos group"
 
msgstr "添加新版本库组"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_add.html:50
 
#: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:50
 
msgid "Group parent"
 
msgstr "上级组"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_add.html:58
 
#: rhodecode/templates/admin/users/user_add.html:94
 
#: rhodecode/templates/admin/users_groups/users_group_add.html:49
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:90
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:113
 
msgid "save"
 
msgstr "保存"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:5
 
msgid "Edit repos group"
 
msgstr "编辑版本库组"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:12
 
msgid "edit repos group"
 
msgstr "编辑版本库组"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_edit.html:70
 
msgid ""
 
"Enable lock-by-pulling on group. This option will be applied to all other "
 
"groups and repositories inside"
 
msgstr "启用组的拉取锁定。这个选项将应用到组内的其他组和版本库"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:5
 
msgid "Repositories groups administration"
 
msgstr "版本库管理员"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:22
 
msgid "ADD NEW GROUP"
 
msgstr "添加组"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:35
 
msgid "Number of toplevel repositories"
 
msgstr "顶层版本库数量"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:36
 
#: rhodecode/templates/admin/users/users.html:87
 
#: rhodecode/templates/admin/users_groups/users_groups.html:35
 
msgid "action"
 
msgstr "操作"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:54
 
#: rhodecode/templates/admin/users/user_edit.html:255
 
#: rhodecode/templates/admin/users_groups/users_groups.html:44
 
#: rhodecode/templates/data_table/_dt_elements.html:7
 
#: rhodecode/templates/data_table/_dt_elements.html:103
 
msgid "delete"
 
msgstr "删除"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:54
 
#, python-format
 
msgid "Confirm to delete this group: %s"
 
msgstr "确认删除该版本库组: %s"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:62
 
msgid "There are no repositories groups yet"
 
msgstr "没有版本库组"
 

	
 
#: rhodecode/templates/admin/settings/hooks.html:5
 
#: rhodecode/templates/admin/settings/settings.html:5
 
msgid "Settings administration"
 
msgstr "设置管理员"
 
msgstr "系统设置"
 

	
 
#: rhodecode/templates/admin/settings/hooks.html:9
 
#: rhodecode/templates/admin/settings/settings.html:9
 
#: rhodecode/templates/settings/repo_settings.html:13
 
msgid "Settings"
 
msgstr "设置"
 

	
 
#: rhodecode/templates/admin/settings/hooks.html:24
 
msgid "Built in hooks - read only"
 
msgstr "内建钩子 - 只读"
 

	
 
#: rhodecode/templates/admin/settings/hooks.html:40
 
msgid "Custom hooks"
 
msgstr "自定义钩子"
 

	
 
#: rhodecode/templates/admin/settings/hooks.html:56
 
msgid "remove"
 
msgstr "删除"
 

	
 
#: rhodecode/templates/admin/settings/hooks.html:88
 
msgid "Failed to remove hook"
 
msgstr "移除钩子失败"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:24
 
msgid "Remap and rescan repositories"
 
msgstr "重新扫描并映射版本库"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:32
 
msgid "rescan option"
 
msgstr "重新扫描选项"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:38
 
msgid ""
 
"In case a repository was deleted from filesystem and there are leftovers in "
 
"the database check this option to scan obsolete data in database and remove "
 
"it."
 
msgstr ""
 
"如果版本库已经从文件系统删除,但数据库仍然有遗留信息,请勾选该项进行清理"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:39
 
msgid "destroy old data"
 
msgstr "清理旧数据"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:41
 
msgid ""
 
"Rescan repositories location for new repositories. Also deletes obsolete if "
 
"`destroy` flag is checked "
 
msgstr ""
 
@@ -2344,96 +2421,97 @@ msgid "Update repository after push (hg 
 
msgstr "推送后更新版本库(hg update)"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:207
 
msgid "Show repository size after push"
 
msgstr "推送后显示版本库大小"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:211
 
msgid "Log user push commands"
 
msgstr "记录用户推送命令"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:215
 
msgid "Log user pull commands"
 
msgstr "记录用户拉取命令"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:219
 
msgid "advanced setup"
 
msgstr "高级设置"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:224
 
msgid "Mercurial Extensions"
 
msgstr "Mercurial 扩展"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:229
 
msgid "largefiles extensions"
 
msgstr "大文件扩展"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:233
 
msgid "hgsubversion extensions"
 
msgstr "hgsubversion 扩展"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:235
 
msgid ""
 
"Requires hgsubversion library installed. Allows clonning from svn remote "
 
"locations"
 
msgstr " 允许从远程 svn 地址克隆。需要安装 hgsubversion 库"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:245
 
msgid "Repositories location"
 
msgstr "版本库路径"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:250
 
msgid ""
 
"This a crucial application setting. If you are really sure you need to "
 
"change this, you must restart application in order to make this setting take "
 
"effect. Click this label to unlock."
 
msgstr "这是一个关键设置。如果确认修改该项设置,请重启服务以便设置生效。"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:251
 
#: rhodecode/templates/base/base.html:218
 
msgid "unlock"
 
msgstr "解锁"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:252
 
msgid ""
 
"Location where repositories are stored. After changing this value a restart, "
 
"and rescan is required"
 
msgstr "版本库存储路径。 修改后需要重启和重新扫描"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:272
 
msgid "Test Email"
 
msgstr "测试邮件"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:280
 
msgid "Email to"
 
msgstr "发送到"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:288
 
msgid "Send"
 
msgstr "发送"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:294
 
msgid "System Info and Packages"
 
msgstr "系统和软件包信息"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:297
 
msgid "show"
 
msgstr "显示"
 

	
 
#: rhodecode/templates/admin/users/user_add.html:5
 
msgid "Add user"
 
msgstr "添加用户"
 

	
 
#: rhodecode/templates/admin/users/user_add.html:10
 
#: rhodecode/templates/admin/users/user_edit.html:11
 
msgid "Users"
 
msgstr "用户"
 

	
 
#: rhodecode/templates/admin/users/user_add.html:12
 
msgid "add new user"
 
msgstr "添加新用户"
 

	
 
#: rhodecode/templates/admin/users/user_add.html:50
 
msgid "Password confirmation"
 
msgstr "确认密码"
 

	
 
#: rhodecode/templates/admin/users/user_add.html:86
 
#: rhodecode/templates/admin/users/user_edit.html:113
 
@@ -2571,356 +2649,338 @@ msgid "Confirm to delete this pull reque
 
msgstr "确认删除拉取请求"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account_pullrequests.html:26
 
msgid "I participate in"
 
msgstr "我参与的"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account_pullrequests.html:33
 
#: rhodecode/templates/pullrequests/pullrequest_show_all.html:30
 
#, python-format
 
msgid "Pull request #%s opened by %s on %s"
 
msgstr "拉取请求 #%s 由 %s 创建于 %s"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account_repos.html:7
 
#: rhodecode/templates/bookmarks/bookmarks.html:40
 
#: rhodecode/templates/bookmarks/bookmarks_data.html:9
 
#: rhodecode/templates/branches/branches.html:55
 
#: rhodecode/templates/journal/journal.html:60
 
#: rhodecode/templates/tags/tags.html:40
 
#: rhodecode/templates/tags/tags_data.html:9
 
msgid "Revision"
 
msgstr "修订"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account_repos.html:28
 
#: rhodecode/templates/journal/journal.html:81
 
msgid "private"
 
msgstr "私有"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account_repos.html:31
 
#: rhodecode/templates/data_table/_dt_elements.html:7
 
#, python-format
 
msgid "Confirm to delete this repository: %s"
 
msgstr "确认删除版本库: %s"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account_repos.html:38
 
#: rhodecode/templates/journal/journal.html:94
 
msgid "No repositories yet"
 
msgstr "没有任何版本库"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account_repos.html:40
 
#: rhodecode/templates/journal/journal.html:96
 
msgid "create one now"
 
msgstr "创建一个"
 

	
 
#: rhodecode/templates/admin/users/users.html:5
 
msgid "Users administration"
 
msgstr "用户管理员"
 

	
 
#: rhodecode/templates/admin/users/users.html:9
 
#: rhodecode/templates/base/base.html:223
 
#: rhodecode/templates/base/base.html:232
 
msgid "users"
 
msgstr "用户"
 

	
 
#: rhodecode/templates/admin/users/users.html:23
 
msgid "ADD NEW USER"
 
msgstr "添加用户"
 

	
 
#: rhodecode/templates/admin/users/users.html:77
 
msgid "username"
 
msgstr "用户名"
 

	
 
#: rhodecode/templates/admin/users/users.html:80
 
msgid "firstname"
 
msgstr "名"
 

	
 
#: rhodecode/templates/admin/users/users.html:81
 
msgid "lastname"
 
msgstr "姓"
 

	
 
#: rhodecode/templates/admin/users/users.html:82
 
msgid "last login"
 
msgstr "最后登录"
 

	
 
#: rhodecode/templates/admin/users/users.html:84
 
#: rhodecode/templates/admin/users_groups/users_groups.html:34
 
msgid "active"
 
msgstr "启用"
 

	
 
#: rhodecode/templates/admin/users/users.html:86
 
#: rhodecode/templates/base/base.html:226
 
#: rhodecode/templates/base/base.html:235
 
msgid "ldap"
 
msgstr "LDAP"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_add.html:5
 
msgid "Add users group"
 
msgstr "添加用户组"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_add.html:10
 
#: rhodecode/templates/admin/users_groups/users_groups.html:9
 
msgid "Users groups"
 
msgstr "用户组"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_add.html:12
 
msgid "add new users group"
 
msgstr "添加新用户组"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:5
 
msgid "Edit users group"
 
msgstr "编辑用户组"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:11
 
msgid "UsersGroups"
 
msgstr "用户组"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:50
 
msgid "Members"
 
msgstr "成员"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:58
 
msgid "Choosen group members"
 
msgstr "选择组成员"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:61
 
msgid "Remove all elements"
 
msgstr "移除全部项目"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:75
 
msgid "Available members"
 
msgstr "启用成员"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:79
 
msgid "Add all elements"
 
msgstr "添加全部项目"
 

	
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:146
 
msgid "Group members"
 
msgstr "拥护者成员"
 

	
 
#: rhodecode/templates/admin/users_groups/users_groups.html:5
 
msgid "Users groups administration"
 
msgstr "用户组管理"
 

	
 
#: rhodecode/templates/admin/users_groups/users_groups.html:23
 
msgid "ADD NEW USER GROUP"
 
msgstr "添加新用户组"
 

	
 
#: rhodecode/templates/admin/users_groups/users_groups.html:32
 
msgid "group name"
 
msgstr "组名"
 

	
 
#: rhodecode/templates/admin/users_groups/users_groups.html:33
 
#: rhodecode/templates/base/root.html:46
 
msgid "members"
 
msgstr "成员"
 

	
 
#: rhodecode/templates/admin/users_groups/users_groups.html:45
 
#, python-format
 
msgid "Confirm to delete this users group: %s"
 
msgstr "确认删除该组: %s"
 

	
 
#: rhodecode/templates/base/base.html:41
 
msgid "Submit a bug"
 
msgstr "提交 bug"
 

	
 
#: rhodecode/templates/base/base.html:77
 
msgid "Login to your account"
 
msgstr "登录"
 

	
 
#: rhodecode/templates/base/base.html:100
 
msgid "Forgot password ?"
 
msgstr "忘记密码?"
 

	
 
#: rhodecode/templates/base/base.html:107
 
msgid "Log In"
 
msgstr "登录"
 

	
 
#: rhodecode/templates/base/base.html:118
 
msgid "Inbox"
 
msgstr "收件箱"
 

	
 
#: rhodecode/templates/base/base.html:122
 
#: rhodecode/templates/base/base.html:300
 
#: rhodecode/templates/base/base.html:302
 
#: rhodecode/templates/base/base.html:304
 
#: rhodecode/templates/bookmarks/bookmarks.html:11
 
#: rhodecode/templates/branches/branches.html:10
 
#: rhodecode/templates/changelog/changelog.html:10
 
#: rhodecode/templates/changeset/changeset.html:10
 
#: rhodecode/templates/changeset/changeset_range.html:9
 
#: rhodecode/templates/compare/compare_diff.html:9
 
#: rhodecode/templates/files/file_diff.html:8
 
#: rhodecode/templates/files/files.html:8
 
#: rhodecode/templates/files/files_add.html:15
 
#: rhodecode/templates/files/files_edit.html:15
 
#: rhodecode/templates/followers/followers.html:9
 
#: rhodecode/templates/forks/fork.html:9
 
#: rhodecode/templates/forks/forks.html:9
 
#: rhodecode/templates/pullrequests/pullrequest.html:8
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:8
 
#: rhodecode/templates/pullrequests/pullrequest_show_all.html:8
 
#: rhodecode/templates/settings/repo_settings.html:9
 
#: rhodecode/templates/shortlog/shortlog.html:10
 
#: rhodecode/templates/summary/summary.html:8
 
#: rhodecode/templates/tags/tags.html:11
 
msgid "Home"
 
msgstr "首页"
 

	
 
#: rhodecode/templates/base/base.html:123
 
#: rhodecode/templates/base/base.html:309
 
#: rhodecode/templates/base/base.html:311
 
#: rhodecode/templates/base/base.html:313
 
#: rhodecode/templates/base/base.html:318
 
#: rhodecode/templates/base/base.html:320
 
#: rhodecode/templates/base/base.html:322
 
#: rhodecode/templates/journal/journal.html:4
 
#: rhodecode/templates/journal/journal.html:21
 
#: rhodecode/templates/journal/public_journal.html:4
 
msgid "Journal"
 
msgstr "日志"
 

	
 
#: rhodecode/templates/base/base.html:125
 
msgid "Log Out"
 
msgstr "退出"
 

	
 
#: rhodecode/templates/base/base.html:144
 
msgid "Switch repository"
 
msgstr "切换版本库"
 

	
 
#: rhodecode/templates/base/base.html:146
 
msgid "Products"
 
msgstr "产品"
 

	
 
#: rhodecode/templates/base/base.html:152
 
#: rhodecode/templates/base/base.html:182
 
msgid "loading..."
 
msgstr "载入中..."
 

	
 
#: rhodecode/templates/base/base.html:158
 
#: rhodecode/templates/base/base.html:160
 
#: rhodecode/templates/base/base.html:162
 
#: rhodecode/templates/data_table/_dt_elements.html:15
 
#: rhodecode/templates/data_table/_dt_elements.html:17
 
#: rhodecode/templates/data_table/_dt_elements.html:19
 
msgid "Summary"
 
msgstr "概况"
 

	
 
#: rhodecode/templates/base/base.html:166
 
#: rhodecode/templates/base/base.html:168
 
#: rhodecode/templates/base/base.html:170
 
#: rhodecode/templates/changelog/changelog.html:15
 
#: rhodecode/templates/data_table/_dt_elements.html:23
 
#: rhodecode/templates/data_table/_dt_elements.html:25
 
#: rhodecode/templates/data_table/_dt_elements.html:27
 
msgid "Changelog"
 
msgstr "修订记录"
 

	
 
#: rhodecode/templates/base/base.html:175
 
#: rhodecode/templates/base/base.html:177
 
#: rhodecode/templates/base/base.html:179
 
msgid "Switch to"
 
msgstr "切换到"
 

	
 
#: rhodecode/templates/base/base.html:186
 
#: rhodecode/templates/base/base.html:188
 
#: rhodecode/templates/base/base.html:190
 
#: rhodecode/templates/data_table/_dt_elements.html:31
 
#: rhodecode/templates/data_table/_dt_elements.html:33
 
#: rhodecode/templates/data_table/_dt_elements.html:35
 
msgid "Files"
 
msgstr "浏览"
 

	
 
#: rhodecode/templates/base/base.html:195
 
#: rhodecode/templates/base/base.html:199
 
msgid "Options"
 
msgstr "选项"
 

	
 
#: rhodecode/templates/base/base.html:204
 
#: rhodecode/templates/base/base.html:206
 
#: rhodecode/templates/base/base.html:227
 
msgid "settings"
 
msgstr "设置"
 

	
 
#: rhodecode/templates/base/base.html:209
 
#| msgid "Repository creation"
 
msgid "repository settings"
 
msgstr "版本库设置"
 

	
 
#: rhodecode/templates/base/base.html:210
 
#: rhodecode/templates/data_table/_dt_elements.html:80
 
#: rhodecode/templates/forks/fork.html:13
 
msgid "fork"
 
msgstr "分支"
 

	
 
#: rhodecode/templates/base/base.html:211
 
#: rhodecode/templates/base/base.html:212
 
#: rhodecode/templates/changelog/changelog.html:40
 
msgid "Open new pull request"
 
msgstr "新建拉取请求"
 

	
 
#: rhodecode/templates/base/base.html:213
 
#: rhodecode/templates/base/base.html:214
 
msgid "search"
 
msgstr "搜索"
 

	
 
#: rhodecode/templates/base/base.html:222
 
#: rhodecode/templates/base/base.html:220
 
#| msgid "unlock"
 
msgid "lock"
 
msgstr "锁定"
 

	
 
#: rhodecode/templates/base/base.html:231
 
msgid "repositories groups"
 
msgstr "版本库组"
 

	
 
#: rhodecode/templates/base/base.html:224
 
#: rhodecode/templates/base/base.html:233
 
msgid "users groups"
 
msgstr "用户组"
 

	
 
#: rhodecode/templates/base/base.html:225
 
#: rhodecode/templates/base/base.html:234
 
msgid "permissions"
 
msgstr "权限"
 

	
 
#: rhodecode/templates/base/base.html:238
 
#: rhodecode/templates/base/base.html:240
 
#: rhodecode/templates/base/base.html:236
 
msgid "settings"
 
msgstr "设置"
 

	
 
#: rhodecode/templates/base/base.html:247
 
#: rhodecode/templates/base/base.html:249
 
msgid "Followers"
 
msgstr "关注者"
 

	
 
#: rhodecode/templates/base/base.html:246
 
#: rhodecode/templates/base/base.html:248
 
#: rhodecode/templates/base/base.html:255
 
#: rhodecode/templates/base/base.html:257
 
msgid "Forks"
 
msgstr "分支"
 

	
 
#: rhodecode/templates/base/base.html:327
 
#: rhodecode/templates/base/base.html:329
 
#: rhodecode/templates/base/base.html:331
 
#: rhodecode/templates/base/base.html:336
 
#: rhodecode/templates/base/base.html:338
 
#: rhodecode/templates/base/base.html:340
 
#: rhodecode/templates/search/search.html:52
 
msgid "Search"
 
msgstr "搜索"
 

	
 
#: rhodecode/templates/base/root.html:42
 
msgid "add another comment"
 
msgstr "添加新的评论"
 

	
 
#: rhodecode/templates/base/root.html:43
 
#: rhodecode/templates/journal/journal.html:120
 
#: rhodecode/templates/summary/summary.html:57
 
msgid "Stop following this repository"
 
msgstr "停止关注该版本库"
 

	
 
#: rhodecode/templates/base/root.html:44
 
#: rhodecode/templates/summary/summary.html:61
 
msgid "Start following this repository"
 
msgstr "开始关注该版本库"
 

	
 
#: rhodecode/templates/base/root.html:45
 
msgid "Group"
 
msgstr "组"
 

	
 
#: rhodecode/templates/base/root.html:47
 
msgid "search truncated"
 
msgstr "搜索被截断"
 

	
 
#: rhodecode/templates/base/root.html:48
 
msgid "no matching files"
 
msgstr "没有符合的文件"
 

	
 
#: rhodecode/templates/bookmarks/bookmarks.html:5
 
#, python-format
 
msgid "%s Bookmarks"
 
msgstr "%s 书签"
 

	
 
#: rhodecode/templates/bookmarks/bookmarks.html:39
 
#: rhodecode/templates/bookmarks/bookmarks_data.html:8
 
#: rhodecode/templates/branches/branches.html:54
 
#: rhodecode/templates/tags/tags.html:39
 
#: rhodecode/templates/tags/tags_data.html:8
 
msgid "Author"
 
msgstr "作者"
 

	
 
#: rhodecode/templates/branches/branches.html:5
 
#, python-format
 
msgid "%s Branches"
 
msgstr "%s 分支"
 
@@ -3062,100 +3122,96 @@ msgstr "添加"
 
#: rhodecode/templates/changelog/changelog_details.html:8
 
#: rhodecode/templates/changelog/changelog_details.html:9
 
#: rhodecode/templates/changelog/changelog_details.html:10
 
#: rhodecode/templates/changeset/changeset.html:70
 
#: rhodecode/templates/changeset/changeset.html:71
 
#: rhodecode/templates/changeset/changeset.html:72
 
#, python-format
 
msgid "affected %s files"
 
msgstr "影响 %s 文件"
 

	
 
#: rhodecode/templates/changeset/changeset.html:6
 
#, python-format
 
msgid "%s Changeset"
 
msgstr "%s 修订集"
 

	
 
#: rhodecode/templates/changeset/changeset.html:14
 
msgid "Changeset"
 
msgstr "修订集"
 

	
 
#: rhodecode/templates/changeset/changeset.html:43
 
#: rhodecode/templates/changeset/diff_block.html:20
 
msgid "raw diff"
 
msgstr "原始 diff"
 

	
 
#: rhodecode/templates/changeset/changeset.html:44
 
#: rhodecode/templates/changeset/diff_block.html:21
 
msgid "download diff"
 
msgstr "下载 diff"
 

	
 
#: rhodecode/templates/changeset/changeset.html:48
 
#: rhodecode/templates/changeset/changeset_file_comment.html:82
 
#, python-format
 
msgid "%d comment"
 
msgid_plural "%d comments"
 
msgstr[0] "%d 条评论"
 

	
 
#: rhodecode/templates/changeset/changeset.html:48
 
#: rhodecode/templates/changeset/changeset_file_comment.html:82
 
#, python-format
 
msgid "(%d inline)"
 
msgid_plural "(%d inline)"
 
msgstr[0] "(%d 内嵌)"
 

	
 
#: rhodecode/templates/changeset/changeset.html:103
 
#, python-format
 
msgid "%s files affected with %s insertions and %s deletions:"
 
msgstr "%s 个文件受影响包括 %s 行插入和 %s 行删除:"
 

	
 
#: rhodecode/templates/changeset/changeset.html:119
 
msgid "Changeset was too big and was cut off..."
 
msgstr "修订集太大已被截断......"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:42
 
msgid "Submitting..."
 
msgstr "提交中……"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:45
 
msgid "Commenting on line {1}."
 
msgstr "在 {1} 行上评论"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:46
 
#: rhodecode/templates/changeset/changeset_file_comment.html:121
 
#, python-format
 
msgid "Comments parsed using %s syntax with %s support."
 
msgstr "评论使用 %s 语法并支持 %s"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:48
 
#: rhodecode/templates/changeset/changeset_file_comment.html:123
 
msgid ""
 
"Use @username inside this text to send notification to this RhodeCode user"
 
msgstr "在文本中使用 @用户名 以发送通知到该 RhodeCode 用户"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:59
 
#: rhodecode/templates/changeset/changeset_file_comment.html:138
 
msgid "Comment"
 
msgstr "评论"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:60
 
#: rhodecode/templates/changeset/changeset_file_comment.html:71
 
msgid "Hide"
 
msgstr "影藏"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:67
 
msgid "You need to be logged in to comment."
 
msgstr "必须登录才能评论"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:67
 
msgid "Login now"
 
msgstr "现在登陆"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:118
 
msgid "Leave a comment"
 
msgstr "发表评论"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:124
 
msgid "Check this to change current status of code-review for this changeset"
 
msgstr "勾选以改变这个修订集的代码审查状态"
 

	
 
#: rhodecode/templates/changeset/changeset_file_comment.html:124
 
msgid "change status"
 
@@ -3570,143 +3626,147 @@ msgid "New pull request"
 
msgstr "新建拉取请求"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest.html:28
 
msgid "refresh overview"
 
msgstr "刷新概览"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest.html:66
 
msgid "Detailed compare view"
 
msgstr "详细比较显示"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest.html:70
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:82
 
msgid "Pull request reviewers"
 
msgstr "拉取请求检视人员"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest.html:79
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:94
 
msgid "owner"
 
msgstr "所有者"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest.html:91
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:109
 
msgid "Add reviewer to this pull request."
 
msgstr "为这个拉取请求增加检视人员"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest.html:97
 
msgid "Create new pull request"
 
msgstr "创建新的拉取请求"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest.html:106
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:25
 
#: rhodecode/templates/pullrequests/pullrequest_show_all.html:33
 
msgid "Title"
 
msgstr "标题"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest.html:115
 
msgid "description"
 
msgstr "描述"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest.html:123
 
msgid "Send pull request"
 
msgstr "发送拉取请求"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:23
 
#, python-format
 
msgid "Closed %s"
 
msgstr "关闭于 %s"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:23
 
#, python-format
 
msgid "with status %s"
 
msgstr "状态%s"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:31
 
msgid "Status"
 
msgstr "状态"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:36
 
msgid "Pull request status"
 
msgstr "拉取请求状态"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:44
 
msgid "Still not reviewed by"
 
msgstr "还未检视的检视人员"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:47
 
#, python-format
 
msgid "%d reviewer"
 
msgid_plural "%d reviewers"
 
msgstr[0] "%d 个检视者"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:54
 
msgid "Created on"
 
msgstr "创建于 %s"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:61
 
msgid "Compare view"
 
msgstr "比较显示"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show.html:65
 
msgid "Incoming changesets"
 
msgstr "传入修订集"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show_all.html:4
 
msgid "all pull requests"
 
msgstr "所有拉取请求"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show_all.html:12
 
msgid "All pull requests"
 
msgstr "所有拉取请求"
 

	
 
#: rhodecode/templates/pullrequests/pullrequest_show_all.html:27
 
msgid "Closed"
 
msgstr "已关闭"
 

	
 
# 中文中 repo name 在前面 serch term在后面
 
#: rhodecode/templates/search/search.html:6
 
#, python-format
 
msgid "Search \"%s\" in repository: %s"
 
msgstr "在版本库 %2s 中搜索 \"%1s\""
 
msgstr "搜索 \"%s\" 于版本库 %s 中"
 

	
 
#: rhodecode/templates/search/search.html:8
 
#, python-format
 
msgid "Search \"%s\" in all repositories"
 
msgstr "在所有的版本库中搜索  \"%s\""
 

	
 
#: rhodecode/templates/search/search.html:12
 
#: rhodecode/templates/search/search.html:32
 
#, python-format
 
msgid "Search in repository: %s"
 
msgstr "在版本库 %s 中搜索"
 

	
 
#: rhodecode/templates/search/search.html:14
 
#: rhodecode/templates/search/search.html:34
 
msgid "Search in all repositories"
 
msgstr "在所有的版本库中搜索"
 

	
 
#: rhodecode/templates/search/search.html:48
 
msgid "Search term"
 
msgstr "搜索短语"
 

	
 
#: rhodecode/templates/search/search.html:60
 
msgid "Search in"
 
msgstr "搜索范围"
 

	
 
#: rhodecode/templates/search/search.html:63
 
msgid "File contents"
 
msgstr "文件内容"
 

	
 
#: rhodecode/templates/search/search.html:64
 
msgid "Commit messages"
 
msgstr "提交信息"
 

	
 
#: rhodecode/templates/search/search.html:65
 
msgid "File names"
 
msgstr "文件名"
 

	
 
#: rhodecode/templates/search/search_commit.html:35
 
#: rhodecode/templates/search/search_content.html:21
 
#: rhodecode/templates/search/search_path.html:15
 
msgid "Permission denied"
 
msgstr "权限不足"
 

	
 
#: rhodecode/templates/settings/repo_settings.html:5
 
#, python-format
 
msgid "%s Settings"
 
msgstr "%s 设置"
 

	
 
@@ -3847,48 +3907,51 @@ msgstr "快速入门"
 
msgid "Readme file at revision '%s'"
 
msgstr "修订 '%s' 中的README"
 

	
 
#: rhodecode/templates/summary/summary.html:236
 
msgid "Permalink to this readme"
 
msgstr "这个 README 的固定链接"
 

	
 
#: rhodecode/templates/summary/summary.html:293
 
#, python-format
 
msgid "Download %s as %s"
 
msgstr "下载 %s 作为 %s"
 

	
 
#: rhodecode/templates/summary/summary.html:650
 
msgid "commits"
 
msgstr "提交"
 

	
 
#: rhodecode/templates/summary/summary.html:651
 
msgid "files added"
 
msgstr "文件已添加"
 

	
 
#: rhodecode/templates/summary/summary.html:652
 
msgid "files changed"
 
msgstr "文件已更改"
 

	
 
#: rhodecode/templates/summary/summary.html:653
 
msgid "files removed"
 
msgstr "文件已删除"
 

	
 
#: rhodecode/templates/summary/summary.html:656
 
msgid "commit"
 
msgstr "提交"
 

	
 
#: rhodecode/templates/summary/summary.html:657
 
msgid "file added"
 
msgstr "文件已添加"
 

	
 
#: rhodecode/templates/summary/summary.html:658
 
msgid "file changed"
 
msgstr "文件已更改"
 

	
 
#: rhodecode/templates/summary/summary.html:659
 
msgid "file removed"
 
msgstr "文件已删除"
 

	
 
#: rhodecode/templates/tags/tags.html:5
 
#, python-format
 
msgid "%s Tags"
 
msgstr "%s 标签"
 

	
 
#~ msgid "Groups"
 
#~ msgstr "组"
rhodecode/lib/helpers.py
Show inline comments
 
@@ -955,123 +955,142 @@ def urlify_changesets(text_, repository)
 
        pref = ''
 
        if match_obj.group().startswith(' '):
 
            pref = ' '
 
        tmpl = (
 
        '%(pref)s<a class="%(cls)s" href="%(url)s">'
 
        '%(rev)s'
 
        '</a>'
 
        )
 
        return tmpl % {
 
         'pref': pref,
 
         'cls': 'revision-link',
 
         'url': url('changeset_home', repo_name=repository, revision=rev),
 
         'rev': rev,
 
        }
 

	
 
    newtext = URL_PAT.sub(url_func, text_)
 

	
 
    return newtext
 

	
 

	
 
def urlify_commit(text_, repository=None, link_=None):
 
    """
 
    Parses given text message and makes proper links.
 
    issues are linked to given issue-server, and rest is a changeset link
 
    if link_ is given, in other case it's a plain text
 

	
 
    :param text_:
 
    :param repository:
 
    :param link_: changeset link
 
    """
 
    import re
 
    import traceback
 

	
 
    def escaper(string):
 
        return string.replace('<', '&lt;').replace('>', '&gt;')
 

	
 
    def linkify_others(t, l):
 
        urls = re.compile(r'(\<a.*?\<\/a\>)',)
 
        links = []
 
        for e in urls.split(t):
 
            if not urls.match(e):
 
                links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
 
            else:
 
                links.append(e)
 

	
 
        return ''.join(links)
 

	
 
    # urlify changesets - extrac revisions and make link out of them
 
    text_ = urlify_changesets(escaper(text_), repository)
 
    newtext = urlify_changesets(escaper(text_), repository)
 

	
 
    try:
 
        conf = config['app_conf']
 

	
 
        URL_PAT = re.compile(r'%s' % conf.get('issue_pat'))
 
        # allow multiple issue servers to be used
 
        valid_indices = [
 
            x.group(1)
 
            for x in map(lambda x: re.match(r'issue_pat(.*)', x), conf.keys())
 
            if x and 'issue_server_link%s' % x.group(1) in conf
 
            and 'issue_prefix%s' % x.group(1) in conf
 
        ]
 

	
 
        log.debug('found issue server suffixes `%s` during valuation of: %s'
 
                  % (','.join(valid_indices), newtext))
 

	
 
        if URL_PAT:
 
            ISSUE_SERVER_LNK = conf.get('issue_server_link')
 
            ISSUE_PREFIX = conf.get('issue_prefix')
 
        for pattern_index in valid_indices:
 
            ISSUE_PATTERN = conf.get('issue_pat%s' % pattern_index)
 
            ISSUE_SERVER_LNK = conf.get('issue_server_link%s' % pattern_index)
 
            ISSUE_PREFIX = conf.get('issue_prefix%s' % pattern_index)
 

	
 
            log.debug('pattern suffix `%s` PAT:%s SERVER_LINK:%s PREFIX:%s'
 
                      % (pattern_index, ISSUE_PATTERN, ISSUE_SERVER_LNK,
 
                         ISSUE_PREFIX))
 

	
 
            URL_PAT = re.compile(r'%s' % ISSUE_PATTERN)
 

	
 
            def url_func(match_obj):
 
                pref = ''
 
                if match_obj.group().startswith(' '):
 
                    pref = ' '
 

	
 
                issue_id = ''.join(match_obj.groups())
 
                tmpl = (
 
                '%(pref)s<a class="%(cls)s" href="%(url)s">'
 
                '%(issue-prefix)s%(id-repr)s'
 
                '</a>'
 
                )
 
                url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
 
                if repository:
 
                    url = url.replace('{repo}', repository)
 
                    repo_name = repository.split(URL_SEP)[-1]
 
                    url = url.replace('{repo_name}', repo_name)
 

	
 
                return tmpl % {
 
                     'pref': pref,
 
                     'cls': 'issue-tracker-link',
 
                     'url': url,
 
                     'id-repr': issue_id,
 
                     'issue-prefix': ISSUE_PREFIX,
 
                     'serv': ISSUE_SERVER_LNK,
 
                }
 
            newtext = URL_PAT.sub(url_func, newtext)
 
            log.debug('processed prefix:`%s` => %s' % (pattern_index, newtext))
 

	
 
            newtext = URL_PAT.sub(url_func, text_)
 

	
 
        # if we actually did something above
 
        if valid_indices:
 
            if link_:
 
                # wrap not links into final link => link_
 
                newtext = linkify_others(newtext, link_)
 

	
 
            return literal(newtext)
 
    except:
 
        log.error(traceback.format_exc())
 
        pass
 

	
 
    return text_
 
    return newtext
 

	
 

	
 
def rst(source):
 
    return literal('<div class="rst-block">%s</div>' %
 
                   MarkupRenderer.rst(source))
 

	
 

	
 
def rst_w_mentions(source):
 
    """
 
    Wrapped rst renderer with @mention highlighting
 

	
 
    :param source:
 
    """
 
    return literal('<div class="rst-block">%s</div>' %
 
                   MarkupRenderer.rst_with_mentions(source))
 

	
 

	
 
def changeset_status(repo, revision):
 
    return ChangesetStatusModel().get_status(repo, revision)
 

	
 

	
 
def changeset_status_lbl(changeset_status):
 
    return dict(ChangesetStatus.STATUSES).get(changeset_status)
 

	
 

	
 
def get_permission_name(key):
 
    return dict(Permission.PERMS).get(key)
rhodecode/lib/hooks.py
Show inline comments
 
@@ -272,111 +272,111 @@ def log_create_repository(repository_dic
 
     'created_on',
 
     'enable_downloads',
 
     'repo_id',
 
     'user_id',
 
     'enable_statistics',
 
     'clone_uri',
 
     'fork_id',
 
     'group_id',
 
     'repo_name'
 

	
 
    """
 
    from rhodecode import EXTENSIONS
 
    callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None)
 
    if isfunction(callback):
 
        kw = {}
 
        kw.update(repository_dict)
 
        kw.update({'created_by': created_by})
 
        kw.update(kwargs)
 
        return callback(**kw)
 

	
 
    return 0
 

	
 
handle_git_pre_receive = (lambda repo_path, revs, env:
 
    handle_git_receive(repo_path, revs, env, hook_type='pre'))
 
handle_git_post_receive = (lambda repo_path, revs, env:
 
    handle_git_receive(repo_path, revs, env, hook_type='post'))
 

	
 

	
 
def handle_git_receive(repo_path, revs, env, hook_type='post'):
 
    """
 
    A really hacky method that is runned by git post-receive hook and logs
 
    an push action together with pushed revisions. It's executed by subprocess
 
    thus needs all info to be able to create a on the fly pylons enviroment,
 
    connect to database and run the logging code. Hacky as sh*t but works.
 

	
 
    :param repo_path:
 
    :type repo_path:
 
    :param revs:
 
    :type revs:
 
    :param env:
 
    :type env:
 
    """
 
    from paste.deploy import appconfig
 
    from sqlalchemy import engine_from_config
 
    from rhodecode.config.environment import load_environment
 
    from rhodecode.model import init_model
 
    from rhodecode.model.db import RhodeCodeUi
 
    from rhodecode.lib.utils import make_ui
 
    extras = json.loads(env['RHODECODE_EXTRAS'])
 

	
 
    path, ini_name = os.path.split(env['RHODECODE_CONFIG_FILE'])
 
    path, ini_name = os.path.split(extras['config'])
 
    conf = appconfig('config:%s' % ini_name, relative_to=path)
 
    load_environment(conf.global_conf, conf.local_conf)
 

	
 
    engine = engine_from_config(conf, 'sqlalchemy.db1.')
 
    init_model(engine)
 

	
 
    baseui = make_ui('db')
 
    # fix if it's not a bare repo
 
    if repo_path.endswith('.git'):
 
        repo_path = repo_path[:-4]
 
    if repo_path.endswith(os.sep + '.git'):
 
        repo_path = repo_path[:-5]
 

	
 
    repo = Repository.get_by_full_path(repo_path)
 
    if not repo:
 
        raise OSError('Repository %s not found in database'
 
                      % (safe_str(repo_path)))
 

	
 
    _hooks = dict(baseui.configitems('hooks')) or {}
 

	
 
    extras = json.loads(env['RHODECODE_EXTRAS'])
 
    for k, v in extras.items():
 
        baseui.setconfig('rhodecode_extras', k, v)
 
    repo = repo.scm_instance
 
    repo.ui = baseui
 

	
 
    if hook_type == 'pre':
 
        pre_push(baseui, repo)
 

	
 
    # if push hook is enabled via web interface
 
    elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH):
 

	
 
        rev_data = []
 
        for l in revs:
 
            old_rev, new_rev, ref = l.split(' ')
 
            _ref_data = ref.split('/')
 
            if _ref_data[1] in ['tags', 'heads']:
 
                rev_data.append({'old_rev': old_rev,
 
                                 'new_rev': new_rev,
 
                                 'ref': ref,
 
                                 'type': _ref_data[1],
 
                                 'name': _ref_data[2].strip()})
 

	
 
        git_revs = []
 
        for push_ref  in rev_data:
 
            _type = push_ref['type']
 
            if _type == 'heads':
 
                if push_ref['old_rev'] == EmptyChangeset().raw_id:
 
                    cmd = "for-each-ref --format='%(refname)' 'refs/heads/*'"
 
                    heads = repo.run_git_command(cmd)[0]
 
                    heads = heads.replace(push_ref['ref'], '')
 
                    heads = ' '.join(map(lambda c: c.strip('\n').strip(),
 
                                         heads.splitlines()))
 
                    cmd = (('log %(new_rev)s' % push_ref) +
 
                           ' --reverse --pretty=format:"%H" --not ' + heads)
 
                else:
 
                    cmd = (('log %(old_rev)s..%(new_rev)s' % push_ref) +
 
                           ' --reverse --pretty=format:"%H"')
 
                git_revs += repo.run_git_command(cmd)[0].splitlines()
 
            elif _type == 'tags':
 
                git_revs += [push_ref['name']]
 

	
 
        log_push_action(baseui, repo, _git_revs=git_revs)
rhodecode/lib/middleware/pygrack.py
Show inline comments
 
import os
 
import socket
 
import logging
 
import subprocess
 
import traceback
 

	
 
from webob import Request, Response, exc
 

	
 
from rhodecode.lib import subprocessio
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class FileWrapper(object):
 

	
 
    def __init__(self, fd, content_length):
 
        self.fd = fd
 
        self.content_length = content_length
 
        self.remain = content_length
 

	
 
    def read(self, size):
 
        if size <= self.remain:
 
            try:
 
                data = self.fd.read(size)
 
            except socket.error:
 
                raise IOError(self)
 
            self.remain -= size
 
        elif self.remain:
 
            data = self.fd.read(self.remain)
 
            self.remain = 0
 
        else:
 
            data = None
 
        return data
 

	
 
    def __repr__(self):
 
        return '<FileWrapper %s len: %s, read: %s>' % (
 
            self.fd, self.content_length, self.content_length - self.remain
 
        )
 

	
 

	
 
class GitRepository(object):
 
    git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
 
    commands = ['git-upload-pack', 'git-receive-pack']
 

	
 
    def __init__(self, repo_name, content_path, extras):
 
        files = set([f.lower() for f in os.listdir(content_path)])
 
        if  not (self.git_folder_signature.intersection(files)
 
                == self.git_folder_signature):
 
            raise OSError('%s missing git signature' % content_path)
 
        self.content_path = content_path
 
        self.valid_accepts = ['application/x-%s-result' %
 
                              c for c in self.commands]
 
        self.repo_name = repo_name
 
        self.extras = extras
 

	
 
    def _get_fixedpath(self, path):
 
        """
 
        Small fix for repo_path
 

	
 
        :param path:
 
        :type path:
 
        """
 
        return path.split(self.repo_name, 1)[-1].strip('/')
 

	
 
    def inforefs(self, request, environ):
 
        """
 
        WSGI Response producer for HTTP GET Git Smart
 
        HTTP /info/refs request.
 
        """
 

	
 
        git_command = request.GET.get('service')
 
        if git_command not in self.commands:
 
            log.debug('command %s not allowed' % git_command)
 
            return exc.HTTPMethodNotAllowed()
 

	
 
        # note to self:
 
        # please, resist the urge to add '\n' to git capture and increment
 
        # line count by 1.
 
        # The code in Git client not only does NOT need '\n', but actually
 
        # blows up if you sprinkle "flush" (0000) as "0001\n".
 
        # It reads binary, per number of bytes specified.
 
        # if you do add '\n' as part of data, count it.
 
        server_advert = '# service=%s' % git_command
 
        packet_len = str(hex(len(server_advert) + 4)[2:].rjust(4, '0')).lower()
 
        try:
 
            out = subprocessio.SubprocessIOChunker(
 
                r'git %s --stateless-rpc --advertise-refs "%s"' % (
 
                                git_command[4:], self.content_path),
 
                starting_values=[
 
                    packet_len + server_advert + '0000'
 
                ]
 
            )
 
        except EnvironmentError, e:
 
            log.exception(e)
 
            log.error(traceback.format_exc())
 
            raise exc.HTTPExpectationFailed()
 
        resp = Response()
 
        resp.content_type = 'application/x-%s-advertisement' % str(git_command)
 
        resp.charset = None
 
        resp.app_iter = out
 
        return resp
 

	
 
    def backend(self, request, environ):
 
        """
 
        WSGI Response producer for HTTP POST Git Smart HTTP requests.
 
        Reads commands and data from HTTP POST's body.
 
        returns an iterator obj with contents of git command's
 
        response to stdout
 
        """
 
        git_command = self._get_fixedpath(request.path_info)
 
        if git_command not in self.commands:
 
            log.debug('command %s not allowed' % git_command)
 
            return exc.HTTPMethodNotAllowed()
 

	
 
        if 'CONTENT_LENGTH' in environ:
 
            inputstream = FileWrapper(environ['wsgi.input'],
 
                                      request.content_length)
 
        else:
 
            inputstream = environ['wsgi.input']
 

	
 
        try:
 
            gitenv = os.environ
 
            from rhodecode import CONFIG
 
            from rhodecode.lib.compat import json
 
            gitenv['RHODECODE_EXTRAS'] = json.dumps(self.extras)
 
            # forget all configs
 
            gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
 
            # we need current .ini file used to later initialize rhodecode
 
            # env and connect to db
 
            gitenv['RHODECODE_CONFIG_FILE'] = CONFIG['__file__']
 
            opts = dict(
 
                env=gitenv,
 
                cwd=os.getcwd()
 
            )
 
            cmd = r'git %s --stateless-rpc "%s"' % (git_command[4:],
 
                                                    self.content_path),
 
            log.debug('handling cmd %s' % cmd)
 
            out = subprocessio.SubprocessIOChunker(
 
                r'git %s --stateless-rpc "%s"' % (git_command[4:],
 
                                                  self.content_path),
 
                cmd,
 
                inputstream=inputstream,
 
                **opts
 
            )
 
        except EnvironmentError, e:
 
            log.exception(e)
 
            log.error(traceback.format_exc())
 
            raise exc.HTTPExpectationFailed()
 

	
 
        if git_command in [u'git-receive-pack']:
 
            # updating refs manually after each push.
 
            # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
 
            subprocess.call(u'git --git-dir "%s" '
 
                            'update-server-info' % self.content_path,
 
                            shell=True)
 
            cmd = (u'git --git-dir "%s" '
 
                    'update-server-info' % self.content_path)
 
            log.debug('handling cmd %s' % cmd)
 
            subprocess.call(cmd, shell=True)
 

	
 
        resp = Response()
 
        resp.content_type = 'application/x-%s-result' % git_command.encode('utf8')
 
        resp.charset = None
 
        resp.app_iter = out
 
        return resp
 

	
 
    def __call__(self, environ, start_response):
 
        request = Request(environ)
 
        _path = self._get_fixedpath(request.path_info)
 
        if _path.startswith('info/refs'):
 
            app = self.inforefs
 
        elif [a for a in self.valid_accepts if a in request.accept]:
 
            app = self.backend
 
        try:
 
            resp = app(request, environ)
 
        except exc.HTTPException, e:
 
            resp = e
 
            log.exception(e)
 
            log.error(traceback.format_exc())
 
        except Exception, e:
 
            log.exception(e)
 
            log.error(traceback.format_exc())
 
            resp = exc.HTTPInternalServerError()
 
        return resp(environ, start_response)
 

	
 

	
 
class GitDirectory(object):
 

	
 
    def __init__(self, repo_root, repo_name, extras):
 
        repo_location = os.path.join(repo_root, repo_name)
 
        if not os.path.isdir(repo_location):
 
            raise OSError(repo_location)
 

	
 
        self.content_path = repo_location
 
        self.repo_name = repo_name
 
        self.repo_location = repo_location
 
        self.extras = extras
 

	
 
    def __call__(self, environ, start_response):
 
        content_path = self.content_path
 
        try:
 
            app = GitRepository(self.repo_name, content_path, self.extras)
 
        except (AssertionError, OSError):
 
            content_path = os.path.join(content_path, '.git')
 
            if os.path.isdir(content_path):
 
                app = GitRepository(self.repo_name, content_path, self.extras)
 
            else:
 
                return exc.HTTPNotFound()(environ, start_response)
 
        return app(environ, start_response)
 

	
 

	
 
def make_wsgi_app(repo_name, repo_root, extras):
 
    return GitDirectory(repo_root, repo_name, extras)
rhodecode/lib/middleware/simplegit.py
Show inline comments
 
@@ -34,97 +34,97 @@ from dulwich.web import LimitedInputFilt
 
from rhodecode.lib.exceptions import HTTPLockedRC
 
from rhodecode.lib.hooks import pre_pull
 

	
 

	
 
class SimpleGitUploadPackHandler(dulserver.UploadPackHandler):
 

	
 
    def handle(self):
 
        write = lambda x: self.proto.write_sideband(1, x)
 

	
 
        graph_walker = dulserver.ProtocolGraphWalker(self,
 
                                                     self.repo.object_store,
 
                                                     self.repo.get_peeled)
 
        objects_iter = self.repo.fetch_objects(
 
          graph_walker.determine_wants, graph_walker, self.progress,
 
          get_tagged=self.get_tagged)
 

	
 
        # Did the process short-circuit (e.g. in a stateless RPC call)? Note
 
        # that the client still expects a 0-object pack in most cases.
 
        if objects_iter is None:
 
            return
 

	
 
        self.progress("counting objects: %d, done.\n" % len(objects_iter))
 
        dulserver.write_pack_objects(dulserver.ProtocolFile(None, write),
 
                                     objects_iter)
 
        messages = []
 
        messages.append('thank you for using rhodecode')
 

	
 
        for msg in messages:
 
            self.progress(msg + "\n")
 
        # we are done
 
        self.proto.write("0000")
 

	
 

	
 
dulserver.DEFAULT_HANDLERS = {
 
  #git-ls-remote, git-clone, git-fetch and git-pull
 
  'git-upload-pack': SimpleGitUploadPackHandler,
 
  #git-push
 
  'git-receive-pack': dulserver.ReceivePackHandler,
 
}
 

	
 
# not used for now until dulwich get's fixed
 
#from dulwich.repo import Repo
 
#from dulwich.web import make_wsgi_chain
 

	
 
from paste.httpheaders import REMOTE_USER, AUTH_TYPE
 
from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
 
    HTTPBadRequest, HTTPNotAcceptable
 

	
 
from rhodecode.lib.utils2 import safe_str
 
from rhodecode.lib.utils2 import safe_str, fix_PATH
 
from rhodecode.lib.base import BaseVCSController
 
from rhodecode.lib.auth import get_container_username
 
from rhodecode.lib.utils import is_valid_repo, make_ui
 
from rhodecode.lib.compat import json
 
from rhodecode.model.db import User, RhodeCodeUi
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
GIT_PROTO_PAT = re.compile(r'^/(.+)/(info/refs|git-upload-pack|git-receive-pack)')
 

	
 

	
 
def is_git(environ):
 
    path_info = environ['PATH_INFO']
 
    isgit_path = GIT_PROTO_PAT.match(path_info)
 
    log.debug('pathinfo: %s detected as GIT %s' % (
 
        path_info, isgit_path != None)
 
    )
 
    return isgit_path
 

	
 

	
 
class SimpleGit(BaseVCSController):
 

	
 
    def _handle_request(self, environ, start_response):
 
        if not is_git(environ):
 
            return self.application(environ, start_response)
 
        if not self._check_ssl(environ, start_response):
 
            return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
 

	
 
        ipaddr = self._get_ip_addr(environ)
 
        username = None
 
        self._git_first_op = False
 
        # skip passing error to error controller
 
        environ['pylons.status_code_redirect'] = True
 

	
 
        #======================================================================
 
        # EXTRACT REPOSITORY NAME FROM ENV
 
        #======================================================================
 
        try:
 
            repo_name = self.__get_repository(environ)
 
            log.debug('Extracted repo name is %s' % repo_name)
 
        except:
 
            return HTTPInternalServerError()(environ, start_response)
 

	
 
        # quick check if that dir exists...
 
        if is_valid_repo(repo_name, self.basepath, 'git') is False:
 
            return HTTPNotFound()(environ, start_response)
 

	
 
@@ -143,126 +143,129 @@ class SimpleGit(BaseVCSController):
 
                                                    repo_name)
 

	
 
            if anonymous_perm is not True or anonymous_user.active is False:
 
                if anonymous_perm is not True:
 
                    log.debug('Not enough credentials to access this '
 
                              'repository as anonymous user')
 
                if anonymous_user.active is False:
 
                    log.debug('Anonymous access is disabled, running '
 
                              'authentication')
 
                #==============================================================
 
                # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
 
                # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
 
                #==============================================================
 

	
 
                # Attempting to retrieve username from the container
 
                username = get_container_username(environ, self.config)
 

	
 
                # If not authenticated by the container, running basic auth
 
                if not username:
 
                    self.authenticate.realm = \
 
                        safe_str(self.config['rhodecode_realm'])
 
                    result = self.authenticate(environ)
 
                    if isinstance(result, str):
 
                        AUTH_TYPE.update(environ, 'basic')
 
                        REMOTE_USER.update(environ, result)
 
                        username = result
 
                    else:
 
                        return result.wsgi_application(environ, start_response)
 

	
 
                #==============================================================
 
                # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
 
                #==============================================================
 
                try:
 
                    user = self.__get_user(username)
 
                    if user is None or not user.active:
 
                        return HTTPForbidden()(environ, start_response)
 
                    username = user.username
 
                except:
 
                    log.error(traceback.format_exc())
 
                    return HTTPInternalServerError()(environ, start_response)
 

	
 
                #check permissions for this repository
 
                perm = self._check_permission(action, user, repo_name)
 
                if perm is not True:
 
                    return HTTPForbidden()(environ, start_response)
 

	
 
        # extras are injected into UI object and later available
 
        # in hooks executed by rhodecode
 
        from rhodecode import CONFIG
 
        extras = {
 
            'ip': ipaddr,
 
            'username': username,
 
            'action': action,
 
            'repository': repo_name,
 
            'scm': 'git',
 
            'config': CONFIG['__file__'],
 
            'make_lock': None,
 
            'locked_by': [None, None]
 
        }
 

	
 
        #===================================================================
 
        # GIT REQUEST HANDLING
 
        #===================================================================
 
        repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
 
        log.debug('Repository path is %s' % repo_path)
 

	
 
        # CHECK LOCKING only if it's not ANONYMOUS USER
 
        if username != User.DEFAULT_USER:
 
            log.debug('Checking locking on repository')
 
            (make_lock,
 
             locked,
 
             locked_by) = self._check_locking_state(
 
                            environ=environ, action=action,
 
                            repo=repo_name, user_id=user.user_id
 
                       )
 
            # store the make_lock for later evaluation in hooks
 
            extras.update({'make_lock': make_lock,
 
                           'locked_by': locked_by})
 
        # set the environ variables for this request
 
        os.environ['RC_SCM_DATA'] = json.dumps(extras)
 
        fix_PATH()
 
        log.debug('HOOKS extras is %s' % extras)
 
        baseui = make_ui('db')
 
        self.__inject_extras(repo_path, baseui, extras)
 

	
 
        try:
 
            # invalidate cache on push
 
            if action == 'push':
 
                self._invalidate_cache(repo_name)
 
            self._handle_githooks(repo_name, action, baseui, environ)
 

	
 
            log.info('%s action on GIT repo "%s"' % (action, repo_name))
 
            app = self.__make_app(repo_name, repo_path, extras)
 
            return app(environ, start_response)
 
        except HTTPLockedRC, e:
 
            log.debug('Repositry LOCKED ret code 423!')
 
            return e(environ, start_response)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            return HTTPInternalServerError()(environ, start_response)
 

	
 
    def __make_app(self, repo_name, repo_path, extras):
 
        """
 
        Make an wsgi application using dulserver
 

	
 
        :param repo_name: name of the repository
 
        :param repo_path: full path to the repository
 
        """
 

	
 
        from rhodecode.lib.middleware.pygrack import make_wsgi_app
 
        app = make_wsgi_app(
 
            repo_root=safe_str(self.basepath),
 
            repo_name=repo_name,
 
            extras=extras,
 
        )
 
        app = GunzipFilter(LimitedInputFilter(app))
 
        return app
 

	
 
    def __get_repository(self, environ):
 
        """
 
        Get's repository name out of PATH_INFO header
 

	
 
        :param environ: environ where PATH_INFO is stored
 
        """
 
        try:
 
            environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
 
            repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
 
        except:
 
            log.error(traceback.format_exc())
rhodecode/lib/middleware/simplehg.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.lib.middleware.simplehg
 
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
    SimpleHG middleware for handling mercurial protocol request
 
    (push/clone etc.). It's implemented with basic auth function
 

	
 
    :created_on: Apr 28, 2010
 
    :author: marcink
 
    :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
 
    :license: GPLv3, see COPYING for more details.
 
"""
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 

	
 
import os
 
import logging
 
import traceback
 
import urllib
 

	
 
from mercurial.error import RepoError
 
from mercurial.hgweb import hgweb_mod
 

	
 
from paste.httpheaders import REMOTE_USER, AUTH_TYPE
 
from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
 
    HTTPBadRequest, HTTPNotAcceptable
 

	
 
from rhodecode.lib.utils2 import safe_str
 
from rhodecode.lib.utils2 import safe_str, fix_PATH
 
from rhodecode.lib.base import BaseVCSController
 
from rhodecode.lib.auth import get_container_username
 
from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
 
from rhodecode.lib.compat import json
 
from rhodecode.model.db import User
 
from rhodecode.lib.exceptions import HTTPLockedRC
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
def is_mercurial(environ):
 
    """
 
    Returns True if request's target is mercurial server - header
 
    ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``.
 
    """
 
    http_accept = environ.get('HTTP_ACCEPT')
 
    path_info = environ['PATH_INFO']
 
    if http_accept and http_accept.startswith('application/mercurial'):
 
        ishg_path = True
 
    else:
 
        ishg_path = False
 

	
 
    log.debug('pathinfo: %s detected as HG %s' % (
 
        path_info, ishg_path)
 
    )
 
    return ishg_path
 

	
 

	
 
class SimpleHg(BaseVCSController):
 

	
 
    def _handle_request(self, environ, start_response):
 
        if not is_mercurial(environ):
 
            return self.application(environ, start_response)
 
        if not self._check_ssl(environ, start_response):
 
            return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response)
 

	
 
        ipaddr = self._get_ip_addr(environ)
 
        username = None
 
        # skip passing error to error controller
 
        environ['pylons.status_code_redirect'] = True
 

	
 
        #======================================================================
 
        # EXTRACT REPOSITORY NAME FROM ENV
 
        #======================================================================
 
        try:
 
            repo_name = environ['REPO_NAME'] = self.__get_repository(environ)
 
            log.debug('Extracted repo name is %s' % repo_name)
 
@@ -107,126 +106,129 @@ class SimpleHg(BaseVCSController):
 
                                                    repo_name)
 

	
 
            if anonymous_perm is not True or anonymous_user.active is False:
 
                if anonymous_perm is not True:
 
                    log.debug('Not enough credentials to access this '
 
                              'repository as anonymous user')
 
                if anonymous_user.active is False:
 
                    log.debug('Anonymous access is disabled, running '
 
                              'authentication')
 
                #==============================================================
 
                # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
 
                # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
 
                #==============================================================
 

	
 
                # Attempting to retrieve username from the container
 
                username = get_container_username(environ, self.config)
 

	
 
                # If not authenticated by the container, running basic auth
 
                if not username:
 
                    self.authenticate.realm = \
 
                        safe_str(self.config['rhodecode_realm'])
 
                    result = self.authenticate(environ)
 
                    if isinstance(result, str):
 
                        AUTH_TYPE.update(environ, 'basic')
 
                        REMOTE_USER.update(environ, result)
 
                        username = result
 
                    else:
 
                        return result.wsgi_application(environ, start_response)
 

	
 
                #==============================================================
 
                # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
 
                #==============================================================
 
                try:
 
                    user = self.__get_user(username)
 
                    if user is None or not user.active:
 
                        return HTTPForbidden()(environ, start_response)
 
                    username = user.username
 
                except:
 
                    log.error(traceback.format_exc())
 
                    return HTTPInternalServerError()(environ, start_response)
 

	
 
                #check permissions for this repository
 
                perm = self._check_permission(action, user, repo_name)
 
                if perm is not True:
 
                    return HTTPForbidden()(environ, start_response)
 

	
 
        # extras are injected into mercurial UI object and later available
 
        # in hg hooks executed by rhodecode
 
        from rhodecode import CONFIG
 
        extras = {
 
            'ip': ipaddr,
 
            'username': username,
 
            'action': action,
 
            'repository': repo_name,
 
            'scm': 'hg',
 
            'config': CONFIG['__file__'],
 
            'make_lock': None,
 
            'locked_by': [None, None]
 
        }
 
        #======================================================================
 
        # MERCURIAL REQUEST HANDLING
 
        #======================================================================
 
        repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name))
 
        log.debug('Repository path is %s' % repo_path)
 

	
 
        # CHECK LOCKING only if it's not ANONYMOUS USER
 
        if username != User.DEFAULT_USER:
 
            log.debug('Checking locking on repository')
 
            (make_lock,
 
             locked,
 
             locked_by) = self._check_locking_state(
 
                            environ=environ, action=action,
 
                            repo=repo_name, user_id=user.user_id
 
                       )
 
            # store the make_lock for later evaluation in hooks
 
            extras.update({'make_lock': make_lock,
 
                           'locked_by': locked_by})
 

	
 
        # set the environ variables for this request
 
        os.environ['RC_SCM_DATA'] = json.dumps(extras)
 
        fix_PATH()
 
        log.debug('HOOKS extras is %s' % extras)
 
        baseui = make_ui('db')
 
        self.__inject_extras(repo_path, baseui, extras)
 

	
 
        try:
 
            # invalidate cache on push
 
            if action == 'push':
 
                self._invalidate_cache(repo_name)
 
            log.info('%s action on HG repo "%s"' % (action, repo_name))
 
            app = self.__make_app(repo_path, baseui, extras)
 
            return app(environ, start_response)
 
        except RepoError, e:
 
            if str(e).find('not found') != -1:
 
                return HTTPNotFound()(environ, start_response)
 
        except HTTPLockedRC, e:
 
            log.debug('Repositry LOCKED ret code 423!')
 
            return e(environ, start_response)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            return HTTPInternalServerError()(environ, start_response)
 

	
 
    def __make_app(self, repo_name, baseui, extras):
 
        """
 
        Make an wsgi application using hgweb, and inject generated baseui
 
        instance, additionally inject some extras into ui object
 
        """
 
        return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
 

	
 
    def __get_repository(self, environ):
 
        """
 
        Get's repository name out of PATH_INFO header
 

	
 
        :param environ: environ where PATH_INFO is stored
 
        """
 
        try:
 
            environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
 
            repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
 
            if repo_name.endswith('/'):
 
                repo_name = repo_name.rstrip('/')
 
        except:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
        return repo_name
 

	
 
    def __get_user(self, username):
 
        return User.get_by_username(username)
 

	
rhodecode/lib/utils2.py
Show inline comments
 
@@ -436,48 +436,64 @@ def get_changeset_safe(repo, rev):
 
    from rhodecode.lib.vcs.exceptions import RepositoryError
 
    from rhodecode.lib.vcs.backends.base import EmptyChangeset
 
    if not isinstance(repo, BaseRepository):
 
        raise Exception('You must pass an Repository '
 
                        'object as first argument got %s', type(repo))
 

	
 
    try:
 
        cs = repo.get_changeset(rev)
 
    except RepositoryError:
 
        cs = EmptyChangeset(requested_revision=rev)
 
    return cs
 

	
 

	
 
def datetime_to_time(dt):
 
    if dt:
 
        return time.mktime(dt.timetuple())
 

	
 

	
 
def time_to_datetime(tm):
 
    if tm:
 
        if isinstance(tm, basestring):
 
            try:
 
                tm = float(tm)
 
            except ValueError:
 
                return
 
        return datetime.datetime.fromtimestamp(tm)
 

	
 
MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
 

	
 

	
 
def extract_mentioned_users(s):
 
    """
 
    Returns unique usernames from given string s that have @mention
 

	
 
    :param s: string to get mentions
 
    """
 
    usrs = set()
 
    for username in re.findall(MENTIONS_REGEX, s):
 
        usrs.add(username)
 

	
 
    return sorted(list(usrs), key=lambda k: k.lower())
 

	
 

	
 
class AttributeDict(dict):
 
    def __getattr__(self, attr):
 
        return self.get(attr, None)
 
    __setattr__ = dict.__setitem__
 
    __delattr__ = dict.__delitem__
 

	
 

	
 
def fix_PATH(os_=None):
 
    """
 
    Get current active python path, and append it to PATH variable to fix issues
 
    of subprocess calls and different python versions
 
    """
 
    import sys
 
    if os_ is None:
 
        import os
 
    else:
 
        os = os_
 

	
 
    cur_path = os.path.split(sys.executable)[0]
 
    if not os.environ['PATH'].startswith(cur_path):
 
        os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
rhodecode/model/user.py
Show inline comments
 
@@ -479,98 +479,102 @@ class UserModel(BaseModel):
 
            # select the lowest permissions first (more explicit)
 
            ##TODO: do this^^
 
            if not gr.inherit_default_permissions:
 
                # NEED TO IGNORE all configurable permissions and
 
                # replace them with explicitly set
 
                user.permissions[GLOBAL] = user.permissions[GLOBAL]\
 
                                                .difference(_configurable)
 
            for perm in perms:
 
                user.permissions[GLOBAL].add(perm.permission.permission_name)
 

	
 
        # user specific global permissions
 
        user_perms = self.sa.query(UserToPerm)\
 
                .options(joinedload(UserToPerm.permission))\
 
                .filter(UserToPerm.user_id == uid).all()
 

	
 
        if not user.inherit_default_permissions:
 
            # NEED TO IGNORE all configurable permissions and
 
            # replace them with explicitly set
 
            user.permissions[GLOBAL] = user.permissions[GLOBAL]\
 
                                            .difference(_configurable)
 

	
 
            for perm in user_perms:
 
                user.permissions[GLOBAL].add(perm.permission.permission_name)
 

	
 
        #======================================================================
 
        # !! REPO PERMISSIONS !!
 
        #======================================================================
 
        #======================================================================
 
        # check if user is part of user groups for this repository and
 
        # fill in (or NOT replace with higher `or 1` permissions
 
        #======================================================================
 
        # users group for repositories permissions
 
        user_repo_perms_from_users_groups = \
 
         self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\
 
            .join((Repository, UsersGroupRepoToPerm.repository_id ==
 
                   Repository.repo_id))\
 
            .join((Permission, UsersGroupRepoToPerm.permission_id ==
 
                   Permission.permission_id))\
 
            .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id ==
 
                   UsersGroupMember.users_group_id))\
 
            .filter(UsersGroupMember.user_id == uid)\
 
            .all()
 

	
 
        for perm in user_repo_perms_from_users_groups:
 
            r_k = perm.UsersGroupRepoToPerm.repository.repo_name
 
            p = perm.Permission.permission_name
 
            cur_perm = user.permissions[RK][r_k]
 
            # overwrite permission only if it's greater than permission
 
            # given from other sources
 
            # given from other sources - disabled with `or 1` now
 
            if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm] or 1:  # disable check
 
                if perm.Repository.user_id == uid:
 
                    # set admin if owner
 
                    p = 'repository.admin'
 

	
 
                user.permissions[RK][r_k] = p
 

	
 
        # user explicit permissions for repositories
 
        user_repo_perms = \
 
         self.sa.query(UserRepoToPerm, Permission, Repository)\
 
            .join((Repository, UserRepoToPerm.repository_id ==
 
                   Repository.repo_id))\
 
            .join((Permission, UserRepoToPerm.permission_id ==
 
                   Permission.permission_id))\
 
            .filter(UserRepoToPerm.user_id == uid)\
 
            .all()
 

	
 
        for perm in user_repo_perms:
 
            # set admin if owner
 
            r_k = perm.UserRepoToPerm.repository.repo_name
 
            if perm.Repository.user_id == uid:
 
                p = 'repository.admin'
 
            else:
 
                p = perm.Permission.permission_name
 
            user.permissions[RK][r_k] = p
 

	
 
        # REPO GROUP
 
        #==================================================================
 
        # get access for this user for repos group and override defaults
 
        #==================================================================
 

	
 
        # user explicit permissions for repository
 
        user_repo_groups_perms = \
 
         self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\
 
         .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
 
         .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
 
         .filter(UserRepoGroupToPerm.user_id == uid)\
 
         .all()
 

	
 
        for perm in user_repo_groups_perms:
 
            rg_k = perm.UserRepoGroupToPerm.group.group_name
 
            p = perm.Permission.permission_name
 
            cur_perm = user.permissions[GK][rg_k]
 
            if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm] or 1:  # disable check
 
                user.permissions[GK][rg_k] = p
 

	
 
        # REPO GROUP + USER GROUP
 
        #==================================================================
 
        # check if user is part of user groups for this repo group and
 
        # fill in (or replace with higher) permissions
 
        #==================================================================
 

	
 
        # users group for repositories permissions
rhodecode/public/css/codemirror.css
Show inline comments
 
.CodeMirror {
 
  line-height: 1em;
 
  font-family: monospace;
 

	
 
  /* Necessary so the scrollbar can be absolutely positioned within the wrapper on Lion. */
 
  position: relative;
 
  /* This prevents unwanted scrollbars from showing up on the body and wrapper in IE. */
 
  overflow: hidden;
 
}
 

	
 
.CodeMirror-scroll {
 
  overflow-x: auto;
 
  overflow-y: hidden;
 
  overflow: auto;
 
  height: 300px;
 
  /* This is needed to prevent an IE[67] bug where the scrolled content
 
     is visible outside of the scrolling box. */
 
  position: relative;
 
  outline: none;
 
}
 

	
 
/* Vertical scrollbar */
 
.CodeMirror-scrollbar {
 
  float: right;
 
  position: absolute;
 
  right: 0; top: 0;
 
  overflow-x: hidden;
 
  overflow-y: scroll;
 

	
 
  /* This corrects for the 1px gap introduced to the left of the scrollbar
 
     by the rule for .CodeMirror-scrollbar-inner. */
 
  margin-left: -1px;
 
  z-index: 5;
 
}
 
.CodeMirror-scrollbar-inner {
 
  /* This needs to have a nonzero width in order for the scrollbar to appear
 
     in Firefox and IE9. */
 
  width: 1px;
 
}
 
.CodeMirror-scrollbar.cm-sb-overlap {
 
  /* Ensure that the scrollbar appears in Lion, and that it overlaps the content
 
     rather than sitting to the right of it. */
 
  position: absolute;
 
  z-index: 1;
 
  float: none;
 
  right: 0;
 
  min-width: 12px;
 
}
 
.CodeMirror-scrollbar.cm-sb-nonoverlap {
 
  min-width: 12px;
 
}
 
.CodeMirror-scrollbar.cm-sb-ie7 {
 
  min-width: 18px;
 
}
 

	
 
.CodeMirror-gutter {
 
  position: absolute; left: 0; top: 0;
 
  z-index: 10;
 
  background-color: #f7f7f7;
 
  border-right: 1px solid #eee;
 
  min-width: 2em;
 
  height: 100%;
 
}
 
.CodeMirror-gutter-text {
 
  color: #aaa;
 
  text-align: right;
 
  padding: .4em .2em .4em .4em;
 
  white-space: pre !important;
 
  cursor: default;
 
}
 
.CodeMirror-lines {
 
  padding: .4em;
 
  white-space: pre;
 
  cursor: text;
 
}
 
.CodeMirror-lines * {
 
  /* Necessary for throw-scrolling to decelerate properly on Safari. */
 
  pointer-events: none;
 
}
 

	
 
.CodeMirror pre {
 
  -moz-border-radius: 0;
 
  -webkit-border-radius: 0;
 
  -o-border-radius: 0;
 
  border-radius: 0;
 
  border-width: 0; margin: 0; padding: 0; background: transparent;
 
  font-family: inherit;
 
  font-size: inherit;
 
  padding: 0; margin: 0;
 
  white-space: pre;
 
  word-wrap: normal;
 
  line-height: inherit;
 
  color: inherit;
 
}
 

	
 
.CodeMirror-wrap pre {
 
  word-wrap: break-word;
 
  white-space: pre-wrap;
 
  word-break: normal;
 
}
 
.CodeMirror-wrap .CodeMirror-scroll {
 
  overflow-x: hidden;
 
}
 

	
 
.CodeMirror textarea {
 
  outline: none !important;
 
}
 

	
 
.CodeMirror pre.CodeMirror-cursor {
 
  z-index: 10;
 
  position: absolute;
 
  visibility: hidden;
 
  border-left: 1px solid black;
 
  border-right: none;
 
  width: 0;
 
}
 
.cm-keymap-fat-cursor pre.CodeMirror-cursor {
 
  width: auto;
 
  border: 0;
 
  background: transparent;
 
  background: rgba(0, 200, 0, .4);
 
  filter: progid:DXImageTransform.Microsoft.gradient(startColorstr=#6600c800, endColorstr=#4c00c800);
 
}
 
/* Kludge to turn off filter in ie9+, which also accepts rgba */
 
.cm-keymap-fat-cursor pre.CodeMirror-cursor:not(#nonsense_id) {
 
  filter: progid:DXImageTransform.Microsoft.gradient(enabled=false);
 
}
 
.CodeMirror pre.CodeMirror-cursor.CodeMirror-overwrite {}
 
.CodeMirror-focused pre.CodeMirror-cursor {
 
  visibility: visible;
 
}
 

	
 
div.CodeMirror-selected { background: #d9d9d9; }
 
.CodeMirror-focused div.CodeMirror-selected { background: #d7d4f0; }
 

	
 
.CodeMirror-searching {
 
  background: #ffa;
 
  background: rgba(255, 255, 0, .4);
 
}
 

	
 
/* Default theme */
 

	
 
.cm-s-default span.cm-keyword {color: #708;}
 
.cm-s-default span.cm-atom {color: #219;}
 
.cm-s-default span.cm-number {color: #164;}
 
.cm-s-default span.cm-def {color: #00f;}
 
.cm-s-default span.cm-variable {color: black;}
 
.cm-s-default span.cm-variable-2 {color: #05a;}
 
.cm-s-default span.cm-variable-3 {color: #085;}
 
.cm-s-default span.cm-property {color: black;}
 
.cm-s-default span.cm-operator {color: black;}
 
.cm-s-default span.cm-comment {color: #a50;}
 
.cm-s-default span.cm-string {color: #a11;}
 
.cm-s-default span.cm-string-2 {color: #f50;}
 
.cm-s-default span.cm-meta {color: #555;}
 
.cm-s-default span.cm-error {color: #f00;}
 
.cm-s-default span.cm-qualifier {color: #555;}
 
.cm-s-default span.cm-builtin {color: #30a;}
 
.cm-s-default span.cm-bracket {color: #cc7;}
 
.cm-s-default span.cm-bracket {color: #997;}
 
.cm-s-default span.cm-tag {color: #170;}
 
.cm-s-default span.cm-attribute {color: #00c;}
 
.cm-s-default span.cm-header {color: blue;}
 
.cm-s-default span.cm-quote {color: #090;}
 
.cm-s-default span.cm-hr {color: #999;}
 
.cm-s-default span.cm-link {color: #00c;}
 

	
 
span.cm-header, span.cm-strong {font-weight: bold;}
 
span.cm-em {font-style: italic;}
 
span.cm-emstrong {font-style: italic; font-weight: bold;}
 
span.cm-link {text-decoration: underline;}
 

	
 
span.cm-invalidchar {color: #f00;}
 

	
 
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
 
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
 

	
 
@media print {
 

	
 
  /* Hide the cursor when printing */
 
  .CodeMirror pre.CodeMirror-cursor {
 
    visibility: hidden;
 
  }
 

	
 
}
rhodecode/public/css/style.css
Show inline comments
 
@@ -4217,100 +4217,113 @@ form.comment-form {
 
	padding-top:5px;
 
}
 
 
.add-another-button {
 
    margin-left: 10px;
 
    margin-top: 10px;
 
    margin-bottom: 10px;
 
}
 
 
.comment .buttons {
 
	float: right;
 
	padding:2px 2px 0px 0px;
 
}
 
 
 
.show-inline-comments{
 
	position: relative;
 
	top:1px
 
}
 
 
/** comment inline form **/
 
.comment-inline-form .overlay{
 
	display: none;
 
}
 
.comment-inline-form .overlay.submitting{
 
	display:block;
 
    background: none repeat scroll 0 0 white;
 
    font-size: 16px;
 
    opacity: 0.5;
 
    position: absolute;
 
    text-align: center;
 
    vertical-align: top;
 
 
}
 
.comment-inline-form .overlay.submitting .overlay-text{
 
	width:100%;
 
	margin-top:5%;
 
}
 
 
.comment-inline-form .clearfix{
 
    background: #EEE;
 
    -webkit-border-radius: 4px;
 
    -moz-border-radius: 4px;
 
    border-radius: 4px;
 
    padding: 5px;
 
}
 
 
div.comment-inline-form {
 
    margin-top: 5px;
 
    padding:2px 6px 8px 6px;
 
 
}
 
    padding:4px 0px 6px 0px;
 
}
 
 
 
tr.hl-comment{
 
/*
 
	background-color: #FFFFCC !important;
 
*/
 
}
 
 
/*
 
tr.hl-comment pre {
 
	border-top: 2px solid #FFEE33;
 
	border-left: 2px solid #FFEE33;
 
	border-right: 2px solid #FFEE33;
 
}
 
*/
 
 
.comment-inline-form strong {
 
    display: block;
 
    margin-bottom: 15px;
 
}
 
 
.comment-inline-form textarea {
 
    width: 100%;
 
    height: 100px;
 
    font-family: 'Monaco', 'Courier', 'Courier New', monospace;
 
}
 
 
form.comment-inline-form {
 
    margin-top: 10px;
 
    margin-left: 10px;
 
}
 
 
.comment-inline-form-submit {
 
    margin-top: 5px;
 
    margin-left: 525px;
 
}
 
 
.file-comments {
 
    display: none;
 
}
 
 
.comment-inline-form .comment {
 
    margin-left: 10px;
 
}
 
 
.comment-inline-form .comment-help{
 
    padding: 0px 0px 2px 0px;
 
    color: #666666;
 
    font-size: 10px;
 
}
 
 
.comment-inline-form .comment-button{
 
    padding-top:5px;
 
}
 
 
/** comment inline **/
 
.inline-comments {
 
    padding:10px 20px;
 
}
 
 
.inline-comments div.rst-block  {
 
	clear:both;
 
	overflow:hidden;
rhodecode/public/js/codemirror.js
Show inline comments
 
// All functions that need access to the editor's state live inside
 
// the CodeMirror function. Below that, at the bottom of the file,
 
// some utilities are defined.
 

	
 
// CodeMirror is the only global var we claim
 
var CodeMirror = (function() {
 
window.CodeMirror = (function() {
 
  "use strict";
 
  // This is the function that produces an editor instance. Its
 
  // closure is used to store the editor state.
 
  function CodeMirror(place, givenOptions) {
 
    // Determine effective options based on given values and defaults.
 
    var options = {}, defaults = CodeMirror.defaults;
 
    for (var opt in defaults)
 
      if (defaults.hasOwnProperty(opt))
 
        options[opt] = (givenOptions && givenOptions.hasOwnProperty(opt) ? givenOptions : defaults)[opt];
 

	
 
    var input = elt("textarea", null, null, "position: absolute; padding: 0; width: 1px; height: 1em");
 
    input.setAttribute("wrap", "off"); input.setAttribute("autocorrect", "off"); input.setAttribute("autocapitalize", "off");
 
    // Wraps and hides input textarea
 
    var inputDiv = elt("div", [input], null, "overflow: hidden; position: relative; width: 3px; height: 0px;");
 
    // The empty scrollbar content, used solely for managing the scrollbar thumb.
 
    var scrollbarInner = elt("div", null, "CodeMirror-scrollbar-inner");
 
    // The vertical scrollbar. Horizontal scrolling is handled by the scroller itself.
 
    var scrollbar = elt("div", [scrollbarInner], "CodeMirror-scrollbar");
 
    // DIVs containing the selection and the actual code
 
    var lineDiv = elt("div"), selectionDiv = elt("div", null, null, "position: relative; z-index: -1");
 
    // Blinky cursor, and element used to ensure cursor fits at the end of a line
 
    var cursor = elt("pre", "\u00a0", "CodeMirror-cursor"), widthForcer = elt("pre", "\u00a0", "CodeMirror-cursor", "visibility: hidden");
 
    // Used to measure text size
 
    var measure = elt("div", null, null, "position: absolute; width: 100%; height: 0px; overflow: hidden; visibility: hidden;");
 
    var lineSpace = elt("div", [measure, cursor, widthForcer, selectionDiv, lineDiv], null, "position: relative; z-index: 0");
 
    var gutterText = elt("div", null, "CodeMirror-gutter-text"), gutter = elt("div", [gutterText], "CodeMirror-gutter");
 
    // Moved around its parent to cover visible view
 
    var mover = elt("div", [gutter, elt("div", [lineSpace], "CodeMirror-lines")], null, "position: relative");
 
    // Set to the height of the text, causes scrolling
 
    var sizer = elt("div", [mover], null, "position: relative");
 
    // Provides scrolling
 
    var scroller = elt("div", [sizer], "CodeMirror-scroll");
 
    scroller.setAttribute("tabIndex", "-1");
 
    // The element in which the editor lives.
 
    var wrapper = document.createElement("div");
 
    wrapper.className = "CodeMirror" + (options.lineWrapping ? " CodeMirror-wrap" : "");
 
    // This mess creates the base DOM structure for the editor.
 
    wrapper.innerHTML =
 
      '<div style="overflow: hidden; position: relative; width: 3px; height: 0px;">' + // Wraps and hides input textarea
 
        '<textarea style="position: absolute; padding: 0; width: 1px; height: 1em" wrap="off" ' +
 
          'autocorrect="off" autocapitalize="off"></textarea></div>' +
 
      '<div class="CodeMirror-scrollbar">' + // The vertical scrollbar. Horizontal scrolling is handled by the scroller itself.
 
        '<div class="CodeMirror-scrollbar-inner">' + // The empty scrollbar content, used solely for managing the scrollbar thumb.
 
      '</div></div>' + // This must be before the scroll area because it's float-right.
 
      '<div class="CodeMirror-scroll" tabindex="-1">' +
 
        '<div style="position: relative">' + // Set to the height of the text, causes scrolling
 
          '<div style="position: relative">' + // Moved around its parent to cover visible view
 
            '<div class="CodeMirror-gutter"><div class="CodeMirror-gutter-text"></div></div>' +
 
            // Provides positioning relative to (visible) text origin
 
            '<div class="CodeMirror-lines"><div style="position: relative; z-index: 0">' +
 
              // Used to measure text size
 
              '<div style="position: absolute; width: 100%; height: 0; overflow: hidden; visibility: hidden;"></div>' +
 
              '<pre class="CodeMirror-cursor">&#160;</pre>' + // Absolutely positioned blinky cursor
 
              '<pre class="CodeMirror-cursor" style="visibility: hidden">&#160;</pre>' + // Used to force a width
 
              '<div style="position: relative; z-index: -1"></div><div></div>' + // DIVs containing the selection and the actual code
 
            '</div></div></div></div></div>';
 
    var wrapper = elt("div", [inputDiv, scrollbar, scroller], "CodeMirror" + (options.lineWrapping ? " CodeMirror-wrap" : ""));
 
    if (place.appendChild) place.appendChild(wrapper); else place(wrapper);
 
    // I've never seen more elegant code in my life.
 
    var inputDiv = wrapper.firstChild, input = inputDiv.firstChild,
 
        scroller = wrapper.lastChild, code = scroller.firstChild,
 
        mover = code.firstChild, gutter = mover.firstChild, gutterText = gutter.firstChild,
 
        lineSpace = gutter.nextSibling.firstChild, measure = lineSpace.firstChild,
 
        cursor = measure.nextSibling, widthForcer = cursor.nextSibling,
 
        selectionDiv = widthForcer.nextSibling, lineDiv = selectionDiv.nextSibling,
 
        scrollbar = inputDiv.nextSibling, scrollbarInner = scrollbar.firstChild;
 

	
 
    themeChanged(); keyMapChanged();
 
    // Needed to hide big blue blinking cursor on Mobile Safari
 
    if (ios) input.style.width = "0px";
 
    if (!webkit) scroller.draggable = true;
 
    lineSpace.style.outline = "none";
 
    if (options.tabindex != null) input.tabIndex = options.tabindex;
 
    if (options.autofocus) focusInput();
 
    if (!options.gutter && !options.lineNumbers) gutter.style.display = "none";
 
    // Needed to handle Tab key in KHTML
 
    if (khtml) inputDiv.style.height = "1px", inputDiv.style.position = "absolute";
 

	
 
    // Check for OS X >= 10.7. If so, we need to force a width on the scrollbar, and 
 
    // make it overlap the content. (But we only do this if the scrollbar doesn't already
 
    // have a natural width. If the mouse is plugged in or the user sets the system pref
 
    // to always show scrollbars, the scrollbar shouldn't overlap.)
 
    if (mac_geLion) {
 
      scrollbar.className += (overlapScrollbars() ? " cm-sb-overlap" : " cm-sb-nonoverlap");
 
    } else if (ie_lt8) {
 
    // Check for OS X >= 10.7. This has transparent scrollbars, so the
 
    // overlaying of one scrollbar with another won't work. This is a
 
    // temporary hack to simply turn off the overlay scrollbar. See
 
    // issue #727.
 
    if (mac_geLion) { scrollbar.style.zIndex = -2; scrollbar.style.visibility = "hidden"; }
 
      // Need to set a minimum width to see the scrollbar on IE7 (but must not set it on IE8).
 
      scrollbar.className += " cm-sb-ie7";
 
    }
 

	
 
    // Check for problem with IE innerHTML not working when we have a
 
    // P (or similar) parent node.
 
    try { stringWidth("x"); }
 
    catch (e) {
 
      if (e.message.match(/runtime/i))
 
        e = new Error("A CodeMirror inside a P-style element does not work in Internet Explorer. (innerHTML bug)");
 
      throw e;
 
    }
 
    else if (ie_lt8) scrollbar.style.minWidth = "18px";
 

	
 
    // Delayed object wrap timeouts, making sure only one is active. blinker holds an interval.
 
    var poll = new Delayed(), highlight = new Delayed(), blinker;
 

	
 
    // mode holds a mode API object. doc is the tree of Line objects,
 
    // work an array of lines that should be parsed, and history the
 
    // undo history (instance of History constructor).
 
    var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), work, focused;
 
    // frontier is the point up to which the content has been parsed,
 
    // and history the undo history (instance of History constructor).
 
    var mode, doc = new BranchChunk([new LeafChunk([new Line("")])]), frontier = 0, focused;
 
    loadMode();
 
    // The selection. These are always maintained to point at valid
 
    // positions. Inverted is used to remember that the user is
 
    // selecting bottom-to-top.
 
    var sel = {from: {line: 0, ch: 0}, to: {line: 0, ch: 0}, inverted: false};
 
    // Selection-related flags. shiftSelecting obviously tracks
 
    // whether the user is holding shift.
 
    var shiftSelecting, lastClick, lastDoubleClick, lastScrollTop = 0, lastScrollLeft = 0, draggingText,
 
    var shiftSelecting, lastClick, lastDoubleClick, lastScrollTop = 0, draggingText,
 
        overwrite = false, suppressEdits = false;
 
    // Variables used by startOperation/endOperation to track what
 
    // happened during the operation.
 
    var updateInput, userSelChange, changes, textChanged, selectionChanged, leaveInputAlone,
 
    var updateInput, userSelChange, changes, textChanged, selectionChanged,
 
        gutterDirty, callbacks;
 
    // Current visible range (may be bigger than the view window).
 
    var displayOffset = 0, showingFrom = 0, showingTo = 0, lastSizeC = 0;
 
    // bracketHighlighted is used to remember that a bracket has been
 
    // marked.
 
    var bracketHighlighted;
 
    // Tracks the maximum line length so that the horizontal scrollbar
 
    // can be kept static when scrolling.
 
    var maxLine = "", updateMaxLine = false, maxLineChanged = true;
 
    var tabCache = {};
 
    var maxLine = getLine(0), updateMaxLine = false, maxLineChanged = true;
 
    var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll
 
    var goalColumn = null;
 

	
 
    // Initialize the content.
 
    operation(function(){setValue(options.value || ""); updateInput = false;})();
 
    var history = new History();
 

	
 
    // Register our event handlers.
 
    connect(scroller, "mousedown", operation(onMouseDown));
 
    connect(scroller, "dblclick", operation(onDoubleClick));
 
    connect(lineSpace, "selectstart", e_preventDefault);
 
    // Gecko browsers fire contextmenu *after* opening the menu, at
 
    // which point we can't mess with it anymore. Context menu is
 
    // handled in onMouseDown for Gecko.
 
    if (!gecko) connect(scroller, "contextmenu", onContextMenu);
 
    connect(scroller, "scroll", onScroll);
 
    connect(scrollbar, "scroll", onScroll);
 
    connect(scrollbar, "mousedown", function() {setTimeout(focusInput, 0);});
 
    connect(scroller, "mousewheel", onMouseWheel);
 
    connect(scroller, "DOMMouseScroll", onMouseWheel);
 
    connect(window, "resize", function() {updateDisplay(true);});
 
    connect(scroller, "scroll", onScrollMain);
 
    connect(scrollbar, "scroll", onScrollBar);
 
    connect(scrollbar, "mousedown", function() {if (focused) setTimeout(focusInput, 0);});
 
    var resizeHandler = connect(window, "resize", function() {
 
      if (wrapper.parentNode) updateDisplay(true);
 
      else resizeHandler();
 
    }, true);
 
    connect(input, "keyup", operation(onKeyUp));
 
    connect(input, "input", fastPoll);
 
    connect(input, "keydown", operation(onKeyDown));
 
    connect(input, "keypress", operation(onKeyPress));
 
    connect(input, "focus", onFocus);
 
    connect(input, "blur", onBlur);
 

	
 
    if (options.dragDrop) {
 
      connect(scroller, "dragstart", onDragStart);
 
      function drag_(e) {
 
        if (options.onDragEvent && options.onDragEvent(instance, addStop(e))) return;
 
        e_stop(e);
 
      }
 
    if (options.dragDrop) {
 
      connect(scroller, "dragstart", onDragStart);
 
      connect(scroller, "dragenter", drag_);
 
      connect(scroller, "dragover", drag_);
 
      connect(scroller, "drop", operation(onDrop));
 
    }
 
    connect(scroller, "paste", function(){focusInput(); fastPoll();});
 
    connect(input, "paste", fastPoll);
 
    connect(input, "cut", operation(function(){
 
      if (!options.readOnly) replaceSelection("");
 
    }));
 

	
 
    // Needed to handle Tab key in KHTML
 
    if (khtml) connect(code, "mouseup", function() {
 
    if (khtml) connect(sizer, "mouseup", function() {
 
        if (document.activeElement == input) input.blur();
 
        focusInput();
 
    });
 

	
 
    // IE throws unspecified error in certain cases, when
 
    // trying to access activeElement before onload
 
    var hasFocus; try { hasFocus = (document.activeElement == input); } catch(e) { }
 
    if (hasFocus || options.autofocus) setTimeout(onFocus, 20);
 
    else onBlur();
 

	
 
    function isLine(l) {return l >= 0 && l < doc.size;}
 
    // The instance object that we'll return. Mostly calls out to
 
    // local functions in the CodeMirror function. Some do some extra
 
    // range checking and/or clipping. operation is used to wrap the
 
    // call so that changes it makes are tracked, and the display is
 
    // updated afterwards.
 
    var instance = wrapper.CodeMirror = {
 
      getValue: getValue,
 
      setValue: operation(setValue),
 
      getSelection: getSelection,
 
      replaceSelection: operation(replaceSelection),
 
      focus: function(){window.focus(); focusInput(); onFocus(); fastPoll();},
 
      setOption: function(option, value) {
 
        var oldVal = options[option];
 
        options[option] = value;
 
        if (option == "mode" || option == "indentUnit") loadMode();
 
        else if (option == "readOnly" && value == "nocursor") {onBlur(); input.blur();}
 
        else if (option == "readOnly" && !value) {resetInput(true);}
 
        else if (option == "theme") themeChanged();
 
        else if (option == "lineWrapping" && oldVal != value) operation(wrappingChanged)();
 
        else if (option == "tabSize") updateDisplay(true);
 
        else if (option == "keyMap") keyMapChanged();
 
        if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" || option == "theme") {
 
        if (option == "lineNumbers" || option == "gutter" || option == "firstLineNumber" ||
 
            option == "theme" || option == "lineNumberFormatter") {
 
          gutterChanged();
 
          updateDisplay(true);
 
        }
 
      },
 
      getOption: function(option) {return options[option];},
 
      getMode: function() {return mode;},
 
      undo: operation(undo),
 
      redo: operation(redo),
 
      indentLine: operation(function(n, dir) {
 
        if (typeof dir != "string") {
 
          if (dir == null) dir = options.smartIndent ? "smart" : "prev";
 
          else dir = dir ? "add" : "subtract";
 
        }
 
        if (isLine(n)) indentLine(n, dir);
 
      }),
 
      indentSelection: operation(indentSelected),
 
      historySize: function() {return {undo: history.done.length, redo: history.undone.length};},
 
      clearHistory: function() {history = new History();},
 
      setHistory: function(histData) {
 
        history = new History();
 
        history.done = histData.done;
 
        history.undone = histData.undone;
 
      },
 
      getHistory: function() {
 
        function cp(arr) {
 
          for (var i = 0, nw = [], nwelt; i < arr.length; ++i) {
 
            nw.push(nwelt = []);
 
            for (var j = 0, elt = arr[i]; j < elt.length; ++j) {
 
              var old = [], cur = elt[j];
 
              nwelt.push({start: cur.start, added: cur.added, old: old});
 
              for (var k = 0; k < cur.old.length; ++k) old.push(hlText(cur.old[k]));
 
            }
 
          }
 
          return nw;
 
        }
 
        return {done: cp(history.done), undone: cp(history.undone)};
 
      },
 
      matchBrackets: operation(function(){matchBrackets(true);}),
 
      getTokenAt: operation(function(pos) {
 
        pos = clipPos(pos);
 
        return getLine(pos.line).getTokenAt(mode, getStateBefore(pos.line), pos.ch);
 
        return getLine(pos.line).getTokenAt(mode, getStateBefore(pos.line), options.tabSize, pos.ch);
 
      }),
 
      getStateAfter: function(line) {
 
        line = clipLine(line == null ? doc.size - 1: line);
 
        return getStateBefore(line + 1);
 
      },
 
      cursorCoords: function(start, mode) {
 
        if (start == null) start = sel.inverted;
 
        return this.charCoords(start ? sel.from : sel.to, mode);
 
      },
 
      charCoords: function(pos, mode) {
 
        pos = clipPos(pos);
 
        if (mode == "local") return localCoords(pos, false);
 
        if (mode == "div") return localCoords(pos, true);
 
        return pageCoords(pos);
 
      },
 
      coordsChar: function(coords) {
 
        var off = eltOffset(lineSpace);
 
        return coordsChar(coords.x - off.left, coords.y - off.top);
 
      },
 
      markText: operation(markText),
 
      setBookmark: setBookmark,
 
      findMarksAt: findMarksAt,
 
      setMarker: operation(addGutterMarker),
 
      clearMarker: operation(removeGutterMarker),
 
      setLineClass: operation(setLineClass),
 
      hideLine: operation(function(h) {return setLineHidden(h, true);}),
 
      showLine: operation(function(h) {return setLineHidden(h, false);}),
 
      onDeleteLine: function(line, f) {
 
        if (typeof line == "number") {
 
          if (!isLine(line)) return null;
 
          line = getLine(line);
 
        }
 
        (line.handlers || (line.handlers = [])).push(f);
 
        return line;
 
      },
 
      lineInfo: lineInfo,
 
      getViewport: function() { return {from: showingFrom, to: showingTo};},
 
      addWidget: function(pos, node, scroll, vert, horiz) {
 
        pos = localCoords(clipPos(pos));
 
        var top = pos.yBot, left = pos.x;
 
        node.style.position = "absolute";
 
        code.appendChild(node);
 
        sizer.appendChild(node);
 
        if (vert == "over") top = pos.y;
 
        else if (vert == "near") {
 
          var vspace = Math.max(scroller.offsetHeight, doc.height * textHeight()),
 
              hspace = Math.max(code.clientWidth, lineSpace.clientWidth) - paddingLeft();
 
              hspace = Math.max(sizer.clientWidth, lineSpace.clientWidth) - paddingLeft();
 
          if (pos.yBot + node.offsetHeight > vspace && pos.y > node.offsetHeight)
 
            top = pos.y - node.offsetHeight;
 
          if (left + node.offsetWidth > hspace)
 
            left = hspace - node.offsetWidth;
 
        }
 
        node.style.top = (top + paddingTop()) + "px";
 
        node.style.left = node.style.right = "";
 
        if (horiz == "right") {
 
          left = code.clientWidth - node.offsetWidth;
 
          left = sizer.clientWidth - node.offsetWidth;
 
          node.style.right = "0px";
 
        } else {
 
          if (horiz == "left") left = 0;
 
          else if (horiz == "middle") left = (code.clientWidth - node.offsetWidth) / 2;
 
          else if (horiz == "middle") left = (sizer.clientWidth - node.offsetWidth) / 2;
 
          node.style.left = (left + paddingLeft()) + "px";
 
        }
 
        if (scroll)
 
          scrollIntoView(left, top, left + node.offsetWidth, top + node.offsetHeight);
 
      },
 

	
 
      lineCount: function() {return doc.size;},
 
      clipPos: clipPos,
 
      getCursor: function(start) {
 
        if (start == null) start = sel.inverted;
 
        return copyPos(start ? sel.from : sel.to);
 
      },
 
      somethingSelected: function() {return !posEq(sel.from, sel.to);},
 
      setCursor: operation(function(line, ch, user) {
 
        if (ch == null && typeof line.line == "number") setCursor(line.line, line.ch, user);
 
        else setCursor(line, ch, user);
 
      }),
 
      setSelection: operation(function(from, to, user) {
 
        (user ? setSelectionUser : setSelection)(clipPos(from), clipPos(to || from));
 
      }),
 
      getLine: function(line) {if (isLine(line)) return getLine(line).text;},
 
      getLineHandle: function(line) {if (isLine(line)) return getLine(line);},
 
      setLine: operation(function(line, text) {
 
        if (isLine(line)) replaceRange(text, {line: line, ch: 0}, {line: line, ch: getLine(line).text.length});
 
      }),
 
      removeLine: operation(function(line) {
 
        if (isLine(line)) replaceRange("", {line: line, ch: 0}, clipPos({line: line+1, ch: 0}));
 
      }),
 
      replaceRange: operation(replaceRange),
 
      getRange: function(from, to) {return getRange(clipPos(from), clipPos(to));},
 
      getRange: function(from, to, lineSep) {return getRange(clipPos(from), clipPos(to), lineSep);},
 

	
 
      triggerOnKeyDown: operation(onKeyDown),
 
      execCommand: function(cmd) {return commands[cmd](instance);},
 
      // Stuff used by commands, probably not much use to outside code.
 
      moveH: operation(moveH),
 
      deleteH: operation(deleteH),
 
      moveV: operation(moveV),
 
      toggleOverwrite: function() {
 
        if(overwrite){
 
          overwrite = false;
 
          cursor.className = cursor.className.replace(" CodeMirror-overwrite", "");
 
        } else {
 
          overwrite = true;
 
          cursor.className += " CodeMirror-overwrite";
 
        }
 
      },
 

	
 
      posFromIndex: function(off) {
 
        var lineNo = 0, ch;
 
        doc.iter(0, doc.size, function(line) {
 
          var sz = line.text.length + 1;
 
          if (sz > off) { ch = off; return true; }
 
          off -= sz;
 
          ++lineNo;
 
        });
 
        return clipPos({line: lineNo, ch: ch});
 
      },
 
      indexFromPos: function (coords) {
 
        if (coords.line < 0 || coords.ch < 0) return 0;
 
        var index = coords.ch;
 
        doc.iter(0, coords.line, function (line) {
 
          index += line.text.length + 1;
 
        });
 
        return index;
 
      },
 
      scrollTo: function(x, y) {
 
        if (x != null) scroller.scrollLeft = x;
 
        if (y != null) scrollbar.scrollTop = y;
 
        if (y != null) scrollbar.scrollTop = scroller.scrollTop = y;
 
        updateDisplay([]);
 
      },
 
      getScrollInfo: function() {
 
        return {x: scroller.scrollLeft, y: scrollbar.scrollTop,
 
                height: scrollbar.scrollHeight, width: scroller.scrollWidth};
 
      },
 
      setSize: function(width, height) {
 
        function interpret(val) {
 
          val = String(val);
 
          return /^\d+$/.test(val) ? val + "px" : val;
 
        }
 
        if (width != null) wrapper.style.width = interpret(width);
 
        if (height != null) scroller.style.height = interpret(height);
 
        instance.refresh();
 
      },
 

	
 
      operation: function(f){return operation(f)();},
 
      compoundChange: function(f){return compoundChange(f);},
 
      refresh: function(){
 
        updateDisplay(true);
 
        updateDisplay(true, null, lastScrollTop);
 
        if (scrollbar.scrollHeight > lastScrollTop)
 
          scrollbar.scrollTop = lastScrollTop;
 
      },
 
      getInputField: function(){return input;},
 
      getWrapperElement: function(){return wrapper;},
 
      getScrollerElement: function(){return scroller;},
 
      getGutterElement: function(){return gutter;}
 
    };
 

	
 
    function getLine(n) { return getLineAt(doc, n); }
 
    function updateLineHeight(line, height) {
 
      gutterDirty = true;
 
      var diff = height - line.height;
 
      for (var n = line; n; n = n.parent) n.height += diff;
 
    }
 

	
 
    function lineContent(line, wrapAt) {
 
      if (!line.styles)
 
        line.highlight(mode, line.stateAfter = getStateBefore(lineNo(line)), options.tabSize);
 
      return line.getContent(options.tabSize, wrapAt, options.lineWrapping);
 
    }
 

	
 
    function setValue(code) {
 
      var top = {line: 0, ch: 0};
 
      updateLines(top, {line: doc.size - 1, ch: getLine(doc.size-1).text.length},
 
                  splitLines(code), top, top);
 
      updateInput = true;
 
    }
 
    function getValue() {
 
    function getValue(lineSep) {
 
      var text = [];
 
      doc.iter(0, doc.size, function(line) { text.push(line.text); });
 
      return text.join("\n");
 
      return text.join(lineSep || "\n");
 
    }
 

	
 
    function onScroll(e) {
 
      if (lastScrollTop != scrollbar.scrollTop || lastScrollLeft != scroller.scrollLeft) {
 
        lastScrollTop = scrollbar.scrollTop;
 
        lastScrollLeft = scroller.scrollLeft;
 
    function onScrollBar(e) {
 
      if (scrollbar.scrollTop != lastScrollTop) {
 
        lastScrollTop = scroller.scrollTop = scrollbar.scrollTop;
 
        updateDisplay([]);
 
        if (options.fixedGutter) gutter.style.left = scroller.scrollLeft + "px";
 
      }
 
    }
 

	
 
    function onScrollMain(e) {
 
      if (options.fixedGutter && gutter.style.left != scroller.scrollLeft + "px")
 
        gutter.style.left = scroller.scrollLeft + "px";
 
      if (scroller.scrollTop != lastScrollTop) {
 
        lastScrollTop = scroller.scrollTop;
 
        if (scrollbar.scrollTop != lastScrollTop)
 
          scrollbar.scrollTop = lastScrollTop;
 
        updateDisplay([]);
 
      }
 
        if (options.onScroll) options.onScroll(instance);
 
      }
 
    }
 

	
 
    function onMouseDown(e) {
 
      setShift(e_prop(e, "shiftKey"));
 
      // Check whether this is a click in a widget
 
      for (var n = e_target(e); n != wrapper; n = n.parentNode)
 
        if (n.parentNode == code && n != mover) return;
 
        if (n.parentNode == sizer && n != mover) return;
 

	
 
      // See if this is a click in the gutter
 
      for (var n = e_target(e); n != wrapper; n = n.parentNode)
 
        if (n.parentNode == gutterText) {
 
          if (options.onGutterClick)
 
            options.onGutterClick(instance, indexOf(gutterText.childNodes, n) + showingFrom, e);
 
          return e_preventDefault(e);
 
        }
 

	
 
      var start = posFromMouse(e);
 

	
 
      switch (e_button(e)) {
 
      case 3:
 
        if (gecko && !mac) onContextMenu(e);
 
        if (gecko) onContextMenu(e);
 
        return;
 
      case 2:
 
        if (start) setCursor(start.line, start.ch, true);
 
        setTimeout(focusInput, 20);
 
        e_preventDefault(e);
 
        return;
 
      }
 
      // For button 1, if it was clicked inside the editor
 
      // (posFromMouse returning non-null), we have to adjust the
 
      // selection.
 
      if (!start) {if (e_target(e) == scroller) e_preventDefault(e); return;}
 

	
 
      if (!focused) onFocus();
 

	
 
      var now = +new Date;
 
      var now = +new Date, type = "single";
 
      if (lastDoubleClick && lastDoubleClick.time > now - 400 && posEq(lastDoubleClick.pos, start)) {
 
        type = "triple";
 
        e_preventDefault(e);
 
        setTimeout(focusInput, 20);
 
        return selectLine(start.line);
 
        selectLine(start.line);
 
      } else if (lastClick && lastClick.time > now - 400 && posEq(lastClick.pos, start)) {
 
        type = "double";
 
        lastDoubleClick = {time: now, pos: start};
 
        e_preventDefault(e);
 
        return selectWordAt(start);
 
        var word = findWordAt(start);
 
        setSelectionUser(word.from, word.to);
 
      } else { lastClick = {time: now, pos: start}; }
 

	
 
      var last = start, going;
 
      if (options.dragDrop && dragAndDrop && !options.readOnly && !posEq(sel.from, sel.to) &&
 
          !posLess(start, sel.from) && !posLess(sel.to, start)) {
 
        // Let the drag handler handle this.
 
        if (webkit) scroller.draggable = true;
 
        function dragEnd(e2) {
 
          if (webkit) scroller.draggable = false;
 
          draggingText = false;
 
          up(); drop();
 
          if (Math.abs(e.clientX - e2.clientX) + Math.abs(e.clientY - e2.clientY) < 10) {
 
            e_preventDefault(e2);
 
            setCursor(start.line, start.ch, true);
 
            focusInput();
 
          }
 
        }
 
      var last = start, going;
 
      if (options.dragDrop && dragAndDrop && !options.readOnly && !posEq(sel.from, sel.to) &&
 
          !posLess(start, sel.from) && !posLess(sel.to, start) && type == "single") {
 
        // Let the drag handler handle this.
 
        if (webkit) scroller.draggable = true;
 
        var up = connect(document, "mouseup", operation(dragEnd), true);
 
        var drop = connect(scroller, "drop", operation(dragEnd), true);
 
        draggingText = true;
 
        // IE's approach to draggable
 
        if (scroller.dragDrop) scroller.dragDrop();
 
        return;
 
      }
 
      e_preventDefault(e);
 
      setCursor(start.line, start.ch, true);
 
      if (type == "single") setCursor(start.line, start.ch, true);
 

	
 
      var startstart = sel.from, startend = sel.to;
 

	
 
      function doSelect(cur) {
 
        if (type == "single") {
 
          setSelectionUser(start, cur);
 
        } else if (type == "double") {
 
          var word = findWordAt(cur);
 
          if (posLess(cur, startstart)) setSelectionUser(word.from, startend);
 
          else setSelectionUser(startstart, word.to);
 
        } else if (type == "triple") {
 
          if (posLess(cur, startstart)) setSelectionUser(startend, clipPos({line: cur.line, ch: 0}));
 
          else setSelectionUser(startstart, clipPos({line: cur.line + 1, ch: 0}));
 
        }
 
      }
 

	
 
      function extend(e) {
 
        var cur = posFromMouse(e, true);
 
        if (cur && !posEq(cur, last)) {
 
          if (!focused) onFocus();
 
          last = cur;
 
          setSelectionUser(start, cur);
 
          doSelect(cur);
 
          updateInput = false;
 
          var visible = visibleLines();
 
          if (cur.line >= visible.to || cur.line < visible.from)
 
            going = setTimeout(operation(function(){extend(e);}), 150);
 
        }
 
      }
 

	
 
      function done(e) {
 
        clearTimeout(going);
 
        var cur = posFromMouse(e);
 
        if (cur) setSelectionUser(start, cur);
 
        if (cur) doSelect(cur);
 
        e_preventDefault(e);
 
        focusInput();
 
        updateInput = true;
 
        move(); up();
 
      }
 
      var move = connect(document, "mousemove", operation(function(e) {
 
        clearTimeout(going);
 
        e_preventDefault(e);
 
        if (!ie && !e_button(e)) done(e);
 
        else extend(e);
 
      }), true);
 
      var up = connect(document, "mouseup", operation(done), true);
 
    }
 
    function onDoubleClick(e) {
 
      for (var n = e_target(e); n != wrapper; n = n.parentNode)
 
        if (n.parentNode == gutterText) return e_preventDefault(e);
 
      var start = posFromMouse(e);
 
      if (!start) return;
 
      lastDoubleClick = {time: +new Date, pos: start};
 
      e_preventDefault(e);
 
      selectWordAt(start);
 
    }
 
    function onDrop(e) {
 
      if (options.onDragEvent && options.onDragEvent(instance, addStop(e))) return;
 
      e.preventDefault();
 
      e_preventDefault(e);
 
      var pos = posFromMouse(e, true), files = e.dataTransfer.files;
 
      if (!pos || options.readOnly) return;
 
      if (files && files.length && window.FileReader && window.File) {
 
        function loadFile(file, i) {
 
        var n = files.length, text = Array(n), read = 0;
 
        var loadFile = function(file, i) {
 
          var reader = new FileReader;
 
          reader.onload = function() {
 
            text[i] = reader.result;
 
            if (++read == n) {
 
              pos = clipPos(pos);
 
              operation(function() {
 
                var end = replaceRange(text.join(""), pos, pos);
 
                setSelectionUser(pos, end);
 
              })();
 
            }
 
          };
 
          reader.readAsText(file);
 
        }
 
        var n = files.length, text = Array(n), read = 0;
 
        };
 
        for (var i = 0; i < n; ++i) loadFile(files[i], i);
 
      } else {
 
        // Don't do a replace if the drop happened inside of the selected text.
 
        if (draggingText && !(posLess(pos, sel.from) || posLess(sel.to, pos))) return;
 
        try {
 
          var text = e.dataTransfer.getData("Text");
 
          if (text) {
 
            compoundChange(function() {
 
              var curFrom = sel.from, curTo = sel.to;
 
              setSelectionUser(pos, pos);
 
              if (draggingText) replaceRange("", curFrom, curTo);
 
              replaceSelection(text);
 
              focusInput();
 
            });
 
          }
 
        }
 
        catch(e){}
 
      }
 
    }
 
    function onDragStart(e) {
 
      var txt = getSelection();
 
      e.dataTransfer.setData("Text", txt);
 
      
 
      // Use dummy image instead of default browsers image.
 
      if (gecko || chrome || opera) {
 
        var img = document.createElement('img');
 
        img.scr = 'data:image/gif;base64,R0lGODdhAgACAIAAAAAAAP///ywAAAAAAgACAAACAoRRADs='; //1x1 image
 
        e.dataTransfer.setDragImage(img, 0, 0);
 
      }
 
      if (e.dataTransfer.setDragImage)
 
        e.dataTransfer.setDragImage(elt('img'), 0, 0);
 
    }
 

	
 
    function doHandleBinding(bound, dropShift) {
 
      if (typeof bound == "string") {
 
        bound = commands[bound];
 
        if (!bound) return false;
 
      }
 
      var prevShift = shiftSelecting;
 
      try {
 
        if (options.readOnly) suppressEdits = true;
 
        if (dropShift) shiftSelecting = null;
 
        bound(instance);
 
      } catch(e) {
 
        if (e != Pass) throw e;
 
        return false;
 
      } finally {
 
        shiftSelecting = prevShift;
 
        suppressEdits = false;
 
      }
 
      return true;
 
    }
 
    var maybeTransition;
 
    function handleKeyBinding(e) {
 
      // Handle auto keymap transitions
 
      var startMap = getKeyMap(options.keyMap), next = startMap.auto;
 
      clearTimeout(maybeTransition);
 
      if (next && !isModifierKey(e)) maybeTransition = setTimeout(function() {
 
        if (getKeyMap(options.keyMap) == startMap) {
 
          options.keyMap = (next.call ? next.call(null, instance) : next);
 
        }
 
      }, 50);
 

	
 
      var name = keyNames[e_prop(e, "keyCode")], handled = false;
 
      var flipCtrlCmd = opera && mac;
 
      if (name == null || e.altGraphKey) return false;
 
      if (e_prop(e, "altKey")) name = "Alt-" + name;
 
      if (e_prop(e, "ctrlKey")) name = "Ctrl-" + name;
 
      if (e_prop(e, "metaKey")) name = "Cmd-" + name;
 
      if (e_prop(e, flipCtrlCmd ? "metaKey" : "ctrlKey")) name = "Ctrl-" + name;
 
      if (e_prop(e, flipCtrlCmd ? "ctrlKey" : "metaKey")) name = "Cmd-" + name;
 

	
 
      var stopped = false;
 
      function stop() { stopped = true; }
 

	
 
      if (e_prop(e, "shiftKey")) {
 
        handled = lookupKey("Shift-" + name, options.extraKeys, options.keyMap,
 
                            function(b) {return doHandleBinding(b, true);}, stop)
 
               || lookupKey(name, options.extraKeys, options.keyMap, function(b) {
 
                 if (typeof b == "string" && /^go[A-Z]/.test(b)) return doHandleBinding(b);
 
               }, stop);
 
      } else {
 
        handled = lookupKey(name, options.extraKeys, options.keyMap, doHandleBinding, stop);
 
      }
 
      if (stopped) handled = false;
 
      if (handled) {
 
        e_preventDefault(e);
 
        restartBlink();
 
        if (ie) { e.oldKeyCode = e.keyCode; e.keyCode = 0; }
 
      }
 
      return handled;
 
    }
 
    function handleCharBinding(e, ch) {
 
      var handled = lookupKey("'" + ch + "'", options.extraKeys,
 
                              options.keyMap, function(b) { return doHandleBinding(b, true); });
 
      if (handled) {
 
        e_preventDefault(e);
 
        restartBlink();
 
      }
 
      return handled;
 
    }
 

	
 
    var lastStoppedKey = null, maybeTransition;
 
    var lastStoppedKey = null;
 
    function onKeyDown(e) {
 
      if (!focused) onFocus();
 
      if (ie && e.keyCode == 27) { e.returnValue = false; }
 
      if (pollingFast) { if (readInput()) pollingFast = false; }
 
      if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return;
 
      var code = e_prop(e, "keyCode");
 
      // IE does strange things with escape.
 
      setShift(code == 16 || e_prop(e, "shiftKey"));
 
      // First give onKeyEvent option a chance to handle this.
 
      var handled = handleKeyBinding(e);
 
      if (opera) {
 
        lastStoppedKey = handled ? code : null;
 
        // Opera has no cut event... we try to at least catch the key combo
 
        if (!handled && code == 88 && e_prop(e, mac ? "metaKey" : "ctrlKey"))
 
          replaceSelection("");
 
      }
 
    }
 
    function onKeyPress(e) {
 
      if (pollingFast) readInput();
 
      if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return;
 
      var keyCode = e_prop(e, "keyCode"), charCode = e_prop(e, "charCode");
 
      if (opera && keyCode == lastStoppedKey) {lastStoppedKey = null; e_preventDefault(e); return;}
 
      if (((opera && (!e.which || e.which < 10)) || khtml) && handleKeyBinding(e)) return;
 
      var ch = String.fromCharCode(charCode == null ? keyCode : charCode);
 
      if (options.electricChars && mode.electricChars && options.smartIndent && !options.readOnly) {
 
        if (mode.electricChars.indexOf(ch) > -1)
 
          setTimeout(operation(function() {indentLine(sel.to.line, "smart");}), 75);
 
      }
 
      if (handleCharBinding(e, ch)) return;
 
      fastPoll();
 
    }
 
    function onKeyUp(e) {
 
      if (options.onKeyEvent && options.onKeyEvent(instance, addStop(e))) return;
 
      if (e_prop(e, "keyCode") == 16) shiftSelecting = null;
 
    }
 

	
 
    function onFocus() {
 
      if (options.readOnly == "nocursor") return;
 
      if (!focused) {
 
        if (options.onFocus) options.onFocus(instance);
 
        focused = true;
 
        if (scroller.className.search(/\bCodeMirror-focused\b/) == -1)
 
          scroller.className += " CodeMirror-focused";
 
        if (!leaveInputAlone) resetInput(true);
 
      }
 
      slowPoll();
 
      restartBlink();
 
    }
 
    function onBlur() {
 
      if (focused) {
 
        if (options.onBlur) options.onBlur(instance);
 
        focused = false;
 
        if (bracketHighlighted)
 
          operation(function(){
 
            if (bracketHighlighted) { bracketHighlighted(); bracketHighlighted = null; }
 
          })();
 
        scroller.className = scroller.className.replace(" CodeMirror-focused", "");
 
      }
 
      clearInterval(blinker);
 
      setTimeout(function() {if (!focused) shiftSelecting = null;}, 150);
 
    }
 

	
 
    function chopDelta(delta) {
 
      // Make sure we always scroll a little bit for any nonzero delta.
 
      if (delta > 0.0 && delta < 1.0) return 1;
 
      else if (delta > -1.0 && delta < 0.0) return -1;
 
      else return Math.round(delta);
 
    }
 

	
 
    function onMouseWheel(e) {
 
      var deltaX = 0, deltaY = 0;
 
      if (e.type == "DOMMouseScroll") { // Firefox
 
        var delta = -e.detail * 8.0;
 
        if (e.axis == e.HORIZONTAL_AXIS) deltaX = delta;
 
        else if (e.axis == e.VERTICAL_AXIS) deltaY = delta;
 
      } else if (e.wheelDeltaX !== undefined && e.wheelDeltaY !== undefined) { // WebKit
 
        deltaX = e.wheelDeltaX / 3.0;
 
        deltaY = e.wheelDeltaY / 3.0;
 
      } else if (e.wheelDelta !== undefined) { // IE or Opera
 
        deltaY = e.wheelDelta / 3.0;
 
      }
 

	
 
      var scrolled = false;
 
      deltaX = chopDelta(deltaX);
 
      deltaY = chopDelta(deltaY);
 
      if ((deltaX > 0 && scroller.scrollLeft > 0) ||
 
          (deltaX < 0 && scroller.scrollLeft + scroller.clientWidth < scroller.scrollWidth)) {
 
        scroller.scrollLeft -= deltaX;
 
        scrolled = true;
 
      }
 
      if ((deltaY > 0 && scrollbar.scrollTop > 0) ||
 
          (deltaY < 0 && scrollbar.scrollTop + scrollbar.clientHeight < scrollbar.scrollHeight)) {
 
        scrollbar.scrollTop -= deltaY;
 
        scrolled = true;
 
      }
 
      if (scrolled) e_stop(e);
 
    }
 

	
 
    // Replace the range from from to to by the strings in newText.
 
    // Afterwards, set the selection to selFrom, selTo.
 
    function updateLines(from, to, newText, selFrom, selTo) {
 
      if (suppressEdits) return;
 
      var old = [];
 
      doc.iter(from.line, to.line + 1, function(line) {
 
        old.push(newHL(line.text, line.markedSpans));
 
      });
 
      if (history) {
 
        var old = [];
 
        doc.iter(from.line, to.line + 1, function(line) { old.push(line.text); });
 
        history.addChange(from.line, newText.length, old);
 
        while (history.done.length > options.undoDepth) history.done.shift();
 
      }
 
      updateLinesNoUndo(from, to, newText, selFrom, selTo);
 
      var lines = updateMarkedSpans(hlSpans(old[0]), hlSpans(lst(old)), from.ch, to.ch, newText);
 
      updateLinesNoUndo(from, to, lines, selFrom, selTo);
 
    }
 
    function unredoHelper(from, to) {
 
      if (!from.length) return;
 
      var set = from.pop(), out = [];
 
      for (var i = set.length - 1; i >= 0; i -= 1) {
 
        var change = set[i];
 
        var replaced = [], end = change.start + change.added;
 
        doc.iter(change.start, end, function(line) { replaced.push(line.text); });
 
        doc.iter(change.start, end, function(line) { replaced.push(newHL(line.text, line.markedSpans)); });
 
        out.push({start: change.start, added: change.old.length, old: replaced});
 
        var pos = {line: change.start + change.old.length - 1,
 
                   ch: editEnd(replaced[replaced.length-1], change.old[change.old.length-1])};
 
        updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length}, change.old, pos, pos);
 
                   ch: editEnd(hlText(lst(replaced)), hlText(lst(change.old)))};
 
        updateLinesNoUndo({line: change.start, ch: 0}, {line: end - 1, ch: getLine(end-1).text.length},
 
                          change.old, pos, pos);
 
      }
 
      updateInput = true;
 
      to.push(out);
 
    }
 
    function undo() {unredoHelper(history.done, history.undone);}
 
    function redo() {unredoHelper(history.undone, history.done);}
 

	
 
    function updateLinesNoUndo(from, to, newText, selFrom, selTo) {
 
    function updateLinesNoUndo(from, to, lines, selFrom, selTo) {
 
      if (suppressEdits) return;
 
      var recomputeMaxLength = false, maxLineLength = maxLine.length;
 
      var recomputeMaxLength = false, maxLineLength = maxLine.text.length;
 
      if (!options.lineWrapping)
 
        doc.iter(from.line, to.line + 1, function(line) {
 
          if (!line.hidden && line.text.length == maxLineLength) {recomputeMaxLength = true; return true;}
 
        });
 
      if (from.line != to.line || newText.length > 1) gutterDirty = true;
 
      if (from.line != to.line || lines.length > 1) gutterDirty = true;
 

	
 
      var nlines = to.line - from.line, firstLine = getLine(from.line), lastLine = getLine(to.line);
 
      // First adjust the line structure, taking some care to leave highlighting intact.
 
      if (from.ch == 0 && to.ch == 0 && newText[newText.length - 1] == "") {
 
      var lastHL = lst(lines);
 

	
 
      // First adjust the line structure
 
      if (from.ch == 0 && to.ch == 0 && hlText(lastHL) == "") {
 
        // This is a whole-line replace. Treated specially to make
 
        // sure line objects move the way they are supposed to.
 
        var added = [], prevLine = null;
 
        if (from.line) {
 
          prevLine = getLine(from.line - 1);
 
          prevLine.fixMarkEnds(lastLine);
 
        } else lastLine.fixMarkStarts();
 
        for (var i = 0, e = newText.length - 1; i < e; ++i)
 
          added.push(Line.inheritMarks(newText[i], prevLine));
 
        for (var i = 0, e = lines.length - 1; i < e; ++i)
 
          added.push(new Line(hlText(lines[i]), hlSpans(lines[i])));
 
        lastLine.update(lastLine.text, hlSpans(lastHL));
 
        if (nlines) doc.remove(from.line, nlines, callbacks);
 
        if (added.length) doc.insert(from.line, added);
 
      } else if (firstLine == lastLine) {
 
        if (newText.length == 1)
 
          firstLine.replace(from.ch, to.ch, newText[0]);
 
        else {
 
          lastLine = firstLine.split(to.ch, newText[newText.length-1]);
 
          firstLine.replace(from.ch, null, newText[0]);
 
          firstLine.fixMarkEnds(lastLine);
 
          var added = [];
 
          for (var i = 1, e = newText.length - 1; i < e; ++i)
 
            added.push(Line.inheritMarks(newText[i], firstLine));
 
          added.push(lastLine);
 
        if (lines.length == 1) {
 
          firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]) + firstLine.text.slice(to.ch), hlSpans(lines[0]));
 
        } else {
 
          for (var added = [], i = 1, e = lines.length - 1; i < e; ++i)
 
            added.push(new Line(hlText(lines[i]), hlSpans(lines[i])));
 
          added.push(new Line(hlText(lastHL) + firstLine.text.slice(to.ch), hlSpans(lastHL)));
 
          firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]), hlSpans(lines[0]));
 
          doc.insert(from.line + 1, added);
 
        }
 
      } else if (newText.length == 1) {
 
        firstLine.replace(from.ch, null, newText[0]);
 
        lastLine.replace(null, to.ch, "");
 
        firstLine.append(lastLine);
 
      } else if (lines.length == 1) {
 
        firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]) + lastLine.text.slice(to.ch), hlSpans(lines[0]));
 
        doc.remove(from.line + 1, nlines, callbacks);
 
      } else {
 
        var added = [];
 
        firstLine.replace(from.ch, null, newText[0]);
 
        lastLine.replace(null, to.ch, newText[newText.length-1]);
 
        firstLine.fixMarkEnds(lastLine);
 
        for (var i = 1, e = newText.length - 1; i < e; ++i)
 
          added.push(Line.inheritMarks(newText[i], firstLine));
 
        firstLine.update(firstLine.text.slice(0, from.ch) + hlText(lines[0]), hlSpans(lines[0]));
 
        lastLine.update(hlText(lastHL) + lastLine.text.slice(to.ch), hlSpans(lastHL));
 
        for (var i = 1, e = lines.length - 1; i < e; ++i)
 
          added.push(new Line(hlText(lines[i]), hlSpans(lines[i])));
 
        if (nlines > 1) doc.remove(from.line + 1, nlines - 1, callbacks);
 
        doc.insert(from.line + 1, added);
 
      }
 
      if (options.lineWrapping) {
 
        var perLine = Math.max(5, scroller.clientWidth / charWidth() - 3);
 
        doc.iter(from.line, from.line + newText.length, function(line) {
 
        doc.iter(from.line, from.line + lines.length, function(line) {
 
          if (line.hidden) return;
 
          var guess = Math.ceil(line.text.length / perLine) || 1;
 
          if (guess != line.height) updateLineHeight(line, guess);
 
        });
 
      } else {
 
        doc.iter(from.line, from.line + newText.length, function(line) {
 
        doc.iter(from.line, from.line + lines.length, function(line) {
 
          var l = line.text;
 
          if (!line.hidden && l.length > maxLineLength) {
 
            maxLine = l; maxLineLength = l.length; maxLineChanged = true;
 
            maxLine = line; maxLineLength = l.length; maxLineChanged = true;
 
            recomputeMaxLength = false;
 
          }
 
        });
 
        if (recomputeMaxLength) updateMaxLine = true;
 
      }
 

	
 
      // Add these lines to the work array, so that they will be
 
      // highlighted. Adjust work lines if lines were added/removed.
 
      var newWork = [], lendiff = newText.length - nlines - 1;
 
      for (var i = 0, l = work.length; i < l; ++i) {
 
        var task = work[i];
 
        if (task < from.line) newWork.push(task);
 
        else if (task > to.line) newWork.push(task + lendiff);
 
      }
 
      var hlEnd = from.line + Math.min(newText.length, 500);
 
      highlightLines(from.line, hlEnd);
 
      newWork.push(hlEnd);
 
      work = newWork;
 
      startWorker(100);
 
      // Adjust frontier, schedule worker
 
      frontier = Math.min(frontier, from.line);
 
      startWorker(400);
 

	
 
      var lendiff = lines.length - nlines - 1;
 
      // Remember that these lines changed, for updating the display
 
      changes.push({from: from.line, to: to.line + 1, diff: lendiff});
 
      var changeObj = {from: from, to: to, text: newText};
 
      if (options.onChange) {
 
        // Normalize lines to contain only strings, since that's what
 
        // the change event handler expects
 
        for (var i = 0; i < lines.length; ++i)
 
          if (typeof lines[i] != "string") lines[i] = lines[i].text;
 
        var changeObj = {from: from, to: to, text: lines};
 
      if (textChanged) {
 
        for (var cur = textChanged; cur.next; cur = cur.next) {}
 
        cur.next = changeObj;
 
      } else textChanged = changeObj;
 
      }
 

	
 
      // Update the selection
 
      function updateLine(n) {return n <= Math.min(to.line, to.line + lendiff) ? n : n + lendiff;}
 
      setSelection(clipPos(selFrom), clipPos(selTo),
 
                   updateLine(sel.from.line), updateLine(sel.to.line));
 
    }
 

	
 
    function updateVerticalScroll(scrollTop) {
 
      var th = textHeight(), virtualHeight = Math.floor(doc.height * th + 2 * paddingTop()), scrollbarHeight = scroller.clientHeight;
 
      scrollbar.style.height = scrollbarHeight + "px";
 
      if (scroller.clientHeight)
 
        scrollbarInner.style.height = virtualHeight + "px";
 
      // Position the mover div to align with the current virtual scroll position
 
      if (scrollTop != null) scrollbar.scrollTop = scrollTop;
 
      mover.style.top = (displayOffset * th - scrollbar.scrollTop) + "px";
 
      scrollbar.style.display = (virtualHeight > scrollbarHeight) ? "block" : "none";
 
    function needsScrollbar() {
 
      var realHeight = doc.height * textHeight() + 2 * paddingTop();
 
      return realHeight * .99 > scroller.offsetHeight ? realHeight : false;
 
    }
 
  
 
    // On Mac OS X Lion and up, detect whether the mouse is plugged in by measuring 
 
    // the width of a div with a scrollbar in it. If the width is <= 1, then
 
    // the mouse isn't plugged in and scrollbars should overlap the content.
 
    function overlapScrollbars() {
 
      var tmpSb = document.createElement('div'),
 
          tmpSbInner = document.createElement('div');
 
      tmpSb.className = "CodeMirror-scrollbar";
 
      tmpSb.style.cssText = "position: absolute; left: -9999px; height: 100px;";
 
      tmpSbInner.className = "CodeMirror-scrollbar-inner";
 
      tmpSbInner.style.height = "200px";
 
      tmpSb.appendChild(tmpSbInner);
 

	
 
      document.body.appendChild(tmpSb);
 
      var result = (tmpSb.offsetWidth <= 1);
 
      document.body.removeChild(tmpSb);
 
      return result;
 
    function updateVerticalScroll(scrollTop) {
 
      var scrollHeight = needsScrollbar();
 
      scrollbar.style.display = scrollHeight ? "block" : "none";
 
      if (scrollHeight) {
 
        scrollbarInner.style.height = sizer.style.minHeight = scrollHeight + "px";
 
        scrollbar.style.height = scroller.clientHeight + "px";
 
        if (scrollTop != null) {
 
          scrollbar.scrollTop = scroller.scrollTop = scrollTop;
 
          // 'Nudge' the scrollbar to work around a Webkit bug where,
 
          // in some situations, we'd end up with a scrollbar that
 
          // reported its scrollTop (and looked) as expected, but
 
          // *behaved* as if it was still in a previous state (i.e.
 
          // couldn't scroll up, even though it appeared to be at the
 
          // bottom).
 
          if (webkit) setTimeout(function() {
 
            if (scrollbar.scrollTop != scrollTop) return;
 
            scrollbar.scrollTop = scrollTop + (scrollTop ? -1 : 1);
 
            scrollbar.scrollTop = scrollTop;
 
          }, 0);
 
        }
 
      } else {
 
        sizer.style.minHeight = "";
 
      }
 
      // Position the mover div to align with the current virtual scroll position
 
      mover.style.top = displayOffset * textHeight() + "px";
 
    }
 

	
 
    function computeMaxLength() {
 
      var maxLineLength = 0; 
 
      maxLine = ""; maxLineChanged = true;
 
      doc.iter(0, doc.size, function(line) {
 
      maxLine = getLine(0); maxLineChanged = true;
 
      var maxLineLength = maxLine.text.length;
 
      doc.iter(1, doc.size, function(line) {
 
        var l = line.text;
 
        if (!line.hidden && l.length > maxLineLength) {
 
          maxLineLength = l.length; maxLine = l;
 
          maxLineLength = l.length; maxLine = line;
 
        }
 
      });
 
      updateMaxLine = false;
 
    }
 

	
 
    function replaceRange(code, from, to) {
 
      from = clipPos(from);
 
      if (!to) to = from; else to = clipPos(to);
 
      code = splitLines(code);
 
      function adjustPos(pos) {
 
        if (posLess(pos, from)) return pos;
 
        if (!posLess(to, pos)) return end;
 
        var line = pos.line + code.length - (to.line - from.line) - 1;
 
        var ch = pos.ch;
 
        if (pos.line == to.line)
 
          ch += code[code.length-1].length - (to.ch - (to.line == from.line ? from.ch : 0));
 
          ch += lst(code).length - (to.ch - (to.line == from.line ? from.ch : 0));
 
        return {line: line, ch: ch};
 
      }
 
      var end;
 
      replaceRange1(code, from, to, function(end1) {
 
        end = end1;
 
        return {from: adjustPos(sel.from), to: adjustPos(sel.to)};
 
      });
 
      return end;
 
    }
 
    function replaceSelection(code, collapse) {
 
      replaceRange1(splitLines(code), sel.from, sel.to, function(end) {
 
        if (collapse == "end") return {from: end, to: end};
 
        else if (collapse == "start") return {from: sel.from, to: sel.from};
 
        else return {from: sel.from, to: end};
 
      });
 
    }
 
    function replaceRange1(code, from, to, computeSel) {
 
      var endch = code.length == 1 ? code[0].length + from.ch : code[code.length-1].length;
 
      var endch = code.length == 1 ? code[0].length + from.ch : lst(code).length;
 
      var newSel = computeSel({line: from.line + code.length - 1, ch: endch});
 
      updateLines(from, to, code, newSel.from, newSel.to);
 
    }
 

	
 
    function getRange(from, to) {
 
    function getRange(from, to, lineSep) {
 
      var l1 = from.line, l2 = to.line;
 
      if (l1 == l2) return getLine(l1).text.slice(from.ch, to.ch);
 
      var code = [getLine(l1).text.slice(from.ch)];
 
      doc.iter(l1 + 1, l2, function(line) { code.push(line.text); });
 
      code.push(getLine(l2).text.slice(0, to.ch));
 
      return code.join("\n");
 
    }
 
    function getSelection() {
 
      return getRange(sel.from, sel.to);
 
      return code.join(lineSep || "\n");
 
    }
 
    function getSelection(lineSep) {
 
      return getRange(sel.from, sel.to, lineSep);
 
    }
 

	
 
    var pollingFast = false; // Ensures slowPoll doesn't cancel fastPoll
 
    function slowPoll() {
 
      if (pollingFast) return;
 
      poll.set(options.pollInterval, function() {
 
        startOperation();
 
        readInput();
 
        if (focused) slowPoll();
 
        endOperation();
 
      });
 
    }
 
    function fastPoll() {
 
      var missed = false;
 
      pollingFast = true;
 
      function p() {
 
        startOperation();
 
        var changed = readInput();
 
        if (!changed && !missed) {missed = true; poll.set(60, p);}
 
        else {pollingFast = false; slowPoll();}
 
        endOperation();
 
      }
 
      poll.set(20, p);
 
    }
 

	
 
    // Previnput is a hack to work with IME. If we reset the textarea
 
    // on every change, that breaks IME. So we look for changes
 
    // compared to the previous content instead. (Modern browsers have
 
    // events that indicate IME taking place, but these are not widely
 
    // supported or compatible enough yet to rely on.)
 
    var prevInput = "";
 
    function readInput() {
 
      if (leaveInputAlone || !focused || hasSelection(input) || options.readOnly) return false;
 
      if (!focused || hasSelection(input) || options.readOnly) return false;
 
      var text = input.value;
 
      if (text == prevInput) return false;
 
      if (!nestedOperation) startOperation();
 
      shiftSelecting = null;
 
      var same = 0, l = Math.min(prevInput.length, text.length);
 
      while (same < l && prevInput[same] == text[same]) ++same;
 
      if (same < prevInput.length)
 
        sel.from = {line: sel.from.line, ch: sel.from.ch - (prevInput.length - same)};
 
      else if (overwrite && posEq(sel.from, sel.to))
 
        sel.to = {line: sel.to.line, ch: Math.min(getLine(sel.to.line).text.length, sel.to.ch + (text.length - same))};
 
      replaceSelection(text.slice(same), "end");
 
      if (text.length > 1000) { input.value = prevInput = ""; }
 
      else prevInput = text;
 
      if (!nestedOperation) endOperation();
 
      return true;
 
    }
 
    function resetInput(user) {
 
      if (!posEq(sel.from, sel.to)) {
 
        prevInput = "";
 
        input.value = getSelection();
 
        selectInput(input);
 
        if (focused) selectInput(input);
 
      } else if (user) prevInput = input.value = "";
 
    }
 

	
 
    function focusInput() {
 
      if (options.readOnly != "nocursor") input.focus();
 
    }
 

	
 
    function scrollEditorIntoView() {
 
      if (!cursor.getBoundingClientRect) return;
 
      var rect = cursor.getBoundingClientRect();
 
      // IE returns bogus coordinates when the instance sits inside of an iframe and the cursor is hidden
 
      if (ie && rect.top == rect.bottom) return;
 
      var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight);
 
      if (rect.top < 0 || rect.bottom > winH) scrollCursorIntoView();
 
    }
 
    function scrollCursorIntoView() {
 
      var coords = calculateCursorCoords();
 
      return scrollIntoView(coords.x, coords.y, coords.x, coords.yBot);
 
      scrollIntoView(coords.x, coords.y, coords.x, coords.yBot);
 
      if (!focused) return;
 
      var box = sizer.getBoundingClientRect(), doScroll = null;
 
      if (coords.y + box.top < 0) doScroll = true;
 
      else if (coords.y + box.top + textHeight() > (window.innerHeight || document.documentElement.clientHeight)) doScroll = false;
 
      if (doScroll != null) {
 
        var hidden = cursor.style.display == "none";
 
        if (hidden) {
 
          cursor.style.display = "";
 
          cursor.style.left = coords.x + "px";
 
          cursor.style.top = (coords.y - displayOffset) + "px";
 
        }
 
        cursor.scrollIntoView(doScroll);
 
        if (hidden) cursor.style.display = "none";
 
      }
 
    }
 
    function calculateCursorCoords() {
 
      var cursor = localCoords(sel.inverted ? sel.from : sel.to);
 
      var x = options.lineWrapping ? Math.min(cursor.x, lineSpace.offsetWidth) : cursor.x;
 
      return {x: x, y: cursor.y, yBot: cursor.yBot};
 
    }
 
    function scrollIntoView(x1, y1, x2, y2) {
 
      var scrollPos = calculateScrollPos(x1, y1, x2, y2), scrolled = false;
 
      if (scrollPos.scrollLeft != null) {scroller.scrollLeft = scrollPos.scrollLeft; scrolled = true;}
 
      if (scrollPos.scrollTop != null) {scrollbar.scrollTop = scrollPos.scrollTop; scrolled = true;}
 
      if (scrolled && options.onScroll) options.onScroll(instance);
 
      var scrollPos = calculateScrollPos(x1, y1, x2, y2);
 
      if (scrollPos.scrollLeft != null) {scroller.scrollLeft = scrollPos.scrollLeft;}
 
      if (scrollPos.scrollTop != null) {scrollbar.scrollTop = scroller.scrollTop = scrollPos.scrollTop;}
 
    }
 
    function calculateScrollPos(x1, y1, x2, y2) {
 
      var pl = paddingLeft(), pt = paddingTop();
 
      y1 += pt; y2 += pt; x1 += pl; x2 += pl;
 
      var screen = scroller.clientHeight, screentop = scrollbar.scrollTop, result = {};
 
      var atTop = y1 < paddingTop() + 10;
 
      var docBottom = needsScrollbar() || Infinity;
 
      var atTop = y1 < pt + 10, atBottom = y2 + pt > docBottom - 10;
 
      if (y1 < screentop) result.scrollTop = atTop ? 0 : Math.max(0, y1);
 
      else if (y2 > screentop + screen) result.scrollTop = y2 - screen;
 
      else if (y2 > screentop + screen) result.scrollTop = (atBottom ? docBottom : y2) - screen;
 

	
 
      var screenw = scroller.clientWidth, screenleft = scroller.scrollLeft;
 
      var gutterw = options.fixedGutter ? gutter.clientWidth : 0;
 
      var atLeft = x1 < gutterw + pl + 10;
 
      if (x1 < screenleft + gutterw || atLeft) {
 
        if (atLeft) x1 = 0;
 
        result.scrollLeft = Math.max(0, x1 - 10 - gutterw);
 
      } else if (x2 > screenw + screenleft - 3) {
 
        result.scrollLeft = x2 + 10 - screenw;
 
      }
 
      return result;
 
    }
 

	
 
    function visibleLines(scrollTop) {
 
      var lh = textHeight(), top = (scrollTop != null ? scrollTop : scrollbar.scrollTop) - paddingTop();
 
      var fromHeight = Math.max(0, Math.floor(top / lh));
 
      var toHeight = Math.ceil((top + scroller.clientHeight) / lh);
 
      return {from: lineAtHeight(doc, fromHeight),
 
              to: lineAtHeight(doc, toHeight)};
 
    }
 
    // Uses a set of changes plus the current scroll position to
 
    // determine which DOM updates have to be made, and makes the
 
    // updates.
 
    function updateDisplay(changes, suppressCallback, scrollTop) {
 
      if (!scroller.clientWidth) {
 
        showingFrom = showingTo = displayOffset = 0;
 
        return;
 
      }
 
      // Compute the new visible window
 
      // If scrollTop is specified, use that to determine which lines
 
      // to render instead of the current scrollbar position.
 
      var visible = visibleLines(scrollTop);
 
      // Bail out if the visible area is already rendered and nothing changed.
 
      if (changes !== true && changes.length == 0 && visible.from > showingFrom && visible.to < showingTo) {
 
        updateVerticalScroll(scrollTop);
 
        return;
 
      }
 
      var from = Math.max(visible.from - 100, 0), to = Math.min(doc.size, visible.to + 100);
 
      if (showingFrom < from && from - showingFrom < 20) from = showingFrom;
 
      if (showingTo > to && showingTo - to < 20) to = Math.min(doc.size, showingTo);
 

	
 
      // Create a range of theoretically intact lines, and punch holes
 
      // in that using the change info.
 
      var intact = changes === true ? [] :
 
        computeIntact([{from: showingFrom, to: showingTo, domStart: 0}], changes);
 
      // Clip off the parts that won't be visible
 
      var intactLines = 0;
 
      for (var i = 0; i < intact.length; ++i) {
 
        var range = intact[i];
 
        if (range.from < from) {range.domStart += (from - range.from); range.from = from;}
 
        if (range.to > to) range.to = to;
 
        if (range.from >= range.to) intact.splice(i--, 1);
 
        else intactLines += range.to - range.from;
 
      }
 
      if (intactLines == to - from && from == showingFrom && to == showingTo) {
 
        updateVerticalScroll(scrollTop);
 
        return;
 
      }
 
      intact.sort(function(a, b) {return a.domStart - b.domStart;});
 

	
 
      var th = textHeight(), gutterDisplay = gutter.style.display;
 
      lineDiv.style.display = "none";
 
      patchDisplay(from, to, intact);
 
      lineDiv.style.display = gutter.style.display = "";
 

	
 
      var different = from != showingFrom || to != showingTo || lastSizeC != scroller.clientHeight + th;
 
      // This is just a bogus formula that detects when the editor is
 
      // resized or the font size changes.
 
      if (different) lastSizeC = scroller.clientHeight + th;
 
      if (from != showingFrom || to != showingTo && options.onViewportChange)
 
        setTimeout(function(){
 
          if (options.onViewportChange) options.onViewportChange(instance, from, to);
 
        });
 
      showingFrom = from; showingTo = to;
 
      displayOffset = heightAtLine(doc, from);
 
      startWorker(100);
 

	
 
      // Since this is all rather error prone, it is honoured with the
 
      // only assertion in the whole file.
 
      if (lineDiv.childNodes.length != showingTo - showingFrom)
 
        throw new Error("BAD PATCH! " + JSON.stringify(intact) + " size=" + (showingTo - showingFrom) +
 
                        " nodes=" + lineDiv.childNodes.length);
 

	
 
      function checkHeights() {
 
        var curNode = lineDiv.firstChild, heightChanged = false;
 
        doc.iter(showingFrom, showingTo, function(line) {
 
          // Work around bizarro IE7 bug where, sometimes, our curNode
 
          // is magically replaced with a new node in the DOM, leaving
 
          // us with a reference to an orphan (nextSibling-less) node.
 
          if (!curNode) return;
 
          if (!line.hidden) {
 
            var height = Math.round(curNode.offsetHeight / th) || 1;
 
            if (line.height != height) {
 
              updateLineHeight(line, height);
 
              gutterDirty = heightChanged = true;
 
            }
 
          }
 
          curNode = curNode.nextSibling;
 
        });
 
        return heightChanged;
 
      }
 

	
 
      if (options.lineWrapping) {
 
        // Guess whether we're going to need the scrollbar, so that we don't end up changing the linewrapping
 
        // after the scrollbar appears (during updateVerticalScroll()). Only do this if the scrollbar is
 
        // appearing (if it's disappearing, we don't have to worry about the scroll position, and there are
 
        // issues on IE7 if we turn it off too early).
 
        var virtualHeight = Math.floor(doc.height * th + 2 * paddingTop()), scrollbarHeight = scroller.clientHeight;
 
        if (virtualHeight > scrollbarHeight) scrollbar.style.display = "block";
 
        checkHeights();
 
      }
 
      if (options.lineWrapping) checkHeights();
 

	
 
      gutter.style.display = gutterDisplay;
 
      if (different || gutterDirty) {
 
        // If the gutter grew in size, re-check heights. If those changed, re-draw gutter.
 
        updateGutter() && options.lineWrapping && checkHeights() && updateGutter();
 
      }
 
      updateVerticalScroll(scrollTop);
 
      updateSelection();
 
      updateVerticalScroll(scrollTop);
 
      if (!suppressCallback && options.onUpdate) options.onUpdate(instance);
 
      return true;
 
    }
 

	
 
    function computeIntact(intact, changes) {
 
      for (var i = 0, l = changes.length || 0; i < l; ++i) {
 
        var change = changes[i], intact2 = [], diff = change.diff || 0;
 
        for (var j = 0, l2 = intact.length; j < l2; ++j) {
 
          var range = intact[j];
 
          if (change.to <= range.from && change.diff)
 
            intact2.push({from: range.from + diff, to: range.to + diff,
 
                          domStart: range.domStart});
 
          else if (change.to <= range.from || change.from >= range.to)
 
            intact2.push(range);
 
          else {
 
            if (change.from > range.from)
 
              intact2.push({from: range.from, to: change.from, domStart: range.domStart});
 
            if (change.to < range.to)
 
              intact2.push({from: change.to + diff, to: range.to + diff,
 
                            domStart: range.domStart + (change.to - range.from)});
 
          }
 
        }
 
        intact = intact2;
 
      }
 
      return intact;
 
    }
 

	
 
    function patchDisplay(from, to, intact) {
 
      // The first pass removes the DOM nodes that aren't intact.
 
      if (!intact.length) lineDiv.innerHTML = "";
 
      else {
 
        function killNode(node) {
 
          var tmp = node.nextSibling;
 
          node.parentNode.removeChild(node);
 
          return tmp;
 
        }
 
      // The first pass removes the DOM nodes that aren't intact.
 
      if (!intact.length) removeChildren(lineDiv);
 
      else {
 
        var domPos = 0, curNode = lineDiv.firstChild, n;
 
        for (var i = 0; i < intact.length; ++i) {
 
          var cur = intact[i];
 
          while (cur.domStart > domPos) {curNode = killNode(curNode); domPos++;}
 
          for (var j = 0, e = cur.to - cur.from; j < e; ++j) {curNode = curNode.nextSibling; domPos++;}
 
        }
 
        while (curNode) curNode = killNode(curNode);
 
      }
 
      // This pass fills in the lines that actually changed.
 
      var nextIntact = intact.shift(), curNode = lineDiv.firstChild, j = from;
 
      var scratch = document.createElement("div");
 
      doc.iter(from, to, function(line) {
 
        if (nextIntact && nextIntact.to == j) nextIntact = intact.shift();
 
        if (!nextIntact || nextIntact.from > j) {
 
          if (line.hidden) var html = scratch.innerHTML = "<pre></pre>";
 
          if (line.hidden) var lineElement = elt("pre");
 
          else {
 
            var html = '<pre' + (line.className ? ' class="' + line.className + '"' : '') + '>'
 
              + line.getHTML(makeTab) + '</pre>';
 
            var lineElement = lineContent(line);
 
            if (line.className) lineElement.className = line.className;
 
            // Kludge to make sure the styled element lies behind the selection (by z-index)
 
            if (line.bgClassName)
 
              html = '<div style="position: relative"><pre class="' + line.bgClassName +
 
              '" style="position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: -2">&#160;</pre>' + html + "</div>";
 
          }
 
          scratch.innerHTML = html;
 
          lineDiv.insertBefore(scratch.firstChild, curNode);
 
            if (line.bgClassName) {
 
              var pre = elt("pre", "\u00a0", line.bgClassName, "position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: -2");
 
              lineElement = elt("div", [pre, lineElement], null, "position: relative");
 
            }
 
          }
 
          lineDiv.insertBefore(lineElement, curNode);
 
        } else {
 
          curNode = curNode.nextSibling;
 
        }
 
        ++j;
 
      });
 
    }
 

	
 
    function updateGutter() {
 
      if (!options.gutter && !options.lineNumbers) return;
 
      var hText = mover.offsetHeight, hEditor = scroller.clientHeight;
 
      gutter.style.height = (hText - hEditor < 2 ? hEditor : hText) + "px";
 
      var html = [], i = showingFrom, normalNode;
 
      var fragment = document.createDocumentFragment(), i = showingFrom, normalNode;
 
      doc.iter(showingFrom, Math.max(showingTo, showingFrom + 1), function(line) {
 
        if (line.hidden) {
 
          html.push("<pre></pre>");
 
          fragment.appendChild(elt("pre"));
 
        } else {
 
          var marker = line.gutterMarker;
 
          var text = options.lineNumbers ? i + options.firstLineNumber : null;
 
          var text = options.lineNumbers ? options.lineNumberFormatter(i + options.firstLineNumber) : null;
 
          if (marker && marker.text)
 
            text = marker.text.replace("%N%", text != null ? text : "");
 
          else if (text == null)
 
            text = "\u00a0";
 
          html.push((marker && marker.style ? '<pre class="' + marker.style + '">' : "<pre>"), text);
 
          for (var j = 1; j < line.height; ++j) html.push("<br/>&#160;");
 
          html.push("</pre>");
 
          var markerElement = fragment.appendChild(elt("pre", null, marker && marker.style));
 
          markerElement.innerHTML = text;
 
          for (var j = 1; j < line.height; ++j) {
 
            markerElement.appendChild(elt("br"));
 
            markerElement.appendChild(document.createTextNode("\u00a0"));
 
          }
 
          if (!marker) normalNode = i;
 
        }
 
        ++i;
 
      });
 
      gutter.style.display = "none";
 
      gutterText.innerHTML = html.join("");
 
      removeChildrenAndAdd(gutterText, fragment);
 
      // Make sure scrolling doesn't cause number gutter size to pop
 
      if (normalNode != null && options.lineNumbers) {
 
        var node = gutterText.childNodes[normalNode - showingFrom];
 
        var minwidth = String(doc.size).length, val = eltText(node.firstChild), pad = "";
 
        while (val.length + pad.length < minwidth) pad += "\u00a0";
 
        if (pad) node.insertBefore(document.createTextNode(pad), node.firstChild);
 
      }
 
      gutter.style.display = "";
 
      var resized = Math.abs((parseInt(lineSpace.style.marginLeft) || 0) - gutter.offsetWidth) > 2;
 
      lineSpace.style.marginLeft = gutter.offsetWidth + "px";
 
      gutterDirty = false;
 
      return resized;
 
    }
 
    function updateSelection() {
 
      var collapsed = posEq(sel.from, sel.to);
 
      var fromPos = localCoords(sel.from, true);
 
      var toPos = collapsed ? fromPos : localCoords(sel.to, true);
 
      var headPos = sel.inverted ? fromPos : toPos, th = textHeight();
 
      var wrapOff = eltOffset(wrapper), lineOff = eltOffset(lineDiv);
 
      inputDiv.style.top = Math.max(0, Math.min(scroller.offsetHeight, headPos.y + lineOff.top - wrapOff.top)) + "px";
 
      inputDiv.style.left = Math.max(0, Math.min(scroller.offsetWidth, headPos.x + lineOff.left - wrapOff.left)) + "px";
 
      if (collapsed) {
 
        cursor.style.top = headPos.y + "px";
 
        cursor.style.left = (options.lineWrapping ? Math.min(headPos.x, lineSpace.offsetWidth) : headPos.x) + "px";
 
        cursor.style.display = "";
 
        selectionDiv.style.display = "none";
 
      } else {
 
        var sameLine = fromPos.y == toPos.y, html = "";
 
        var sameLine = fromPos.y == toPos.y, fragment = document.createDocumentFragment();
 
        var clientWidth = lineSpace.clientWidth || lineSpace.offsetWidth;
 
        var clientHeight = lineSpace.clientHeight || lineSpace.offsetHeight;
 
        function add(left, top, right, height) {
 
        var add = function(left, top, right, height) {
 
          var rstyle = quirksMode ? "width: " + (!right ? clientWidth : clientWidth - right - left) + "px"
 
                                  : "right: " + right + "px";
 
          html += '<div class="CodeMirror-selected" style="position: absolute; left: ' + left +
 
            'px; top: ' + top + 'px; ' + rstyle + '; height: ' + height + 'px"></div>';
 
        }
 
          fragment.appendChild(elt("div", null, "CodeMirror-selected", "position: absolute; left: " + left +
 
                                   "px; top: " + top + "px; " + rstyle + "; height: " + height + "px"));
 
        };
 
        if (sel.from.ch && fromPos.y >= 0) {
 
          var right = sameLine ? clientWidth - toPos.x : 0;
 
          add(fromPos.x, fromPos.y, right, th);
 
        }
 
        var middleStart = Math.max(0, fromPos.y + (sel.from.ch ? th : 0));
 
        var middleHeight = Math.min(toPos.y, clientHeight) - middleStart;
 
        if (middleHeight > 0.2 * th)
 
          add(0, middleStart, 0, middleHeight);
 
        if ((!sameLine || !sel.from.ch) && toPos.y < clientHeight - .5 * th)
 
          add(0, toPos.y, clientWidth - toPos.x, th);
 
        selectionDiv.innerHTML = html;
 
        removeChildrenAndAdd(selectionDiv, fragment);
 
        cursor.style.display = "none";
 
        selectionDiv.style.display = "";
 
      }
 
    }
 

	
 
    function setShift(val) {
 
      if (val) shiftSelecting = shiftSelecting || (sel.inverted ? sel.to : sel.from);
 
      else shiftSelecting = null;
 
    }
 
    function setSelectionUser(from, to) {
 
      var sh = shiftSelecting && clipPos(shiftSelecting);
 
      if (sh) {
 
        if (posLess(sh, from)) from = sh;
 
        else if (posLess(to, sh)) to = sh;
 
      }
 
      setSelection(from, to);
 
      userSelChange = true;
 
    }
 
    // Update the selection. Last two args are only used by
 
    // updateLines, since they have to be expressed in the line
 
    // numbers before the update.
 
    function setSelection(from, to, oldFrom, oldTo) {
 
      goalColumn = null;
 
      if (oldFrom == null) {oldFrom = sel.from.line; oldTo = sel.to.line;}
 
      if (posEq(sel.from, from) && posEq(sel.to, to)) return;
 
      if (posLess(to, from)) {var tmp = to; to = from; from = tmp;}
 

	
 
      // Skip over hidden lines.
 
      if (from.line != oldFrom) {
 
        var from1 = skipHidden(from, oldFrom, sel.from.ch);
 
        // If there is no non-hidden line left, force visibility on current line
 
        if (!from1) setLineHidden(from.line, false);
 
        else from = from1;
 
      }
 
      if (to.line != oldTo) to = skipHidden(to, oldTo, sel.to.ch);
 

	
 
      if (posEq(from, to)) sel.inverted = false;
 
      else if (posEq(from, sel.to)) sel.inverted = false;
 
      else if (posEq(to, sel.from)) sel.inverted = true;
 

	
 
      if (options.autoClearEmptyLines && posEq(sel.from, sel.to)) {
 
        var head = sel.inverted ? from : to;
 
        if (head.line != sel.from.line && sel.from.line < doc.size) {
 
          var oldLine = getLine(sel.from.line);
 
          if (/^\s+$/.test(oldLine.text))
 
            setTimeout(operation(function() {
 
              if (oldLine.parent && /^\s+$/.test(oldLine.text)) {
 
                var no = lineNo(oldLine);
 
@@ -1336,815 +1348,785 @@ var CodeMirror = (function() {
 
      if (pos.line < 0) return {line: 0, ch: 0};
 
      if (pos.line >= doc.size) return {line: doc.size-1, ch: getLine(doc.size-1).text.length};
 
      var ch = pos.ch, linelen = getLine(pos.line).text.length;
 
      if (ch == null || ch > linelen) return {line: pos.line, ch: linelen};
 
      else if (ch < 0) return {line: pos.line, ch: 0};
 
      else return pos;
 
    }
 

	
 
    function findPosH(dir, unit) {
 
      var end = sel.inverted ? sel.from : sel.to, line = end.line, ch = end.ch;
 
      var lineObj = getLine(line);
 
      function findNextLine() {
 
        for (var l = line + dir, e = dir < 0 ? -1 : doc.size; l != e; l += dir) {
 
          var lo = getLine(l);
 
          if (!lo.hidden) { line = l; lineObj = lo; return true; }
 
        }
 
      }
 
      function moveOnce(boundToLine) {
 
        if (ch == (dir < 0 ? 0 : lineObj.text.length)) {
 
          if (!boundToLine && findNextLine()) ch = dir < 0 ? lineObj.text.length : 0;
 
          else return false;
 
        } else ch += dir;
 
        return true;
 
      }
 
      if (unit == "char") moveOnce();
 
      else if (unit == "column") moveOnce(true);
 
      else if (unit == "word") {
 
        var sawWord = false;
 
        for (;;) {
 
          if (dir < 0) if (!moveOnce()) break;
 
          if (isWordChar(lineObj.text.charAt(ch))) sawWord = true;
 
          else if (sawWord) {if (dir < 0) {dir = 1; moveOnce();} break;}
 
          if (dir > 0) if (!moveOnce()) break;
 
        }
 
      }
 
      return {line: line, ch: ch};
 
    }
 
    function moveH(dir, unit) {
 
      var pos = dir < 0 ? sel.from : sel.to;
 
      if (shiftSelecting || posEq(sel.from, sel.to)) pos = findPosH(dir, unit);
 
      setCursor(pos.line, pos.ch, true);
 
    }
 
    function deleteH(dir, unit) {
 
      if (!posEq(sel.from, sel.to)) replaceRange("", sel.from, sel.to);
 
      else if (dir < 0) replaceRange("", findPosH(dir, unit), sel.to);
 
      else replaceRange("", sel.from, findPosH(dir, unit));
 
      userSelChange = true;
 
    }
 
    var goalColumn = null;
 
    function moveV(dir, unit) {
 
      var dist = 0, pos = localCoords(sel.inverted ? sel.from : sel.to, true);
 
      if (goalColumn != null) pos.x = goalColumn;
 
      if (unit == "page") dist = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight);
 
      else if (unit == "line") dist = textHeight();
 
      var target = coordsChar(pos.x, pos.y + dist * dir + 2);
 
      if (unit == "page") {
 
        var screen = Math.min(scroller.clientHeight, window.innerHeight || document.documentElement.clientHeight);
 
        var target = coordsChar(pos.x, pos.y + screen * dir);
 
      } else if (unit == "line") {
 
        var th = textHeight();
 
        var target = coordsChar(pos.x, pos.y + .5 * th + dir * th);
 
      }
 
      if (unit == "page") scrollbar.scrollTop += localCoords(target, true).y - pos.y;
 
      setCursor(target.line, target.ch, true);
 
      goalColumn = pos.x;
 
    }
 

	
 
    function selectWordAt(pos) {
 
    function findWordAt(pos) {
 
      var line = getLine(pos.line).text;
 
      var start = pos.ch, end = pos.ch;
 
      while (start > 0 && isWordChar(line.charAt(start - 1))) --start;
 
      while (end < line.length && isWordChar(line.charAt(end))) ++end;
 
      setSelectionUser({line: pos.line, ch: start}, {line: pos.line, ch: end});
 
      if (line) {
 
        if (pos.after === false || end == line.length) --start; else ++end;
 
        var startChar = line.charAt(start);
 
        var check = isWordChar(startChar) ? isWordChar :
 
                    /\s/.test(startChar) ? function(ch) {return /\s/.test(ch);} :
 
                    function(ch) {return !/\s/.test(ch) && !isWordChar(ch);};
 
        while (start > 0 && check(line.charAt(start - 1))) --start;
 
        while (end < line.length && check(line.charAt(end))) ++end;
 
      }
 
      return {from: {line: pos.line, ch: start}, to: {line: pos.line, ch: end}};
 
    }
 
    function selectLine(line) {
 
      setSelectionUser({line: line, ch: 0}, clipPos({line: line + 1, ch: 0}));
 
    }
 
    function indentSelected(mode) {
 
      if (posEq(sel.from, sel.to)) return indentLine(sel.from.line, mode);
 
      var e = sel.to.line - (sel.to.ch ? 0 : 1);
 
      for (var i = sel.from.line; i <= e; ++i) indentLine(i, mode);
 
    }
 

	
 
    function indentLine(n, how) {
 
      if (!how) how = "add";
 
      if (how == "smart") {
 
        if (!mode.indent) how = "prev";
 
        else var state = getStateBefore(n);
 
      }
 

	
 
      var line = getLine(n), curSpace = line.indentation(options.tabSize),
 
          curSpaceString = line.text.match(/^\s*/)[0], indentation;
 
      if (how == "smart") {
 
        indentation = mode.indent(state, line.text.slice(curSpaceString.length), line.text);
 
        if (indentation == Pass) how = "prev";
 
      }
 
      if (how == "prev") {
 
        if (n) indentation = getLine(n-1).indentation(options.tabSize);
 
        else indentation = 0;
 
      }
 
      else if (how == "add") indentation = curSpace + options.indentUnit;
 
      else if (how == "subtract") indentation = curSpace - options.indentUnit;
 
      indentation = Math.max(0, indentation);
 
      var diff = indentation - curSpace;
 

	
 
      if (!diff) {
 
        if (sel.from.line != n && sel.to.line != n) return;
 
        var indentString = curSpaceString;
 
      } else {
 
        var indentString = "", pos = 0;
 
        if (options.indentWithTabs)
 
          for (var i = Math.floor(indentation / options.tabSize); i; --i) {pos += options.tabSize; indentString += "\t";}
 
        while (pos < indentation) {++pos; indentString += " ";}
 
      }
 
      if (pos < indentation) indentString += spaceStr(indentation - pos);
 

	
 
      if (indentString != curSpaceString)
 
      replaceRange(indentString, {line: n, ch: 0}, {line: n, ch: curSpaceString.length});
 
    }
 

	
 
    function loadMode() {
 
      mode = CodeMirror.getMode(options, options.mode);
 
      doc.iter(0, doc.size, function(line) { line.stateAfter = null; });
 
      work = [0];
 
      startWorker();
 
      frontier = 0;
 
      startWorker(100);
 
    }
 
    function gutterChanged() {
 
      var visible = options.gutter || options.lineNumbers;
 
      gutter.style.display = visible ? "" : "none";
 
      if (visible) gutterDirty = true;
 
      else lineDiv.parentNode.style.marginLeft = 0;
 
    }
 
    function wrappingChanged(from, to) {
 
      if (options.lineWrapping) {
 
        wrapper.className += " CodeMirror-wrap";
 
        var perLine = scroller.clientWidth / charWidth() - 3;
 
        doc.iter(0, doc.size, function(line) {
 
          if (line.hidden) return;
 
          var guess = Math.ceil(line.text.length / perLine) || 1;
 
          if (guess != 1) updateLineHeight(line, guess);
 
        });
 
        lineSpace.style.width = code.style.width = "";
 
        widthForcer.style.left = "";
 
        lineSpace.style.minWidth = widthForcer.style.left = "";
 
      } else {
 
        wrapper.className = wrapper.className.replace(" CodeMirror-wrap", "");
 
        maxLine = ""; maxLineChanged = true;
 
        computeMaxLength();
 
        doc.iter(0, doc.size, function(line) {
 
          if (line.height != 1 && !line.hidden) updateLineHeight(line, 1);
 
          if (line.text.length > maxLine.length) maxLine = line.text;
 
        });
 
      }
 
      changes.push({from: 0, to: doc.size});
 
    }
 
    function makeTab(col) {
 
      var w = options.tabSize - col % options.tabSize, cached = tabCache[w];
 
      if (cached) return cached;
 
      for (var str = '<span class="cm-tab">', i = 0; i < w; ++i) str += " ";
 
      return (tabCache[w] = {html: str + "</span>", width: w});
 
    }
 
    function themeChanged() {
 
      scroller.className = scroller.className.replace(/\s*cm-s-\S+/g, "") +
 
        options.theme.replace(/(^|\s)\s*/g, " cm-s-");
 
    }
 
    function keyMapChanged() {
 
      var style = keyMap[options.keyMap].style;
 
      wrapper.className = wrapper.className.replace(/\s*cm-keymap-\S+/g, "") +
 
        (style ? " cm-keymap-" + style : "");
 
    }
 

	
 
    function TextMarker() { this.set = []; }
 
    function TextMarker(type, style) { this.lines = []; this.type = type; if (style) this.style = style; }
 
    TextMarker.prototype.clear = operation(function() {
 
      var min = Infinity, max = -Infinity;
 
      for (var i = 0, e = this.set.length; i < e; ++i) {
 
        var line = this.set[i], mk = line.marked;
 
        if (!mk || !line.parent) continue;
 
      for (var i = 0; i < this.lines.length; ++i) {
 
        var line = this.lines[i];
 
        var span = getMarkedSpanFor(line.markedSpans, this, true);
 
        if (span.from != null || span.to != null) {
 
        var lineN = lineNo(line);
 
        min = Math.min(min, lineN); max = Math.max(max, lineN);
 
        for (var j = 0; j < mk.length; ++j)
 
          if (mk[j].marker == this) mk.splice(j--, 1);
 
        }
 
      }
 
      if (min != Infinity)
 
        changes.push({from: min, to: max + 1});
 
      this.lines.length = 0;
 
    });
 
    TextMarker.prototype.find = function() {
 
      var from, to;
 
      for (var i = 0, e = this.set.length; i < e; ++i) {
 
        var line = this.set[i], mk = line.marked;
 
        for (var j = 0; j < mk.length; ++j) {
 
          var mark = mk[j];
 
          if (mark.marker == this) {
 
            if (mark.from != null || mark.to != null) {
 
      for (var i = 0; i < this.lines.length; ++i) {
 
        var line = this.lines[i];
 
        var span = getMarkedSpanFor(line.markedSpans, this);
 
        if (span.from != null || span.to != null) {
 
              var found = lineNo(line);
 
              if (found != null) {
 
                if (mark.from != null) from = {line: found, ch: mark.from};
 
                if (mark.to != null) to = {line: found, ch: mark.to};
 
              }
 
            }
 
          }
 
        }
 
      }
 
      return {from: from, to: to};
 
          if (span.from != null) from = {line: found, ch: span.from};
 
          if (span.to != null) to = {line: found, ch: span.to};
 
        }
 
      }
 
      if (this.type == "bookmark") return from;
 
      return from && {from: from, to: to};
 
    };
 

	
 
    function markText(from, to, className) {
 
    function markText(from, to, className, options) {
 
      from = clipPos(from); to = clipPos(to);
 
      var tm = new TextMarker();
 
      if (!posLess(from, to)) return tm;
 
      function add(line, from, to, className) {
 
        getLine(line).addMark(new MarkedText(from, to, className, tm));
 
      }
 
      if (from.line == to.line) add(from.line, from.ch, to.ch, className);
 
      else {
 
        add(from.line, from.ch, null, className);
 
        for (var i = from.line + 1, e = to.line; i < e; ++i)
 
          add(i, null, null, className);
 
        add(to.line, null, to.ch, className);
 
      }
 
      var marker = new TextMarker("range", className);
 
      if (options) for (var opt in options) if (options.hasOwnProperty(opt))
 
        marker[opt] = options[opt];
 
      var curLine = from.line;
 
      doc.iter(curLine, to.line + 1, function(line) {
 
        var span = {from: curLine == from.line ? from.ch : null,
 
                    to: curLine == to.line ? to.ch : null,
 
                    marker: marker};
 
        (line.markedSpans || (line.markedSpans = [])).push(span);
 
        marker.lines.push(line);
 
        ++curLine;
 
      });
 
      changes.push({from: from.line, to: to.line + 1});
 
      return tm;
 
      return marker;
 
    }
 

	
 
    function setBookmark(pos) {
 
      pos = clipPos(pos);
 
      var bm = new Bookmark(pos.ch);
 
      getLine(pos.line).addMark(bm);
 
      return bm;
 
      var marker = new TextMarker("bookmark"), line = getLine(pos.line);
 
      var span = {from: pos.ch, to: pos.ch, marker: marker};
 
      (line.markedSpans || (line.markedSpans = [])).push(span);
 
      marker.lines.push(line);
 
      return marker;
 
    }
 

	
 
    function findMarksAt(pos) {
 
      pos = clipPos(pos);
 
      var markers = [], marked = getLine(pos.line).marked;
 
      if (!marked) return markers;
 
      for (var i = 0, e = marked.length; i < e; ++i) {
 
        var m = marked[i];
 
        if ((m.from == null || m.from <= pos.ch) &&
 
            (m.to == null || m.to >= pos.ch))
 
          markers.push(m.marker || m);
 
      var markers = [], spans = getLine(pos.line).markedSpans;
 
      if (spans) for (var i = 0; i < spans.length; ++i) {
 
        var span = spans[i];
 
        if ((span.from == null || span.from <= pos.ch) &&
 
            (span.to == null || span.to >= pos.ch))
 
          markers.push(span.marker);
 
      }
 
      return markers;
 
    }
 

	
 
    function addGutterMarker(line, text, className) {
 
      if (typeof line == "number") line = getLine(clipLine(line));
 
      line.gutterMarker = {text: text, style: className};
 
      gutterDirty = true;
 
      return line;
 
    }
 
    function removeGutterMarker(line) {
 
      if (typeof line == "number") line = getLine(clipLine(line));
 
      line.gutterMarker = null;
 
      gutterDirty = true;
 
    }
 

	
 
    function changeLine(handle, op) {
 
      var no = handle, line = handle;
 
      if (typeof handle == "number") line = getLine(clipLine(handle));
 
      else no = lineNo(handle);
 
      if (no == null) return null;
 
      if (op(line, no)) changes.push({from: no, to: no + 1});
 
      else return null;
 
      return line;
 
    }
 
    function setLineClass(handle, className, bgClassName) {
 
      return changeLine(handle, function(line) {
 
        if (line.className != className || line.bgClassName != bgClassName) {
 
          line.className = className;
 
          line.bgClassName = bgClassName;
 
          return true;
 
        }
 
      });
 
    }
 
    function setLineHidden(handle, hidden) {
 
      return changeLine(handle, function(line, no) {
 
        if (line.hidden != hidden) {
 
          line.hidden = hidden;
 
          if (!options.lineWrapping) {
 
            var l = line.text;
 
            if (hidden && l.length == maxLine.length) {
 
            if (hidden && line.text.length == maxLine.text.length) {
 
              updateMaxLine = true;
 
            } else if (!hidden && l.length > maxLine.length) {
 
              maxLine = l; maxWidth = null; updateMaxLine = false;
 
            } else if (!hidden && line.text.length > maxLine.text.length) {
 
              maxLine = line; updateMaxLine = false;
 
            }
 
          }
 
          updateLineHeight(line, hidden ? 0 : 1);
 
          var fline = sel.from.line, tline = sel.to.line;
 
          if (hidden && (fline == no || tline == no)) {
 
            var from = fline == no ? skipHidden({line: fline, ch: 0}, fline, 0) : sel.from;
 
            var to = tline == no ? skipHidden({line: tline, ch: 0}, tline, 0) : sel.to;
 
            // Can't hide the last visible line, we'd have no place to put the cursor
 
            if (!to) return;
 
            setSelection(from, to);
 
          }
 
          return (gutterDirty = true);
 
        }
 
      });
 
    }
 

	
 
    function lineInfo(line) {
 
      if (typeof line == "number") {
 
        if (!isLine(line)) return null;
 
        var n = line;
 
        line = getLine(line);
 
        if (!line) return null;
 
      } else {
 
        var n = lineNo(line);
 
        if (n == null) return null;
 
      }
 
      var marker = line.gutterMarker;
 
      return {line: n, handle: line, text: line.text, markerText: marker && marker.text,
 
              markerClass: marker && marker.style, lineClass: line.className, bgClass: line.bgClassName};
 
    }
 

	
 
    function stringWidth(str) {
 
      measure.innerHTML = "<pre><span>x</span></pre>";
 
      measure.firstChild.firstChild.firstChild.nodeValue = str;
 
      return measure.firstChild.firstChild.offsetWidth || 10;
 
    }
 
    // These are used to go from pixel positions to character
 
    // positions, taking varying character widths into account.
 
    function charFromX(line, x) {
 
      if (x <= 0) return 0;
 
      var lineObj = getLine(line), text = lineObj.text;
 
      function getX(len) {
 
        return measureLine(lineObj, len).left;
 
      }
 
      var from = 0, fromX = 0, to = text.length, toX;
 
      // Guess a suitable upper bound for our search.
 
      var estimated = Math.min(to, Math.ceil(x / charWidth()));
 
      for (;;) {
 
        var estX = getX(estimated);
 
        if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2));
 
        else {toX = estX; to = estimated; break;}
 
      }
 
      if (x > toX) return to;
 
      // Try to guess a suitable lower bound as well.
 
      estimated = Math.floor(to * 0.8); estX = getX(estimated);
 
      if (estX < x) {from = estimated; fromX = estX;}
 
      // Do a binary search between these bounds.
 
      for (;;) {
 
        if (to - from <= 1) return (toX - x > x - fromX) ? from : to;
 
        var middle = Math.ceil((from + to) / 2), middleX = getX(middle);
 
        if (middleX > x) {to = middle; toX = middleX;}
 
        else {from = middle; fromX = middleX;}
 
      }
 
    }
 

	
 
    var tempId = "CodeMirror-temp-" + Math.floor(Math.random() * 0xffffff).toString(16);
 
    function measureLine(line, ch) {
 
      if (ch == 0) return {top: 0, left: 0};
 
      var wbr = options.lineWrapping && ch < line.text.length &&
 
                spanAffectsWrapping.test(line.text.slice(ch - 1, ch + 1));
 
      measure.innerHTML = "<pre>" + line.getHTML(makeTab, ch, tempId, wbr) + "</pre>";
 
      var elt = document.getElementById(tempId);
 
      var top = elt.offsetTop, left = elt.offsetLeft;
 
      var pre = lineContent(line, ch);
 
      removeChildrenAndAdd(measure, pre);
 
      var anchor = pre.anchor;
 
      var top = anchor.offsetTop, left = anchor.offsetLeft;
 
      // Older IEs report zero offsets for spans directly after a wrap
 
      if (ie && top == 0 && left == 0) {
 
        var backup = document.createElement("span");
 
        backup.innerHTML = "x";
 
        elt.parentNode.insertBefore(backup, elt.nextSibling);
 
        var backup = elt("span", "x");
 
        anchor.parentNode.insertBefore(backup, anchor.nextSibling);
 
        top = backup.offsetTop;
 
      }
 
      return {top: top, left: left};
 
    }
 
    function localCoords(pos, inLineWrap) {
 
      var x, lh = textHeight(), y = lh * (heightAtLine(doc, pos.line) - (inLineWrap ? displayOffset : 0));
 
      if (pos.ch == 0) x = 0;
 
      else {
 
        var sp = measureLine(getLine(pos.line), pos.ch);
 
        x = sp.left;
 
        if (options.lineWrapping) y += Math.max(0, sp.top);
 
      }
 
      return {x: x, y: y, yBot: y + lh};
 
    }
 
    // Coords must be lineSpace-local
 
    function coordsChar(x, y) {
 
      if (y < 0) y = 0;
 
      var th = textHeight(), cw = charWidth(), heightPos = displayOffset + Math.floor(y / th);
 
      if (heightPos < 0) return {line: 0, ch: 0};
 
      var lineNo = lineAtHeight(doc, heightPos);
 
      if (lineNo >= doc.size) return {line: doc.size - 1, ch: getLine(doc.size - 1).text.length};
 
      var lineObj = getLine(lineNo), text = lineObj.text;
 
      var tw = options.lineWrapping, innerOff = tw ? heightPos - heightAtLine(doc, lineNo) : 0;
 
      if (x <= 0 && innerOff == 0) return {line: lineNo, ch: 0};
 
      var wrongLine = false;
 
      function getX(len) {
 
        var sp = measureLine(lineObj, len);
 
        if (tw) {
 
          var off = Math.round(sp.top / th);
 
          wrongLine = off != innerOff;
 
          return Math.max(0, sp.left + (off - innerOff) * scroller.clientWidth);
 
        }
 
        return sp.left;
 
      }
 
      var from = 0, fromX = 0, to = text.length, toX;
 
      // Guess a suitable upper bound for our search.
 
      var estimated = Math.min(to, Math.ceil((x + innerOff * scroller.clientWidth * .9) / cw));
 
      for (;;) {
 
        var estX = getX(estimated);
 
        if (estX <= x && estimated < to) estimated = Math.min(to, Math.ceil(estimated * 1.2));
 
        else {toX = estX; to = estimated; break;}
 
      }
 
      if (x > toX) return {line: lineNo, ch: to};
 
      // Try to guess a suitable lower bound as well.
 
      estimated = Math.floor(to * 0.8); estX = getX(estimated);
 
      if (estX < x) {from = estimated; fromX = estX;}
 
      // Do a binary search between these bounds.
 
      for (;;) {
 
        if (to - from <= 1) return {line: lineNo, ch: (toX - x > x - fromX) ? from : to};
 
        if (to - from <= 1) {
 
          var after = x - fromX < toX - x;
 
          return {line: lineNo, ch: after ? from : to, after: after};
 
        }
 
        var middle = Math.ceil((from + to) / 2), middleX = getX(middle);
 
        if (middleX > x) {to = middle; toX = middleX;}
 
        if (middleX > x) {to = middle; toX = middleX; if (wrongLine) toX += 1000; }
 
        else {from = middle; fromX = middleX;}
 
      }
 
    }
 
    function pageCoords(pos) {
 
      var local = localCoords(pos, true), off = eltOffset(lineSpace);
 
      return {x: off.left + local.x, y: off.top + local.y, yBot: off.top + local.yBot};
 
    }
 

	
 
    var cachedHeight, cachedHeightFor, measureText;
 
    var cachedHeight, cachedHeightFor, measurePre;
 
    function textHeight() {
 
      if (measureText == null) {
 
        measureText = "<pre>";
 
        for (var i = 0; i < 49; ++i) measureText += "x<br/>";
 
        measureText += "x</pre>";
 
      if (measurePre == null) {
 
        measurePre = elt("pre");
 
        for (var i = 0; i < 49; ++i) {
 
          measurePre.appendChild(document.createTextNode("x"));
 
          measurePre.appendChild(elt("br"));
 
        }
 
        measurePre.appendChild(document.createTextNode("x"));
 
      }
 
      var offsetHeight = lineDiv.clientHeight;
 
      if (offsetHeight == cachedHeightFor) return cachedHeight;
 
      cachedHeightFor = offsetHeight;
 
      measure.innerHTML = measureText;
 
      removeChildrenAndAdd(measure, measurePre.cloneNode(true));
 
      cachedHeight = measure.firstChild.offsetHeight / 50 || 1;
 
      measure.innerHTML = "";
 
      removeChildren(measure);
 
      return cachedHeight;
 
    }
 
    var cachedWidth, cachedWidthFor = 0;
 
    function charWidth() {
 
      if (scroller.clientWidth == cachedWidthFor) return cachedWidth;
 
      cachedWidthFor = scroller.clientWidth;
 
      return (cachedWidth = stringWidth("x"));
 
      var anchor = elt("span", "x");
 
      var pre = elt("pre", [anchor]);
 
      removeChildrenAndAdd(measure, pre);
 
      return (cachedWidth = anchor.offsetWidth || 10);
 
    }
 
    function paddingTop() {return lineSpace.offsetTop;}
 
    function paddingLeft() {return lineSpace.offsetLeft;}
 

	
 
    function posFromMouse(e, liberal) {
 
      var offW = eltOffset(scroller, true), x, y;
 
      // Fails unpredictably on IE[67] when mouse is dragged around quickly.
 
      try { x = e.clientX; y = e.clientY; } catch (e) { return null; }
 
      // This is a mess of a heuristic to try and determine whether a
 
      // scroll-bar was clicked or not, and to return null if one was
 
      // (and !liberal).
 
      if (!liberal && (x - offW.left > scroller.clientWidth || y - offW.top > scroller.clientHeight))
 
        return null;
 
      var offL = eltOffset(lineSpace, true);
 
      return coordsChar(x - offL.left, y - offL.top);
 
    }
 
    var detectingSelectAll;
 
    function onContextMenu(e) {
 
      var pos = posFromMouse(e), scrollPos = scrollbar.scrollTop;
 
      if (!pos || opera) return; // Opera is difficult.
 
      if (posEq(sel.from, sel.to) || posLess(pos, sel.from) || !posLess(pos, sel.to))
 
        operation(setCursor)(pos.line, pos.ch);
 

	
 
      var oldCSS = input.style.cssText;
 
      inputDiv.style.position = "absolute";
 
      input.style.cssText = "position: fixed; width: 30px; height: 30px; top: " + (e.clientY - 5) +
 
        "px; left: " + (e.clientX - 5) + "px; z-index: 1000; background: white; " +
 
        "border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";
 
      leaveInputAlone = true;
 
      var val = input.value = getSelection();
 
      focusInput();
 
      selectInput(input);
 
      resetInput(true);
 
      // Adds "Select all" to context menu in FF
 
      if (posEq(sel.from, sel.to)) input.value = prevInput = " ";
 

	
 
      function rehide() {
 
        var newVal = splitLines(input.value).join("\n");
 
        if (newVal != val && !options.readOnly) operation(replaceSelection)(newVal, "end");
 
        inputDiv.style.position = "relative";
 
        input.style.cssText = oldCSS;
 
        if (ie_lt9) scrollbar.scrollTop = scrollPos;
 
        leaveInputAlone = false;
 
        resetInput(true);
 
        slowPoll();
 

	
 
        // Try to detect the user choosing select-all 
 
        if (input.selectionStart != null) {
 
          clearTimeout(detectingSelectAll);
 
          var extval = input.value = " " + (posEq(sel.from, sel.to) ? "" : input.value), i = 0;
 
          prevInput = " ";
 
          input.selectionStart = 1; input.selectionEnd = extval.length;
 
          detectingSelectAll = setTimeout(function poll(){
 
            if (prevInput == " " && input.selectionStart == 0)
 
              operation(commands.selectAll)(instance);
 
            else if (i++ < 10) detectingSelectAll = setTimeout(poll, 500);
 
            else resetInput();
 
          }, 200);
 
        }
 
      }
 

	
 
      if (gecko) {
 
        e_stop(e);
 
        var mouseup = connect(window, "mouseup", function() {
 
          mouseup();
 
          setTimeout(rehide, 20);
 
        }, true);
 
      } else {
 
        setTimeout(rehide, 50);
 
      }
 
    }
 

	
 
    // Cursor-blinking
 
    function restartBlink() {
 
      clearInterval(blinker);
 
      var on = true;
 
      cursor.style.visibility = "";
 
      blinker = setInterval(function() {
 
        cursor.style.visibility = (on = !on) ? "" : "hidden";
 
      }, 650);
 
      }, options.cursorBlinkRate);
 
    }
 

	
 
    var matching = {"(": ")>", ")": "(<", "[": "]>", "]": "[<", "{": "}>", "}": "{<"};
 
    function matchBrackets(autoclear) {
 
      var head = sel.inverted ? sel.from : sel.to, line = getLine(head.line), pos = head.ch - 1;
 
      var match = (pos >= 0 && matching[line.text.charAt(pos)]) || matching[line.text.charAt(++pos)];
 
      if (!match) return;
 
      var ch = match.charAt(0), forward = match.charAt(1) == ">", d = forward ? 1 : -1, st = line.styles;
 
      for (var off = pos + 1, i = 0, e = st.length; i < e; i+=2)
 
        if ((off -= st[i].length) <= 0) {var style = st[i+1]; break;}
 

	
 
      var stack = [line.text.charAt(pos)], re = /[(){}[\]]/;
 
      function scan(line, from, to) {
 
        if (!line.text) return;
 
        var st = line.styles, pos = forward ? 0 : line.text.length - 1, cur;
 
        for (var i = forward ? 0 : st.length - 2, e = forward ? st.length : -2; i != e; i += 2*d) {
 
          var text = st[i];
 
          if (st[i+1] != style) {pos += d * text.length; continue;}
 
          for (var j = forward ? 0 : text.length - 1, te = forward ? text.length : -1; j != te; j += d, pos+=d) {
 
            if (pos >= from && pos < to && re.test(cur = text.charAt(j))) {
 
              var match = matching[cur];
 
              if (match.charAt(1) == ">" == forward) stack.push(cur);
 
              else if (stack.pop() != match.charAt(0)) return {pos: pos, match: false};
 
              else if (!stack.length) return {pos: pos, match: true};
 
            }
 
          }
 
        }
 
      }
 
      for (var i = head.line, e = forward ? Math.min(i + 100, doc.size) : Math.max(-1, i - 100); i != e; i+=d) {
 
        var line = getLine(i), first = i == head.line;
 
        var found = scan(line, first && forward ? pos + 1 : 0, first && !forward ? pos : line.text.length);
 
        if (found) break;
 
      }
 
      if (!found) found = {pos: null, match: false};
 
      var style = found.match ? "CodeMirror-matchingbracket" : "CodeMirror-nonmatchingbracket";
 
      var one = markText({line: head.line, ch: pos}, {line: head.line, ch: pos+1}, style),
 
          two = found.pos != null && markText({line: i, ch: found.pos}, {line: i, ch: found.pos + 1}, style);
 
      var clear = operation(function(){one.clear(); two && two.clear();});
 
      if (autoclear) setTimeout(clear, 800);
 
      else bracketHighlighted = clear;
 
    }
 

	
 
    // Finds the line to start with when starting a parse. Tries to
 
    // find a line with a stateAfter, so that it can start with a
 
    // valid state. If that fails, it returns the line with the
 
    // smallest indentation, which tends to need the least context to
 
    // parse correctly.
 
    function findStartLine(n) {
 
      var minindent, minline;
 
      for (var search = n, lim = n - 40; search > lim; --search) {
 
        if (search == 0) return 0;
 
        var line = getLine(search-1);
 
        if (line.stateAfter) return search;
 
        var indented = line.indentation(options.tabSize);
 
        if (minline == null || minindent > indented) {
 
          minline = search - 1;
 
          minindent = indented;
 
        }
 
      }
 
      return minline;
 
    }
 
    function getStateBefore(n) {
 
      var start = findStartLine(n), state = start && getLine(start-1).stateAfter;
 
      var pos = findStartLine(n), state = pos && getLine(pos-1).stateAfter;
 
      if (!state) state = startState(mode);
 
      else state = copyState(mode, state);
 
      doc.iter(start, n, function(line) {
 
        line.highlight(mode, state, options.tabSize);
 
        line.stateAfter = copyState(mode, state);
 
      doc.iter(pos, n, function(line) {
 
        line.process(mode, state, options.tabSize);
 
        line.stateAfter = (pos == n - 1 || pos % 5 == 0) ? copyState(mode, state) : null;
 
      });
 
      if (start < n) changes.push({from: start, to: n});
 
      if (n < doc.size && !getLine(n).stateAfter) work.push(n);
 
      return state;
 
    }
 
    function highlightLines(start, end) {
 
      var state = getStateBefore(start);
 
      doc.iter(start, end, function(line) {
 
        line.highlight(mode, state, options.tabSize);
 
        line.stateAfter = copyState(mode, state);
 
      });
 
    }
 
    function highlightWorker() {
 
      var end = +new Date + options.workTime;
 
      var foundWork = work.length;
 
      while (work.length) {
 
        if (!getLine(showingFrom).stateAfter) var task = showingFrom;
 
        else var task = work.pop();
 
        if (task >= doc.size) continue;
 
        var start = findStartLine(task), state = start && getLine(start-1).stateAfter;
 
        if (state) state = copyState(mode, state);
 
        else state = startState(mode);
 

	
 
        var unchanged = 0, compare = mode.compareStates, realChange = false,
 
            i = start, bail = false;
 
        doc.iter(i, doc.size, function(line) {
 
          var hadState = line.stateAfter;
 
      if (frontier >= showingTo) return;
 
      var end = +new Date + options.workTime, state = copyState(mode, getStateBefore(frontier));
 
      var startFrontier = frontier;
 
      doc.iter(frontier, showingTo, function(line) {
 
        if (frontier >= showingFrom) { // Visible
 
          line.highlight(mode, state, options.tabSize);
 
          line.stateAfter = copyState(mode, state);
 
        } else {
 
          line.process(mode, state, options.tabSize);
 
          line.stateAfter = frontier % 5 == 0 ? copyState(mode, state) : null;
 
        }
 
        ++frontier;
 
          if (+new Date > end) {
 
            work.push(i);
 
            startWorker(options.workDelay);
 
            if (realChange) changes.push({from: task, to: i + 1});
 
            return (bail = true);
 
          }
 
          var changed = line.highlight(mode, state, options.tabSize);
 
          if (changed) realChange = true;
 
          line.stateAfter = copyState(mode, state);
 
          var done = null;
 
          if (compare) {
 
            var same = hadState && compare(hadState, state);
 
            if (same != Pass) done = !!same;
 
          }
 
          if (done == null) {
 
            if (changed !== false || !hadState) unchanged = 0;
 
            else if (++unchanged > 3 && (!mode.indent || mode.indent(hadState, "") == mode.indent(state, "")))
 
              done = true;
 
          }
 
          if (done) return true;
 
          ++i;
 
          return true;
 
        }
 
        });
 
        if (bail) return;
 
        if (realChange) changes.push({from: task, to: i + 1});
 
      }
 
      if (foundWork && options.onHighlightComplete)
 
        options.onHighlightComplete(instance);
 
      if (showingTo > startFrontier && frontier >= showingFrom)
 
        operation(function() {changes.push({from: startFrontier, to: frontier});})();
 
    }
 
    function startWorker(time) {
 
      if (!work.length) return;
 
      highlight.set(time, operation(highlightWorker));
 
      if (frontier < showingTo)
 
        highlight.set(time, highlightWorker);
 
    }
 

	
 
    // Operations are used to wrap changes in such a way that each
 
    // change won't have to update the cursor and display (which would
 
    // be awkward, slow, and error-prone), but instead updates are
 
    // batched and then all combined and executed at once.
 
    function startOperation() {
 
      updateInput = userSelChange = textChanged = null;
 
      changes = []; selectionChanged = false; callbacks = [];
 
    }
 
    function endOperation() {
 
      if (updateMaxLine) computeMaxLength();
 
      if (maxLineChanged && !options.lineWrapping) {
 
        widthForcer.style.left = stringWidth(maxLine) + "px";
 
        var cursorWidth = widthForcer.offsetWidth, left = measureLine(maxLine, maxLine.text.length).left;
 
        if (!ie_lt8) {
 
          widthForcer.style.left = left + "px";
 
          lineSpace.style.minWidth = (left + cursorWidth) + "px";
 
        }
 
        maxLineChanged = false;
 
      }
 
      var newScrollPos, updated;
 
      if (selectionChanged) {
 
        var coords = calculateCursorCoords();
 
        newScrollPos = calculateScrollPos(coords.x, coords.y, coords.x, coords.yBot);
 
      }
 
      if (changes.length) updated = updateDisplay(changes, true, (newScrollPos ? newScrollPos.scrollTop : null));
 
      else {
 
      if (changes.length || newScrollPos && newScrollPos.scrollTop != null)
 
        updated = updateDisplay(changes, true, newScrollPos && newScrollPos.scrollTop);
 
      if (!updated) {
 
        if (selectionChanged) updateSelection();
 
        if (gutterDirty) updateGutter();
 
      }
 
      if (newScrollPos) scrollCursorIntoView();
 
      if (selectionChanged) {scrollEditorIntoView(); restartBlink();}
 
      if (selectionChanged) restartBlink();
 

	
 
      if (focused && !leaveInputAlone &&
 
          (updateInput === true || (updateInput !== false && selectionChanged)))
 
      if (focused && (updateInput === true || (updateInput !== false && selectionChanged)))
 
        resetInput(userSelChange);
 

	
 
      if (selectionChanged && options.matchBrackets)
 
        setTimeout(operation(function() {
 
          if (bracketHighlighted) {bracketHighlighted(); bracketHighlighted = null;}
 
          if (posEq(sel.from, sel.to)) matchBrackets(false);
 
        }), 20);
 
      var sc = selectionChanged, cbs = callbacks; // these can be reset by callbacks
 
      if (textChanged && options.onChange && instance)
 
        options.onChange(instance, textChanged);
 
      if (sc && options.onCursorActivity)
 
        options.onCursorActivity(instance);
 
      for (var i = 0; i < cbs.length; ++i) cbs[i](instance);
 
      if (updated && options.onUpdate) options.onUpdate(instance);
 
    }
 
    var nestedOperation = 0;
 
    function operation(f) {
 
      return function() {
 
        if (!nestedOperation++) startOperation();
 
        try {var result = f.apply(this, arguments);}
 
        finally {if (!--nestedOperation) endOperation();}
 
        return result;
 
      };
 
    }
 

	
 
    function compoundChange(f) {
 
      history.startCompound();
 
      try { return f(); } finally { history.endCompound(); }
 
    }
 

	
 
    for (var ext in extensions)
 
      if (extensions.propertyIsEnumerable(ext) &&
 
          !instance.propertyIsEnumerable(ext))
 
        instance[ext] = extensions[ext];
 
    return instance;
 
  } // (end of function CodeMirror)
 

	
 
  // The default configuration options.
 
  CodeMirror.defaults = {
 
    value: "",
 
    mode: null,
 
    theme: "default",
 
    indentUnit: 2,
 
    indentWithTabs: false,
 
    smartIndent: true,
 
    tabSize: 4,
 
    keyMap: "default",
 
    extraKeys: null,
 
    electricChars: true,
 
    autoClearEmptyLines: false,
 
    onKeyEvent: null,
 
    onDragEvent: null,
 
    lineWrapping: false,
 
    lineNumbers: false,
 
    gutter: false,
 
    fixedGutter: false,
 
    firstLineNumber: 1,
 
    readOnly: false,
 
    dragDrop: true,
 
    onChange: null,
 
    onCursorActivity: null,
 
    onViewportChange: null,
 
    onGutterClick: null,
 
    onHighlightComplete: null,
 
    onUpdate: null,
 
    onFocus: null, onBlur: null, onScroll: null,
 
    matchBrackets: false,
 
    cursorBlinkRate: 530,
 
    workTime: 100,
 
    workDelay: 200,
 
    pollInterval: 100,
 
    undoDepth: 40,
 
    tabindex: null,
 
    autofocus: null
 
    autofocus: null,
 
    lineNumberFormatter: function(integer) { return integer; }
 
  };
 

	
 
  var ios = /AppleWebKit/.test(navigator.userAgent) && /Mobile\/\w+/.test(navigator.userAgent);
 
  var mac = ios || /Mac/.test(navigator.platform);
 
  var win = /Win/.test(navigator.platform);
 

	
 
  // Known modes, by name and by MIME
 
  var modes = CodeMirror.modes = {}, mimeModes = CodeMirror.mimeModes = {};
 
  CodeMirror.defineMode = function(name, mode) {
 
    if (!CodeMirror.defaults.mode && name != "null") CodeMirror.defaults.mode = name;
 
    if (arguments.length > 2) {
 
      mode.dependencies = [];
 
      for (var i = 2; i < arguments.length; ++i) mode.dependencies.push(arguments[i]);
 
    }
 
    modes[name] = mode;
 
  };
 
  CodeMirror.defineMIME = function(mime, spec) {
 
    mimeModes[mime] = spec;
 
  };
 
  CodeMirror.resolveMode = function(spec) {
 
    if (typeof spec == "string" && mimeModes.hasOwnProperty(spec))
 
      spec = mimeModes[spec];
 
    else if (typeof spec == "string" && /^[\w\-]+\/[\w\-]+\+xml$/.test(spec))
 
      return CodeMirror.resolveMode("application/xml");
 
    if (typeof spec == "string") return {name: spec};
 
    else return spec || {name: "null"};
 
  };
 
  CodeMirror.getMode = function(options, spec) {
 
    var spec = CodeMirror.resolveMode(spec);
 
    var mfactory = modes[spec.name];
 
    if (!mfactory) return CodeMirror.getMode(options, "text/plain");
 
    return mfactory(options, spec);
 
    var modeObj = mfactory(options, spec);
 
    if (modeExtensions.hasOwnProperty(spec.name)) {
 
      var exts = modeExtensions[spec.name];
 
      for (var prop in exts) if (exts.hasOwnProperty(prop)) modeObj[prop] = exts[prop];
 
    }
 
    modeObj.name = spec.name;
 
    return modeObj;
 
  };
 
  CodeMirror.listModes = function() {
 
    var list = [];
 
    for (var m in modes)
 
      if (modes.propertyIsEnumerable(m)) list.push(m);
 
    return list;
 
  };
 
  CodeMirror.listMIMEs = function() {
 
    var list = [];
 
    for (var m in mimeModes)
 
      if (mimeModes.propertyIsEnumerable(m)) list.push({mime: m, mode: mimeModes[m]});
 
    return list;
 
  };
 

	
 
  var extensions = CodeMirror.extensions = {};
 
  CodeMirror.defineExtension = function(name, func) {
 
    extensions[name] = func;
 
  };
 

	
 
  var modeExtensions = CodeMirror.modeExtensions = {};
 
  CodeMirror.extendMode = function(mode, properties) {
 
    var exts = modeExtensions.hasOwnProperty(mode) ? modeExtensions[mode] : (modeExtensions[mode] = {});
 
    for (var prop in properties) if (properties.hasOwnProperty(prop))
 
      exts[prop] = properties[prop];
 
  };
 

	
 
  var commands = CodeMirror.commands = {
 
    selectAll: function(cm) {cm.setSelection({line: 0, ch: 0}, {line: cm.lineCount() - 1});},
 
    killLine: function(cm) {
 
      var from = cm.getCursor(true), to = cm.getCursor(false), sel = !posEq(from, to);
 
      if (!sel && cm.getLine(from.line).length == from.ch) cm.replaceRange("", from, {line: from.line + 1, ch: 0});
 
      else cm.replaceRange("", from, sel ? to : {line: from.line});
 
    },
 
    deleteLine: function(cm) {var l = cm.getCursor().line; cm.replaceRange("", {line: l, ch: 0}, {line: l});},
 
    undo: function(cm) {cm.undo();},
 
    redo: function(cm) {cm.redo();},
 
    goDocStart: function(cm) {cm.setCursor(0, 0, true);},
 
    goDocEnd: function(cm) {cm.setSelection({line: cm.lineCount() - 1}, null, true);},
 
    goLineStart: function(cm) {cm.setCursor(cm.getCursor().line, 0, true);},
 
    goLineStartSmart: function(cm) {
 
      var cur = cm.getCursor();
 
      var text = cm.getLine(cur.line), firstNonWS = Math.max(0, text.search(/\S/));
 
      cm.setCursor(cur.line, cur.ch <= firstNonWS && cur.ch ? 0 : firstNonWS, true);
 
    },
 
    goLineEnd: function(cm) {cm.setSelection({line: cm.getCursor().line}, null, true);},
 
    goLineUp: function(cm) {cm.moveV(-1, "line");},
 
    goLineDown: function(cm) {cm.moveV(1, "line");},
 
    goPageUp: function(cm) {cm.moveV(-1, "page");},
 
    goPageDown: function(cm) {cm.moveV(1, "page");},
 
    goCharLeft: function(cm) {cm.moveH(-1, "char");},
 
    goCharRight: function(cm) {cm.moveH(1, "char");},
 
    goColumnLeft: function(cm) {cm.moveH(-1, "column");},
 
    goColumnRight: function(cm) {cm.moveH(1, "column");},
 
    goWordLeft: function(cm) {cm.moveH(-1, "word");},
 
    goWordRight: function(cm) {cm.moveH(1, "word");},
 
    delCharLeft: function(cm) {cm.deleteH(-1, "char");},
 
    delCharRight: function(cm) {cm.deleteH(1, "char");},
 
    delWordLeft: function(cm) {cm.deleteH(-1, "word");},
 
    delWordRight: function(cm) {cm.deleteH(1, "word");},
 
    indentAuto: function(cm) {cm.indentSelection("smart");},
 
    indentMore: function(cm) {cm.indentSelection("add");},
 
    indentLess: function(cm) {cm.indentSelection("subtract");},
 
    insertTab: function(cm) {cm.replaceSelection("\t", "end");},
 
    defaultTab: function(cm) {
 
      if (cm.somethingSelected()) cm.indentSelection("add");
 
      else cm.replaceSelection("\t", "end");
 
    },
 
    transposeChars: function(cm) {
 
      var cur = cm.getCursor(), line = cm.getLine(cur.line);
 
      if (cur.ch > 0 && cur.ch < line.length - 1)
 
        cm.replaceRange(line.charAt(cur.ch) + line.charAt(cur.ch - 1),
 
                        {line: cur.line, ch: cur.ch - 1}, {line: cur.line, ch: cur.ch + 1});
 
    },
 
    newlineAndIndent: function(cm) {
 
@@ -2152,593 +2134,586 @@ var CodeMirror = (function() {
 
      cm.indentLine(cm.getCursor().line);
 
    },
 
    toggleOverwrite: function(cm) {cm.toggleOverwrite();}
 
  };
 

	
 
  var keyMap = CodeMirror.keyMap = {};
 
  keyMap.basic = {
 
    "Left": "goCharLeft", "Right": "goCharRight", "Up": "goLineUp", "Down": "goLineDown",
 
    "End": "goLineEnd", "Home": "goLineStartSmart", "PageUp": "goPageUp", "PageDown": "goPageDown",
 
    "Delete": "delCharRight", "Backspace": "delCharLeft", "Tab": "defaultTab", "Shift-Tab": "indentAuto",
 
    "Enter": "newlineAndIndent", "Insert": "toggleOverwrite"
 
  };
 
  // Note that the save and find-related commands aren't defined by
 
  // default. Unknown commands are simply ignored.
 
  keyMap.pcDefault = {
 
    "Ctrl-A": "selectAll", "Ctrl-D": "deleteLine", "Ctrl-Z": "undo", "Shift-Ctrl-Z": "redo", "Ctrl-Y": "redo",
 
    "Ctrl-Home": "goDocStart", "Alt-Up": "goDocStart", "Ctrl-End": "goDocEnd", "Ctrl-Down": "goDocEnd",
 
    "Ctrl-Left": "goWordLeft", "Ctrl-Right": "goWordRight", "Alt-Left": "goLineStart", "Alt-Right": "goLineEnd",
 
    "Ctrl-Backspace": "delWordLeft", "Ctrl-Delete": "delWordRight", "Ctrl-S": "save", "Ctrl-F": "find",
 
    "Ctrl-G": "findNext", "Shift-Ctrl-G": "findPrev", "Shift-Ctrl-F": "replace", "Shift-Ctrl-R": "replaceAll",
 
    "Ctrl-[": "indentLess", "Ctrl-]": "indentMore",
 
    fallthrough: "basic"
 
  };
 
  keyMap.macDefault = {
 
    "Cmd-A": "selectAll", "Cmd-D": "deleteLine", "Cmd-Z": "undo", "Shift-Cmd-Z": "redo", "Cmd-Y": "redo",
 
    "Cmd-Up": "goDocStart", "Cmd-End": "goDocEnd", "Cmd-Down": "goDocEnd", "Alt-Left": "goWordLeft",
 
    "Alt-Right": "goWordRight", "Cmd-Left": "goLineStart", "Cmd-Right": "goLineEnd", "Alt-Backspace": "delWordLeft",
 
    "Ctrl-Alt-Backspace": "delWordRight", "Alt-Delete": "delWordRight", "Cmd-S": "save", "Cmd-F": "find",
 
    "Cmd-G": "findNext", "Shift-Cmd-G": "findPrev", "Cmd-Alt-F": "replace", "Shift-Cmd-Alt-F": "replaceAll",
 
    "Cmd-[": "indentLess", "Cmd-]": "indentMore",
 
    fallthrough: ["basic", "emacsy"]
 
  };
 
  keyMap["default"] = mac ? keyMap.macDefault : keyMap.pcDefault;
 
  keyMap.emacsy = {
 
    "Ctrl-F": "goCharRight", "Ctrl-B": "goCharLeft", "Ctrl-P": "goLineUp", "Ctrl-N": "goLineDown",
 
    "Alt-F": "goWordRight", "Alt-B": "goWordLeft", "Ctrl-A": "goLineStart", "Ctrl-E": "goLineEnd",
 
    "Ctrl-V": "goPageUp", "Shift-Ctrl-V": "goPageDown", "Ctrl-D": "delCharRight", "Ctrl-H": "delCharLeft",
 
    "Alt-D": "delWordRight", "Alt-Backspace": "delWordLeft", "Ctrl-K": "killLine", "Ctrl-T": "transposeChars"
 
  };
 

	
 
  function getKeyMap(val) {
 
    if (typeof val == "string") return keyMap[val];
 
    else return val;
 
  }
 
  function lookupKey(name, extraMap, map, handle, stop) {
 
    function lookup(map) {
 
      map = getKeyMap(map);
 
      var found = map[name];
 
      if (found === false) {
 
        if (stop) stop();
 
        return true;
 
      }
 
      if (found != null && handle(found)) return true;
 
      if (map.nofallthrough) {
 
        if (stop) stop();
 
        return true;
 
      }
 
      var fallthrough = map.fallthrough;
 
      if (fallthrough == null) return false;
 
      if (Object.prototype.toString.call(fallthrough) != "[object Array]")
 
        return lookup(fallthrough);
 
      for (var i = 0, e = fallthrough.length; i < e; ++i) {
 
        if (lookup(fallthrough[i])) return true;
 
      }
 
      return false;
 
    }
 
    if (extraMap && lookup(extraMap)) return true;
 
    return lookup(map);
 
  }
 
  function isModifierKey(event) {
 
    var name = keyNames[e_prop(event, "keyCode")];
 
    return name == "Ctrl" || name == "Alt" || name == "Shift" || name == "Mod";
 
  }
 

	
 
  CodeMirror.fromTextArea = function(textarea, options) {
 
    if (!options) options = {};
 
    options.value = textarea.value;
 
    if (!options.tabindex && textarea.tabindex)
 
      options.tabindex = textarea.tabindex;
 
    if (options.autofocus == null && textarea.getAttribute("autofocus") != null)
 
      options.autofocus = true;
 
    // Set autofocus to true if this textarea is focused, or if it has
 
    // autofocus and no other element is focused.
 
    if (options.autofocus == null) {
 
      var hasFocus = document.body;
 
      // doc.activeElement occasionally throws on IE
 
      try { hasFocus = document.activeElement; } catch(e) {}
 
      options.autofocus = hasFocus == textarea ||
 
        textarea.getAttribute("autofocus") != null && hasFocus == document.body;
 
    }
 

	
 
    function save() {textarea.value = instance.getValue();}
 
    if (textarea.form) {
 
      // Deplorable hack to make the submit method do the right thing.
 
      var rmSubmit = connect(textarea.form, "submit", save, true);
 
      if (typeof textarea.form.submit == "function") {
 
        var realSubmit = textarea.form.submit;
 
        function wrappedSubmit() {
 
        textarea.form.submit = function wrappedSubmit() {
 
          save();
 
          textarea.form.submit = realSubmit;
 
          textarea.form.submit();
 
          textarea.form.submit = wrappedSubmit;
 
        }
 
        textarea.form.submit = wrappedSubmit;
 
        };
 
      }
 
    }
 

	
 
    textarea.style.display = "none";
 
    var instance = CodeMirror(function(node) {
 
      textarea.parentNode.insertBefore(node, textarea.nextSibling);
 
    }, options);
 
    instance.save = save;
 
    instance.getTextArea = function() { return textarea; };
 
    instance.toTextArea = function() {
 
      save();
 
      textarea.parentNode.removeChild(instance.getWrapperElement());
 
      textarea.style.display = "";
 
      if (textarea.form) {
 
        rmSubmit();
 
        if (typeof textarea.form.submit == "function")
 
          textarea.form.submit = realSubmit;
 
      }
 
    };
 
    return instance;
 
  };
 

	
 
  var gecko = /gecko\/\d{7}/i.test(navigator.userAgent);
 
  var ie = /MSIE \d/.test(navigator.userAgent);
 
  var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);
 
  var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent);
 
  var quirksMode = ie && document.documentMode == 5;
 
  var webkit = /WebKit\//.test(navigator.userAgent);
 
  var chrome = /Chrome\//.test(navigator.userAgent);
 
  var opera = /Opera\//.test(navigator.userAgent);
 
  var safari = /Apple Computer/.test(navigator.vendor);
 
  var khtml = /KHTML\//.test(navigator.userAgent);
 
  var mac_geLion = /Mac OS X 10\D([7-9]|\d\d)\D/.test(navigator.userAgent);
 

	
 
  // Utility functions for working with state. Exported because modes
 
  // sometimes need to do this.
 
  function copyState(mode, state) {
 
    if (state === true) return state;
 
    if (mode.copyState) return mode.copyState(state);
 
    var nstate = {};
 
    for (var n in state) {
 
      var val = state[n];
 
      if (val instanceof Array) val = val.concat([]);
 
      nstate[n] = val;
 
    }
 
    return nstate;
 
  }
 
  CodeMirror.copyState = copyState;
 
  function startState(mode, a1, a2) {
 
    return mode.startState ? mode.startState(a1, a2) : true;
 
  }
 
  CodeMirror.startState = startState;
 
  CodeMirror.innerMode = function(mode, state) {
 
    while (mode.innerMode) {
 
      var info = mode.innerMode(state);
 
      state = info.state;
 
      mode = info.mode;
 
    }
 
    return info || {mode: mode, state: state};
 
  };
 

	
 
  // The character stream used by a mode's parser.
 
  function StringStream(string, tabSize) {
 
    this.pos = this.start = 0;
 
    this.string = string;
 
    this.tabSize = tabSize || 8;
 
  }
 
  StringStream.prototype = {
 
    eol: function() {return this.pos >= this.string.length;},
 
    sol: function() {return this.pos == 0;},
 
    peek: function() {return this.string.charAt(this.pos);},
 
    peek: function() {return this.string.charAt(this.pos) || undefined;},
 
    next: function() {
 
      if (this.pos < this.string.length)
 
        return this.string.charAt(this.pos++);
 
    },
 
    eat: function(match) {
 
      var ch = this.string.charAt(this.pos);
 
      if (typeof match == "string") var ok = ch == match;
 
      else var ok = ch && (match.test ? match.test(ch) : match(ch));
 
      if (ok) {++this.pos; return ch;}
 
    },
 
    eatWhile: function(match) {
 
      var start = this.pos;
 
      while (this.eat(match)){}
 
      return this.pos > start;
 
    },
 
    eatSpace: function() {
 
      var start = this.pos;
 
      while (/[\s\u00a0]/.test(this.string.charAt(this.pos))) ++this.pos;
 
      return this.pos > start;
 
    },
 
    skipToEnd: function() {this.pos = this.string.length;},
 
    skipTo: function(ch) {
 
      var found = this.string.indexOf(ch, this.pos);
 
      if (found > -1) {this.pos = found; return true;}
 
    },
 
    backUp: function(n) {this.pos -= n;},
 
    column: function() {return countColumn(this.string, this.start, this.tabSize);},
 
    indentation: function() {return countColumn(this.string, null, this.tabSize);},
 
    match: function(pattern, consume, caseInsensitive) {
 
      if (typeof pattern == "string") {
 
        function cased(str) {return caseInsensitive ? str.toLowerCase() : str;}
 
        var cased = function(str) {return caseInsensitive ? str.toLowerCase() : str;};
 
        if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
 
          if (consume !== false) this.pos += pattern.length;
 
          return true;
 
        }
 
      } else {
 
        var match = this.string.slice(this.pos).match(pattern);
 
        if (match && match.index > 0) return null;
 
        if (match && consume !== false) this.pos += match[0].length;
 
        return match;
 
      }
 
    },
 
    current: function(){return this.string.slice(this.start, this.pos);}
 
  };
 
  CodeMirror.StringStream = StringStream;
 

	
 
  function MarkedText(from, to, className, marker) {
 
    this.from = from; this.to = to; this.style = className; this.marker = marker;
 
  }
 
  MarkedText.prototype = {
 
    attach: function(line) { this.marker.set.push(line); },
 
    detach: function(line) {
 
      var ix = indexOf(this.marker.set, line);
 
      if (ix > -1) this.marker.set.splice(ix, 1);
 
    },
 
    split: function(pos, lenBefore) {
 
      if (this.to <= pos && this.to != null) return null;
 
      var from = this.from < pos || this.from == null ? null : this.from - pos + lenBefore;
 
      var to = this.to == null ? null : this.to - pos + lenBefore;
 
      return new MarkedText(from, to, this.style, this.marker);
 
    },
 
    dup: function() { return new MarkedText(null, null, this.style, this.marker); },
 
    clipTo: function(fromOpen, from, toOpen, to, diff) {
 
      if (fromOpen && to > this.from && (to < this.to || this.to == null))
 
        this.from = null;
 
      else if (this.from != null && this.from >= from)
 
        this.from = Math.max(to, this.from) + diff;
 
      if (toOpen && (from < this.to || this.to == null) && (from > this.from || this.from == null))
 
        this.to = null;
 
      else if (this.to != null && this.to > from)
 
        this.to = to < this.to ? this.to + diff : from;
 
    },
 
    isDead: function() { return this.from != null && this.to != null && this.from >= this.to; },
 
    sameSet: function(x) { return this.marker == x.marker; }
 
  };
 
  function MarkedSpan(from, to, marker) {
 
    this.from = from; this.to = to; this.marker = marker;
 
  }
 

	
 
  function getMarkedSpanFor(spans, marker, del) {
 
    if (spans) for (var i = 0; i < spans.length; ++i) {
 
      var span = spans[i];
 
      if (span.marker == marker) {
 
        if (del) spans.splice(i, 1);
 
        return span;
 
      }
 
    }
 
  }
 

	
 
  function markedSpansBefore(old, startCh, endCh) {
 
    if (old) for (var i = 0, nw; i < old.length; ++i) {
 
      var span = old[i], marker = span.marker;
 
      var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= startCh : span.from < startCh);
 
      if (startsBefore || marker.type == "bookmark" && span.from == startCh && span.from != endCh) {
 
        var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= startCh : span.to > startCh);
 
        (nw || (nw = [])).push({from: span.from,
 
                                to: endsAfter ? null : span.to,
 
                                marker: marker});
 
      }
 
    }
 
    return nw;
 
  }
 

	
 
  function markedSpansAfter(old, endCh) {
 
    if (old) for (var i = 0, nw; i < old.length; ++i) {
 
      var span = old[i], marker = span.marker;
 
      var endsAfter = span.to == null || (marker.inclusiveRight ? span.to >= endCh : span.to > endCh);
 
      if (endsAfter || marker.type == "bookmark" && span.from == endCh) {
 
        var startsBefore = span.from == null || (marker.inclusiveLeft ? span.from <= endCh : span.from < endCh);
 
        (nw || (nw = [])).push({from: startsBefore ? null : span.from - endCh,
 
                                to: span.to == null ? null : span.to - endCh,
 
                                marker: marker});
 
      }
 
    }
 
    return nw;
 
  }
 

	
 
  function updateMarkedSpans(oldFirst, oldLast, startCh, endCh, newText) {
 
    if (!oldFirst && !oldLast) return newText;
 
    // Get the spans that 'stick out' on both sides
 
    var first = markedSpansBefore(oldFirst, startCh);
 
    var last = markedSpansAfter(oldLast, endCh);
 

	
 
  function Bookmark(pos) {
 
    this.from = pos; this.to = pos; this.line = null;
 
  }
 
  Bookmark.prototype = {
 
    attach: function(line) { this.line = line; },
 
    detach: function(line) { if (this.line == line) this.line = null; },
 
    split: function(pos, lenBefore) {
 
      if (pos < this.from) {
 
        this.from = this.to = (this.from - pos) + lenBefore;
 
        return this;
 
      }
 
    },
 
    isDead: function() { return this.from > this.to; },
 
    clipTo: function(fromOpen, from, toOpen, to, diff) {
 
      if ((fromOpen || from < this.from) && (toOpen || to > this.to)) {
 
        this.from = 0; this.to = -1;
 
      } else if (this.from > from) {
 
        this.from = this.to = Math.max(to, this.from) + diff;
 
      }
 
    },
 
    sameSet: function(x) { return false; },
 
    find: function() {
 
      if (!this.line || !this.line.parent) return null;
 
      return {line: lineNo(this.line), ch: this.from};
 
    },
 
    clear: function() {
 
      if (this.line) {
 
        var found = indexOf(this.line.marked, this);
 
        if (found != -1) this.line.marked.splice(found, 1);
 
        this.line = null;
 
      }
 
    }
 
  };
 
    // Next, merge those two ends
 
    var sameLine = newText.length == 1, offset = lst(newText).length + (sameLine ? startCh : 0);
 
    if (first) {
 
      // Fix up .to properties of first
 
      for (var i = 0; i < first.length; ++i) {
 
        var span = first[i];
 
        if (span.to == null) {
 
          var found = getMarkedSpanFor(last, span.marker);
 
          if (!found) span.to = startCh;
 
          else if (sameLine) span.to = found.to == null ? null : found.to + offset;
 
        }
 
      }
 
    }
 
    if (last) {
 
      // Fix up .from in last (or move them into first in case of sameLine)
 
      for (var i = 0; i < last.length; ++i) {
 
        var span = last[i];
 
        if (span.to != null) span.to += offset;
 
        if (span.from == null) {
 
          var found = getMarkedSpanFor(first, span.marker);
 
          if (!found) {
 
            span.from = offset;
 
            if (sameLine) (first || (first = [])).push(span);
 
          }
 
        } else {
 
          span.from += offset;
 
          if (sameLine) (first || (first = [])).push(span);
 
        }
 
      }
 
    }
 

	
 
    var newMarkers = [newHL(newText[0], first)];
 
    if (!sameLine) {
 
      // Fill gap with whole-line-spans
 
      var gap = newText.length - 2, gapMarkers;
 
      if (gap > 0 && first)
 
        for (var i = 0; i < first.length; ++i)
 
          if (first[i].to == null)
 
            (gapMarkers || (gapMarkers = [])).push({from: null, to: null, marker: first[i].marker});
 
      for (var i = 0; i < gap; ++i)
 
        newMarkers.push(newHL(newText[i+1], gapMarkers));
 
      newMarkers.push(newHL(lst(newText), last));
 
    }
 
    return newMarkers;
 
  }
 

	
 
  // hl stands for history-line, a data structure that can be either a
 
  // string (line without markers) or a {text, markedSpans} object.
 
  function hlText(val) { return typeof val == "string" ? val : val.text; }
 
  function hlSpans(val) { return typeof val == "string" ? null : val.markedSpans; }
 
  function newHL(text, spans) { return spans ? {text: text, markedSpans: spans} : text; }
 

	
 
  function detachMarkedSpans(line) {
 
    var spans = line.markedSpans;
 
    if (!spans) return;
 
    for (var i = 0; i < spans.length; ++i) {
 
      var lines = spans[i].marker.lines;
 
      var ix = indexOf(lines, line);
 
      lines.splice(ix, 1);
 
    }
 
    line.markedSpans = null;
 
  }
 

	
 
  function attachMarkedSpans(line, spans) {
 
    if (!spans) return;
 
    for (var i = 0; i < spans.length; ++i)
 
      var marker = spans[i].marker.lines.push(line);
 
    line.markedSpans = spans;
 
  }
 

	
 
  // When measuring the position of the end of a line, different
 
  // browsers require different approaches. If an empty span is added,
 
  // many browsers report bogus offsets. Of those, some (Webkit,
 
  // recent IE) will accept a space without moving the whole span to
 
  // the next line when wrapping it, others work with a zero-width
 
  // space.
 
  var eolSpanContent = " ";
 
  if (gecko || (ie && !ie_lt8)) eolSpanContent = "\u200b";
 
  else if (opera) eolSpanContent = "";
 

	
 
  // Line objects. These hold state related to a line, including
 
  // highlighting info (the styles array).
 
  function Line(text, styles) {
 
    this.styles = styles || [text, null];
 
  function Line(text, markedSpans) {
 
    this.text = text;
 
    this.height = 1;
 
    this.marked = this.gutterMarker = this.className = this.bgClassName = this.handlers = null;
 
    this.stateAfter = this.parent = this.hidden = null;
 
  }
 
  Line.inheritMarks = function(text, orig) {
 
    var ln = new Line(text), mk = orig && orig.marked;
 
    if (mk) {
 
      for (var i = 0; i < mk.length; ++i) {
 
        if (mk[i].to == null && mk[i].style) {
 
          var newmk = ln.marked || (ln.marked = []), mark = mk[i];
 
          var nmark = mark.dup(); newmk.push(nmark); nmark.attach(ln);
 
        }
 
      }
 
    }
 
    return ln;
 
    attachMarkedSpans(this, markedSpans);
 
  }
 
  Line.prototype = {
 
    // Replace a piece of a line, keeping the styles around it intact.
 
    replace: function(from, to_, text) {
 
      var st = [], mk = this.marked, to = to_ == null ? this.text.length : to_;
 
      copyStyles(0, from, this.styles, st);
 
      if (text) st.push(text, null);
 
      copyStyles(to, this.text.length, this.styles, st);
 
      this.styles = st;
 
      this.text = this.text.slice(0, from) + text + this.text.slice(to);
 
      this.stateAfter = null;
 
      if (mk) {
 
        var diff = text.length - (to - from);
 
        for (var i = 0; i < mk.length; ++i) {
 
          var mark = mk[i];
 
          mark.clipTo(from == null, from || 0, to_ == null, to, diff);
 
          if (mark.isDead()) {mark.detach(this); mk.splice(i--, 1);}
 
        }
 
      }
 
    },
 
    // Split a part off a line, keeping styles and markers intact.
 
    split: function(pos, textBefore) {
 
      var st = [textBefore, null], mk = this.marked;
 
      copyStyles(pos, this.text.length, this.styles, st);
 
      var taken = new Line(textBefore + this.text.slice(pos), st);
 
      if (mk) {
 
        for (var i = 0; i < mk.length; ++i) {
 
          var mark = mk[i];
 
          var newmark = mark.split(pos, textBefore.length);
 
          if (newmark) {
 
            if (!taken.marked) taken.marked = [];
 
            taken.marked.push(newmark); newmark.attach(taken);
 
            if (newmark == mark) mk.splice(i--, 1);
 
          }
 
        }
 
      }
 
      return taken;
 
    },
 
    append: function(line) {
 
      var mylen = this.text.length, mk = line.marked, mymk = this.marked;
 
      this.text += line.text;
 
      copyStyles(0, line.text.length, line.styles, this.styles);
 
      if (mymk) {
 
        for (var i = 0; i < mymk.length; ++i)
 
          if (mymk[i].to == null) mymk[i].to = mylen;
 
      }
 
      if (mk && mk.length) {
 
        if (!mymk) this.marked = mymk = [];
 
        outer: for (var i = 0; i < mk.length; ++i) {
 
          var mark = mk[i];
 
          if (!mark.from) {
 
            for (var j = 0; j < mymk.length; ++j) {
 
              var mymark = mymk[j];
 
              if (mymark.to == mylen && mymark.sameSet(mark)) {
 
                mymark.to = mark.to == null ? null : mark.to + mylen;
 
                if (mymark.isDead()) {
 
                  mymark.detach(this);
 
                  mk.splice(i--, 1);
 
                }
 
                continue outer;
 
              }
 
            }
 
          }
 
          mymk.push(mark);
 
          mark.attach(this);
 
          mark.from += mylen;
 
          if (mark.to != null) mark.to += mylen;
 
        }
 
      }
 
    },
 
    fixMarkEnds: function(other) {
 
      var mk = this.marked, omk = other.marked;
 
      if (!mk) return;
 
      for (var i = 0; i < mk.length; ++i) {
 
        var mark = mk[i], close = mark.to == null;
 
        if (close && omk) {
 
          for (var j = 0; j < omk.length; ++j)
 
            if (omk[j].sameSet(mark)) {close = false; break;}
 
        }
 
        if (close) mark.to = this.text.length;
 
      }
 
    },
 
    fixMarkStarts: function() {
 
      var mk = this.marked;
 
      if (!mk) return;
 
      for (var i = 0; i < mk.length; ++i)
 
        if (mk[i].from == null) mk[i].from = 0;
 
    },
 
    addMark: function(mark) {
 
      mark.attach(this);
 
      if (this.marked == null) this.marked = [];
 
      this.marked.push(mark);
 
      this.marked.sort(function(a, b){return (a.from || 0) - (b.from || 0);});
 
    update: function(text, markedSpans) {
 
      this.text = text;
 
      this.stateAfter = this.styles = null;
 
      detachMarkedSpans(this);
 
      attachMarkedSpans(this, markedSpans);
 
    },
 
    // Run the given mode's parser over a line, update the styles
 
    // array, which contains alternating fragments of text and CSS
 
    // classes.
 
    highlight: function(mode, state, tabSize) {
 
      var stream = new StringStream(this.text, tabSize), st = this.styles, pos = 0;
 
      var changed = false, curWord = st[0], prevWord;
 
      var stream = new StringStream(this.text, tabSize), st = this.styles || (this.styles = []);
 
      var pos = st.length = 0;
 
      if (this.text == "" && mode.blankLine) mode.blankLine(state);
 
      while (!stream.eol()) {
 
        var style = mode.token(stream, state);
 
        var substr = this.text.slice(stream.start, stream.pos);
 
        var style = mode.token(stream, state), substr = stream.current();
 
        stream.start = stream.pos;
 
        if (pos && st[pos-1] == style)
 
        if (pos && st[pos-1] == style) {
 
          st[pos-2] += substr;
 
        else if (substr) {
 
          if (!changed && (st[pos+1] != style || (pos && st[pos-2] != prevWord))) changed = true;
 
        } else if (substr) {
 
          st[pos++] = substr; st[pos++] = style;
 
          prevWord = curWord; curWord = st[pos];
 
        }
 
        // Give up when line is ridiculously long
 
        if (stream.pos > 5000) {
 
          st[pos++] = this.text.slice(stream.pos); st[pos++] = null;
 
          break;
 
        }
 
      }
 
      if (st.length != pos) {st.length = pos; changed = true;}
 
      if (pos && st[pos-2] != prevWord) changed = true;
 
      // Short lines with simple highlights return null, and are
 
      // counted as changed by the driver because they are likely to
 
      // highlight the same way in various contexts.
 
      return changed || (st.length < 5 && this.text.length < 10 ? null : false);
 
    },
 
    process: function(mode, state, tabSize) {
 
      var stream = new StringStream(this.text, tabSize);
 
      if (this.text == "" && mode.blankLine) mode.blankLine(state);
 
      while (!stream.eol() && stream.pos <= 5000) {
 
        mode.token(stream, state);
 
        stream.start = stream.pos;
 
      }
 
    },
 
    // Fetch the parser token for a given character. Useful for hacks
 
    // that want to inspect the mode state (say, for completion).
 
    getTokenAt: function(mode, state, ch) {
 
      var txt = this.text, stream = new StringStream(txt);
 
    getTokenAt: function(mode, state, tabSize, ch) {
 
      var txt = this.text, stream = new StringStream(txt, tabSize);
 
      while (stream.pos < ch && !stream.eol()) {
 
        stream.start = stream.pos;
 
        var style = mode.token(stream, state);
 
      }
 
      return {start: stream.start,
 
              end: stream.pos,
 
              string: stream.current(),
 
              className: style || null,
 
              state: state};
 
    },
 
    indentation: function(tabSize) {return countColumn(this.text, null, tabSize);},
 
    // Produces an HTML fragment for the line, taking selection,
 
    // marking, and highlighting into account.
 
    getHTML: function(makeTab, wrapAt, wrapId, wrapWBR) {
 
      var html = [], first = true, col = 0;
 
      function span_(text, style) {
 
    getContent: function(tabSize, wrapAt, compensateForWrapping) {
 
      var first = true, col = 0, specials = /[\t\u0000-\u0019\u200b\u2028\u2029\uFEFF]/g;
 
      var pre = elt("pre");
 
      function span_(html, text, style) {
 
        if (!text) return;
 
        // Work around a bug where, in some compat modes, IE ignores leading spaces
 
        if (first && ie && text.charAt(0) == " ") text = "\u00a0" + text.slice(1);
 
        first = false;
 
        if (text.indexOf("\t") == -1) {
 
        if (!specials.test(text)) {
 
          col += text.length;
 
          var escaped = htmlEscape(text);
 
          var content = document.createTextNode(text);
 
        } else {
 
          var escaped = "";
 
          for (var pos = 0;;) {
 
            var idx = text.indexOf("\t", pos);
 
            if (idx == -1) {
 
              escaped += htmlEscape(text.slice(pos));
 
              col += text.length - pos;
 
              break;
 
          var content = document.createDocumentFragment(), pos = 0;
 
          while (true) {
 
            specials.lastIndex = pos;
 
            var m = specials.exec(text);
 
            var skipped = m ? m.index - pos : text.length - pos;
 
            if (skipped) {
 
              content.appendChild(document.createTextNode(text.slice(pos, pos + skipped)));
 
              col += skipped;
 
            }
 
            if (!m) break;
 
            pos += skipped + 1;
 
            if (m[0] == "\t") {
 
              var tabWidth = tabSize - col % tabSize;
 
              content.appendChild(elt("span", spaceStr(tabWidth), "cm-tab"));
 
              col += tabWidth;
 
            } else {
 
              col += idx - pos;
 
              var tab = makeTab(col);
 
              escaped += htmlEscape(text.slice(pos, idx)) + tab.html;
 
              col += tab.width;
 
              pos = idx + 1;
 
            }
 
          }
 
        }
 
        if (style) html.push('<span class="', style, '">', escaped, "</span>");
 
        else html.push(escaped);
 
              var token = elt("span", "\u2022", "cm-invalidchar");
 
              token.title = "\\u" + m[0].charCodeAt(0).toString(16);
 
              content.appendChild(token);
 
              col += 1;
 
            }
 
          }
 
        }
 
        if (style) html.appendChild(elt("span", [content], style));
 
        else html.appendChild(content);
 
      }
 
      var span = span_;
 
      if (wrapAt != null) {
 
        var outPos = 0, open = "<span id=\"" + wrapId + "\">";
 
        span = function(text, style) {
 
        var outPos = 0, anchor = pre.anchor = elt("span");
 
        span = function(html, text, style) {
 
          var l = text.length;
 
          if (wrapAt >= outPos && wrapAt < outPos + l) {
 
            if (wrapAt > outPos) {
 
              span_(text.slice(0, wrapAt - outPos), style);
 
              span_(html, text.slice(0, wrapAt - outPos), style);
 
              // See comment at the definition of spanAffectsWrapping
 
              if (wrapWBR) html.push("<wbr>");
 
            }
 
            html.push(open);
 
              if (compensateForWrapping) html.appendChild(elt("wbr"));
 
            }
 
            html.appendChild(anchor);
 
            var cut = wrapAt - outPos;
 
            span_(opera ? text.slice(cut, cut + 1) : text.slice(cut), style);
 
            html.push("</span>");
 
            if (opera) span_(text.slice(cut + 1), style);
 
            span_(anchor, opera ? text.slice(cut, cut + 1) : text.slice(cut), style);
 
            if (opera) span_(html, text.slice(cut + 1), style);
 
            wrapAt--;
 
            outPos += l;
 
          } else {
 
            outPos += l;
 
            span_(text, style);
 
            // Output empty wrapper when at end of line
 
            if (outPos == wrapAt && outPos == len) html.push(open + " </span>");
 
            span_(html, text, style);
 
            if (outPos == wrapAt && outPos == len) {
 
              setTextContent(anchor, eolSpanContent);
 
              html.appendChild(anchor);
 
            }
 
            // Stop outputting HTML when gone sufficiently far beyond measure
 
            else if (outPos > wrapAt + 10 && /\s/.test(text)) span = function(){};
 
          }
 
        }
 
        };
 
      }
 

	
 
      var st = this.styles, allText = this.text, marked = this.marked;
 
      var st = this.styles, allText = this.text, marked = this.markedSpans;
 
      var len = allText.length;
 
      function styleToClass(style) {
 
        if (!style) return null;
 
        return "cm-" + style.replace(/ +/g, " cm-");
 
      }
 

	
 
      if (!allText && wrapAt == null) {
 
        span(" ");
 
        span(pre, " ");
 
      } else if (!marked || !marked.length) {
 
        for (var i = 0, ch = 0; ch < len; i+=2) {
 
          var str = st[i], style = st[i+1], l = str.length;
 
          if (ch + l > len) str = str.slice(0, len - ch);
 
          ch += l;
 
          span(str, styleToClass(style));
 
          span(pre, str, styleToClass(style));
 
        }
 
      } else {
 
        marked.sort(function(a, b) { return a.from - b.from; });
 
        var pos = 0, i = 0, text = "", style, sg = 0;
 
        var nextChange = marked[0].from || 0, marks = [], markpos = 0;
 
        function advanceMarks() {
 
        var advanceMarks = function() {
 
          var m;
 
          while (markpos < marked.length &&
 
                 ((m = marked[markpos]).from == pos || m.from == null)) {
 
            if (m.style != null) marks.push(m);
 
            if (m.marker.type == "range") marks.push(m);
 
            ++markpos;
 
          }
 
          nextChange = markpos < marked.length ? marked[markpos].from : Infinity;
 
          for (var i = 0; i < marks.length; ++i) {
 
            var to = marks[i].to || Infinity;
 
            var to = marks[i].to;
 
            if (to == null) to = Infinity;
 
            if (to == pos) marks.splice(i--, 1);
 
            else nextChange = Math.min(to, nextChange);
 
          }
 
        }
 
        };
 
        var m = 0;
 
        while (pos < len) {
 
          if (nextChange == pos) advanceMarks();
 
          var upto = Math.min(len, nextChange);
 
          while (true) {
 
            if (text) {
 
              var end = pos + text.length;
 
              var appliedStyle = style;
 
              for (var j = 0; j < marks.length; ++j)
 
                appliedStyle = (appliedStyle ? appliedStyle + " " : "") + marks[j].style;
 
              span(end > upto ? text.slice(0, upto - pos) : text, appliedStyle);
 
              for (var j = 0; j < marks.length; ++j) {
 
                var mark = marks[j];
 
                appliedStyle = (appliedStyle ? appliedStyle + " " : "") + mark.marker.style;
 
                if (mark.marker.endStyle && mark.to === Math.min(end, upto)) appliedStyle += " " + mark.marker.endStyle;
 
                if (mark.marker.startStyle && mark.from === pos) appliedStyle += " " + mark.marker.startStyle;
 
              }
 
              span(pre, end > upto ? text.slice(0, upto - pos) : text, appliedStyle);
 
              if (end >= upto) {text = text.slice(upto - pos); pos = upto; break;}
 
              pos = end;
 
            }
 
            text = st[i++]; style = styleToClass(st[i++]);
 
          }
 
        }
 
      }
 
      return html.join("");
 
      return pre;
 
    },
 
    cleanUp: function() {
 
      this.parent = null;
 
      if (this.marked)
 
        for (var i = 0, e = this.marked.length; i < e; ++i) this.marked[i].detach(this);
 
      detachMarkedSpans(this);
 
    }
 
  };
 
  // Utility used by replace and split above
 
  function copyStyles(from, to, source, dest) {
 
    for (var i = 0, pos = 0, state = 0; pos < to; i+=2) {
 
      var part = source[i], end = pos + part.length;
 
      if (state == 0) {
 
        if (end > from) dest.push(part.slice(from - pos, Math.min(part.length, to - pos)), source[i+1]);
 
        if (end >= from) state = 1;
 
      } else if (state == 1) {
 
        if (end > to) dest.push(part.slice(0, to - pos), source[i+1]);
 
        else dest.push(part, source[i+1]);
 
      }
 
      pos = end;
 
    }
 
  }
 

	
 
  // Data structure that holds the sequence of lines.
 
  function LeafChunk(lines) {
 
    this.lines = lines;
 
    this.parent = null;
 
    for (var i = 0, e = lines.length, height = 0; i < e; ++i) {
 
      lines[i].parent = this;
 
      height += lines[i].height;
 
    }
 
    this.height = height;
 
  }
 
  LeafChunk.prototype = {
 
    chunkSize: function() { return this.lines.length; },
 
    remove: function(at, n, callbacks) {
 
      for (var i = at, e = at + n; i < e; ++i) {
 
        var line = this.lines[i];
 
        this.height -= line.height;
 
        line.cleanUp();
 
        if (line.handlers)
 
          for (var j = 0; j < line.handlers.length; ++j) callbacks.push(line.handlers[j]);
 
      }
 
      this.lines.splice(at, n);
 
    },
 
    collapse: function(lines) {
 
      lines.splice.apply(lines, [lines.length, 0].concat(this.lines));
 
    },
 
    insertHeight: function(at, lines, height) {
 
      this.height += height;
 
      this.lines = this.lines.slice(0, at).concat(lines).concat(this.lines.slice(at));
 
      for (var i = 0, e = lines.length; i < e; ++i) lines[i].parent = this;
 
    },
 
    iterN: function(at, n, op) {
 
      for (var e = at + n; at < e; ++at)
 
        if (op(this.lines[at])) return true;
 
    }
 
  };
 
  function BranchChunk(children) {
 
    this.children = children;
 
    var size = 0, height = 0;
 
    for (var i = 0, e = children.length; i < e; ++i) {
 
      var ch = children[i];
 
      size += ch.chunkSize(); height += ch.height;
 
      ch.parent = this;
 
    }
 
    this.size = size;
 
    this.height = height;
 
    this.parent = null;
 
  }
 
@@ -2849,331 +2824,318 @@ var CodeMirror = (function() {
 
      }
 
    }
 
    return no;
 
  }
 
  function lineAtHeight(chunk, h) {
 
    var n = 0;
 
    outer: do {
 
      for (var i = 0, e = chunk.children.length; i < e; ++i) {
 
        var child = chunk.children[i], ch = child.height;
 
        if (h < ch) { chunk = child; continue outer; }
 
        h -= ch;
 
        n += child.chunkSize();
 
      }
 
      return n;
 
    } while (!chunk.lines);
 
    for (var i = 0, e = chunk.lines.length; i < e; ++i) {
 
      var line = chunk.lines[i], lh = line.height;
 
      if (h < lh) break;
 
      h -= lh;
 
    }
 
    return n + i;
 
  }
 
  function heightAtLine(chunk, n) {
 
    var h = 0;
 
    outer: do {
 
      for (var i = 0, e = chunk.children.length; i < e; ++i) {
 
        var child = chunk.children[i], sz = child.chunkSize();
 
        if (n < sz) { chunk = child; continue outer; }
 
        n -= sz;
 
        h += child.height;
 
      }
 
      return h;
 
    } while (!chunk.lines);
 
    for (var i = 0; i < n; ++i) h += chunk.lines[i].height;
 
    return h;
 
  }
 

	
 
  // The history object 'chunks' changes that are made close together
 
  // and at almost the same time into bigger undoable units.
 
  function History() {
 
    this.time = 0;
 
    this.done = []; this.undone = [];
 
    this.compound = 0;
 
    this.closed = false;
 
  }
 
  History.prototype = {
 
    addChange: function(start, added, old) {
 
      this.undone.length = 0;
 
      var time = +new Date, cur = this.done[this.done.length - 1], last = cur && cur[cur.length - 1];
 
      var time = +new Date, cur = lst(this.done), last = cur && lst(cur);
 
      var dtime = time - this.time;
 

	
 
      if (this.compound && cur && !this.closed) {
 
        cur.push({start: start, added: added, old: old});
 
      } else if (dtime > 400 || !last || this.closed ||
 
                 last.start > start + old.length || last.start + last.added < start) {
 
        this.done.push([{start: start, added: added, old: old}]);
 
        this.closed = false;
 
      } else {
 
        var startBefore = Math.max(0, last.start - start),
 
            endAfter = Math.max(0, (start + old.length) - (last.start + last.added));
 
        for (var i = startBefore; i > 0; --i) last.old.unshift(old[i - 1]);
 
        for (var i = endAfter; i > 0; --i) last.old.push(old[old.length - i]);
 
        if (startBefore) last.start = start;
 
        last.added += added - (old.length - startBefore - endAfter);
 
      }
 
      this.time = time;
 
    },
 
    startCompound: function() {
 
      if (!this.compound++) this.closed = true;
 
    },
 
    endCompound: function() {
 
      if (!--this.compound) this.closed = true;
 
    }
 
  };
 

	
 
  function stopMethod() {e_stop(this);}
 
  // Ensure an event has a stop method.
 
  function addStop(event) {
 
    if (!event.stop) event.stop = stopMethod;
 
    return event;
 
  }
 

	
 
  function e_preventDefault(e) {
 
    if (e.preventDefault) e.preventDefault();
 
    else e.returnValue = false;
 
  }
 
  function e_stopPropagation(e) {
 
    if (e.stopPropagation) e.stopPropagation();
 
    else e.cancelBubble = true;
 
  }
 
  function e_stop(e) {e_preventDefault(e); e_stopPropagation(e);}
 
  CodeMirror.e_stop = e_stop;
 
  CodeMirror.e_preventDefault = e_preventDefault;
 
  CodeMirror.e_stopPropagation = e_stopPropagation;
 

	
 
  function e_target(e) {return e.target || e.srcElement;}
 
  function e_button(e) {
 
    if (e.which) return e.which;
 
    else if (e.button & 1) return 1;
 
    else if (e.button & 2) return 3;
 
    else if (e.button & 4) return 2;
 
    var b = e.which;
 
    if (b == null) {
 
      if (e.button & 1) b = 1;
 
      else if (e.button & 2) b = 3;
 
      else if (e.button & 4) b = 2;
 
    }
 
    if (mac && e.ctrlKey && b == 1) b = 3;
 
    return b;
 
  }
 

	
 
  // Allow 3rd-party code to override event properties by adding an override
 
  // object to an event object.
 
  function e_prop(e, prop) {
 
    var overridden = e.override && e.override.hasOwnProperty(prop);
 
    return overridden ? e.override[prop] : e[prop];
 
  }
 

	
 
  // Event handler registration. If disconnect is true, it'll return a
 
  // function that unregisters the handler.
 
  function connect(node, type, handler, disconnect) {
 
    if (typeof node.addEventListener == "function") {
 
      node.addEventListener(type, handler, false);
 
      if (disconnect) return function() {node.removeEventListener(type, handler, false);};
 
    } else {
 
      var wrapHandler = function(event) {handler(event || window.event);};
 
      node.attachEvent("on" + type, wrapHandler);
 
      if (disconnect) return function() {node.detachEvent("on" + type, wrapHandler);};
 
    }
 
  }
 
  CodeMirror.connect = connect;
 

	
 
  function Delayed() {this.id = null;}
 
  Delayed.prototype = {set: function(ms, f) {clearTimeout(this.id); this.id = setTimeout(f, ms);}};
 

	
 
  var Pass = CodeMirror.Pass = {toString: function(){return "CodeMirror.Pass";}};
 

	
 
  var gecko = /gecko\/\d{7}/i.test(navigator.userAgent);
 
  var ie = /MSIE \d/.test(navigator.userAgent);
 
  var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);
 
  var ie_lt9 = /MSIE [1-8]\b/.test(navigator.userAgent);
 
  var quirksMode = ie && document.documentMode == 5;
 
  var webkit = /WebKit\//.test(navigator.userAgent);
 
  var chrome = /Chrome\//.test(navigator.userAgent);
 
  var opera = /Opera\//.test(navigator.userAgent);
 
  var safari = /Apple Computer/.test(navigator.vendor);
 
  var khtml = /KHTML\//.test(navigator.userAgent);
 
  var mac_geLion = /Mac OS X 10\D([7-9]|\d\d)\D/.test(navigator.userAgent);
 

	
 
  // Detect drag-and-drop
 
  var dragAndDrop = function() {
 
    // There is *some* kind of drag-and-drop support in IE6-8, but I
 
    // couldn't get it to work yet.
 
    if (ie_lt9) return false;
 
    var div = document.createElement('div');
 
    var div = elt('div');
 
    return "draggable" in div || "dragDrop" in div;
 
  }();
 

	
 
  // Feature-detect whether newlines in textareas are converted to \r\n
 
  var lineSep = function () {
 
    var te = document.createElement("textarea");
 
    var te = elt("textarea");
 
    te.value = "foo\nbar";
 
    if (te.value.indexOf("\r") > -1) return "\r\n";
 
    return "\n";
 
  }();
 

	
 
  // For a reason I have yet to figure out, some browsers disallow
 
  // word wrapping between certain characters *only* if a new inline
 
  // element is started between them. This makes it hard to reliably
 
  // measure the position of things, since that requires inserting an
 
  // extra span. This terribly fragile set of regexps matches the
 
  // character combinations that suffer from this phenomenon on the
 
  // various browsers.
 
  var spanAffectsWrapping = /^$/; // Won't match any two-character string
 
  if (gecko) spanAffectsWrapping = /$'/;
 
  else if (safari) spanAffectsWrapping = /\-[^ \-?]|\?[^ !'\"\),.\-\/:;\?\]\}]/;
 
  else if (chrome) spanAffectsWrapping = /\-[^ \-\.?]|\?[^ \-\.?\]\}:;!'\"\),\/]|[\.!\"#&%\)*+,:;=>\]|\}~][\(\{\[<]|\$'/;
 

	
 
  // Counts the column offset in a string, taking tabs into account.
 
  // Used mostly to find indentation.
 
  function countColumn(string, end, tabSize) {
 
    if (end == null) {
 
      end = string.search(/[^\s\u00a0]/);
 
      if (end == -1) end = string.length;
 
    }
 
    for (var i = 0, n = 0; i < end; ++i) {
 
      if (string.charAt(i) == "\t") n += tabSize - (n % tabSize);
 
      else ++n;
 
    }
 
    return n;
 
  }
 

	
 
  function computedStyle(elt) {
 
    if (elt.currentStyle) return elt.currentStyle;
 
    return window.getComputedStyle(elt, null);
 
  }
 

	
 
  // Find the position of an element by following the offsetParent chain.
 
  // If screen==true, it returns screen (rather than page) coordinates.
 
  function eltOffset(node, screen) {
 
    var bod = node.ownerDocument.body;
 
    var x = 0, y = 0, skipBody = false;
 
    for (var n = node; n; n = n.offsetParent) {
 
      var ol = n.offsetLeft, ot = n.offsetTop;
 
      // Firefox reports weird inverted offsets when the body has a border.
 
      if (n == bod) { x += Math.abs(ol); y += Math.abs(ot); }
 
      else { x += ol, y += ot; }
 
      if (screen && computedStyle(n).position == "fixed")
 
        skipBody = true;
 
    }
 
    var e = screen && !skipBody ? null : bod;
 
    for (var n = node.parentNode; n != e; n = n.parentNode)
 
      if (n.scrollLeft != null) { x -= n.scrollLeft; y -= n.scrollTop;}
 
    return {left: x, top: y};
 
  }
 
  // Use the faster and saner getBoundingClientRect method when possible.
 
  if (document.documentElement.getBoundingClientRect != null) eltOffset = function(node, screen) {
 
    // Take the parts of bounding client rect that we are interested in so we are able to edit if need be,
 
    // since the returned value cannot be changed externally (they are kept in sync as the element moves within the page)
 
    try { var box = node.getBoundingClientRect(); box = { top: box.top, left: box.left }; }
 
    catch(e) { box = {top: 0, left: 0}; }
 
    if (!screen) {
 
      // Get the toplevel scroll, working around browser differences.
 
      if (window.pageYOffset == null) {
 
        var t = document.documentElement || document.body.parentNode;
 
        if (t.scrollTop == null) t = document.body;
 
        box.top += t.scrollTop; box.left += t.scrollLeft;
 
      } else {
 
        box.top += window.pageYOffset; box.left += window.pageXOffset;
 
      }
 
    }
 
    return box;
 
  };
 
  }
 

	
 
  // Get a node's text content.
 
  function eltText(node) {
 
    return node.textContent || node.innerText || node.nodeValue || "";
 
  }
 

	
 
  var spaceStrs = [""];
 
  function spaceStr(n) {
 
    while (spaceStrs.length <= n)
 
      spaceStrs.push(lst(spaceStrs) + " ");
 
    return spaceStrs[n];
 
  }
 

	
 
  function lst(arr) { return arr[arr.length-1]; }
 

	
 
  function selectInput(node) {
 
    if (ios) { // Mobile Safari apparently has a bug where select() is broken.
 
      node.selectionStart = 0;
 
      node.selectionEnd = node.value.length;
 
    } else node.select();
 
  }
 

	
 
  // Operations on {line, ch} objects.
 
  function posEq(a, b) {return a.line == b.line && a.ch == b.ch;}
 
  function posLess(a, b) {return a.line < b.line || (a.line == b.line && a.ch < b.ch);}
 
  function copyPos(x) {return {line: x.line, ch: x.ch};}
 

	
 
  var escapeElement = document.createElement("pre");
 
  function htmlEscape(str) {
 
    escapeElement.textContent = str;
 
    return escapeElement.innerHTML;
 
  }
 
  // Recent (late 2011) Opera betas insert bogus newlines at the start
 
  // of the textContent, so we strip those.
 
  if (htmlEscape("a") == "\na") {
 
    htmlEscape = function(str) {
 
      escapeElement.textContent = str;
 
      return escapeElement.innerHTML.slice(1);
 
    };
 
  // Some IEs don't preserve tabs through innerHTML
 
  } else if (htmlEscape("\t") != "\t") {
 
    htmlEscape = function(str) {
 
      escapeElement.innerHTML = "";
 
      escapeElement.appendChild(document.createTextNode(str));
 
      return escapeElement.innerHTML;
 
    };
 
  }
 
  CodeMirror.htmlEscape = htmlEscape;
 
  function elt(tag, content, className, style) {
 
    var e = document.createElement(tag);
 
    if (className) e.className = className;
 
    if (style) e.style.cssText = style;
 
    if (typeof content == "string") setTextContent(e, content);
 
    else if (content) for (var i = 0; i < content.length; ++i) e.appendChild(content[i]);
 
    return e;
 
  }
 
  function removeChildren(e) {
 
    e.innerHTML = "";
 
    return e;
 
  }
 
  function removeChildrenAndAdd(parent, e) {
 
    removeChildren(parent).appendChild(e);
 
  }
 
  function setTextContent(e, str) {
 
    if (ie_lt9) {
 
      e.innerHTML = "";
 
      e.appendChild(document.createTextNode(str));
 
    } else e.textContent = str;
 
  }
 

	
 
  // Used to position the cursor after an undo/redo by finding the
 
  // last edited character.
 
  function editEnd(from, to) {
 
    if (!to) return 0;
 
    if (!from) return to.length;
 
    for (var i = from.length, j = to.length; i >= 0 && j >= 0; --i, --j)
 
      if (from.charAt(i) != to.charAt(j)) break;
 
    return j + 1;
 
  }
 

	
 
  function indexOf(collection, elt) {
 
    if (collection.indexOf) return collection.indexOf(elt);
 
    for (var i = 0, e = collection.length; i < e; ++i)
 
      if (collection[i] == elt) return i;
 
    return -1;
 
  }
 
  function isWordChar(ch) {
 
    return /\w/.test(ch) || ch.toUpperCase() != ch.toLowerCase();
 
  }
 

	
 
  // See if "".split is the broken IE version, if so, provide an
 
  // alternative way to split lines.
 
  var splitLines = "\n\nb".split(/\n/).length != 3 ? function(string) {
 
    var pos = 0, nl, result = [];
 
    while ((nl = string.indexOf("\n", pos)) > -1) {
 
      result.push(string.slice(pos, string.charAt(nl-1) == "\r" ? nl - 1 : nl));
 
    var pos = 0, result = [], l = string.length;
 
    while (pos <= l) {
 
      var nl = string.indexOf("\n", pos);
 
      if (nl == -1) nl = string.length;
 
      var line = string.slice(pos, string.charAt(nl - 1) == "\r" ? nl - 1 : nl);
 
      var rt = line.indexOf("\r");
 
      if (rt != -1) {
 
        result.push(line.slice(0, rt));
 
        pos += rt + 1;
 
      } else {
 
        result.push(line);
 
      pos = nl + 1;
 
    }
 
    result.push(string.slice(pos));
 
    }
 
    return result;
 
  } : function(string){return string.split(/\r?\n/);};
 
  } : function(string){return string.split(/\r\n?|\n/);};
 
  CodeMirror.splitLines = splitLines;
 

	
 
  var hasSelection = window.getSelection ? function(te) {
 
    try { return te.selectionStart != te.selectionEnd; }
 
    catch(e) { return false; }
 
  } : function(te) {
 
    try {var range = te.ownerDocument.selection.createRange();}
 
    catch(e) {}
 
    if (!range || range.parentElement() != te) return false;
 
    return range.compareEndPoints("StartToEnd", range) != 0;
 
  };
 

	
 
  CodeMirror.defineMode("null", function() {
 
    return {token: function(stream) {stream.skipToEnd();}};
 
  });
 
  CodeMirror.defineMIME("text/plain", "null");
 

	
 
  var keyNames = {3: "Enter", 8: "Backspace", 9: "Tab", 13: "Enter", 16: "Shift", 17: "Ctrl", 18: "Alt",
 
                  19: "Pause", 20: "CapsLock", 27: "Esc", 32: "Space", 33: "PageUp", 34: "PageDown", 35: "End",
 
                  36: "Home", 37: "Left", 38: "Up", 39: "Right", 40: "Down", 44: "PrintScrn", 45: "Insert",
 
                  46: "Delete", 59: ";", 91: "Mod", 92: "Mod", 93: "Mod", 109: "-", 107: "=", 127: "Delete",
 
                  186: ";", 187: "=", 188: ",", 189: "-", 190: ".", 191: "/", 192: "`", 219: "[", 220: "\\",
 
                  221: "]", 222: "'", 63276: "PageUp", 63277: "PageDown", 63275: "End", 63273: "Home",
 
                  63234: "Left", 63232: "Up", 63235: "Right", 63233: "Down", 63302: "Insert", 63272: "Delete"};
 
  CodeMirror.keyNames = keyNames;
 
  (function() {
 
    // Number keys
 
    for (var i = 0; i < 10; i++) keyNames[i + 48] = String(i);
 
    // Alphabetic keys
 
    for (var i = 65; i <= 90; i++) keyNames[i] = String.fromCharCode(i);
 
    // Function keys
 
    for (var i = 1; i <= 12; i++) keyNames[i + 111] = keyNames[i + 63235] = "F" + i;
 
  })();
 

	
 
  CodeMirror.version = "2.34";
 

	
 
  return CodeMirror;
 
})();
rhodecode/public/js/rhodecode.js
Show inline comments
 
@@ -372,117 +372,119 @@ var q_filter = function(target,nodes,dis
 
    		   show_node(target_element);
 
    		   showing+=1;
 
    	   }
 
       }	  	   
 

	
 
	   // if repo_count is set update the number
 
	   var cnt = YUD.get('repo_count');
 
	   if(cnt){
 
		   YUD.get('repo_count').innerHTML = showing;
 
	   }       
 
       
 
	}	
 
};
 

	
 
var tableTr = function(cls, body){
 
	var _el = document.createElement('div');
 
	var cont = new YAHOO.util.Element(body);
 
	var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
 
	var id = 'comment-tr-{0}'.format(comment_id);
 
	var _html = ('<table><tbody><tr id="{0}" class="{1}">'+
 
	              '<td class="lineno-inline new-inline"></td>'+
 
    			  '<td class="lineno-inline old-inline"></td>'+ 
 
                  '<td>{2}</td>'+
 
                 '</tr></tbody></table>').format(id, cls, body);
 
	_el.innerHTML = _html;
 
	return _el.children[0].children[0].children[0];
 
};
 

	
 
/** comments **/
 
var removeInlineForm = function(form) {
 
	form.parentNode.removeChild(form);
 
};
 

	
 
var createInlineForm = function(parent_tr, f_path, line) {
 
	var tmpl = YUD.get('comment-inline-form-template').innerHTML;
 
	tmpl = tmpl.format(f_path, line);
 
	var form = tableTr('comment-form-inline',tmpl)
 

	
 
	// create event for hide button
 
	form = new YAHOO.util.Element(form);
 
	var form_hide_button = new YAHOO.util.Element(YUD.getElementsByClassName('hide-inline-form',null,form)[0]);
 
	form_hide_button.on('click', function(e) {
 
		var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
 
		if(YUD.hasClass(newtr.nextElementSibling,'inline-comments-button')){
 
			YUD.setStyle(newtr.nextElementSibling,'display','');
 
		}
 
		removeInlineForm(newtr);
 
		YUD.removeClass(parent_tr, 'form-open');
 
		YUD.removeClass(parent_tr, 'hl-comment');
 
		
 
	});
 
	
 
	return form
 
};
 

	
 
/**
 
 * Inject inline comment for on given TR this tr should be always an .line
 
 * tr containing the line. Code will detect comment, and always put the comment
 
 * block at the very bottom
 
 */
 
var injectInlineForm = function(tr){
 
	  if(!YUD.hasClass(tr, 'line')){
 
		  return
 
	  }
 
	  var submit_url = AJAX_COMMENT_URL;
 
	  var _td = YUD.getElementsByClassName('code',null,tr)[0];
 
	  if(YUD.hasClass(tr,'form-open') || YUD.hasClass(tr,'context') || YUD.hasClass(_td,'no-comment')){
 
		  return
 
	  }	
 
	  YUD.addClass(tr,'form-open');
 
	  YUD.addClass(tr,'hl-comment');
 
	  var node = YUD.getElementsByClassName('full_f_path',null,tr.parentNode.parentNode.parentNode)[0];
 
	  var f_path = YUD.getAttribute(node,'path');
 
	  var lineno = getLineNo(tr);
 
	  var form = createInlineForm(tr, f_path, lineno, submit_url);
 
	  
 
	  var parent = tr;
 
	  while (1){
 
		  var n = parent.nextElementSibling;
 
		  // next element are comments !
 
		  if(YUD.hasClass(n,'inline-comments')){
 
			  parent = n;
 
		  }
 
		  else{
 
			  break;
 
		  }
 
	  }	  
 
	  YUD.insertAfter(form,parent);
 
	  var f = YUD.get(form);
 
	  var overlay = YUD.getElementsByClassName('overlay',null,f)[0];
 
	  var _form = YUD.getElementsByClassName('inline-form',null,f)[0];
 
	  
 
	  YUE.on(YUD.get(_form), 'submit',function(e){
 
		  YUE.preventDefault(e);
 
		  
 
		  //ajax submit
 
		  var text = YUD.get('text_'+lineno).value;
 
		  var postData = {
 
	            'text':text,
 
	            'f_path':f_path,
 
	            'line':lineno
 
		  };
 
		  
 
		  if(lineno === undefined){
 
			  alert('missing line !');
 
			  return
 
		  }
 
		  if(f_path === undefined){
 
			  alert('missing file path !');
 
			  return
 
		  }
 
		  
 
		  if(text == ""){
 
			  return
 
		  }
 
		  
 
		  var success = function(o){
 
			  YUD.removeClass(tr, 'form-open');
 
			  removeInlineForm(f);			  
rhodecode/templates/files/files.html
Show inline comments
 
@@ -44,96 +44,98 @@ var CACHE_EXPIRE = 60*1000; //cache for 
 
var node_list_url = '${h.url("files_home",repo_name=c.repo_name,revision='__REV__',f_path='__FPATH__')}';
 
//send the nodelist request to this url
 
var url_base = '${h.url("files_nodelist_home",repo_name=c.repo_name,revision='__REV__',f_path='__FPATH__')}';
 

	
 
var ypjax_links = function(){
 
    YUE.on(YUQ('.ypjax-link'), 'click',function(e){
 

	
 
    	//don't do ypjax on middle click
 
    	if(e.which == 2 || !History.enabled){
 
    		return true;
 
    	}
 

	
 
        var el = e.currentTarget;
 
        var url = el.href;
 

	
 
        var _base_url = '${h.url("files_home",repo_name=c.repo_name,revision='',f_path='')}';
 
        _base_url = _base_url.replace('//','/')
 

	
 
        //extract rev and the f_path from url.
 
        parts = url.split(_base_url)
 
        if(parts.length != 2){
 
        	return false;
 
        }
 

	
 
        var parts2 = parts[1].split('/');
 
      	var rev = parts2.shift(); // pop the first element which is the revision
 
      	var f_path = parts2.join('/');
 

	
 
        var title = "${_('%s files') % c.repo_name}" + " - " + f_path;
 

	
 
        var _node_list_url = node_list_url.replace('__REV__',rev);
 
        var _url_base = url_base.replace('__REV__',rev).replace('__FPATH__', f_path);
 

	
 
        // Change our States and save some data for handling events
 
        var data = {url:url,title:title, url_base:_url_base,
 
                    node_list_url:_node_list_url};
 
        History.pushState(data, title, url);
 

	
 
        //now we're sure that we can do ypjax things
 
        YUE.preventDefault(e)
 
        return false;
 
    });
 
}
 

	
 
var callbacks = function(State){
 
    ypjax_links();
 
    tooltip_activate();
 
    fileBrowserListeners(State.url, State.data.node_list_url, State.data.url_base);
 
    YUE.on('hlcode','mouseup',getSelectionLink("${_('Selection link')}"));
 

	
 
    // Inform Google Analytics of the change
 
    if ( typeof window.pageTracker !== 'undefined' ) {
 
        window.pageTracker._trackPageview(State.url);
 
    }
 
}
 

	
 
YUE.onDOMReady(function(){
 
    ypjax_links();
 
    var container = 'files_data';
 
    //Bind to StateChange Event
 
    History.Adapter.bind(window,'statechange',function(){
 
        var State = History.getState();
 
        cache_key = State.url;
 
        //check if we have this request in cache maybe ?
 
        var _cache_obj = CACHE[cache_key];
 
        var _cur_time = new Date().getTime();
 
        // get from cache if it's there and not yet expired !
 
        if(_cache_obj !== undefined && _cache_obj[0] > _cur_time){
 
            YUD.get(container).innerHTML=_cache_obj[1];
 
            YUD.setStyle(container,'opacity','1.0');
 

	
 
            //callbacks after ypjax call
 
            callbacks(State);
 
        }
 
        else{
 
          ypjax(State.url,container,function(o){
 
          	//callbacks after ypjax call
 
          	callbacks(State);
 
          	if (o !== undefined){
 
          	  //store our request in cache
 
          	  var _expire_on = new Date().getTime()+CACHE_EXPIRE;
 
              CACHE[cache_key] = [_expire_on, o.responseText];
 
            }
 
          });
 
        }
 
    });
 

	
 
    // init the search filter
 
    var _State = {
 
       url: "${h.url.current()}",
 
       data: {
 
         node_list_url: node_list_url.replace('__REV__',"${c.changeset.raw_id}"),
 
         url_base: url_base.replace('__REV__',"${c.changeset.raw_id}").replace('__FPATH__', "${h.safe_unicode(c.file.path)}")
 
       }
 
    }
 
    fileBrowserListeners(_State.url, _State.data.node_list_url, _State.data.url_base);
 
});
 

	
rhodecode/templates/pullrequests/pullrequest.html
Show inline comments
 
<%inherit file="/base/base.html"/>
 

	
 
<%def name="title()">
 
    ${c.repo_name} ${_('New pull request')}
 
</%def>
 

	
 
<%def name="breadcrumbs_links()">
 
    ${h.link_to(_(u'Home'),h.url('/'))}
 
    &raquo;
 
    ${h.link_to(c.repo_name,h.url('changelog_home',repo_name=c.repo_name))}
 
    &raquo;
 
    ${_('New pull request')}
 
</%def>
 

	
 
<%def name="main()">
 

	
 
<div class="box">
 
    <!-- box / title -->
 
    <div class="title">
 
        ${self.breadcrumbs()}
 
    </div>
 
    ${h.form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
 
    <div style="float:left;padding:0px 30px 30px 30px">
 
       <div style="padding:0px 5px 5px 5px">
 
         <span>
 
           <a id="refresh" href="#">
 
             <img class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/>
 
             ${_('refresh overview')}
 
           </a>
 
         </span>
 
       </div>
 

	
 
        ##ORG
 
        <div style="float:left">
 
            <div class="fork_user">
 
                <div class="gravatar">
 
                    <img alt="gravatar" src="${h.gravatar_url(c.rhodecode_db_repo.user.email,24)}"/>
 
                </div>
 
                <span style="font-size: 20px">
 
                ${h.select('org_repo','',c.org_repos,class_='refs')}:${h.select('org_ref','',c.org_refs,class_='refs')}
 
                </span>
 
                 <div style="padding:5px 3px 3px 42px;">${c.rhodecode_db_repo.description}</div>
 
            </div>
 
            <div style="clear:both;padding-top: 10px"></div>
 
        </div>
 
          <div style="float:left;font-size:24px;padding:0px 20px">
 
          <img height=32 width=32 src="${h.url('/images/arrow_right_64.png')}"/>
 
          </div>
 

	
 
        ##OTHER, most Probably the PARENT OF THIS FORK
 
        <div style="float:left">
 
            <div class="fork_user">
 
                <div class="gravatar">
 
                    <img id="other_repo_gravatar" alt="gravatar" src=""/>
 
                </div>
 
                <span style="font-size: 20px">
 
                ${h.select('other_repo',c.default_pull_request ,c.other_repos,class_='refs')}:${h.select('other_ref',c.default_pull_request_rev,c.default_revs,class_='refs')}
 
                </span>
 
         <span style="padding:3px">
 
           <a id="refresh" href="#" class="tooltip" title="${h.tooltip(_('refresh overview'))}">
 
             <img style="margin:3px" class="icon" title="${_('Refresh')}" alt="${_('Refresh')}" src="${h.url('/images/icons/arrow_refresh.png')}"/>
 
           </a>
 
         </span>
 
                 <div id="other_repo_desc" style="padding:5px 3px 3px 42px;"></div>
 
            </div>
 
            <div style="clear:both;padding-top: 10px"></div>
 
        </div>
 
       <div style="clear:both;padding-top: 10px"></div>
 
       ## overview pulled by ajax
 
       <div style="float:left" id="pull_request_overview"></div>
 
       <div style="float:left;clear:both;padding:10px 10px 10px 0px;display:none">
 
            <a id="pull_request_overview_url" href="#">${_('Detailed compare view')}</a>
 
       </div>
 
     </div>
 
    <div style="float:left; border-left:1px dashed #eee">
 
        <h4>${_('Pull request reviewers')}</h4>
 
        <div id="reviewers" style="padding:0px 0px 0px 15px">
 
          ## members goes here !
 
          <div class="group_members_wrap">
 
            <ul id="review_members" class="group_members">
 
            %for member in c.review_members:
 
              <li id="reviewer_${member.user_id}">
 
                <div class="reviewers_member">
 
                  <div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(member.email,14)}"/> </div>
 
                  <div style="float:left">${member.full_name} (${_('owner')})</div>
 
                  <input type="hidden" value="${member.user_id}" name="review_members" />
 
                  <span class="delete_icon action_button" onclick="removeReviewer(${member.user_id})"></span>
 
                </div>
 
              </li>
 
            %endfor
 
            </ul>
 
          </div>
 

	
 
          <div class='ac'>
 
            <div class="reviewer_ac">
 
               ${h.text('user', class_='yui-ac-input')}
 
               <span class="help-block">${_('Add reviewer to this pull request.')}</span>
 
               <div id="reviewers_container"></div>
 
            </div>
 
          </div>
 
        </div>
 
    </div>
 
    <h3>${_('Create new pull request')}</h3>
 

	
 
    <div class="form">
 
        <!-- fields -->
 

	
 
        <div class="fields">
 

	
 
             <div class="field">
 
                <div class="label">
rhodecode/tests/models/test_permissions.py
Show inline comments
 
import os
 
import unittest
 
from rhodecode.tests import *
 
from rhodecode.tests.models.common import _make_group
 
from rhodecode.model.repos_group import ReposGroupModel
 
from rhodecode.model.repo import RepoModel
 
from rhodecode.model.db import RepoGroup, User, UsersGroupRepoGroupToPerm
 
from rhodecode.model.user import UserModel
 

	
 
from rhodecode.model.meta import Session
 
from rhodecode.model.users_group import UsersGroupModel
 
from rhodecode.lib.auth import AuthUser
 

	
 
from rhodecode.tests.api.api_base import create_repo
 

	
 

	
 
class TestPermissions(unittest.TestCase):
 
    def __init__(self, methodName='runTest'):
 
        super(TestPermissions, self).__init__(methodName=methodName)
 

	
 
    def setUp(self):
 
        self.u1 = UserModel().create_or_update(
 
            username=u'u1', password=u'qweqwe',
 
            email=u'u1@rhodecode.org', firstname=u'u1', lastname=u'u1'
 
        )
 
        self.u2 = UserModel().create_or_update(
 
            username=u'u2', password=u'qweqwe',
 
            email=u'u2@rhodecode.org', firstname=u'u2', lastname=u'u2'
 
        )
 
        self.u3 = UserModel().create_or_update(
 
            username=u'u3', password=u'qweqwe',
 
            email=u'u3@rhodecode.org', firstname=u'u3', lastname=u'u3'
 
        )
 
        self.anon = User.get_by_username('default')
 
        self.a1 = UserModel().create_or_update(
 
            username=u'a1', password=u'qweqwe',
 
            email=u'a1@rhodecode.org', firstname=u'a1', lastname=u'a1', admin=True
 
        )
 
        Session().commit()
 

	
 
    def tearDown(self):
 
        if hasattr(self, 'test_repo'):
 
            RepoModel().delete(repo=self.test_repo)
 

	
 
        UserModel().delete(self.u1)
 
        UserModel().delete(self.u2)
 
        UserModel().delete(self.u3)
 
        UserModel().delete(self.a1)
 
        if hasattr(self, 'g1'):
 
            ReposGroupModel().delete(self.g1.group_id)
 
        if hasattr(self, 'g2'):
 
            ReposGroupModel().delete(self.g2.group_id)
 

	
 
        if hasattr(self, 'ug1'):
 
            UsersGroupModel().delete(self.ug1, force=True)
 

	
 
        Session().commit()
 

	
 
    def test_default_perms_set(self):
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        perms = {
 
            'repositories_groups': {},
 
            'global': set([u'hg.create.repository', u'repository.read',
 
                           u'hg.register.manual_activate']),
 
            'repositories': {u'vcs_test_hg': u'repository.read'}
 
        }
 
        self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
 
                         perms['repositories'][HG_REPO])
 
        new_perm = 'repository.write'
 
        RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1,
 
                                          perm=new_perm)
 
        Session().commit()
 

	
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
 
                         new_perm)
 

	
 
    def test_default_admin_perms_set(self):
 
        a1_auth = AuthUser(user_id=self.a1.user_id)
 
        perms = {
 
            'repositories_groups': {},
 
            'global': set([u'hg.admin']),
 
            'repositories': {u'vcs_test_hg': u'repository.admin'}
 
        }
 
        self.assertEqual(a1_auth.permissions['repositories'][HG_REPO],
 
                         perms['repositories'][HG_REPO])
 
        new_perm = 'repository.write'
 
        RepoModel().grant_user_permission(repo=HG_REPO, user=self.a1,
 
                                          perm=new_perm)
 
        Session().commit()
 
        # cannot really downgrade admins permissions !? they still get's set as
 
        # admin !
 
@@ -380,48 +381,92 @@ class TestPermissions(unittest.TestCase)
 
        usr = 'default'
 
        user_model.revoke_perm(usr, 'hg.create.none')
 
        user_model.grant_perm(usr, 'hg.create.repository')
 
        user_model.revoke_perm(usr, 'hg.fork.none')
 
        user_model.grant_perm(usr, 'hg.fork.repository')
 

	
 
        #disable global perms on specific user
 
        user_model.revoke_perm(self.u1, 'hg.create.repository')
 
        user_model.grant_perm(self.u1, 'hg.create.none')
 
        user_model.revoke_perm(self.u1, 'hg.fork.repository')
 
        user_model.grant_perm(self.u1, 'hg.fork.none')
 

	
 
        # make sure inherit flag is turned off
 
        self.u1.inherit_default_permissions = False
 
        Session().commit()
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        # this user will have non inherited permissions from he's
 
        # explicitly set permissions
 
        self.assertEqual(u1_auth.permissions['global'],
 
                         set(['hg.create.none', 'hg.fork.none',
 
                              'hg.register.manual_activate',
 
                              'repository.read']))
 

	
 
    def test_non_inherited_permissions_from_default_on_user_disabled(self):
 
        user_model = UserModel()
 
        # disable fork and create on default user
 
        usr = 'default'
 
        user_model.revoke_perm(usr, 'hg.create.repository')
 
        user_model.grant_perm(usr, 'hg.create.none')
 
        user_model.revoke_perm(usr, 'hg.fork.repository')
 
        user_model.grant_perm(usr, 'hg.fork.none')
 

	
 
        #enable global perms on specific user
 
        user_model.revoke_perm(self.u1, 'hg.create.none')
 
        user_model.grant_perm(self.u1, 'hg.create.repository')
 
        user_model.revoke_perm(self.u1, 'hg.fork.none')
 
        user_model.grant_perm(self.u1, 'hg.fork.repository')
 

	
 
        # make sure inherit flag is turned off
 
        self.u1.inherit_default_permissions = False
 
        Session().commit()
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        # this user will have non inherited permissions from he's
 
        # explicitly set permissions
 
        self.assertEqual(u1_auth.permissions['global'],
 
                         set(['hg.create.repository', 'hg.fork.repository',
 
                              'hg.register.manual_activate',
 
                              'repository.read']))
 

	
 
    def test_owner_permissions_doesnot_get_overwritten_by_group(self):
 
        #create repo as USER,
 
        self.test_repo = repo = RepoModel().create_repo(repo_name='myownrepo',
 
                                repo_type='hg',
 
                                description='desc',
 
                                owner=self.u1)
 

	
 
        Session().commit()
 
        #he has permissions of admin as owner
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories']['myownrepo'],
 
                         'repository.admin')
 
        #set his permission as users group, he should still be admin
 
        self.ug1 = UsersGroupModel().create('G1')
 
        # add user to group
 
        UsersGroupModel().add_user_to_group(self.ug1, self.u1)
 
        RepoModel().grant_users_group_permission(repo, group_name=self.ug1,
 
                                                 perm='repository.none')
 

	
 
        Session().commit()
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories']['myownrepo'],
 
                         'repository.admin')
 

	
 
    def test_owner_permissions_doesnot_get_overwritten_by_others(self):
 
        #create repo as USER,
 
        self.test_repo = repo = RepoModel().create_repo(repo_name='myownrepo',
 
                                repo_type='hg',
 
                                description='desc',
 
                                owner=self.u1)
 

	
 
        Session().commit()
 
        #he has permissions of admin as owner
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories']['myownrepo'],
 
                         'repository.admin')
 
        #set his permission as user, he should still be admin
 
        RepoModel().grant_user_permission(repo, user=self.u1,
 
                                          perm='repository.none')
 
        Session().commit()
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories']['myownrepo'],
 
                         'repository.admin')
setup.py
Show inline comments
 
@@ -15,100 +15,100 @@ def _get_meta_var(name, data, callback_h
 
    if matches:
 
        if not callable(callback_handler):
 
            callback_handler = lambda v: v
 

	
 
        return callback_handler(eval(matches.groups()[0]))
 

	
 
_meta = open(os.path.join(here, 'rhodecode', '__init__.py'), 'rb')
 
_metadata = _meta.read()
 
_meta.close()
 

	
 
callback = lambda V: ('.'.join(map(str, V[:3])) + '.'.join(V[3:]))
 
__version__ = _get_meta_var('VERSION', _metadata, callback)
 
__license__ = _get_meta_var('__license__', _metadata)
 
__author__ = _get_meta_var('__author__', _metadata)
 
__url__ = _get_meta_var('__url__', _metadata)
 
# defines current platform
 
__platform__ = platform.system()
 

	
 
is_windows = __platform__ in _get_meta_var('PLATFORM_WIN', _metadata)
 

	
 
requirements = [
 
    "waitress==0.8.1",
 
    "webob==1.0.8",
 
    "Pylons==1.0.0",
 
    "Beaker==1.6.4",
 
    "WebHelpers==1.3",
 
    "formencode==1.2.4",
 
    "SQLAlchemy==0.7.8",
 
    "Mako==0.7.2",
 
    "pygments>=1.5",
 
    "whoosh>=2.4.0,<2.5",
 
    "celery>=2.2.5,<2.3",
 
    "babel",
 
    "python-dateutil>=1.5.0,<2.0.0",
 
    "dulwich>=0.8.5,<0.9.0",
 
    "markdown==2.1.1",
 
    "docutils==0.8.1",
 
    "simplejson==2.5.2",
 
    "mock",
 
]
 

	
 
if sys.version_info < (2, 6):
 
    requirements.append("pysqlite")
 

	
 
if sys.version_info < (2, 7):
 
    requirements.append("unittest2")
 

	
 
if is_windows:
 
    requirements.append("mercurial==2.3.0")
 
    requirements.append("mercurial==2.3.1")
 
else:
 
    requirements.append("py-bcrypt")
 
    requirements.append("mercurial==2.3.0")
 
    requirements.append("mercurial==2.3.1")
 

	
 

	
 
dependency_links = [
 
]
 

	
 
classifiers = [
 
    'Development Status :: 5 - Production/Stable',
 
    'Environment :: Web Environment',
 
    'Framework :: Pylons',
 
    'Intended Audience :: Developers',
 
    'License :: OSI Approved :: GNU General Public License (GPL)',
 
    'Operating System :: OS Independent',
 
    'Programming Language :: Python',
 
    'Programming Language :: Python :: 2.5',
 
    'Programming Language :: Python :: 2.6',
 
    'Programming Language :: Python :: 2.7',
 
]
 

	
 

	
 
# additional files from project that goes somewhere in the filesystem
 
# relative to sys.prefix
 
data_files = []
 

	
 
# additional files that goes into package itself
 
package_data = {'rhodecode': ['i18n/*/LC_MESSAGES/*.mo', ], }
 

	
 
description = ('Mercurial repository browser/management with '
 
               'build in push/pull server and full text search')
 
keywords = ' '.join(['rhodecode', 'rhodiumcode', 'mercurial', 'git',
 
                     'code review', 'repo groups', 'ldap'
 
                      'repository management', 'hgweb replacement'
 
                      'hgwebdir', 'gitweb replacement', 'serving hgweb', ])
 
# long description
 
try:
 
    readme_file = 'README.rst'
 
    changelog_file = 'docs/changelog.rst'
 
    long_description = open(readme_file).read() + '\n\n' + \
 
        open(changelog_file).read()
 

	
 
except IOError, err:
 
    sys.stderr.write("[WARNING] Cannot find file specified as "
 
        "long_description (%s)\n or changelog (%s) skipping that file" \
 
            % (readme_file, changelog_file))
 
    long_description = description
 

	
 

	
 
try:
 
    from setuptools import setup, find_packages
0 comments (0 inline, 0 general)