Changeset - c9e3ea5bb59a
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
 
    cejones <>
 
    Thomas Waldmann <tw-public@gmx.de>
 
    Lorenzo M. Catucci <lorenzo@sancho.ccd.uniroma2.it>
 
    Dmitri Kuznetsov
 
    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>
 
\ No newline at end of file
 
    Erwin Kroon <e.kroon@smartmetersolutions.nl>
 
    nansenat16 <nansenat16@null.tw>
 
\ No newline at end of file
README.rst
Show inline comments
 
=========
 
RhodeCode
 
=========
 

	
 
About
 
-----
 

	
 
``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_ 
 
with a built in push/pull server and full text search and code-review.
 
It works on http/https and has a built in permission/authentication system with 
 
the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also provides
 
simple API so it's easy integrable with existing external systems.
 

	
 
RhodeCode is similar in some respects to github_ or bitbucket_, 
 
however RhodeCode can be run as standalone hosted application on your own server.
 
It is open source and donation ware and focuses more on providing a customized, 
 
self administered interface for Mercurial_ and GIT_  repositories. 
 
RhodeCode works on *nix systems and Windows it is powered by a vcs_ library 
 
RhodeCode works on \*nix systems and Windows it is powered by a vcs_ library 
 
that Lukasz Balcerzak and Marcin Kuzminski created to handle multiple 
 
different version control systems.
 

	
 
RhodeCode uses `PEP386 versioning <http://www.python.org/dev/peps/pep-0386/>`_
 

	
 
Installation
 
------------
 
Stable releases of RhodeCode are best installed via::
 

	
 
    easy_install rhodecode
 

	
 
Or::
 

	
 
    pip install rhodecode 
 

	
 
Detailed instructions and links may be found on the Installation page.
 

	
 
Please visit http://packages.python.org/RhodeCode/installation.html for
 
more details
 

	
 
RhodeCode demo
 
--------------
 

	
 
http://demo.rhodecode.org
 

	
 
The default access is anonymous but you can login to an administrative account
 
using the following credentials:
 

	
 
- username: demo
 
- password: demo12
 

	
 
Source code
 
-----------
 

	
 
The latest sources can be obtained from official RhodeCode instance
 
https://secure.rhodecode.org 
 

	
 

	
 
MIRRORS:
 

	
 
Issue tracker and sources at bitbucket_
 

	
 
http://bitbucket.org/marcinkuzminski/rhodecode
 

	
 
Sources at github_
 

	
 
https://github.com/marcinkuzminski/rhodecode
 

	
 

	
 
RhodeCode Features
 
------------------
 

	
 
- Has its own middleware to handle mercurial_ protocol requests. 
 
  Each request can be logged and authenticated.
 
- Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
 
  Supports http/https and LDAP
 
- Full permissions (private/read/write/admin) and authentication per project. 
 
  One account for web interface and mercurial_ push/pull/clone operations.
 
- Have built in users groups for easier permission management
 
- Repository groups let you group repos and manage them easier.
 
- Users can fork other users repo. RhodeCode have also compare view to see
 
  combined changeset for all changeset made within single push.
 
- Build in commit-api let's you add, edit and commit files right from RhodeCode
 
  interface using simple editor or upload form for binaries.
 
- Mako templates let's you customize the look and feel of the application.
 
- Beautiful diffs, annotations and source code browsing all colored by pygments. 
 
  Raw diffs are made in git-diff format, including git_ binary-patches
 
- Mercurial_ branch graph and yui-flot powered graphs with zooming and statistics
 
- Admin interface with user/permission management. Admin activity journal, logs
 
  pulls, pushes, forks, registrations and other actions made by all users.
 
- Server side forks. It is possible to fork a project and modify it freely 
 
  without breaking the main repository. You can even write Your own hooks 
 
  and install them
 
- code review with notification system, inline commenting, all parsed using
 
  rst syntax
 
- rst and markdown README support for repositories  
 
- Full text search powered by Whoosh on the source files, and file names.
 
  Build in indexing daemons, with optional incremental index build
 
  (no external search servers required all in one application)
 
- Setup project descriptions and info inside built in db for easy, non 
 
  file-system operations
 
- Intelligent cache with invalidation after push or project change, provides 
 
  high performance and always up to date data.
 
- Rss / atom feeds, gravatar support, download sources as zip/tar/gz
 
- Optional async tasks for speed and performance using celery_  
 
- Backup scripts can do backup of whole app and send it over scp to desired 
 
  location 
 
- Based on pylons / sqlalchemy / sqlite / whoosh / vcs
 

	
 
    
 
Incoming / Plans
 
----------------
 

	
 
- Finer granular permissions per branch, repo group or subrepo
 
- pull requests and web based merges
 
- per line file history
 
- SSH based authentication with server side key management
 
- Commit based built in wiki system
 
- More statistics and graph (global annotation + some more statistics)
 
- Other advancements as development continues (or you can of course make 
 
  additions and or requests)
 

	
 
License
 
-------
 

	
 
``RhodeCode`` is released under the GPLv3 license.
 

	
 

	
 
Getting help
 
------------
 

	
 
Listed bellow are various support resources that should help.
 

	
 
.. note::
 
   
 
   Please try to read the documentation before posting any issues
 
 
 
- Join the `Google group <http://groups.google.com/group/rhodecode>`_ and ask
 
  any questions.
 

	
 
- Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
 

	
 

	
 
- Join #rhodecode on FreeNode (irc.freenode.net)
 
  or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
 

	
 
- You can also follow me on twitter **@marcinkuzminski** where i often post some
 
  news about RhodeCode
 

	
 

	
 
Online documentation
 
--------------------
 

	
 
Online documentation for the current version of RhodeCode is available at
 
 - http://packages.python.org/RhodeCode/
 
 - http://rhodecode.readthedocs.org/en/latest/index.html
 

	
 
You may also build the documentation for yourself - go into ``docs/`` and run::
 

	
 
   make html
 

	
 
(You need to have sphinx_ installed to build the documentation. If you don't
 
have sphinx_ installed you can install it via the command: 
 
``easy_install sphinx``)
 
 
 
.. _virtualenv: http://pypi.python.org/pypi/virtualenv
 
.. _python: http://www.python.org/
 
.. _sphinx: http://sphinx.pocoo.org/
 
.. _mercurial: http://mercurial.selenic.com/
 
.. _bitbucket: http://bitbucket.org/
 
.. _github: http://github.com/
 
.. _subversion: http://subversion.tigris.org/
 
.. _git: http://git-scm.com/
 
.. _celery: http://celeryproject.org/
 
.. _Sphinx: http://sphinx.pocoo.org/
 
.. _vcs: http://pypi.python.org/pypi/vcs
 
\ No newline at end of file
docs/changelog.rst
Show inline comments
 
modified file chmod 100644 => 100755
 
.. _changelog:
 

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

	
 
1.3.5 (**2012-XX-XX**)
 
1.4.0 (**2012-XX-XX**)
 
----------------------
 

	
 
:status: in-progress
 
:branch: beta
 

	
 
news
 
++++
 

	
 
fixes
 
+++++
 

	
 
1.3.6 (**2012-05-16**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- chinese traditional translation
 

	
 
fixes
 
+++++
 

	
 
- fixed no scm found warning
 
- fixed __future__ import error on rcextensions
 
- made simplejson required lib for speedup on JSON encoding
 
- fixes #449 bad regex could get more than revisions from parsing history
 

	
 
1.3.5 (**2012-05-10**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- use ext_json for json module
 
- unified annotation view with file source view
 
- notification improvements, better inbox + css
 
- #419 don't strip passwords for login forms, make rhodecode 
 
  more compatible with LDAP servers
 
- Added HTTP_X_FORWARDED_FOR as another method of extracting 
 
  IP for pull/push logs. - moved all to base controller  
 
- #415: Adding comment to changeset causes reload. 
 
  Comments are now added via ajax and doesn't reload the page
 
- #374 LDAP config is discarded when LDAP can't be activated
 
- limited push/pull operations are now logged for git in the journal
 
- bumped mercurial to 2.2.X series
 
- added support for displaying submodules in file-browser
 
- #421 added bookmarks in changelog view
 

	
 
fixes
 
+++++
 

	
 
- fixed dev-version marker for stable when served from source codes
 
- fixed missing permission checks on show forks page
 
- #418 cast to unicode fixes in notification objects
 
- #426 fixed mention extracting regex
 
- fixed remote-pulling for git remotes remopositories
 
- fixed #434: Error when accessing files or changesets of a git repository 
 
  with submodules
 
- fixed issue with empty APIKEYS for users after registration ref. #438
 
- fixed issue with getting README files from git repositories
 

	
 
1.3.4 (**2012-03-28**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- Whoosh logging is now controlled by the .ini files logging setup
 
- added clone-url into edit form on /settings page
 
- added help text into repo add/edit forms
 
- created rcextensions module with additional mappings (ref #322) and
 
  post push/pull/create repo hooks callbacks
 
- implemented #377 Users view for his own permissions on account page
 
- #399 added inheritance of permissions for users group on repos groups
 
- #401 repository group is automatically pre-selected when adding repos 
 
  inside a repository group
 
- added alternative HTTP 403 response when client failed to authenticate. Helps 
 
  solving issues with Mercurial and LDAP
 
- #402 removed group prefix from repository name when listing repositories 
 
  inside a group
 
- added gravatars into permission view and permissions autocomplete
 
- #347 when running multiple RhodeCode instances, properly invalidates cache 
 
  for all registered servers
 

	
 
fixes
 
+++++
 

	
 
- fixed #390 cache invalidation problems on repos inside group
 
- fixed #385 clone by ID url was loosing proxy prefix in URL
 
- fixed some unicode problems with waitress
 
- fixed issue with escaping < and > in changeset commits
 
- fixed error occurring during recursive group creation in API 
 
  create_repo function
 
- fixed #393 py2.5 fixes for routes url generator
 
- fixed #397 Private repository groups shows up before login
 
- fixed #396 fixed problems with revoking users in nested groups
 
- fixed mysql unicode issues + specified InnoDB as default engine with 
 
  utf8 charset
 
- #406 trim long branch/tag names in changelog to not break UI
 
  
 
1.3.3 (**2012-03-02**)
 
----------------------
 

	
 
news
 
++++
 

	
 

	
 
fixes
 
+++++
 

	
 
- fixed some python2.5 compatibility issues 
 
- fixed issues with removed repos was accidentally added as groups, after
 
  full rescan of paths
 
- fixes #376 Cannot edit user (using container auth)
 
- fixes #378 Invalid image urls on changeset screen with proxy-prefix 
 
  configuration
 
- fixed initial sorting of repos inside repo group
 
- fixes issue when user tried to resubmit same permission into user/user_groups
 
- bumped beaker version that fixes #375 leap error bug
 
- fixed raw_changeset for git. It was generated with hg patch headers
 
- fixed vcs issue with last_changeset for filenodes
 
- fixed missing commit after hook delete
 
- fixed #372 issues with git operation detection that caused a security issue 
 
  for git repos
 

	
 
1.3.2 (**2012-02-28**)
 
----------------------
 

	
 
news
 
++++
 

	
 

	
 
fixes
 
+++++
 

	
 
- fixed git protocol issues with repos-groups
 
- fixed git remote repos validator that prevented from cloning remote git repos
 
- fixes #370 ending slashes fixes for repo and groups
 
- fixes #368 improved git-protocol detection to handle other clients
 
- fixes #366 When Setting Repository Group To Blank Repo Group Wont Be 
 
  Moved To Root
 
- fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys 
 
- fixed #373 missing cascade drop on user_group_to_perm table
 

	
 
1.3.1 (**2012-02-27**)
 
----------------------
 

	
 
news
 
++++
 

	
 

	
 
fixes
 
+++++
 

	
 
- redirection loop occurs when remember-me wasn't checked during login
 
- fixes issues with git blob history generation 
 
- don't fetch branch for git in file history dropdown. Causes unneeded slowness
 

	
 
1.3.0 (**2012-02-26**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- code review, inspired by github code-comments 
 
- #215 rst and markdown README files support
 
- #252 Container-based and proxy pass-through authentication support
 
- #44 branch browser. Filtering of changelog by branches
 
- mercurial bookmarks support
 
- new hover top menu, optimized to add maximum size for important views
 
- configurable clone url template with possibility to specify  protocol like 
 
  ssh:// or http:// and also manually alter other parts of clone_url.
 
- enabled largefiles extension by default
 
- optimized summary file pages and saved a lot of unused space in them
 
- #239 option to manually mark repository as fork
 
- #320 mapping of commit authors to RhodeCode users
 
- #304 hashes are displayed using monospace font    
 
- diff configuration, toggle white lines and context lines
 
- #307 configurable diffs, whitespace toggle, increasing context lines
 
- sorting on branches, tags and bookmarks using YUI datatable
 
- improved file filter on files page
 
- implements #330 api method for listing nodes ar particular revision
 
- #73 added linking issues in commit messages to chosen issue tracker url
 
  based on user defined regular expression
 
- added linking of changesets in commit messages  
 
- new compact changelog with expandable commit messages
 
- firstname and lastname are optional in user creation
 
- #348 added post-create repository hook
 
- #212 global encoding settings is now configurable from .ini files 
 
- #227 added repository groups permissions
 
- markdown gets codehilite extensions
 
- new API methods, delete_repositories, grante/revoke permissions for groups 
 
  and repos
 
  
 
    
 
fixes
 
+++++
 

	
 
- rewrote dbsession management for atomic operations, and better error handling
 
- fixed sorting of repo tables
 
- #326 escape of special html entities in diffs
 
- normalized user_name => username in api attributes
 
- fixes #298 ldap created users with mixed case emails created conflicts 
 
  on saving a form
 
- fixes issue when owner of a repo couldn't revoke permissions for users 
 
  and groups
 
- fixes #271 rare JSON serialization problem with statistics
 
- fixes #337 missing validation check for conflicting names of a group with a
 
  repositories group
 
- #340 fixed session problem for mysql and celery tasks
 
- fixed #331 RhodeCode mangles repository names if the a repository group 
 
  contains the "full path" to the repositories
 
- #355 RhodeCode doesn't store encrypted LDAP passwords
 

	
 
1.2.5 (**2012-01-28**)
 
----------------------
 

	
 
news
 
++++
 

	
 
fixes
 
+++++
 

	
 
- #340 Celery complains about MySQL server gone away, added session cleanup
 
  for celery tasks
 
- #341 "scanning for repositories in None" log message during Rescan was missing
 
  a parameter
 
- fixed creating archives with subrepos. Some hooks were triggered during that
 
  operation leading to crash.
 
- fixed missing email in account page.
 
- Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
 
  forking on windows impossible 
 

	
 
1.2.4 (**2012-01-19**)
 
----------------------
 

	
 
news
 
++++
 

	
 
- RhodeCode is bundled with mercurial series 2.0.X by default, with
 
  full support to largefiles extension. Enabled by default in new installations
 
- #329 Ability to Add/Remove Groups to/from a Repository via AP
 
- added requires.txt file with requirements
 
     
 
fixes
 
+++++
 

	
 
- fixes db session issues with celery when emailing admins
 
- #331 RhodeCode mangles repository names if the a repository group 
 
  contains the "full path" to the repositories
 
- #298 Conflicting e-mail addresses for LDAP and RhodeCode users
 
- DB session cleanup after hg protocol operations, fixes issues with
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, 0, 'b')
 

	
 
try:
 
    from rhodecode.lib import get_current_revision
 
    _rev = get_current_revision()
 
    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__ = 6  # defines current db version for migrations
 
__platform__ = platform.system()
 
__license__ = 'GPLv3'
 
__py_version__ = sys.version_info
 

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

	
 
is_windows = __platform__ in PLATFORM_WIN
 
is_unix = __platform__ in PLATFORM_OTHERS
 

	
 
requirements = [
 
    "Pylons==1.0.0",
 
    "Beaker==1.6.3",
 
    "WebHelpers==1.3",
 
    "formencode==1.2.4",
 
    "SQLAlchemy==0.7.6",
 
    "Mako==0.7.0",
 
    "pygments>=1.4",
 
    "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",
 
    "webob==1.0.8",
 
    "markdown==2.1.1",
 
    "docutils==0.8.1",
 
    "simplejson==2.5.2",
 
]
 

	
 
if __py_version__ < (2, 6):
 
    requirements.append("simplejson")
 
    requirements.append("pysqlite")
 

	
 
if __platform__ in PLATFORM_WIN:
 
if is_windows:
 
    requirements.append("mercurial>=2.2.1,<2.3")
 
else:
 
    requirements.append("py-bcrypt")
 
    requirements.append("mercurial>=2.2.1,<2.3")
 

	
 

	
 
def get_version():
 
    """Returns shorter version (digit parts only) as string."""
 

	
 
    return '.'.join((str(each) for each in VERSION[:3]))
 

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

	
 
CELERY_ON = False
 

	
 
# link to config for pylons
 
CONFIG = {}
 

	
 
# Linked module for extensions
 
EXTENSIONS = {}
rhodecode/config/rcextensions/make_rcextensions.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.config.rcextensions.make_rcextensions
 
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
    Whoosh indexing module for RhodeCode
 

	
 
    :created_on: Mar 6, 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/>.
 
from __future__ import with_statement
 

	
 
import os
 
import sys
 
import pkg_resources
 
import traceback
 
import logging
 
from os.path import dirname as dn, join as jn
 

	
 
#to get the rhodecode import
 
sys.path.append(dn(dn(dn(os.path.realpath(__file__)))))
 

	
 
from rhodecode.lib.utils import BasePasterCommand, Command, ask_ok
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class MakeRcExt(BasePasterCommand):
 

	
 
    max_args = 1
 
    min_args = 1
 

	
 
    usage = "CONFIG_FILE"
 
    summary = "Creates additional extensions for rhodecode"
 
    group_name = "RhodeCode"
 
    takes_config_file = -1
 
    parser = Command.standard_parser(verbose=True)
 

	
 
    def command(self):
 
        logging.config.fileConfig(self.path_to_ini_file)
 
        from pylons import config
 

	
 
        def _make_file(ext_file):
 
            bdir = os.path.split(ext_file)[0]
 
            if not os.path.isdir(bdir):
 
                os.makedirs(bdir)
 
            with open(ext_file, 'wb') as f:
 
                f.write(tmpl)
 
                log.info('Writen new extensions file to %s' % ext_file)
 

	
 
        here = config['here']
 
        tmpl = pkg_resources.resource_string(
 
            'rhodecode', jn('config', 'rcextensions', '__init__.py')
 
        )
 
        ext_file = jn(here, 'rcextensions', '__init__.py')
 
        if os.path.exists(ext_file):
 
            msg = ('Extension file already exists, do you want '
 
                   'to overwrite it ? [y/n]')
 
            if ask_ok(msg):
 
                _make_file(ext_file)
 
            else:
 
                log.info('nothing done...')
 
        else:
 
            _make_file(ext_file)
 

	
 
    def update_parser(self):
 
        pass
rhodecode/controllers/files.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.controllers.files
 
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
    Files controller for RhodeCode
 

	
 
    :created_on: Apr 21, 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 tempfile
 

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

	
 
from rhodecode.lib import diffs
 
from rhodecode.lib import helpers as h
 

	
 
from rhodecode.lib.compat import OrderedDict
 
from rhodecode.lib.utils2 import convert_line_endings, detect_mode, safe_str
 
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
 
from rhodecode.lib.base import BaseRepoController, render
 
from rhodecode.lib.utils import EmptyChangeset
 
from rhodecode.lib.vcs.conf import settings
 
from rhodecode.lib.vcs.exceptions import RepositoryError, \
 
    ChangesetDoesNotExistError, EmptyRepositoryError, \
 
    ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
 
from rhodecode.lib.vcs.nodes import FileNode
 

	
 
from rhodecode.model.repo import RepoModel
 
from rhodecode.model.scm import ScmModel
 
from rhodecode.model.db import Repository
 

	
 
from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
 
    _context_url, get_line_ctx, get_ignore_ws
 

	
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class FilesController(BaseRepoController):
 

	
 
    @LoginRequired()
 
    def __before__(self):
 
        super(FilesController, self).__before__()
 
        c.cut_off_limit = self.cut_off_limit
 

	
 
    def __get_cs_or_redirect(self, rev, repo_name, redirect_after=True):
 
        """
 
        Safe way to get changeset if error occur it redirects to tip with
 
        proper message
 

	
 
        :param rev: revision to fetch
 
        :param repo_name: repo name to redirect after
 
        """
 

	
 
        try:
 
            return c.rhodecode_repo.get_changeset(rev)
 
        except EmptyRepositoryError, e:
 
            if not redirect_after:
 
                return None
 
            url_ = url('files_add_home',
 
                       repo_name=c.repo_name,
 
                       revision=0, f_path='')
 
            add_new = '<a href="%s">[%s]</a>' % (url_, _('add new'))
 
            h.flash(h.literal(_('There are no files yet %s' % add_new)),
 
                    category='warning')
 
            redirect(h.url('summary_home', repo_name=repo_name))
 

	
 
        except RepositoryError, e:
 
            h.flash(str(e), category='warning')
 
            redirect(h.url('files_home', repo_name=repo_name, revision='tip'))
 

	
 
    def __get_filenode_or_redirect(self, repo_name, cs, path):
 
        """
 
        Returns file_node, if error occurs or given path is directory,
 
        it'll redirect to top level path
 

	
 
        :param repo_name: repo_name
 
        :param cs: given changeset
 
        :param path: path to lookup
 
        """
 

	
 
        try:
 
            file_node = cs.get_node(path)
 
            if file_node.is_dir():
 
                raise RepositoryError('given path is a directory')
 
        except RepositoryError, e:
 
            h.flash(str(e), category='warning')
 
            redirect(h.url('files_home', repo_name=repo_name,
 
                           revision=cs.raw_id))
 

	
 
        return file_node
 

	
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def index(self, repo_name, revision, f_path, annotate=False):
 
        # redirect to given revision from form if given
 
        post_revision = request.POST.get('at_rev', None)
 
        if post_revision:
 
            cs = self.__get_cs_or_redirect(post_revision, repo_name)
 
            redirect(url('files_home', repo_name=c.repo_name,
 
                         revision=cs.raw_id, f_path=f_path))
 

	
 
        c.changeset = self.__get_cs_or_redirect(revision, repo_name)
 
        c.branch = request.GET.get('branch', None)
 
        c.f_path = f_path
 
        c.annotate = annotate
 
        cur_rev = c.changeset.revision
 

	
 
        # prev link
 
        try:
 
            prev_rev = c.rhodecode_repo.get_changeset(cur_rev).prev(c.branch)
 
            c.url_prev = url('files_home', repo_name=c.repo_name,
 
                         revision=prev_rev.raw_id, f_path=f_path)
 
            if c.branch:
 
                c.url_prev += '?branch=%s' % c.branch
 
        except (ChangesetDoesNotExistError, VCSError):
 
            c.url_prev = '#'
 

	
 
        # next link
 
        try:
 
            next_rev = c.rhodecode_repo.get_changeset(cur_rev).next(c.branch)
 
            c.url_next = url('files_home', repo_name=c.repo_name,
 
                     revision=next_rev.raw_id, f_path=f_path)
 
            if c.branch:
 
                c.url_next += '?branch=%s' % c.branch
 
        except (ChangesetDoesNotExistError, VCSError):
 
            c.url_next = '#'
 

	
 
        # files or dirs
 
        try:
 
            c.file = c.changeset.get_node(f_path)
 

	
 
            if c.file.is_file():
 
                c.file_history = self._get_node_history(c.changeset, f_path)
 
            else:
 
                c.file_history = []
 
        except RepositoryError, e:
 
            h.flash(str(e), category='warning')
 
            redirect(h.url('files_home', repo_name=repo_name,
 
                           revision=revision))
 

	
 
        return render('files/files.html')
 

	
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def rawfile(self, repo_name, revision, f_path):
 
        cs = self.__get_cs_or_redirect(revision, repo_name)
 
        file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
 

	
 
        response.content_disposition = 'attachment; filename=%s' % \
 
            safe_str(f_path.split(os.sep)[-1])
 
            safe_str(f_path.split(Repository.url_sep())[-1])
 

	
 
        response.content_type = file_node.mimetype
 
        return file_node.content
 

	
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def raw(self, repo_name, revision, f_path):
 
        cs = self.__get_cs_or_redirect(revision, repo_name)
 
        file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path)
 

	
 
        raw_mimetype_mapping = {
 
            # map original mimetype to a mimetype used for "show as raw"
 
            # you can also provide a content-disposition to override the
 
            # default "attachment" disposition.
 
            # orig_type: (new_type, new_dispo)
 

	
 
            # show images inline:
 
            'image/x-icon': ('image/x-icon', 'inline'),
 
            'image/png': ('image/png', 'inline'),
 
            'image/gif': ('image/gif', 'inline'),
 
            'image/jpeg': ('image/jpeg', 'inline'),
 
            'image/svg+xml': ('image/svg+xml', 'inline'),
 
        }
 

	
 
        mimetype = file_node.mimetype
 
        try:
 
            mimetype, dispo = raw_mimetype_mapping[mimetype]
 
        except KeyError:
 
            # we don't know anything special about this, handle it safely
 
            if file_node.is_binary:
 
                # do same as download raw for binary files
 
                mimetype, dispo = 'application/octet-stream', 'attachment'
 
            else:
 
                # do not just use the original mimetype, but force text/plain,
 
                # otherwise it would serve text/html and that might be unsafe.
 
                # Note: underlying vcs library fakes text/plain mimetype if the
 
                # mimetype can not be determined and it thinks it is not
 
                # binary.This might lead to erroneous text display in some
 
                # cases, but helps in other cases, like with text files
 
                # without extension.
 
                mimetype, dispo = 'text/plain', 'inline'
 

	
 
        if dispo == 'attachment':
 
            dispo = 'attachment; filename=%s' % \
 
                        safe_str(f_path.split(os.sep)[-1])
 

	
 
        response.content_disposition = dispo
 
        response.content_type = mimetype
 
        return file_node.content
 

	
 
    @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
 
    def edit(self, repo_name, revision, f_path):
 
        r_post = request.POST
 

	
 
        c.cs = self.__get_cs_or_redirect(revision, repo_name)
 
        c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path)
 

	
 
        if c.file.is_binary:
 
            return redirect(url('files_home', repo_name=c.repo_name,
 
                         revision=c.cs.raw_id, f_path=f_path))
 

	
 
        c.f_path = f_path
 

	
 
        if r_post:
 

	
 
            old_content = c.file.content
 
            sl = old_content.splitlines(1)
 
            first_line = sl[0] if sl else ''
 
            # modes:  0 - Unix, 1 - Mac, 2 - DOS
 
            mode = detect_mode(first_line, 0)
 
            content = convert_line_endings(r_post.get('content'), mode)
 

	
 
            message = r_post.get('message') or (_('Edited %s via RhodeCode')
 
                                                % (f_path))
 
            author = self.rhodecode_user.full_contact
 

	
 
            if content == old_content:
 
                h.flash(_('No changes'),
 
                    category='warning')
 
                return redirect(url('changeset_home', repo_name=c.repo_name,
 
                                    revision='tip'))
 

	
 
            try:
 
                self.scm_model.commit_change(repo=c.rhodecode_repo,
 
                                             repo_name=repo_name, cs=c.cs,
 
                                             user=self.rhodecode_user,
 
                                             author=author, message=message,
 
                                             content=content, f_path=f_path)
 
                h.flash(_('Successfully committed to %s' % f_path),
 
                        category='success')
 

	
 
            except Exception:
 
                log.error(traceback.format_exc())
 
                h.flash(_('Error occurred during commit'), category='error')
 
            return redirect(url('changeset_home',
 
                                repo_name=c.repo_name, revision='tip'))
 

	
 
        return render('files/files_edit.html')
 

	
 
    @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
 
    def add(self, repo_name, revision, f_path):
 
        r_post = request.POST
 
        c.cs = self.__get_cs_or_redirect(revision, repo_name,
 
                                         redirect_after=False)
 
        if c.cs is None:
 
            c.cs = EmptyChangeset(alias=c.rhodecode_repo.alias)
 

	
 
        c.f_path = f_path
 

	
 
        if r_post:
 
            unix_mode = 0
 
            content = convert_line_endings(r_post.get('content'), unix_mode)
 

	
 
            message = r_post.get('message') or (_('Added %s via RhodeCode')
 
                                                % (f_path))
 
            location = r_post.get('location')
 
            filename = r_post.get('filename')
 
            file_obj = r_post.get('upload_file', None)
 

	
 
            if file_obj is not None and hasattr(file_obj, 'filename'):
 
                filename = file_obj.filename
 
                content = file_obj.file
 

	
 
            node_path = os.path.join(location, filename)
 
            author = self.rhodecode_user.full_contact
 

	
 
            if not content:
 
                h.flash(_('No content'), category='warning')
 
                return redirect(url('changeset_home', repo_name=c.repo_name,
 
                                    revision='tip'))
 
            if not filename:
 
                h.flash(_('No filename'), category='warning')
 
                return redirect(url('changeset_home', repo_name=c.repo_name,
 
                                    revision='tip'))
 

	
 
            try:
 
                self.scm_model.create_node(repo=c.rhodecode_repo,
 
                                           repo_name=repo_name, cs=c.cs,
 
                                           user=self.rhodecode_user,
 
                                           author=author, message=message,
 
                                           content=content, f_path=node_path)
 
                h.flash(_('Successfully committed to %s' % node_path),
 
                        category='success')
 
            except NodeAlreadyExistsError, e:
 
                h.flash(_(e), category='error')
 
            except Exception:
 
                log.error(traceback.format_exc())
 
                h.flash(_('Error occurred during commit'), category='error')
 
            return redirect(url('changeset_home',
 
                                repo_name=c.repo_name, revision='tip'))
 

	
 
        return render('files/files_add.html')
 

	
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def archivefile(self, repo_name, fname):
 

	
 
        fileformat = None
 
        revision = None
 
        ext = None
 
        subrepos = request.GET.get('subrepos') == 'true'
 

	
 
        for a_type, ext_data in settings.ARCHIVE_SPECS.items():
 
            archive_spec = fname.split(ext_data[1])
 
            if len(archive_spec) == 2 and archive_spec[1] == '':
 
                fileformat = a_type or ext_data[1]
 
                revision = archive_spec[0]
 
                ext = ext_data[1]
 

	
 
        try:
 
            dbrepo = RepoModel().get_by_repo_name(repo_name)
 
            if dbrepo.enable_downloads is False:
 
                return _('downloads disabled')
 

	
 
            if c.rhodecode_repo.alias == 'hg':
 
                # patch and reset hooks section of UI config to not run any
 
                # hooks on fetching archives with subrepos
 
                for k, v in c.rhodecode_repo._repo.ui.configitems('hooks'):
 
                    c.rhodecode_repo._repo.ui.setconfig('hooks', k, None)
 

	
 
            cs = c.rhodecode_repo.get_changeset(revision)
 
            content_type = settings.ARCHIVE_SPECS[fileformat][0]
 
        except ChangesetDoesNotExistError:
 
            return _('Unknown revision %s') % revision
 
        except EmptyRepositoryError:
 
            return _('Empty repository')
 
        except (ImproperArchiveTypeError, KeyError):
 
            return _('Unknown archive type')
 

	
 
        archive = tempfile.NamedTemporaryFile(mode='w+r+b', delete=False)
 
        cs.fill_archive(stream=archive, kind=fileformat, subrepos=subrepos)
 
        archive.close()
 
        response.content_type = content_type
 
        response.content_disposition = 'attachment; filename=%s-%s%s' \
 
            % (repo_name, revision, ext)
 

	
 
        import tempfile
 
        archive = tempfile.mkstemp()[1]
 
        t = open(archive, 'wb')
 
        cs.fill_archive(stream=t, kind=fileformat, subrepos=subrepos)
 
            % (repo_name, revision[:12], ext)
 
        response.content_length = str(os.path.getsize(archive.name))
 

	
 
        def get_chunked_archive(archive):
 
            stream = open(archive, 'rb')
 
        def get_chunked_archive(tmpfile):
 
            while True:
 
                data = stream.read(4096)
 
                data = tmpfile.read(16 * 1024)
 
                if not data:
 
                    os.remove(archive)
 
                    tmpfile.close()
 
                    os.unlink(tmpfile.name)
 
                    break
 
                yield data
 

	
 
        return get_chunked_archive(archive)
 
        return get_chunked_archive(tmpfile=open(archive.name,'rb'))
 

	
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def diff(self, repo_name, f_path):
 
        ignore_whitespace = request.GET.get('ignorews') == '1'
 
        line_context = request.GET.get('context', 3)
 
        diff1 = request.GET.get('diff1', '')
 
        diff2 = request.GET.get('diff2', '')
 
        c.action = request.GET.get('diff')
 
        c.no_changes = diff1 == diff2
 
        c.f_path = f_path
 
        c.big_diff = False
 
        c.anchor_url = anchor_url
 
        c.ignorews_url = _ignorews_url
 
        c.context_url = _context_url
 
        c.changes = OrderedDict()
 
        c.changes[diff2] = []
 
        try:
 
            if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
 
                c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
 
                node1 = c.changeset_1.get_node(f_path)
 
            else:
 
                c.changeset_1 = EmptyChangeset(repo=c.rhodecode_repo)
 
                node1 = FileNode('.', '', changeset=c.changeset_1)
 

	
 
            if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
 
                c.changeset_2 = c.rhodecode_repo.get_changeset(diff2)
 
                node2 = c.changeset_2.get_node(f_path)
 
            else:
 
                c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
 
                node2 = FileNode('.', '', changeset=c.changeset_2)
 
        except RepositoryError:
 
            return redirect(url('files_home', repo_name=c.repo_name,
 
                                f_path=f_path))
 

	
 
        if c.action == 'download':
 
            _diff = diffs.get_gitdiff(node1, node2,
 
                                      ignore_whitespace=ignore_whitespace,
 
                                      context=line_context)
 
            diff = diffs.DiffProcessor(_diff, format='gitdiff')
 

	
 
            diff_name = '%s_vs_%s.diff' % (diff1, diff2)
 
            response.content_type = 'text/plain'
 
            response.content_disposition = (
 
                'attachment; filename=%s' % diff_name
 
            )
 
            return diff.raw_diff()
 

	
 
        elif c.action == 'raw':
 
            _diff = diffs.get_gitdiff(node1, node2,
 
                                      ignore_whitespace=ignore_whitespace,
 
                                      context=line_context)
 
            diff = diffs.DiffProcessor(_diff, format='gitdiff')
 
            response.content_type = 'text/plain'
 
            return diff.raw_diff()
 

	
 
        else:
 
            fid = h.FID(diff2, node2.path)
 
            line_context_lcl = get_line_ctx(fid, request.GET)
 
            ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
 

	
 
            lim = request.GET.get('fulldiff') or self.cut_off_limit
 
            _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1,
 
                                         filenode_new=node2,
 
                                         cut_off_limit=lim,
 
                                         ignore_whitespace=ign_whitespace_lcl,
 
                                         line_context=line_context_lcl,
 
                                         enable_comments=False)
 

	
 
            c.changes = [('', node2, diff, cs1, cs2, st,)]
 

	
 
        return render('files/file_diff.html')
 

	
 
    def _get_node_history(self, cs, f_path):
 
        changesets = cs.get_file_history(f_path)
 
        hist_l = []
 

	
 
        changesets_group = ([], _("Changesets"))
 
        branches_group = ([], _("Branches"))
 
        tags_group = ([], _("Tags"))
 
        _hg = cs.repository.alias == 'hg'
 
        for chs in changesets:
 
            _branch = '(%s)' % chs.branch if _hg else ''
 
            n_desc = 'r%s:%s %s' % (chs.revision, chs.short_id, _branch)
 
            changesets_group[0].append((chs.raw_id, n_desc,))
 

	
 
        hist_l.append(changesets_group)
 

	
 
        for name, chs in c.rhodecode_repo.branches.items():
 
            branches_group[0].append((chs, name),)
 
        hist_l.append(branches_group)
 

	
 
        for name, chs in c.rhodecode_repo.tags.items():
 
            tags_group[0].append((chs, name),)
 
        hist_l.append(tags_group)
 

	
 
        return hist_l
 

	
 
    @jsonify
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def nodelist(self, repo_name, revision, f_path):
 
        if request.environ.get('HTTP_X_PARTIAL_XHR'):
 
            cs = self.__get_cs_or_redirect(revision, repo_name)
 
            _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
 
                                          flat=False)
 
            return _d + _f
rhodecode/controllers/summary.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.controllers.summary
 
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
    Summary controller for Rhodecode
 

	
 
    :created_on: Apr 18, 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 traceback
 
import calendar
 
import logging
 
import urllib
 
from time import mktime
 
from datetime import timedelta, date
 
from urlparse import urlparse
 
from rhodecode.lib.compat import product
 

	
 
from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
 
    NodeDoesNotExistError
 

	
 
from pylons import tmpl_context as c, request, url, config
 
from pylons.i18n.translation import _
 

	
 
from beaker.cache import cache_region, region_invalidate
 

	
 
from rhodecode.config.conf import ALL_READMES, ALL_EXTS, LANGUAGES_EXTENSIONS_MAP
 
from rhodecode.model.db import Statistics, CacheInvalidation
 
from rhodecode.lib.utils2 import safe_unicode
 
from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
 
from rhodecode.lib.base import BaseRepoController, render
 
from rhodecode.lib.utils import EmptyChangeset
 
from rhodecode.lib.markup_renderer import MarkupRenderer
 
from rhodecode.lib.celerylib import run_task
 
from rhodecode.lib.celerylib.tasks import get_commits_stats
 
from rhodecode.lib.helpers import RepoPage
 
from rhodecode.lib.compat import json, OrderedDict
 

	
 
log = logging.getLogger(__name__)
 

	
 
README_FILES = [''.join([x[0][0], x[1][0]]) for x in
 
                    sorted(list(product(ALL_READMES, ALL_EXTS)),
 
                           key=lambda y:y[0][1] + y[1][1])]
 

	
 

	
 
class SummaryController(BaseRepoController):
 

	
 
    @LoginRequired()
 
    @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
 
                                   'repository.admin')
 
    def __before__(self):
 
        super(SummaryController, self).__before__()
 

	
 
    def index(self, repo_name):
 
        c.dbrepo = dbrepo = c.rhodecode_db_repo
 
        c.following = self.scm_model.is_following_repo(repo_name,
 
                                                self.rhodecode_user.user_id)
 

	
 
        def url_generator(**kw):
 
            return url('shortlog_home', repo_name=repo_name, size=10, **kw)
 

	
 
        c.repo_changesets = RepoPage(c.rhodecode_repo, page=1,
 
                                     items_per_page=10, url=url_generator)
 

	
 
        if self.rhodecode_user.username == 'default':
 
            # for default(anonymous) user we don't need to pass credentials
 
            username = ''
 
            password = ''
 
        else:
 
            username = str(self.rhodecode_user.username)
 
            password = '@'
 

	
 
        parsed_url = urlparse(url.current(qualified=True))
 

	
 
        default_clone_uri = '{scheme}://{user}{pass}{netloc}{path}'
 

	
 
        uri_tmpl = config.get('clone_uri', default_clone_uri)
 
        uri_tmpl = uri_tmpl.replace('{', '%(').replace('}', ')s')
 
        decoded_path = safe_unicode(urllib.unquote(parsed_url.path))
 
        uri_dict = {
 
           'user': username,
 
           'pass': password,
 
           'scheme': parsed_url.scheme,
 
           'netloc': parsed_url.netloc,
 
           'path': decoded_path
 
        }
 

	
 
        uri = uri_tmpl % uri_dict
 
        # generate another clone url by id
 
        uri_dict.update(
 
         {'path': decoded_path.replace(repo_name, '_%s' % c.dbrepo.repo_id)}
 
        )
 
        uri_id = uri_tmpl % uri_dict
 

	
 
        c.clone_repo_url = uri
 
        c.clone_repo_url_id = uri_id
 
        c.repo_tags = OrderedDict()
 
        for name, hash_ in c.rhodecode_repo.tags.items()[:10]:
 
            try:
 
                c.repo_tags[name] = c.rhodecode_repo.get_changeset(hash_)
 
            except ChangesetError:
 
                c.repo_tags[name] = EmptyChangeset(hash_)
 

	
 
        c.repo_branches = OrderedDict()
 
        for name, hash_ in c.rhodecode_repo.branches.items()[:10]:
 
            try:
 
                c.repo_branches[name] = c.rhodecode_repo.get_changeset(hash_)
 
            except ChangesetError:
 
                c.repo_branches[name] = EmptyChangeset(hash_)
 

	
 
        td = date.today() + timedelta(days=1)
 
        td_1m = td - timedelta(days=calendar.mdays[td.month])
 
        td_1y = td - timedelta(days=365)
 

	
 
        ts_min_m = mktime(td_1m.timetuple())
 
        ts_min_y = mktime(td_1y.timetuple())
 
        ts_max_y = mktime(td.timetuple())
 

	
 
        if dbrepo.enable_statistics:
 
            c.show_stats = True
 
            c.no_data_msg = _('No data loaded yet')
 
            run_task(get_commits_stats, c.dbrepo.repo_name, ts_min_y, ts_max_y)
 
        else:
 
            c.show_stats = False
 
            c.no_data_msg = _('Statistics are disabled for this repository')
 
        c.ts_min = ts_min_m
 
        c.ts_max = ts_max_y
 

	
 
        stats = self.sa.query(Statistics)\
 
            .filter(Statistics.repository == dbrepo)\
 
            .scalar()
 

	
 
        c.stats_percentage = 0
 

	
 
        if stats and stats.languages:
 
            c.no_data = False is dbrepo.enable_statistics
 
            lang_stats_d = json.loads(stats.languages)
 
            c.commit_data = stats.commit_activity
 
            c.overview_data = stats.commit_activity_combined
 

	
 
            lang_stats = ((x, {"count": y,
 
                               "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
 
                          for x, y in lang_stats_d.items())
 

	
 
            c.trending_languages = json.dumps(
 
                sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
 
            )
 
            last_rev = stats.stat_on_revision + 1
 
            c.repo_last_rev = c.rhodecode_repo.count()\
 
                if c.rhodecode_repo.revisions else 0
 
            if last_rev == 0 or c.repo_last_rev == 0:
 
                pass
 
            else:
 
                c.stats_percentage = '%.2f' % ((float((last_rev)) /
 
                                                c.repo_last_rev) * 100)
 
        else:
 
            c.commit_data = json.dumps({})
 
            c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]])
 
            c.trending_languages = json.dumps({})
 
            c.no_data = True
 

	
 
        c.enable_downloads = dbrepo.enable_downloads
 
        if c.enable_downloads:
 
            c.download_options = self._get_download_links(c.rhodecode_repo)
 

	
 
        c.readme_data, c.readme_file = self.__get_readme_data(c.rhodecode_db_repo)
 
        c.readme_data, c.readme_file = self.__get_readme_data(
 
            c.rhodecode_db_repo.repo_name, c.rhodecode_repo
 
        )
 
        return render('summary/summary.html')
 

	
 
    def __get_readme_data(self, repo):
 
    def __get_readme_data(self, repo_name, repo):
 

	
 
        @cache_region('long_term')
 
        def _get_readme_from_cache(key):
 
            readme_data = None
 
            readme_file = None
 
            log.debug('Fetching readme file')
 
            try:
 
                cs = repo.get_changeset('tip')
 
                cs = repo.get_changeset()  # fetches TIP
 
                renderer = MarkupRenderer()
 
                for f in README_FILES:
 
                    try:
 
                        readme = cs.get_node(f)
 
                        readme_file = f
 
                        readme_data = renderer.render(readme.content, f)
 
                        log.debug('Found readme %s' % readme_file)
 
                        break
 
                    except NodeDoesNotExistError:
 
                        continue
 
            except ChangesetError:
 
                log.error(traceback.format_exc())
 
                pass
 
            except EmptyRepositoryError:
 
                pass
 
            except Exception:
 
                log.error(traceback.format_exc())
 

	
 
            return readme_data, readme_file
 

	
 
        key = repo.repo_name + '_README'
 
        key = repo_name + '_README'
 
        inv = CacheInvalidation.invalidate(key)
 
        if inv is not None:
 
            region_invalidate(_get_readme_from_cache, None, key)
 
            CacheInvalidation.set_valid(inv.cache_key)
 
        return _get_readme_from_cache(key)
 

	
 
    def _get_download_links(self, repo):
 

	
 
        download_l = []
 

	
 
        branches_group = ([], _("Branches"))
 
        tags_group = ([], _("Tags"))
 

	
 
        for name, chs in c.rhodecode_repo.branches.items():
 
            #chs = chs.split(':')[-1]
 
            branches_group[0].append((chs, name),)
 
        download_l.append(branches_group)
 

	
 
        for name, chs in c.rhodecode_repo.tags.items():
 
            #chs = chs.split(':')[-1]
 
            tags_group[0].append((chs, name),)
 
        download_l.append(tags_group)
 

	
 
        return download_l
rhodecode/i18n/zh_TW/LC_MESSAGES/rhodecode.mo
Show inline comments
 
new file 100644
 
binary diff not shown
rhodecode/i18n/zh_TW/LC_MESSAGES/rhodecode.po
Show inline comments
 
new file 100644
 
# Translations template for RhodeCode.
 
# Copyright (C) 2011 ORGANIZATION
 
# This file is distributed under the same license as the RhodeCode project.
 
# FIRST AUTHOR <EMAIL@ADDRESS>, 2011.
 
#
 
msgid ""
 
msgstr ""
 
"Project-Id-Version: RhodeCode 1.2.0\n"
 
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
 
"POT-Creation-Date: 2011-09-14 15:50-0300\n"
 
"PO-Revision-Date: 2012-05-09 22:23+0800\n"
 
"Last-Translator: Nansen <nansenat16@gmail.com>\n"
 
"Language-Team: LANGUAGE <LL@li.org>\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-Poedit-Language: Chinese\n"
 
"X-Poedit-Country: TAIWAN\n"
 
"X-Poedit-SourceCharset: utf-8\n"
 

	
 
#: rhodecode/controllers/changeset.py:108
 
#: rhodecode/controllers/changeset.py:149
 
#: rhodecode/controllers/changeset.py:216
 
#: rhodecode/controllers/changeset.py:229
 
msgid "binary file"
 
msgstr "二進位檔"
 

	
 
#: rhodecode/controllers/changeset.py:123
 
#: rhodecode/controllers/changeset.py:168
 
msgid "Changeset is to big and was cut off, see raw changeset instead"
 
msgstr ""
 

	
 
#: rhodecode/controllers/changeset.py:159
 
msgid "Diff is to big and was cut off, see raw diff instead"
 
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:48
 
#, python-format
 
msgid "Changes on %s repository"
 
msgstr "修改於版本庫 %s"
 

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

	
 
#: rhodecode/controllers/files.py:72
 
msgid "There are no files yet"
 
msgstr "尚未有任何檔案"
 

	
 
#: rhodecode/controllers/files.py:262
 
#, python-format
 
msgid "Edited %s via RhodeCode"
 
msgstr "使用 RhodeCode 編輯 %s"
 

	
 
#: rhodecode/controllers/files.py:267
 
#: rhodecode/templates/files/file_diff.html:40
 
msgid "No changes"
 
msgstr "沒有修改"
 

	
 
#: rhodecode/controllers/files.py:278
 
#, python-format
 
msgid "Successfully committed to %s"
 
msgstr "成功遞交至 %s"
 

	
 
#: rhodecode/controllers/files.py:283
 
msgid "Error occurred during commit"
 
msgstr ""
 

	
 
#: rhodecode/controllers/files.py:308
 
msgid "downloads disabled"
 
msgstr "下載已關閉"
 

	
 
#: rhodecode/controllers/files.py:313
 
#, python-format
 
msgid "Unknown revision %s"
 
msgstr "未知修訂 %s"
 

	
 
#: rhodecode/controllers/files.py:315
 
msgid "Empty repository"
 
msgstr "空的版本庫"
 

	
 
#: rhodecode/controllers/files.py:317
 
msgid "Unknown archive type"
 
msgstr "未知的存檔類型"
 

	
 
#: rhodecode/controllers/files.py:385
 
#: rhodecode/controllers/files.py:398
 
msgid "Binary file"
 
msgstr "二進位檔"
 

	
 
#: rhodecode/controllers/files.py:417
 
#: rhodecode/templates/changeset/changeset_range.html:4
 
#: rhodecode/templates/changeset/changeset_range.html:12
 
#: rhodecode/templates/changeset/changeset_range.html:29
 
msgid "Changesets"
 
msgstr "變更"
 

	
 
#: rhodecode/controllers/files.py:418
 
#: rhodecode/controllers/summary.py:175
 
#: rhodecode/templates/branches/branches.html:5
 
#: rhodecode/templates/summary/summary.html:690
 
msgid "Branches"
 
msgstr "分支"
 

	
 
#: rhodecode/controllers/files.py:419
 
#: rhodecode/controllers/summary.py:176
 
#: rhodecode/templates/summary/summary.html:679
 
#: rhodecode/templates/tags/tags.html:5
 
msgid "Tags"
 
msgstr "標籤"
 

	
 
#: rhodecode/controllers/journal.py:50
 
#, python-format
 
msgid "%s public journal %s feed"
 
msgstr "%s 公開日誌 %s feed"
 

	
 
#: rhodecode/controllers/journal.py:178
 
#: rhodecode/controllers/journal.py:212
 
#: rhodecode/templates/admin/repos/repo_edit.html:171
 
#: rhodecode/templates/base/base.html:50
 
msgid "Public journal"
 
msgstr "公開日誌"
 

	
 
#: rhodecode/controllers/login.py:111
 
msgid "You have successfully registered into rhodecode"
 
msgstr "您已經成功註冊rhodecode"
 

	
 
#: rhodecode/controllers/login.py:133
 
msgid "Your password reset link was sent"
 
msgstr "您的密碼重設連結已寄出"
 

	
 
#: rhodecode/controllers/login.py:155
 
msgid "Your password reset was successful, new password has been sent to your email"
 
msgstr "您的密碼重設動作已完成,新的密碼已寄至您的信箱"
 

	
 
#: rhodecode/controllers/search.py:109
 
msgid "Invalid search query. Try quoting it."
 
msgstr "無效的查詢。請使用跳脫字元"
 

	
 
#: rhodecode/controllers/search.py:114
 
msgid "There is no index to search in. Please run whoosh indexer"
 
msgstr "沒有任何索引可以搜尋。請執行 whoosh 建立索引"
 

	
 
#: rhodecode/controllers/search.py:118
 
msgid "An error occurred during this search operation"
 
msgstr ""
 

	
 
#: rhodecode/controllers/settings.py:61
 
#: rhodecode/controllers/settings.py:171
 
#, 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 ""
 

	
 
#: rhodecode/controllers/settings.py:109
 
#: rhodecode/controllers/admin/repos.py:239
 
#, python-format
 
msgid "Repository %s updated successfully"
 
msgstr "版本庫 %s 更新完成"
 

	
 
#: rhodecode/controllers/settings.py:126
 
#: rhodecode/controllers/admin/repos.py:257
 
#, python-format
 
msgid "error occurred during update of repository %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/settings.py:144
 
#: rhodecode/controllers/admin/repos.py:275
 
#, 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 ""
 

	
 
#: rhodecode/controllers/settings.py:156
 
#: rhodecode/controllers/admin/repos.py:287
 
#, python-format
 
msgid "deleted repository %s"
 
msgstr "刪除版本庫 %s"
 

	
 
#: rhodecode/controllers/settings.py:159
 
#: rhodecode/controllers/admin/repos.py:297
 
#: rhodecode/controllers/admin/repos.py:303
 
#, python-format
 
msgid "An error occurred during deletion of %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/settings.py:193
 
#, python-format
 
msgid "forked %s repository as %s"
 
msgstr "forked %s 版本庫為 %s"
 

	
 
#: rhodecode/controllers/settings.py:211
 
#, python-format
 
msgid "An error occurred during repository forking %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/summary.py:123
 
msgid "No data loaded yet"
 
msgstr ""
 

	
 
#: rhodecode/controllers/summary.py:126
 
msgid "Statistics are disabled for this repository"
 
msgstr "這個版本庫的統計功能已停用"
 

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

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

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

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

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

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

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

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

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

	
 
#: rhodecode/controllers/admin/ldap_settings.py:64
 
msgid "LDAPS connection"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:65
 
msgid "START_TLS on LDAP connection"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:115
 
msgid "Ldap settings updated successfully"
 
msgstr "LDAP設定更新完成"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:120
 
msgid "Unable to activate ldap. The \"python-ldap\" library is missing."
 
msgstr "無法啟用LDAP。找不到python-ldap函式庫"
 

	
 
#: rhodecode/controllers/admin/ldap_settings.py:134
 
msgid "error occurred during update of ldap settings"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/permissions.py:56
 
msgid "None"
 
msgstr "無"
 

	
 
#: rhodecode/controllers/admin/permissions.py:57
 
msgid "Read"
 
msgstr "讀"
 

	
 
#: rhodecode/controllers/admin/permissions.py:58
 
msgid "Write"
 
msgstr "寫"
 

	
 
#: rhodecode/controllers/admin/permissions.py:59
 
#: 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:10
 
#: 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:110
 
#: 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:279
 
#: rhodecode/templates/base/base.html:366
 
#: rhodecode/templates/base/base.html:368
 
#: rhodecode/templates/base/base.html:370
 
msgid "Admin"
 
msgstr "管理"
 

	
 
#: rhodecode/controllers/admin/permissions.py:62
 
msgid "disabled"
 
msgstr "停用"
 

	
 
#: rhodecode/controllers/admin/permissions.py:64
 
msgid "allowed with manual account activation"
 
msgstr "允許手動啟用帳號"
 

	
 
#: rhodecode/controllers/admin/permissions.py:66
 
msgid "allowed with automatic account activation"
 
msgstr "允許自動啟用帳號"
 

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

	
 
#: rhodecode/controllers/admin/permissions.py:69
 
msgid "Enabled"
 
msgstr "啟用"
 

	
 
#: rhodecode/controllers/admin/permissions.py:102
 
msgid "Default permissions updated successfully"
 
msgstr "預設權限更新完成"
 

	
 
#: rhodecode/controllers/admin/permissions.py:119
 
msgid "error occurred during update of permissions"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:96
 
#, 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 ""
 

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

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

	
 
#: rhodecode/controllers/admin/repos.py:205
 
#, python-format
 
msgid "error occurred during creation of repository %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:292
 
#, python-format
 
msgid "Cannot delete %s it still contains attached forks"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:320
 
msgid "An error occurred during deletion of repository user"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:335
 
msgid "An error occurred during deletion of repository users groups"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:352
 
msgid "An error occurred during deletion of repository stats"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:367
 
msgid "An error occurred during cache invalidation"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:387
 
msgid "Updated repository visibility in public journal"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:390
 
msgid "An error occurred during setting this repository in public journal"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:395
 
#: rhodecode/model/forms.py:53
 
msgid "Token mismatch"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:408
 
msgid "Pulled from remote location"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos.py:410
 
msgid "An error occurred during pull from remote location"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:83
 
#, python-format
 
msgid "created repos group %s"
 
msgstr "建立版本庫群組 %s"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:96
 
#, python-format
 
msgid "error occurred during creation of repos group %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:130
 
#, python-format
 
msgid "updated repos group %s"
 
msgstr "更新版本庫群組 %s"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:143
 
#, python-format
 
msgid "error occurred during update of repos group %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:164
 
#, python-format
 
msgid "This group contains %s repositores and cannot be deleted"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:171
 
#, python-format
 
msgid "removed repos group %s"
 
msgstr "移除版本庫群組 %s"
 

	
 
#: rhodecode/controllers/admin/repos_groups.py:175
 
#, python-format
 
msgid "error occurred during deletion of repos group %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/settings.py:109
 
#, python-format
 
msgid "Repositories successfully rescanned added: %s,removed: %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/settings.py:118
 
msgid "Whoosh reindex task scheduled"
 
msgstr "Whoosh 重新索引工作排程"
 

	
 
#: rhodecode/controllers/admin/settings.py:143
 
msgid "Updated application settings"
 
msgstr "更新應用設定"
 

	
 
#: rhodecode/controllers/admin/settings.py:148
 
#: rhodecode/controllers/admin/settings.py:215
 
msgid "error occurred during updating application settings"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/settings.py:210
 
msgid "Updated mercurial settings"
 
msgstr "更新 mercurial 設定"
 

	
 
#: rhodecode/controllers/admin/settings.py:236
 
msgid "Added new hook"
 
msgstr "新增hook"
 

	
 
#: rhodecode/controllers/admin/settings.py:247
 
msgid "Updated hooks"
 
msgstr "更新hook"
 

	
 
#: rhodecode/controllers/admin/settings.py:251
 
msgid "error occurred during hook creation"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/settings.py:310
 
msgid "You can't edit this user since it's crucial for entire application"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/settings.py:339
 
msgid "Your account was updated successfully"
 
msgstr "您的帳號已更新完成"
 

	
 
#: rhodecode/controllers/admin/settings.py:359
 
#: rhodecode/controllers/admin/users.py:130
 
#, python-format
 
msgid "error occurred during update of user %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/users.py:78
 
#, python-format
 
msgid "created user %s"
 
msgstr "建立使用者 %s"
 

	
 
#: rhodecode/controllers/admin/users.py:90
 
#, python-format
 
msgid "error occurred during creation of user %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/users.py:116
 
msgid "User updated successfully"
 
msgstr "使用者更新完成"
 

	
 
#: rhodecode/controllers/admin/users.py:146
 
msgid "successfully deleted user"
 
msgstr "成功刪除使用者"
 

	
 
#: rhodecode/controllers/admin/users.py:150
 
msgid "An error occurred during deletion of user"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/users.py:166
 
msgid "You can't edit this user"
 
msgstr "您無法編輯這位使用者"
 

	
 
#: rhodecode/controllers/admin/users.py:195
 
#: rhodecode/controllers/admin/users_groups.py:202
 
msgid "Granted 'repository create' permission to user"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/users.py:204
 
#: rhodecode/controllers/admin/users_groups.py:211
 
msgid "Revoked 'repository create' permission to user"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/users_groups.py:74
 
#, python-format
 
msgid "created users group %s"
 
msgstr "建立使用者群組 %s"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:86
 
#, python-format
 
msgid "error occurred during creation of users group %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/users_groups.py:119
 
#, python-format
 
msgid "updated users group %s"
 
msgstr "更新使用者群組 %s"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:138
 
#, python-format
 
msgid "error occurred during update of users group %s"
 
msgstr ""
 

	
 
#: rhodecode/controllers/admin/users_groups.py:154
 
msgid "successfully deleted users group"
 
msgstr "成功移除使用者群組"
 

	
 
#: rhodecode/controllers/admin/users_groups.py:158
 
msgid "An error occurred during deletion of users group"
 
msgstr ""
 

	
 
#: rhodecode/lib/__init__.py:279
 
msgid "year"
 
msgstr "年"
 

	
 
#: rhodecode/lib/__init__.py:280
 
msgid "month"
 
msgstr "月"
 

	
 
#: rhodecode/lib/__init__.py:281
 
msgid "day"
 
msgstr "日"
 

	
 
#: rhodecode/lib/__init__.py:282
 
msgid "hour"
 
msgstr "時"
 

	
 
#: rhodecode/lib/__init__.py:283
 
msgid "minute"
 
msgstr "分"
 

	
 
#: rhodecode/lib/__init__.py:284
 
msgid "second"
 
msgstr "秒"
 

	
 
#: rhodecode/lib/__init__.py:293
 
msgid "ago"
 
msgstr "之前"
 

	
 
#: rhodecode/lib/__init__.py:296
 
msgid "just now"
 
msgstr "現在"
 

	
 
#: rhodecode/lib/auth.py:377
 
msgid "You need to be a registered user to perform this action"
 
msgstr "您必須是註冊使用者才能執行這個動作"
 

	
 
#: rhodecode/lib/auth.py:421
 
msgid "You need to be a signed in to view this page"
 
msgstr "您必須登入後才能瀏覽這個頁面"
 

	
 
#: rhodecode/lib/helpers.py:307
 
msgid "True"
 
msgstr "真"
 

	
 
#: rhodecode/lib/helpers.py:311
 
msgid "False"
 
msgstr "假"
 

	
 
#: rhodecode/lib/helpers.py:352
 
#, python-format
 
msgid "Show all combined changesets %s->%s"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:356
 
msgid "compare view"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:365
 
msgid "and"
 
msgstr "和"
 

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

	
 
#: rhodecode/lib/helpers.py:367
 
#: rhodecode/templates/changelog/changelog.html:14
 
#: rhodecode/templates/changelog/changelog.html:39
 
msgid "revisions"
 
msgstr "修訂"
 

	
 
#: rhodecode/lib/helpers.py:385
 
msgid "fork name "
 
msgstr "fork 名稱"
 

	
 
#: rhodecode/lib/helpers.py:388
 
msgid "[deleted] repository"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:389
 
#: rhodecode/lib/helpers.py:393
 
msgid "[created] repository"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:390
 
#: rhodecode/lib/helpers.py:394
 
msgid "[forked] repository"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:391
 
#: rhodecode/lib/helpers.py:395
 
msgid "[updated] repository"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:392
 
msgid "[delete] repository"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:396
 
msgid "[pushed] into"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:397
 
msgid "[committed via RhodeCode] into"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:398
 
msgid "[pulled from remote] into"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:399
 
msgid "[pulled] from"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:400
 
msgid "[started following] repository"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:401
 
msgid "[stopped following] repository"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:577
 
#, python-format
 
msgid " and %s more"
 
msgstr ""
 

	
 
#: rhodecode/lib/helpers.py:581
 
msgid "No Files"
 
msgstr "沒有檔案"
 

	
 
#: rhodecode/model/forms.py:66
 
msgid "Invalid username"
 
msgstr "無效的使用者名稱"
 

	
 
#: rhodecode/model/forms.py:75
 
msgid "This username already exists"
 
msgstr "使用者名稱已存在"
 

	
 
#: rhodecode/model/forms.py:79
 
msgid "Username may only contain alphanumeric characters underscores, periods or dashes and must begin with alphanumeric character"
 
msgstr "使用者名稱只能使用字母數字、底線、小數點或破折號,且必須使用數字或字母開頭"
 

	
 
#: rhodecode/model/forms.py:94
 
msgid "Invalid group name"
 
msgstr "無效的群組名稱"
 

	
 
#: rhodecode/model/forms.py:104
 
msgid "This users group already exists"
 
msgstr "這個使用者群組已存在"
 

	
 
#: rhodecode/model/forms.py:110
 
msgid "Group name may only contain alphanumeric characters underscores, periods or dashes and must begin with alphanumeric character"
 
msgstr "群組名稱只能使用字母數字、底線、小數點或破折號,且必須使用數字或字母開頭"
 

	
 
#: rhodecode/model/forms.py:132
 
msgid "Cannot assign this group as parent"
 
msgstr ""
 

	
 
#: rhodecode/model/forms.py:148
 
msgid "This group already exists"
 
msgstr "這個群組已存在"
 

	
 
#: rhodecode/model/forms.py:164
 
#: rhodecode/model/forms.py:172
 
#: rhodecode/model/forms.py:180
 
msgid "Invalid characters in password"
 
msgstr "無效的字元在密碼中"
 

	
 
#: rhodecode/model/forms.py:191
 
msgid "Passwords do not match"
 
msgstr "密碼不相符"
 

	
 
#: rhodecode/model/forms.py:196
 
msgid "invalid password"
 
msgstr "無效的密碼"
 

	
 
#: rhodecode/model/forms.py:197
 
msgid "invalid user name"
 
msgstr "無效的使用者名稱"
 

	
 
#: rhodecode/model/forms.py:198
 
msgid "Your account is disabled"
 
msgstr "您的帳號已被停用"
 

	
 
#: rhodecode/model/forms.py:233
 
msgid "This username is not valid"
 
msgstr "無效的使用者名稱"
 

	
 
#: rhodecode/model/forms.py:245
 
msgid "This repository name is disallowed"
 
msgstr "不允許的版本庫名稱"
 

	
 
#: rhodecode/model/forms.py:266
 
#, python-format
 
msgid "This repository already exists in group \"%s\""
 
msgstr "這個版本庫已存在於群組 \"%s\""
 

	
 
#: rhodecode/model/forms.py:274
 
msgid "This repository already exists"
 
msgstr "這個版本庫已經存在"
 

	
 
#: rhodecode/model/forms.py:312
 
#: rhodecode/model/forms.py:319
 
msgid "invalid clone url"
 
msgstr "無效的複製URL"
 

	
 
#: rhodecode/model/forms.py:322
 
msgid "Invalid clone url, provide a valid clone http\\s url"
 
msgstr ""
 

	
 
#: rhodecode/model/forms.py:334
 
msgid "Fork have to be the same type as original"
 
msgstr "Fork 必須使用相同的版本庫類型"
 

	
 
#: rhodecode/model/forms.py:341
 
msgid "This username or users group name is not valid"
 
msgstr "使用者名稱或群組名稱無效"
 

	
 
#: rhodecode/model/forms.py:403
 
msgid "This is not a valid path"
 
msgstr "不是一個有效的路徑"
 

	
 
#: rhodecode/model/forms.py:416
 
msgid "This e-mail address is already taken"
 
msgstr "這個郵件位址已經使用了"
 

	
 
#: rhodecode/model/forms.py:427
 
msgid "This e-mail address doesn't exist."
 
msgstr "這個郵件位址不存在"
 

	
 
#: rhodecode/model/forms.py:447
 
msgid "The LDAP Login attribute of the CN must be specified - this is the name of the attribute that is equivalent to 'username'"
 
msgstr ""
 

	
 
#: rhodecode/model/forms.py:466
 
msgid "Please enter a login"
 
msgstr "請登入"
 

	
 
#: rhodecode/model/forms.py:467
 
#, python-format
 
msgid "Enter a value %(min)i characters long or more"
 
msgstr ""
 

	
 
#: rhodecode/model/forms.py:475
 
msgid "Please enter a password"
 
msgstr "請輸入密碼"
 

	
 
#: rhodecode/model/forms.py:476
 
#, python-format
 
msgid "Enter %(min)i characters or more"
 
msgstr ""
 

	
 
#: rhodecode/model/user.py:145
 
msgid "[RhodeCode] New User registration"
 
msgstr "[RhodeCode] 新使用者註冊"
 

	
 
#: rhodecode/model/user.py:157
 
#: rhodecode/model/user.py:179
 
msgid "You can't Edit this user since it's crucial for entire application"
 
msgstr "您無法編輯這個使用者,因為他是系統帳號"
 

	
 
#: rhodecode/model/user.py:201
 
msgid "You can't remove this user since it's crucial for entire application"
 
msgstr "您無法移除這個使用者,因為他是系統帳號"
 

	
 
#: rhodecode/model/user.py:204
 
#, python-format
 
msgid "This user still owns %s repositories and cannot be removed. Switch owners or remove those repositories"
 
msgstr "這個使用者擁有 %s 個版本庫所以無法移除,請先變更版本庫擁有者或者刪除版本庫"
 

	
 
#: rhodecode/templates/index.html:4
 
msgid "Dashboard"
 
msgstr "儀表板"
 

	
 
#: rhodecode/templates/index_base.html:22
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:102
 
msgid "quick filter..."
 
msgstr "快速過濾..."
 

	
 
#: rhodecode/templates/index_base.html:23
 
#: rhodecode/templates/base/base.html:300
 
msgid "repositories"
 
msgstr "個版本庫"
 

	
 
#: rhodecode/templates/index_base.html:29
 
#: rhodecode/templates/admin/repos/repos.html:22
 
msgid "ADD NEW REPOSITORY"
 
msgstr "新增版本庫"
 

	
 
#: rhodecode/templates/index_base.html:41
 
#: 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:42
 
#: rhodecode/templates/index_base.html:73
 
#: rhodecode/templates/admin/repos/repo_add_base.html:44
 
#: rhodecode/templates/admin/repos/repo_edit.html:64
 
#: rhodecode/templates/admin/repos/repos.html:31
 
#: 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/settings/repo_fork.html:40
 
#: rhodecode/templates/settings/repo_settings.html:40
 
#: rhodecode/templates/summary/summary.html:92
 
msgid "Description"
 
msgstr "描述"
 

	
 
#: rhodecode/templates/index_base.html:53
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:46
 
msgid "Repositories group"
 
msgstr "版本庫群組"
 

	
 
#: rhodecode/templates/index_base.html:72
 
#: rhodecode/templates/admin/repos/repo_add_base.html:9
 
#: rhodecode/templates/admin/repos/repo_edit.html:32
 
#: rhodecode/templates/admin/repos/repos.html:30
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:117
 
#: rhodecode/templates/files/files_browser.html:157
 
#: rhodecode/templates/settings/repo_settings.html:31
 
#: rhodecode/templates/summary/summary.html:31
 
#: rhodecode/templates/summary/summary.html:107
 
msgid "Name"
 
msgstr "名稱"
 

	
 
#: rhodecode/templates/index_base.html:74
 
#: rhodecode/templates/admin/repos/repos.html:32
 
#: rhodecode/templates/summary/summary.html:114
 
msgid "Last change"
 
msgstr "最後修改"
 

	
 
#: rhodecode/templates/index_base.html:75
 
#: rhodecode/templates/admin/repos/repos.html:33
 
msgid "Tip"
 
msgstr ""
 

	
 
#: rhodecode/templates/index_base.html:76
 
#: rhodecode/templates/admin/repos/repo_edit.html:97
 
msgid "Owner"
 
msgstr "擁有者"
 

	
 
#: rhodecode/templates/index_base.html:77
 
#: rhodecode/templates/journal/public_journal.html:20
 
#: rhodecode/templates/summary/summary.html:180
 
#: rhodecode/templates/summary/summary.html:183
 
msgid "RSS"
 
msgstr ""
 

	
 
#: rhodecode/templates/index_base.html:78
 
#: rhodecode/templates/journal/public_journal.html:23
 
#: rhodecode/templates/summary/summary.html:181
 
#: rhodecode/templates/summary/summary.html:184
 
msgid "Atom"
 
msgstr ""
 

	
 
#: rhodecode/templates/index_base.html:87
 
#: rhodecode/templates/index_base.html:89
 
#: rhodecode/templates/index_base.html:91
 
#: rhodecode/templates/base/base.html:209
 
#: rhodecode/templates/base/base.html:211
 
#: rhodecode/templates/base/base.html:213
 
#: rhodecode/templates/summary/summary.html:4
 
msgid "Summary"
 
msgstr "概況"
 

	
 
#: rhodecode/templates/index_base.html:95
 
#: rhodecode/templates/index_base.html:97
 
#: rhodecode/templates/index_base.html:99
 
#: rhodecode/templates/base/base.html:225
 
#: rhodecode/templates/base/base.html:227
 
#: rhodecode/templates/base/base.html:229
 
#: rhodecode/templates/changelog/changelog.html:6
 
#: rhodecode/templates/changelog/changelog.html:14
 
msgid "Changelog"
 
msgstr "修改紀錄"
 

	
 
#: rhodecode/templates/index_base.html:103
 
#: rhodecode/templates/index_base.html:105
 
#: rhodecode/templates/index_base.html:107
 
#: rhodecode/templates/base/base.html:268
 
#: rhodecode/templates/base/base.html:270
 
#: rhodecode/templates/base/base.html:272
 
#: rhodecode/templates/files/files.html:4
 
msgid "Files"
 
msgstr "檔案"
 

	
 
#: rhodecode/templates/index_base.html:116
 
#: rhodecode/templates/admin/repos/repos.html:42
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:127
 
#: rhodecode/templates/summary/summary.html:48
 
msgid "Mercurial repository"
 
msgstr "Mercurial 版本庫"
 

	
 
#: rhodecode/templates/index_base.html:118
 
#: rhodecode/templates/admin/repos/repos.html:44
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:129
 
#: rhodecode/templates/summary/summary.html:51
 
msgid "Git repository"
 
msgstr "Git 版本庫"
 

	
 
#: rhodecode/templates/index_base.html:123
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:16
 
#: rhodecode/templates/journal/journal.html:53
 
#: rhodecode/templates/summary/summary.html:56
 
msgid "private repository"
 
msgstr "私有版本庫"
 

	
 
#: rhodecode/templates/index_base.html:125
 
#: rhodecode/templates/journal/journal.html:55
 
#: rhodecode/templates/summary/summary.html:58
 
msgid "public repository"
 
msgstr "公開版本庫"
 

	
 
#: rhodecode/templates/index_base.html:133
 
#: rhodecode/templates/base/base.html:291
 
#: rhodecode/templates/settings/repo_fork.html:13
 
msgid "fork"
 
msgstr ""
 

	
 
#: rhodecode/templates/index_base.html:134
 
#: rhodecode/templates/admin/repos/repos.html:60
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:143
 
#: rhodecode/templates/summary/summary.html:69
 
#: rhodecode/templates/summary/summary.html:71
 
msgid "Fork of"
 
msgstr ""
 

	
 
#: rhodecode/templates/index_base.html:155
 
#: rhodecode/templates/admin/repos/repos.html:73
 
msgid "No changesets yet"
 
msgstr "尚未有任何變更"
 

	
 
#: rhodecode/templates/index_base.html:161
 
#: rhodecode/templates/index_base.html:163
 
#, python-format
 
msgid "Subscribe to %s rss feed"
 
msgstr "訂閱 %s rss"
 

	
 
#: rhodecode/templates/index_base.html:168
 
#: rhodecode/templates/index_base.html:170
 
#, python-format
 
msgid "Subscribe to %s atom feed"
 
msgstr "訂閱 %s atom"
 

	
 
#: rhodecode/templates/login.html:5
 
#: rhodecode/templates/login.html:54
 
#: rhodecode/templates/base/base.html:38
 
msgid "Sign In"
 
msgstr "登入"
 

	
 
#: rhodecode/templates/login.html:21
 
msgid "Sign In to"
 
msgstr "登入"
 

	
 
#: rhodecode/templates/login.html:31
 
#: rhodecode/templates/register.html:20
 
#: rhodecode/templates/admin/admin_log.html:5
 
#: rhodecode/templates/admin/users/user_add.html:32
 
#: rhodecode/templates/admin/users/user_edit.html:47
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:45
 
#: rhodecode/templates/base/base.html:15
 
#: rhodecode/templates/summary/summary.html:106
 
msgid "Username"
 
msgstr "帳號"
 

	
 
#: rhodecode/templates/login.html:40
 
#: rhodecode/templates/register.html:29
 
#: rhodecode/templates/admin/ldap/ldap.html:46
 
#: rhodecode/templates/admin/users/user_add.html:41
 
#: rhodecode/templates/base/base.html:24
 
msgid "Password"
 
msgstr "密碼"
 

	
 
#: rhodecode/templates/login.html:60
 
msgid "Forgot your password ?"
 
msgstr "忘記您的密碼?"
 

	
 
#: rhodecode/templates/login.html:63
 
#: rhodecode/templates/base/base.html:35
 
msgid "Don't have an account ?"
 
msgstr "沒有帳號?"
 

	
 
#: rhodecode/templates/password_reset.html:5
 
msgid "Reset your password"
 
msgstr "重設您的密碼"
 

	
 
#: rhodecode/templates/password_reset.html:11
 
msgid "Reset your password to"
 
msgstr "重設您的密碼"
 

	
 
#: rhodecode/templates/password_reset.html:21
 
msgid "Email address"
 
msgstr "郵件位址"
 

	
 
#: rhodecode/templates/password_reset.html:30
 
msgid "Reset my password"
 
msgstr "重設我的密碼"
 

	
 
#: rhodecode/templates/password_reset.html:31
 
msgid "Password reset link will be send to matching email address"
 
msgstr "密碼重設連結已郵寄至您的信箱"
 

	
 
#: rhodecode/templates/register.html:5
 
#: rhodecode/templates/register.html:74
 
msgid "Sign Up"
 
msgstr "登入"
 

	
 
#: rhodecode/templates/register.html:11
 
msgid "Sign Up to"
 
msgstr "登入"
 

	
 
#: rhodecode/templates/register.html:38
 
msgid "Re-enter password"
 
msgstr "確認密碼"
 

	
 
#: rhodecode/templates/register.html:47
 
#: rhodecode/templates/admin/users/user_add.html:50
 
#: rhodecode/templates/admin/users/user_edit.html:74
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:63
 
msgid "First Name"
 
msgstr "名"
 

	
 
#: rhodecode/templates/register.html:56
 
#: rhodecode/templates/admin/users/user_add.html:59
 
#: rhodecode/templates/admin/users/user_edit.html:83
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:72
 
msgid "Last Name"
 
msgstr "姓"
 

	
 
#: rhodecode/templates/register.html:65
 
#: rhodecode/templates/admin/users/user_add.html:68
 
#: rhodecode/templates/admin/users/user_edit.html:92
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:81
 
#: rhodecode/templates/summary/summary.html:108
 
msgid "Email"
 
msgstr "電子郵件"
 

	
 
#: rhodecode/templates/register.html:76
 
msgid "Your account will be activated right after registration"
 
msgstr "您的帳號註冊後將會啟用"
 

	
 
#: rhodecode/templates/register.html:78
 
msgid "Your account must wait for activation by administrator"
 
msgstr "您的帳號註冊後將等待管理員啟用"
 

	
 
#: rhodecode/templates/repo_switcher_list.html:14
 
msgid "Private repository"
 
msgstr "私有的版本庫"
 

	
 
#: rhodecode/templates/repo_switcher_list.html:19
 
msgid "Public repository"
 
msgstr "公開的版本庫"
 

	
 
#: rhodecode/templates/admin/admin.html:5
 
#: rhodecode/templates/admin/admin.html:9
 
msgid "Admin journal"
 
msgstr "管理員日誌"
 

	
 
#: rhodecode/templates/admin/admin_log.html:6
 
msgid "Action"
 
msgstr "動作"
 

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

	
 
#: rhodecode/templates/admin/admin_log.html:8
 
msgid "Date"
 
msgstr "時間"
 

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

	
 
#: rhodecode/templates/admin/admin_log.html:52
 
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 ""
 

	
 
#: 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
 
msgid "Host"
 
msgstr "主機"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:38
 
msgid "Port"
 
msgstr "連接埠"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:42
 
msgid "Account"
 
msgstr "帳號"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:50
 
msgid "Connection security"
 
msgstr "連接安全性"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:54
 
msgid "Certificate Checks"
 
msgstr "憑證確認"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:57
 
msgid "Search settings"
 
msgstr "搜尋選項"
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:59
 
msgid "Base DN"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:63
 
msgid "LDAP Filter"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/ldap/ldap.html:67
 
msgid "LDAP Search Scope"
 
msgstr ""
 

	
 
#: 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/settings/hooks.html:73
 
#: rhodecode/templates/admin/users/user_edit.html:117
 
#: rhodecode/templates/admin/users/user_edit.html:142
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:89
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:263
 
msgid "Save"
 
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:109
 
#: rhodecode/templates/admin/users/user_edit.html:127
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:248
 
#: rhodecode/templates/settings/repo_settings.html:58
 
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 "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/repos.html:10
 
#: 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:80
 
#: rhodecode/templates/summary/summary.html:82
 
msgid "Clone from"
 
msgstr "複製由"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:28
 
#: rhodecode/templates/admin/repos/repo_edit.html:48
 
#: rhodecode/templates/admin/repos_groups/repos_groups.html:4
 
msgid "Repository group"
 
msgstr "版本庫群組"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:36
 
#: rhodecode/templates/admin/repos/repo_edit.html:56
 
msgid "Type"
 
msgstr "類型"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:52
 
#: rhodecode/templates/admin/repos/repo_edit.html:73
 
#: rhodecode/templates/settings/repo_fork.html:48
 
#: rhodecode/templates/settings/repo_settings.html:49
 
msgid "Private"
 
msgstr "私有"
 

	
 
#: rhodecode/templates/admin/repos/repo_add_base.html:59
 
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_my_account.html:148
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:13
 
#: rhodecode/templates/files/files_annotate.html:49
 
#: rhodecode/templates/files/files_source.html:20
 
msgid "edit"
 
msgstr "編輯"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:40
 
msgid "Clone uri"
 
msgstr "複製URL"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:81
 
msgid "Enable statistics"
 
msgstr "啟用統計"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:89
 
msgid "Enable downloads"
 
msgstr "啟用下載"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:127
 
msgid "Administration"
 
msgstr "管理者"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:130
 
msgid "Statistics"
 
msgstr "統計"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:134
 
msgid "Reset current statistics"
 
msgstr "重設目前的統計"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:134
 
msgid "Confirm to remove current statistics"
 
msgstr "確認移除目前的統計"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:137
 
msgid "Fetched to rev"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:138
 
msgid "Percentage of stats gathered"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:147
 
msgid "Remote"
 
msgstr "遠端"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:151
 
msgid "Pull changes from remote location"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:151
 
msgid "Confirm to pull changes from remote side"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:162
 
msgid "Cache"
 
msgstr "快取"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:166
 
msgid "Invalidate repository cache"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:166
 
msgid "Confirm to invalidate repository cache"
 
msgstr "確認廢止版本庫快取"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:177
 
msgid "Remove from public journal"
 
msgstr "從公開日誌移除"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:179
 
msgid "Add to public journal"
 
msgstr "新增至公開日誌"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:185
 
msgid "Delete"
 
msgstr "移除"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:189
 
msgid "Remove this repository"
 
msgstr "移除版本庫"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit.html:189
 
#: rhodecode/templates/admin/repos/repos.html:79
 
msgid "Confirm to delete this repository"
 
msgstr "確認移除這個版本庫"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:3
 
msgid "none"
 
msgstr "無"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:4
 
msgid "read"
 
msgstr "讀"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:5
 
msgid "write"
 
msgstr "寫"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:6
 
#: rhodecode/templates/admin/users/users.html:38
 
#: rhodecode/templates/base/base.html:296
 
msgid "admin"
 
msgstr "管理員"
 

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

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:33
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:53
 
msgid "revoke"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:75
 
msgid "Add another member"
 
msgstr "新增另ㄧ位成員"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:89
 
msgid "Failed to remove user"
 
msgstr "移除使用者失敗"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:104
 
msgid "Failed to remove users group"
 
msgstr "移除使用者群組失敗"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:205
 
msgid "Group"
 
msgstr "群組"
 

	
 
#: rhodecode/templates/admin/repos/repo_edit_perms.html:206
 
#: rhodecode/templates/admin/users_groups/users_groups.html:33
 
msgid "members"
 
msgstr "成員"
 

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

	
 
#: rhodecode/templates/admin/repos/repos.html:34
 
#: rhodecode/templates/summary/summary.html:100
 
msgid "Contact"
 
msgstr "聯絡方式"
 

	
 
#: rhodecode/templates/admin/repos/repos.html:35
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:36
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:119
 
#: rhodecode/templates/admin/users/users.html:40
 
#: rhodecode/templates/admin/users_groups/users_groups.html:35
 
msgid "action"
 
msgstr "動作"
 

	
 
#: rhodecode/templates/admin/repos/repos.html:51
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:134
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:148
 
msgid "private"
 
msgstr "私有"
 

	
 
#: rhodecode/templates/admin/repos/repos.html:53
 
#: rhodecode/templates/admin/repos/repos.html:59
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:136
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:142
 
#: rhodecode/templates/summary/summary.html:68
 
msgid "public"
 
msgstr "公開"
 

	
 
#: rhodecode/templates/admin/repos/repos.html:79
 
#: rhodecode/templates/admin/users/users.html:55
 
msgid "delete"
 
msgstr "刪除"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups.html:8
 
msgid "Groups"
 
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/repos_groups/repos_groups_edit.html:58
 
#: rhodecode/templates/admin/users/user_add.html:85
 
#: rhodecode/templates/admin/users_groups/users_group_add.html:49
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:90
 
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_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 repositories"
 
msgstr "版本庫數量"
 

	
 
#: rhodecode/templates/admin/repos_groups/repos_groups_show.html:54
 
msgid "Confirm to delete this group"
 
msgstr "確認刪除這個群組"
 

	
 
#: 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 "設定管理員"
 

	
 
#: rhodecode/templates/admin/settings/hooks.html:9
 
#: rhodecode/templates/admin/settings/settings.html:9
 
#: rhodecode/templates/settings/repo_settings.html:5
 
#: rhodecode/templates/settings/repo_settings.html:13
 
msgid "Settings"
 
msgstr "設定"
 

	
 
#: rhodecode/templates/admin/settings/hooks.html:24
 
msgid "Built in hooks - read only"
 
msgstr "內建hook - 唯讀"
 

	
 
#: rhodecode/templates/admin/settings/hooks.html:40
 
msgid "Custom hooks"
 
msgstr "自訂hook"
 

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

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

	
 
#: 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:45
 
msgid "Rescan repositories"
 
msgstr "重新掃描版本庫"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:51
 
msgid "Whoosh indexing"
 
msgstr "Whoosh 索引"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:59
 
msgid "index build option"
 
msgstr "索引選項"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:64
 
msgid "build from scratch"
 
msgstr "重頭建立索引"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:70
 
msgid "Reindex"
 
msgstr "重新索引"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:76
 
msgid "Global application settings"
 
msgstr "全域設定"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:85
 
msgid "Application name"
 
msgstr "應用名稱"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:94
 
msgid "Realm text"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/settings/settings.html:103
 
msgid "GA code"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/settings/settings.html:111
 
#: rhodecode/templates/admin/settings/settings.html:177
 
msgid "Save settings"
 
msgstr "儲存設定"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:112
 
#: rhodecode/templates/admin/settings/settings.html:178
 
#: rhodecode/templates/admin/users/user_edit.html:118
 
#: rhodecode/templates/admin/users/user_edit.html:143
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:90
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:264
 
#: rhodecode/templates/files/files_edit.html:50
 
msgid "Reset"
 
msgstr "重設"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:118
 
msgid "Mercurial settings"
 
msgstr "Mercurial 設定"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:127
 
msgid "Web"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/settings/settings.html:132
 
msgid "require ssl for pushing"
 
msgstr "推送時要求使用SSL"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:139
 
msgid "Hooks"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/settings/settings.html:142
 
msgid "advanced setup"
 
msgstr "進階設定"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:147
 
msgid "Update repository after push (hg update)"
 
msgstr "push後更新版本庫 (hg update)"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:151
 
msgid "Show repository size after push"
 
msgstr "push 後顯示版本庫大小"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:155
 
msgid "Log user push commands"
 
msgstr "紀錄使用者推送命令"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:159
 
msgid "Log user pull commands"
 
msgstr "紀錄使用者抓取命令"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:166
 
msgid "Repositories location"
 
msgstr "版本庫路徑"
 

	
 
#: rhodecode/templates/admin/settings/settings.html:171
 
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:172
 
msgid "unlock"
 
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
 
#: rhodecode/templates/admin/users/users.html:9
 
msgid "Users"
 
msgstr "使用者"
 

	
 
#: rhodecode/templates/admin/users/user_add.html:12
 
msgid "add new user"
 
msgstr "新增使用者"
 

	
 
#: rhodecode/templates/admin/users/user_add.html:77
 
#: rhodecode/templates/admin/users/user_edit.html:101
 
#: rhodecode/templates/admin/users_groups/users_group_add.html:41
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:42
 
msgid "Active"
 
msgstr "啟用"
 

	
 
#: rhodecode/templates/admin/users/user_edit.html:5
 
msgid "Edit user"
 
msgstr "編輯使用者"
 

	
 
#: rhodecode/templates/admin/users/user_edit.html:33
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:32
 
msgid "Change your avatar at"
 
msgstr "修改您的頭像於"
 

	
 
#: rhodecode/templates/admin/users/user_edit.html:34
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:33
 
msgid "Using"
 
msgstr "使用中"
 

	
 
#: rhodecode/templates/admin/users/user_edit.html:40
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:39
 
msgid "API key"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/users/user_edit.html:56
 
msgid "LDAP DN"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/users/user_edit.html:65
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:54
 
msgid "New password"
 
msgstr "新密碼"
 

	
 
#: rhodecode/templates/admin/users/user_edit.html:135
 
#: rhodecode/templates/admin/users_groups/users_group_edit.html:256
 
msgid "Create repositories"
 
msgstr "建立版本庫"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:5
 
msgid "My account"
 
msgstr "我的帳號"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:9
 
msgid "My Account"
 
msgstr "我的帳號"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:101
 
msgid "My repositories"
 
msgstr "我的版本庫"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:107
 
msgid "ADD REPOSITORY"
 
msgstr "新增版本庫"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:118
 
#: rhodecode/templates/branches/branches_data.html:7
 
#: rhodecode/templates/shortlog/shortlog_data.html:8
 
#: rhodecode/templates/tags/tags_data.html:7
 
msgid "revision"
 
msgstr "修訂"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:157
 
msgid "No repositories yet"
 
msgstr "沒有任何版本庫"
 

	
 
#: rhodecode/templates/admin/users/user_edit_my_account.html:159
 
msgid "create one now"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/users/users.html:5
 
msgid "Users administration"
 
msgstr "使用者管理員"
 

	
 
#: rhodecode/templates/admin/users/users.html:23
 
msgid "ADD NEW USER"
 
msgstr "新增使用者"
 

	
 
#: rhodecode/templates/admin/users/users.html:33
 
msgid "username"
 
msgstr "使用者名稱"
 

	
 
#: rhodecode/templates/admin/users/users.html:34
 
#: rhodecode/templates/branches/branches_data.html:5
 
#: rhodecode/templates/tags/tags_data.html:5
 
msgid "name"
 
msgstr "名字"
 

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

	
 
#: rhodecode/templates/admin/users/users.html:36
 
msgid "last login"
 
msgstr "最後登入"
 

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

	
 
#: rhodecode/templates/admin/users/users.html:39
 
#: rhodecode/templates/base/base.html:305
 
msgid "ldap"
 
msgstr ""
 

	
 
#: rhodecode/templates/admin/users/users.html:56
 
msgid "Confirm to delete this user"
 
msgstr "確認刪除這個使用者"
 

	
 
#: 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_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/base/base.html:32
 
msgid "Forgot password ?"
 
msgstr "忘記密碼?"
 

	
 
#: rhodecode/templates/base/base.html:57
 
#: rhodecode/templates/base/base.html:338
 
#: rhodecode/templates/base/base.html:340
 
#: rhodecode/templates/base/base.html:342
 
msgid "Home"
 
msgstr "首頁"
 

	
 
#: rhodecode/templates/base/base.html:61
 
#: rhodecode/templates/base/base.html:347
 
#: rhodecode/templates/base/base.html:349
 
#: rhodecode/templates/base/base.html:351
 
#: rhodecode/templates/journal/journal.html:4
 
#: rhodecode/templates/journal/journal.html:17
 
#: rhodecode/templates/journal/public_journal.html:4
 
msgid "Journal"
 
msgstr "日誌"
 

	
 
#: rhodecode/templates/base/base.html:66
 
msgid "Login"
 
msgstr "登入"
 

	
 
#: rhodecode/templates/base/base.html:68
 
msgid "Log Out"
 
msgstr "登出"
 

	
 
#: rhodecode/templates/base/base.html:107
 
msgid "Submit a bug"
 
msgstr "回報錯誤"
 

	
 
#: rhodecode/templates/base/base.html:141
 
msgid "Switch repository"
 
msgstr "切換版本庫"
 

	
 
#: rhodecode/templates/base/base.html:143
 
msgid "Products"
 
msgstr ""
 

	
 
#: rhodecode/templates/base/base.html:149
 
msgid "loading..."
 
msgstr "載入中..."
 

	
 
#: rhodecode/templates/base/base.html:234
 
#: rhodecode/templates/base/base.html:236
 
#: rhodecode/templates/base/base.html:238
 
msgid "Switch to"
 
msgstr "切換至"
 

	
 
#: rhodecode/templates/base/base.html:242
 
#: rhodecode/templates/branches/branches.html:13
 
msgid "branches"
 
msgstr "分支"
 

	
 
#: rhodecode/templates/base/base.html:249
 
#: rhodecode/templates/branches/branches_data.html:52
 
msgid "There are no branches yet"
 
msgstr "沒有任何分支"
 

	
 
#: rhodecode/templates/base/base.html:254
 
#: rhodecode/templates/shortlog/shortlog_data.html:10
 
#: rhodecode/templates/tags/tags.html:14
 
msgid "tags"
 
msgstr "標籤"
 

	
 
#: rhodecode/templates/base/base.html:261
 
#: rhodecode/templates/tags/tags_data.html:32
 
msgid "There are no tags yet"
 
msgstr "沒有任何標籤"
 

	
 
#: rhodecode/templates/base/base.html:277
 
#: rhodecode/templates/base/base.html:281
 
#: rhodecode/templates/files/files_annotate.html:40
 
#: rhodecode/templates/files/files_source.html:11
 
msgid "Options"
 
msgstr "選項"
 

	
 
#: rhodecode/templates/base/base.html:286
 
#: rhodecode/templates/base/base.html:288
 
#: rhodecode/templates/base/base.html:306
 
msgid "settings"
 
msgstr "設定"
 

	
 
#: rhodecode/templates/base/base.html:292
 
msgid "search"
 
msgstr "搜尋"
 

	
 
#: rhodecode/templates/base/base.html:299
 
msgid "journal"
 
msgstr "日誌"
 

	
 
#: rhodecode/templates/base/base.html:301
 
msgid "repositories groups"
 
msgstr "版本庫群組"
 

	
 
#: rhodecode/templates/base/base.html:302
 
msgid "users"
 
msgstr "使用者"
 

	
 
#: rhodecode/templates/base/base.html:303
 
msgid "users groups"
 
msgstr "使用者群組"
 

	
 
#: rhodecode/templates/base/base.html:304
 
msgid "permissions"
 
msgstr "權限"
 

	
 
#: rhodecode/templates/base/base.html:317
 
#: rhodecode/templates/base/base.html:319
 
#: rhodecode/templates/followers/followers.html:5
 
msgid "Followers"
 
msgstr "追蹤者"
 

	
 
#: rhodecode/templates/base/base.html:325
 
#: rhodecode/templates/base/base.html:327
 
#: rhodecode/templates/forks/forks.html:5
 
msgid "Forks"
 
msgstr ""
 

	
 
#: rhodecode/templates/base/base.html:356
 
#: rhodecode/templates/base/base.html:358
 
#: rhodecode/templates/base/base.html:360
 
#: rhodecode/templates/search/search.html:4
 
#: rhodecode/templates/search/search.html:24
 
#: rhodecode/templates/search/search.html:46
 
msgid "Search"
 
msgstr "搜尋"
 

	
 
#: rhodecode/templates/base/root.html:57
 
#: rhodecode/templates/journal/journal.html:48
 
#: rhodecode/templates/summary/summary.html:36
 
msgid "Stop following this repository"
 
msgstr "停止追蹤這個版本庫"
 

	
 
#: rhodecode/templates/base/root.html:66
 
#: rhodecode/templates/summary/summary.html:40
 
msgid "Start following this repository"
 
msgstr "開始追蹤這個版本庫"
 

	
 
#: rhodecode/templates/branches/branches_data.html:4
 
#: rhodecode/templates/tags/tags_data.html:4
 
msgid "date"
 
msgstr "日期"
 

	
 
#: rhodecode/templates/branches/branches_data.html:6
 
#: rhodecode/templates/shortlog/shortlog_data.html:7
 
#: rhodecode/templates/tags/tags_data.html:6
 
msgid "author"
 
msgstr "作者"
 

	
 
#: rhodecode/templates/branches/branches_data.html:8
 
#: rhodecode/templates/shortlog/shortlog_data.html:11
 
#: rhodecode/templates/tags/tags_data.html:8
 
msgid "links"
 
msgstr "連結"
 

	
 
#: rhodecode/templates/branches/branches_data.html:23
 
#: rhodecode/templates/branches/branches_data.html:43
 
#: rhodecode/templates/shortlog/shortlog_data.html:39
 
#: rhodecode/templates/tags/tags_data.html:24
 
msgid "changeset"
 
msgstr "修改"
 

	
 
#: rhodecode/templates/branches/branches_data.html:25
 
#: rhodecode/templates/branches/branches_data.html:45
 
#: rhodecode/templates/files/files.html:12
 
#: rhodecode/templates/shortlog/shortlog_data.html:41
 
#: rhodecode/templates/summary/summary.html:233
 
#: rhodecode/templates/tags/tags_data.html:26
 
msgid "files"
 
msgstr "檔案"
 

	
 
#: rhodecode/templates/changelog/changelog.html:14
 
msgid "showing "
 
msgstr ""
 

	
 
#: rhodecode/templates/changelog/changelog.html:14
 
msgid "out of"
 
msgstr ""
 

	
 
#: rhodecode/templates/changelog/changelog.html:37
 
msgid "Show"
 
msgstr "顯示"
 

	
 
#: rhodecode/templates/changelog/changelog.html:50
 
#: rhodecode/templates/changeset/changeset.html:42
 
#: rhodecode/templates/summary/summary.html:609
 
msgid "commit"
 
msgstr "遞交"
 

	
 
#: rhodecode/templates/changelog/changelog.html:63
 
msgid "Affected number of files, click to show more details"
 
msgstr ""
 

	
 
#: rhodecode/templates/changelog/changelog.html:67
 
#: rhodecode/templates/changeset/changeset.html:66
 
msgid "merge"
 
msgstr "合併"
 

	
 
#: rhodecode/templates/changelog/changelog.html:72
 
#: rhodecode/templates/changeset/changeset.html:72
 
msgid "Parent"
 
msgstr ""
 

	
 
#: rhodecode/templates/changelog/changelog.html:77
 
#: rhodecode/templates/changeset/changeset.html:77
 
msgid "No parents"
 
msgstr ""
 

	
 
#: rhodecode/templates/changelog/changelog.html:82
 
#: rhodecode/templates/changeset/changeset.html:80
 
#: rhodecode/templates/files/files.html:29
 
#: rhodecode/templates/files/files_annotate.html:25
 
#: rhodecode/templates/files/files_edit.html:33
 
#: rhodecode/templates/shortlog/shortlog_data.html:9
 
msgid "branch"
 
msgstr "分支"
 

	
 
#: rhodecode/templates/changelog/changelog.html:86
 
#: rhodecode/templates/changeset/changeset.html:83
 
msgid "tag"
 
msgstr "標籤"
 

	
 
#: rhodecode/templates/changelog/changelog.html:122
 
msgid "Show selected changes __S -> __E"
 
msgstr ""
 

	
 
#: rhodecode/templates/changelog/changelog.html:172
 
#: rhodecode/templates/shortlog/shortlog_data.html:61
 
msgid "There are no changes yet"
 
msgstr "尚未有任何變更"
 

	
 
#: rhodecode/templates/changelog/changelog_details.html:2
 
#: rhodecode/templates/changeset/changeset.html:55
 
msgid "removed"
 
msgstr "移除"
 

	
 
#: rhodecode/templates/changelog/changelog_details.html:3
 
#: rhodecode/templates/changeset/changeset.html:56
 
msgid "changed"
 
msgstr "修改"
 

	
 
#: rhodecode/templates/changelog/changelog_details.html:4
 
#: rhodecode/templates/changeset/changeset.html:57
 
msgid "added"
 
msgstr "新增"
 

	
 
#: rhodecode/templates/changelog/changelog_details.html:6
 
#: rhodecode/templates/changelog/changelog_details.html:7
 
#: rhodecode/templates/changelog/changelog_details.html:8
 
#: rhodecode/templates/changeset/changeset.html:59
 
#: rhodecode/templates/changeset/changeset.html:60
 
#: rhodecode/templates/changeset/changeset.html:61
 
#, python-format
 
msgid "affected %s files"
 
msgstr ""
 

	
 
#: rhodecode/templates/changeset/changeset.html:6
 
#: rhodecode/templates/changeset/changeset.html:14
 
#: rhodecode/templates/changeset/changeset.html:31
 
msgid "Changeset"
 
msgstr ""
 

	
 
#: rhodecode/templates/changeset/changeset.html:32
 
#: rhodecode/templates/changeset/changeset.html:121
 
#: rhodecode/templates/changeset/changeset_range.html:78
 
#: rhodecode/templates/files/file_diff.html:32
 
#: rhodecode/templates/files/file_diff.html:42
 
msgid "raw diff"
 
msgstr "原始差異"
 

	
 
#: rhodecode/templates/changeset/changeset.html:34
 
#: rhodecode/templates/changeset/changeset.html:123
 
#: rhodecode/templates/changeset/changeset_range.html:80
 
#: rhodecode/templates/files/file_diff.html:34
 
msgid "download diff"
 
msgstr "下載差異"
 

	
 
#: rhodecode/templates/changeset/changeset.html:90
 
#, python-format
 
msgid "%s files affected with %s additions and %s deletions."
 
msgstr ""
 

	
 
#: rhodecode/templates/changeset/changeset.html:101
 
msgid "Changeset was too big and was cut off..."
 
msgstr ""
 

	
 
#: rhodecode/templates/changeset/changeset.html:119
 
#: rhodecode/templates/changeset/changeset_range.html:76
 
#: rhodecode/templates/files/file_diff.html:30
 
msgid "diff"
 
msgstr "差異"
 

	
 
#: rhodecode/templates/changeset/changeset.html:132
 
#: rhodecode/templates/changeset/changeset_range.html:89
 
msgid "No changes in this file"
 
msgstr "這個檔案沒有任何變更"
 

	
 
#: rhodecode/templates/changeset/changeset_range.html:30
 
msgid "Compare View"
 
msgstr "比較顯示"
 

	
 
#: rhodecode/templates/changeset/changeset_range.html:52
 
msgid "Files affected"
 
msgstr ""
 

	
 
#: rhodecode/templates/errors/error_document.html:44
 
#, python-format
 
msgid "You will be redirected to %s in %s seconds"
 
msgstr ""
 

	
 
#: rhodecode/templates/files/file_diff.html:4
 
#: rhodecode/templates/files/file_diff.html:12
 
msgid "File diff"
 
msgstr "檔案差異"
 

	
 
#: rhodecode/templates/files/file_diff.html:42
 
msgid "Diff is to big to display"
 
msgstr ""
 

	
 
#: rhodecode/templates/files/files.html:37
 
#: rhodecode/templates/files/files_annotate.html:31
 
#: rhodecode/templates/files/files_edit.html:39
 
msgid "Location"
 
msgstr "位置"
 

	
 
#: rhodecode/templates/files/files.html:46
 
msgid "Go back"
 
msgstr ""
 

	
 
#: rhodecode/templates/files/files.html:47
 
msgid "No files at given path"
 
msgstr ""
 

	
 
#: rhodecode/templates/files/files_annotate.html:4
 
msgid "File annotate"
 
msgstr "檔案註釋"
 

	
 
#: rhodecode/templates/files/files_annotate.html:12
 
msgid "annotate"
 
msgstr "註釋"
 

	
 
#: rhodecode/templates/files/files_annotate.html:33
 
#: rhodecode/templates/files/files_browser.html:160
 
#: rhodecode/templates/files/files_source.html:2
 
msgid "Revision"
 
msgstr "修訂"
 

	
 
#: rhodecode/templates/files/files_annotate.html:36
 
#: rhodecode/templates/files/files_browser.html:158
 
#: rhodecode/templates/files/files_source.html:7
 
msgid "Size"
 
msgstr "大小"
 

	
 
#: rhodecode/templates/files/files_annotate.html:38
 
#: rhodecode/templates/files/files_browser.html:159
 
#: rhodecode/templates/files/files_source.html:9
 
msgid "Mimetype"
 
msgstr ""
 

	
 
#: rhodecode/templates/files/files_annotate.html:41
 
msgid "show source"
 
msgstr "顯示原始碼"
 

	
 
#: rhodecode/templates/files/files_annotate.html:43
 
#: rhodecode/templates/files/files_annotate.html:78
 
#: rhodecode/templates/files/files_source.html:14
 
#: rhodecode/templates/files/files_source.html:51
 
msgid "show as raw"
 
msgstr "顯示原始文件"
 

	
 
#: rhodecode/templates/files/files_annotate.html:45
 
#: rhodecode/templates/files/files_source.html:16
 
msgid "download as raw"
 
msgstr "下載原始文件"
 

	
 
#: rhodecode/templates/files/files_annotate.html:54
 
#: rhodecode/templates/files/files_source.html:25
 
msgid "History"
 
msgstr "歷史"
 

	
 
#: rhodecode/templates/files/files_annotate.html:73
 
#: rhodecode/templates/files/files_source.html:46
 
#, python-format
 
msgid "Binary file (%s)"
 
msgstr "二進位檔 (%s)"
 

	
 
#: rhodecode/templates/files/files_annotate.html:78
 
#: rhodecode/templates/files/files_source.html:51
 
msgid "File is too big to display"
 
msgstr "顯示的檔案太大"
 

	
 
#: rhodecode/templates/files/files_browser.html:13
 
msgid "view"
 
msgstr "顯示"
 

	
 
#: rhodecode/templates/files/files_browser.html:14
 
msgid "previous revision"
 
msgstr "前一個修訂"
 

	
 
#: rhodecode/templates/files/files_browser.html:16
 
msgid "next revision"
 
msgstr "下一個修訂"
 

	
 
#: rhodecode/templates/files/files_browser.html:23
 
msgid "follow current branch"
 
msgstr ""
 

	
 
#: rhodecode/templates/files/files_browser.html:27
 
msgid "search file list"
 
msgstr "搜尋檔案列表"
 

	
 
#: rhodecode/templates/files/files_browser.html:32
 
msgid "Loading file list..."
 
msgstr "載入檔案列表..."
 

	
 
#: rhodecode/templates/files/files_browser.html:111
 
msgid "search truncated"
 
msgstr ""
 

	
 
#: rhodecode/templates/files/files_browser.html:122
 
msgid "no matching files"
 
msgstr "無符合的檔案"
 

	
 
#: rhodecode/templates/files/files_browser.html:161
 
msgid "Last modified"
 
msgstr "最後修改"
 

	
 
#: rhodecode/templates/files/files_browser.html:162
 
msgid "Last commiter"
 
msgstr "最後的遞交者"
 

	
 
#: rhodecode/templates/files/files_edit.html:4
 
msgid "Edit file"
 
msgstr "編輯檔案"
 

	
 
#: rhodecode/templates/files/files_edit.html:19
 
msgid "edit file"
 
msgstr "編輯檔案"
 

	
 
#: rhodecode/templates/files/files_edit.html:45
 
#: rhodecode/templates/shortlog/shortlog_data.html:5
 
msgid "commit message"
 
msgstr "遞交資訊"
 

	
 
#: rhodecode/templates/files/files_edit.html:51
 
msgid "Commit changes"
 
msgstr "遞交修改"
 

	
 
#: rhodecode/templates/files/files_source.html:12
 
msgid "show annotation"
 
msgstr "險是註釋"
 

	
 
#: rhodecode/templates/files/files_source.html:153
 
msgid "Selection link"
 
msgstr ""
 

	
 
#: rhodecode/templates/followers/followers.html:13
 
msgid "followers"
 
msgstr "追蹤者"
 

	
 
#: rhodecode/templates/followers/followers_data.html:12
 
msgid "Started following"
 
msgstr "開始追蹤"
 

	
 
#: rhodecode/templates/forks/forks.html:13
 
msgid "forks"
 
msgstr "分支"
 

	
 
#: rhodecode/templates/forks/forks_data.html:17
 
msgid "forked"
 
msgstr "已建立分支"
 

	
 
#: rhodecode/templates/forks/forks_data.html:34
 
msgid "There are no forks yet"
 
msgstr "尚未有任何 fork"
 

	
 
#: rhodecode/templates/journal/journal.html:34
 
msgid "Following"
 
msgstr "已追蹤"
 

	
 
#: rhodecode/templates/journal/journal.html:41
 
msgid "following user"
 
msgstr "追蹤使用者"
 

	
 
#: rhodecode/templates/journal/journal.html:41
 
msgid "user"
 
msgstr "使用者"
 

	
 
#: rhodecode/templates/journal/journal.html:65
 
msgid "You are not following any users or repositories"
 
msgstr "您尚未追蹤任何使用者或版本庫"
 

	
 
#: rhodecode/templates/journal/journal_data.html:46
 
msgid "No entries yet"
 
msgstr ""
 

	
 
#: rhodecode/templates/journal/public_journal.html:17
 
msgid "Public Journal"
 
msgstr "開放日誌"
 

	
 
#: rhodecode/templates/search/search.html:7
 
#: rhodecode/templates/search/search.html:26
 
msgid "in repository: "
 
msgstr "於版本庫:"
 

	
 
#: rhodecode/templates/search/search.html:9
 
#: rhodecode/templates/search/search.html:28
 
msgid "in all repositories"
 
msgstr "於所有的版本庫"
 

	
 
#: rhodecode/templates/search/search.html:42
 
msgid "Search term"
 
msgstr "搜尋關鍵字"
 

	
 
#: rhodecode/templates/search/search.html:54
 
msgid "Search in"
 
msgstr "搜尋範圍"
 

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

	
 
#: rhodecode/templates/search/search.html:59
 
msgid "File names"
 
msgstr "檔案名稱"
 

	
 
#: rhodecode/templates/search/search_content.html:20
 
#: rhodecode/templates/search/search_path.html:15
 
msgid "Permission denied"
 
msgstr "權限不足"
 

	
 
#: rhodecode/templates/settings/repo_fork.html:5
 
msgid "Fork"
 
msgstr "分支"
 

	
 
#: rhodecode/templates/settings/repo_fork.html:31
 
msgid "Fork name"
 
msgstr "分支名稱"
 

	
 
#: rhodecode/templates/settings/repo_fork.html:55
 
msgid "fork this repository"
 
msgstr "fork 這個版本庫"
 

	
 
#: rhodecode/templates/shortlog/shortlog.html:5
 
#: rhodecode/templates/summary/summary.html:666
 
msgid "Shortlog"
 
msgstr "簡短紀錄"
 

	
 
#: rhodecode/templates/shortlog/shortlog.html:14
 
msgid "shortlog"
 
msgstr "簡短紀錄"
 

	
 
#: rhodecode/templates/shortlog/shortlog_data.html:6
 
msgid "age"
 
msgstr ""
 

	
 
#: rhodecode/templates/summary/summary.html:12
 
msgid "summary"
 
msgstr "概況"
 

	
 
#: rhodecode/templates/summary/summary.html:79
 
msgid "remote clone"
 
msgstr "遠端複製"
 

	
 
#: rhodecode/templates/summary/summary.html:121
 
msgid "by"
 
msgstr ""
 

	
 
#: rhodecode/templates/summary/summary.html:128
 
msgid "Clone url"
 
msgstr "複製連結"
 

	
 
#: rhodecode/templates/summary/summary.html:137
 
msgid "Trending source files"
 
msgstr ""
 

	
 
#: rhodecode/templates/summary/summary.html:146
 
msgid "Download"
 
msgstr "下載"
 

	
 
#: rhodecode/templates/summary/summary.html:150
 
msgid "There are no downloads yet"
 
msgstr "沒有任何下載"
 

	
 
#: rhodecode/templates/summary/summary.html:152
 
msgid "Downloads are disabled for this repository"
 
msgstr "這個版本庫的下載已停用"
 

	
 
#: rhodecode/templates/summary/summary.html:154
 
#: rhodecode/templates/summary/summary.html:320
 
msgid "enable"
 
msgstr "啟用"
 

	
 
#: rhodecode/templates/summary/summary.html:162
 
#: rhodecode/templates/summary/summary.html:297
 
#, python-format
 
msgid "Download %s as %s"
 
msgstr "下載 %s 為 %s"
 

	
 
#: rhodecode/templates/summary/summary.html:168
 
msgid "Check this to download archive with subrepos"
 
msgstr ""
 

	
 
#: rhodecode/templates/summary/summary.html:168
 
msgid "with subrepos"
 
msgstr ""
 

	
 
#: rhodecode/templates/summary/summary.html:176
 
msgid "Feeds"
 
msgstr ""
 

	
 
#: rhodecode/templates/summary/summary.html:257
 
#: rhodecode/templates/summary/summary.html:684
 
#: rhodecode/templates/summary/summary.html:695
 
msgid "show more"
 
msgstr "顯示更多"
 

	
 
#: rhodecode/templates/summary/summary.html:312
 
msgid "Commit activity by day / author"
 
msgstr ""
 

	
 
#: rhodecode/templates/summary/summary.html:324
 
msgid "Loaded in"
 
msgstr ""
 

	
 
#: rhodecode/templates/summary/summary.html:603
 
msgid "commits"
 
msgstr "遞交"
 

	
 
#: rhodecode/templates/summary/summary.html:604
 
msgid "files added"
 
msgstr "多個檔案新增"
 

	
 
#: rhodecode/templates/summary/summary.html:605
 
msgid "files changed"
 
msgstr "多個檔案修改"
 

	
 
#: rhodecode/templates/summary/summary.html:606
 
msgid "files removed"
 
msgstr "移除多個檔案"
 

	
 
#: rhodecode/templates/summary/summary.html:610
 
msgid "file added"
 
msgstr "檔案新增"
 

	
 
#: rhodecode/templates/summary/summary.html:611
 
msgid "file changed"
 
msgstr "檔案修改"
 

	
 
#: rhodecode/templates/summary/summary.html:612
 
msgid "file removed"
 
msgstr "移除檔案"
 

	
rhodecode/lib/auth.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.lib.auth
 
    ~~~~~~~~~~~~~~~~~~
 

	
 
    authentication and permission libraries
 

	
 
    :created_on: Apr 4, 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 random
 
import logging
 
import traceback
 
import hashlib
 

	
 
from tempfile import _RandomNameSequence
 
from decorator import decorator
 

	
 
from pylons import config, url, request
 
from pylons.controllers.util import abort, redirect
 
from pylons.i18n.translation import _
 

	
 
from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
 
from rhodecode.model.meta import Session
 

	
 
if __platform__ in PLATFORM_WIN:
 
    from hashlib import sha256
 
if __platform__ in PLATFORM_OTHERS:
 
    import bcrypt
 

	
 
from rhodecode.lib.utils2 import str2bool, safe_unicode
 
from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
 
from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
 
from rhodecode.lib.auth_ldap import AuthLdap
 

	
 
from rhodecode.model import meta
 
from rhodecode.model.user import UserModel
 
from rhodecode.model.db import Permission, RhodeCodeSetting, User
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class PasswordGenerator(object):
 
    """
 
    This is a simple class for generating password from different sets of
 
    characters
 
    usage::
 

	
 
        passwd_gen = PasswordGenerator()
 
        #print 8-letter password containing only big and small letters
 
            of alphabet
 
        print passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
 
        passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
 
    """
 
    ALPHABETS_NUM = r'''1234567890'''
 
    ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
 
    ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
 
    ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
 
    ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
 
        + ALPHABETS_NUM + ALPHABETS_SPECIAL
 
    ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
 
    ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
 
    ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
 
    ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
 

	
 
    def __init__(self, passwd=''):
 
        self.passwd = passwd
 

	
 
    def gen_password(self, length, type_=None):
 
        if type_ is None:
 
            type_ = self.ALPHABETS_FULL
 
        self.passwd = ''.join([random.choice(type_) for _ in xrange(length)])
 
        return self.passwd
 

	
 

	
 
class RhodeCodeCrypto(object):
 

	
 
    @classmethod
 
    def hash_string(cls, str_):
 
        """
 
        Cryptographic function used for password hashing based on pybcrypt
 
        or pycrypto in windows
 

	
 
        :param password: password to hash
 
        """
 
        if __platform__ in PLATFORM_WIN:
 
            return sha256(str_).hexdigest()
 
        elif __platform__ in PLATFORM_OTHERS:
 
            return bcrypt.hashpw(str_, bcrypt.gensalt(10))
 
        else:
 
            raise Exception('Unknown or unsupported platform %s' \
 
                            % __platform__)
 

	
 
    @classmethod
 
    def hash_check(cls, password, hashed):
 
        """
 
        Checks matching password with it's hashed value, runs different
 
        implementation based on platform it runs on
 

	
 
        :param password: password
 
        :param hashed: password in hashed form
 
        """
 

	
 
        if __platform__ in PLATFORM_WIN:
 
            return sha256(password).hexdigest() == hashed
 
        elif __platform__ in PLATFORM_OTHERS:
 
            return bcrypt.hashpw(password, hashed) == hashed
 
        else:
 
            raise Exception('Unknown or unsupported platform %s' \
 
                            % __platform__)
 

	
 

	
 
def get_crypt_password(password):
 
    return RhodeCodeCrypto.hash_string(password)
 

	
 

	
 
def check_password(password, hashed):
 
    return RhodeCodeCrypto.hash_check(password, hashed)
 

	
 

	
 
def generate_api_key(str_, salt=None):
 
    """
 
    Generates API KEY from given string
 

	
 
    :param str_:
 
    :param salt:
 
    """
 

	
 
    if salt is None:
 
        salt = _RandomNameSequence().next()
 

	
 
    return hashlib.sha1(str_ + salt).hexdigest()
 

	
 

	
 
def authfunc(environ, username, password):
 
    """
 
    Dummy authentication wrapper function used in Mercurial and Git for
 
    access control.
 

	
 
    :param environ: needed only for using in Basic auth
 
    """
 
    return authenticate(username, password)
 

	
 

	
 
def authenticate(username, password):
 
    """
 
    Authentication function used for access control,
 
    firstly checks for db authentication then if ldap is enabled for ldap
 
    authentication, also creates ldap user if not in database
 

	
 
    :param username: username
 
    :param password: password
 
    """
 

	
 
    user_model = UserModel()
 
    user = User.get_by_username(username)
 

	
 
    log.debug('Authenticating user using RhodeCode account')
 
    if user is not None and not user.ldap_dn:
 
        if user.active:
 
            if user.username == 'default' and user.active:
 
                log.info('user %s authenticated correctly as anonymous user' %
 
                         username)
 
                return True
 

	
 
            elif user.username == username and check_password(password,
 
                                                              user.password):
 
                log.info('user %s authenticated correctly' % username)
 
                return True
 
        else:
 
            log.warning('user %s tried auth but is disabled' % username)
 

	
 
    else:
 
        log.debug('Regular authentication failed')
 
        user_obj = User.get_by_username(username, case_insensitive=True)
 

	
 
        if user_obj is not None and not user_obj.ldap_dn:
 
            log.debug('this user already exists as non ldap')
 
            return False
 

	
 
        ldap_settings = RhodeCodeSetting.get_ldap_settings()
 
        #======================================================================
 
        # FALLBACK TO LDAP AUTH IF ENABLE
 
        #======================================================================
 
        if str2bool(ldap_settings.get('ldap_active')):
 
            log.debug("Authenticating user using ldap")
 
            kwargs = {
 
                  'server': ldap_settings.get('ldap_host', ''),
 
                  'base_dn': ldap_settings.get('ldap_base_dn', ''),
 
                  'port': ldap_settings.get('ldap_port'),
 
                  'bind_dn': ldap_settings.get('ldap_dn_user'),
 
                  'bind_pass': ldap_settings.get('ldap_dn_pass'),
 
                  'tls_kind': ldap_settings.get('ldap_tls_kind'),
 
                  'tls_reqcert': ldap_settings.get('ldap_tls_reqcert'),
 
                  'ldap_filter': ldap_settings.get('ldap_filter'),
 
                  'search_scope': ldap_settings.get('ldap_search_scope'),
 
                  'attr_login': ldap_settings.get('ldap_attr_login'),
 
                  'ldap_version': 3,
 
                  }
 
            log.debug('Checking for ldap authentication')
 
            try:
 
                aldap = AuthLdap(**kwargs)
 
                (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
 
                                                                password)
 
                log.debug('Got ldap DN response %s' % user_dn)
 

	
 
                get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
 
                                                           .get(k), [''])[0]
 

	
 
                user_attrs = {
 
                 'name': safe_unicode(get_ldap_attr('ldap_attr_firstname')),
 
                 'lastname': safe_unicode(get_ldap_attr('ldap_attr_lastname')),
 
                 'email': get_ldap_attr('ldap_attr_email'),
 
                }
 

	
 
                # don't store LDAP password since we don't need it. Override
 
                # with some random generated password
 
                _password = PasswordGenerator().gen_password(length=8)
 
                # create this user on the fly if it doesn't exist in rhodecode
 
                # database
 
                if user_model.create_ldap(username, _password, user_dn,
 
                                          user_attrs):
 
                    log.info('created new ldap user %s' % username)
 

	
 
                Session.commit()
 
                return True
 
            except (LdapUsernameError, LdapPasswordError,):
 
                pass
 
            except (Exception,):
 
                log.error(traceback.format_exc())
 
                pass
 
    return False
 

	
 

	
 
def login_container_auth(username):
 
    user = User.get_by_username(username)
 
    if user is None:
 
        user_attrs = {
 
            'name': username,
 
            'lastname': None,
 
            'email': None,
 
        }
 
        user = UserModel().create_for_container_auth(username, user_attrs)
 
        if not user:
 
            return None
rhodecode/lib/ext_json.py
Show inline comments
 
import datetime
 
import functools
 
import decimal
 

	
 
__all__ = ['json', 'simplejson', 'stdjson']
 

	
 

	
 
def _is_aware(value):
 
    """
 
    Determines if a given datetime.time is aware.
 

	
 
    The logic is described in Python's docs:
 
    http://docs.python.org/library/datetime.html#datetime.tzinfo
 
    """
 
    return (value.tzinfo is not None
 
            and value.tzinfo.utcoffset(value) is not None)
 

	
 

	
 
def _obj_dump(obj):
 
    """
 
    Custom function for dumping objects to JSON, if obj has __json__ attribute
 
    or method defined it will be used for serialization
 

	
 
    :param obj:
 
    """
 

	
 
    if isinstance(obj, complex):
 
        return [obj.real, obj.imag]
 
    # See "Date Time String Format" in the ECMA-262 specification.
 
    # some code borrowed from django 1.4
 
    elif isinstance(obj, datetime.datetime):
 
        r = obj.isoformat()
 
        if obj.microsecond:
 
            r = r[:23] + r[26:]
 
        if r.endswith('+00:00'):
 
            r = r[:-6] + 'Z'
 
        return r
 
    elif isinstance(obj, datetime.date):
 
        return obj.isoformat()
 
    elif isinstance(obj, decimal.Decimal):
 
        return str(obj)
 
    elif isinstance(obj, datetime.time):
 
        if _is_aware(obj):
 
            raise ValueError("JSON can't represent timezone-aware times.")
 
        r = obj.isoformat()
 
        if obj.microsecond:
 
            r = r[:12]
 
        return r
 
    elif isinstance(obj, set):
 
        return list(obj)
 
    elif hasattr(obj, '__json__'):
 
        if callable(obj.__json__):
 
            return obj.__json__()
 
        else:
 
            return obj.__json__
 
    else:
 
        raise NotImplementedError
 

	
 

	
 
# Import simplejson
 
try:
 
    # import simplejson initially
 
    import simplejson as _sj
 

	
 
    def extended_encode(obj):
 
        try:
 
            return _obj_dump(obj)
 
        except NotImplementedError:
 
            pass
 
        raise TypeError("%r is not JSON serializable" % (obj,))
 
    # we handle decimals our own it makes unified behavior of json vs
 
    # simplejson
 
    _sj.dumps = functools.partial(_sj.dumps, default=extended_encode,
 
                                  use_decimal=False)
 
    _sj.dump = functools.partial(_sj.dump, default=extended_encode,
 
                                 use_decimal=False)
 
    simplejson = _sj
 

	
 
except ImportError:
 
    # no simplejson set it to None
 
    _sj = None
 

	
 

	
 
try:
 
# simplejson not found try out regular json module
 
import json as _json
 

	
 

	
 
# extended JSON encoder for json
 
class ExtendedEncoder(_json.JSONEncoder):
 
    def default(self, obj):
 
        try:
 
            return _obj_dump(obj)
 
        except NotImplementedError:
 
            pass
 
        return _json.JSONEncoder.default(self, obj)
 
# monkey-patch JSON encoder to use extended version
 
_json.dumps = functools.partial(_json.dumps, cls=ExtendedEncoder)
 
_json.dump = functools.partial(_json.dump, cls=ExtendedEncoder)
 
stdlib = _json
 
except ImportError:
 
    _json = None
 

	
 
# set all available json modules
 
simplejson = _sj
 
stdjson = _json
 
json = _sj if _sj else _json
rhodecode/lib/utils.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
"""
 
    rhodecode.lib.utils
 
    ~~~~~~~~~~~~~~~~~~~
 

	
 
    Utilities library for RhodeCode
 

	
 
    :created_on: Apr 18, 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 re
 
import logging
 
import datetime
 
import traceback
 
import paste
 
import beaker
 
import tarfile
 
import shutil
 
from os.path import abspath
 
from os.path import dirname as dn, join as jn
 

	
 
from paste.script.command import Command, BadCommand
 

	
 
from mercurial import ui, config
 

	
 
from webhelpers.text import collapse, remove_formatting, strip_tags
 

	
 
from rhodecode.lib.vcs import get_backend
 
from rhodecode.lib.vcs.backends.base import BaseChangeset
 
from rhodecode.lib.vcs.utils.lazy import LazyProperty
 
from rhodecode.lib.vcs.utils.helpers import get_scm
 
from rhodecode.lib.vcs.exceptions import VCSError
 

	
 
from rhodecode.lib.caching_query import FromCache
 

	
 
from rhodecode.model import meta
 
from rhodecode.model.db import Repository, User, RhodeCodeUi, \
 
    UserLog, RepoGroup, RhodeCodeSetting, UserRepoGroupToPerm,\
 
    CacheInvalidation
 
from rhodecode.model.meta import Session
 
from rhodecode.model.repos_group import ReposGroupModel
 
from rhodecode.lib.utils2 import safe_str, safe_unicode
 
from rhodecode.lib.vcs.utils.fakemod import create_module
 

	
 
log = logging.getLogger(__name__)
 

	
 
REMOVED_REPO_PAT = re.compile(r'rm__\d{8}_\d{6}_\d{6}__.*')
 

	
 

	
 
def recursive_replace(str_, replace=' '):
 
    """
 
    Recursive replace of given sign to just one instance
 

	
 
    :param str_: given string
 
    :param replace: char to find and replace multiple instances
 

	
 
    Examples::
 
    >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
 
    'Mighty-Mighty-Bo-sstones'
 
    """
 

	
 
    if str_.find(replace * 2) == -1:
 
        return str_
 
    else:
 
        str_ = str_.replace(replace * 2, replace)
 
        return recursive_replace(str_, replace)
 

	
 

	
 
def repo_name_slug(value):
 
    """
 
    Return slug of name of repository
 
    This function is called on each creation/modification
 
    of repository to prevent bad names in repo
 
    """
 

	
 
    slug = remove_formatting(value)
 
    slug = strip_tags(slug)
 

	
 
    for c in """=[]\;'"<>,/~!@#$%^&*()+{}|: """:
 
        slug = slug.replace(c, '-')
 
    slug = recursive_replace(slug, '-')
 
    slug = collapse(slug, '-')
 
    return slug
 

	
 

	
 
def get_repo_slug(request):
 
    _repo = request.environ['pylons.routes_dict'].get('repo_name')
 
    if _repo:
 
        _repo = _repo.rstrip('/')
 
    return _repo
 

	
 

	
 
def get_repos_group_slug(request):
 
    _group = request.environ['pylons.routes_dict'].get('group_name')
 
    if _group:
 
        _group = _group.rstrip('/')
 
    return _group
 

	
 

	
 
def action_logger(user, action, repo, ipaddr='', sa=None, commit=False):
 
    """
 
    Action logger for various actions made by users
 

	
 
    :param user: user that made this action, can be a unique username string or
 
        object containing user_id attribute
 
    :param action: action to log, should be on of predefined unique actions for
 
        easy translations
 
    :param repo: string name of repository or object containing repo_id,
 
        that action was made on
 
    :param ipaddr: optional ip address from what the action was made
 
    :param sa: optional sqlalchemy session
 

	
 
    """
 

	
 
    if not sa:
 
        sa = meta.Session
 

	
 
    try:
 
        if hasattr(user, 'user_id'):
 
            user_obj = user
 
        elif isinstance(user, basestring):
 
            user_obj = User.get_by_username(user)
 
        else:
 
            raise Exception('You have to provide user object or username')
 

	
 
        if hasattr(repo, 'repo_id'):
 
            repo_obj = Repository.get(repo.repo_id)
 
            repo_name = repo_obj.repo_name
 
        elif  isinstance(repo, basestring):
 
            repo_name = repo.lstrip('/')
 
            repo_obj = Repository.get_by_repo_name(repo_name)
 
        else:
 
            raise Exception('You have to provide repository to action logger')
 

	
 
        user_log = UserLog()
 
        user_log.user_id = user_obj.user_id
 
        user_log.action = action
 
        user_log.action = safe_unicode(action)
 

	
 
        user_log.repository_id = repo_obj.repo_id
 
        user_log.repository_name = repo_name
 

	
 
        user_log.action_date = datetime.datetime.now()
 
        user_log.user_ip = ipaddr
 
        sa.add(user_log)
 

	
 
        log.info(
 
            'Adding user %s, action %s on %s' % (user_obj, action,
 
                                                 safe_unicode(repo))
 
        )
 
        if commit:
 
            sa.commit()
 
    except:
 
        log.error(traceback.format_exc())
 
        raise
 

	
 

	
 
def get_repos(path, recursive=False):
 
    """
 
    Scans given path for repos and return (name,(type,path)) tuple
 

	
 
    :param path: path to scan for repositories
 
    :param recursive: recursive search and return names with subdirs in front
 
    """
 

	
 
    # remove ending slash for better results
 
    path = path.rstrip(os.sep)
 

	
 
    def _get_repos(p):
 
        if not os.access(p, os.W_OK):
 
            return
 
        for dirpath in os.listdir(p):
 
            if os.path.isfile(os.path.join(p, dirpath)):
 
                continue
 
            cur_path = os.path.join(p, dirpath)
 
            try:
 
                scm_info = get_scm(cur_path)
 
                yield scm_info[1].split(path, 1)[-1].lstrip(os.sep), scm_info
 
            except VCSError:
 
                if not recursive:
 
                    continue
 
                #check if this dir containts other repos for recursive scan
 
                rec_path = os.path.join(p, dirpath)
 
                if os.path.isdir(rec_path):
 
                    for inner_scm in _get_repos(rec_path):
 
                        yield inner_scm
 

	
 
    return _get_repos(path)
 

	
 

	
 
def is_valid_repo(repo_name, base_path):
 
    """
 
    Returns True if given path is a valid repository False otherwise
 

	
 
    :param repo_name:
 
    :param base_path:
 

	
 
    :return True: if given path is a valid repository
 
    """
 
    full_path = os.path.join(safe_str(base_path), safe_str(repo_name))
 

	
 
    try:
 
        get_scm(full_path)
 
        return True
 
    except VCSError:
 
        return False
 

	
 

	
 
def is_valid_repos_group(repos_group_name, base_path):
 
    """
 
    Returns True if given path is a repos group False otherwise
 

	
 
    :param repo_name:
 
    :param base_path:
 
    """
 
    full_path = os.path.join(safe_str(base_path), safe_str(repos_group_name))
 

	
 
    # check if it's not a repo
 
    if is_valid_repo(repos_group_name, base_path):
 
        return False
 

	
 
    # check if it's a valid path
 
    if os.path.isdir(full_path):
 
        return True
 

	
 
    return False
 

	
 

	
 
def ask_ok(prompt, retries=4, complaint='Yes or no, please!'):
 
    while True:
 
        ok = raw_input(prompt)
 
        if ok in ('y', 'ye', 'yes'):
 
            return True
 
        if ok in ('n', 'no', 'nop', 'nope'):
 
            return False
 
        retries = retries - 1
 
        if retries < 0:
 
            raise IOError
 
        print complaint
 

	
 
#propagated from mercurial documentation
 
ui_sections = ['alias', 'auth',
 
                'decode/encode', 'defaults',
 
                'diff', 'email',
 
                'extensions', 'format',
 
                'merge-patterns', 'merge-tools',
 
                'hooks', 'http_proxy',
 
                'smtp', 'patch',
 
                'paths', 'profiling',
 
                'server', 'trusted',
 
                'ui', 'web', ]
 

	
 

	
 
def make_ui(read_from='file', path=None, checkpaths=True):
 
    """
 
    A function that will read python rc files or database
 
    and make an mercurial ui object from read options
 

	
 
    :param path: path to mercurial config file
 
    :param checkpaths: check the path
 
    :param read_from: read from 'file' or 'db'
 
    """
 

	
 
    baseui = ui.ui()
 

	
 
    # clean the baseui object
 
    baseui._ocfg = config.config()
 
    baseui._ucfg = config.config()
 
    baseui._tcfg = config.config()
 

	
 
    if read_from == 'file':
 
        if not os.path.isfile(path):
 
            log.debug('hgrc file is not present at %s skipping...' % path)
 
            return False
 
        log.debug('reading hgrc from %s' % path)
 
        cfg = config.config()
 
        cfg.read(path)
 
        for section in ui_sections:
 
            for k, v in cfg.items(section):
 
                log.debug('settings ui from file[%s]%s:%s' % (section, k, v))
 
                baseui.setconfig(section, k, v)
 

	
 
    elif read_from == 'db':
 
        sa = meta.Session
 
        ret = sa.query(RhodeCodeUi)\
 
            .options(FromCache("sql_cache_short", "get_hg_ui_settings"))\
 
            .all()
 

	
 
        hg_ui = ret
 
        for ui_ in hg_ui:
 
            if ui_.ui_active:
 
                log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
 
                          ui_.ui_key, ui_.ui_value)
 
                baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
 

	
 
        meta.Session.remove()
 
    return baseui
 

	
 

	
 
def set_rhodecode_config(config):
 
    """
 
    Updates pylons config with new settings from database
 

	
 
    :param config:
 
    """
 
    hgsettings = RhodeCodeSetting.get_app_settings()
 

	
 
    for k, v in hgsettings.items():
 
        config[k] = v
 

	
 

	
 
def invalidate_cache(cache_key, *args):
 
    """
 
    Puts cache invalidation task into db for
 
    further global cache invalidation
 
    """
 

	
 
    from rhodecode.model.scm import ScmModel
 

	
 
    if cache_key.startswith('get_repo_cached_'):
 
        name = cache_key.split('get_repo_cached_')[-1]
 
        ScmModel().mark_for_invalidation(name)
 

	
 

	
 
class EmptyChangeset(BaseChangeset):
 
    """
 
    An dummy empty changeset. It's possible to pass hash when creating
 
    an EmptyChangeset
 
    """
 

	
rhodecode/lib/utils2.py
Show inline comments
 
@@ -26,383 +26,382 @@
 
import re
 
from rhodecode.lib.vcs.utils.lazy import LazyProperty
 

	
 

	
 
def __get_lem():
 
    """
 
    Get language extension map based on what's inside pygments lexers
 
    """
 
    from pygments import lexers
 
    from string import lower
 
    from collections import defaultdict
 

	
 
    d = defaultdict(lambda: [])
 

	
 
    def __clean(s):
 
        s = s.lstrip('*')
 
        s = s.lstrip('.')
 

	
 
        if s.find('[') != -1:
 
            exts = []
 
            start, stop = s.find('['), s.find(']')
 

	
 
            for suffix in s[start + 1:stop]:
 
                exts.append(s[:s.find('[')] + suffix)
 
            return map(lower, exts)
 
        else:
 
            return map(lower, [s])
 

	
 
    for lx, t in sorted(lexers.LEXERS.items()):
 
        m = map(__clean, t[-2])
 
        if m:
 
            m = reduce(lambda x, y: x + y, m)
 
            for ext in m:
 
                desc = lx.replace('Lexer', '')
 
                d[ext].append(desc)
 

	
 
    return dict(d)
 

	
 
def str2bool(_str):
 
    """
 
    returs True/False value from given string, it tries to translate the
 
    string into boolean
 

	
 
    :param _str: string value to translate into boolean
 
    :rtype: boolean
 
    :returns: boolean from given string
 
    """
 
    if _str is None:
 
        return False
 
    if _str in (True, False):
 
        return _str
 
    _str = str(_str).strip().lower()
 
    return _str in ('t', 'true', 'y', 'yes', 'on', '1')
 

	
 

	
 
def convert_line_endings(line, mode):
 
    """
 
    Converts a given line  "line end" accordingly to given mode
 

	
 
    Available modes are::
 
        0 - Unix
 
        1 - Mac
 
        2 - DOS
 

	
 
    :param line: given line to convert
 
    :param mode: mode to convert to
 
    :rtype: str
 
    :return: converted line according to mode
 
    """
 
    from string import replace
 

	
 
    if mode == 0:
 
            line = replace(line, '\r\n', '\n')
 
            line = replace(line, '\r', '\n')
 
    elif mode == 1:
 
            line = replace(line, '\r\n', '\r')
 
            line = replace(line, '\n', '\r')
 
    elif mode == 2:
 
            line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
 
    return line
 

	
 

	
 
def detect_mode(line, default):
 
    """
 
    Detects line break for given line, if line break couldn't be found
 
    given default value is returned
 

	
 
    :param line: str line
 
    :param default: default
 
    :rtype: int
 
    :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
 
    """
 
    if line.endswith('\r\n'):
 
        return 2
 
    elif line.endswith('\n'):
 
        return 0
 
    elif line.endswith('\r'):
 
        return 1
 
    else:
 
        return default
 

	
 

	
 
def generate_api_key(username, salt=None):
 
    """
 
    Generates unique API key for given username, if salt is not given
 
    it'll be generated from some random string
 

	
 
    :param username: username as string
 
    :param salt: salt to hash generate KEY
 
    :rtype: str
 
    :returns: sha1 hash from username+salt
 
    """
 
    from tempfile import _RandomNameSequence
 
    import hashlib
 

	
 
    if salt is None:
 
        salt = _RandomNameSequence().next()
 

	
 
    return hashlib.sha1(username + salt).hexdigest()
 

	
 

	
 
def safe_unicode(str_, from_encoding=None):
 
    """
 
    safe unicode function. Does few trick to turn str_ into unicode
 

	
 
    In case of UnicodeDecode error we try to return it with encoding detected
 
    by chardet library if it fails fallback to unicode with errors replaced
 

	
 
    :param str_: string to decode
 
    :rtype: unicode
 
    :returns: unicode object
 
    """
 
    if isinstance(str_, unicode):
 
        return str_
 

	
 
    if not from_encoding:
 
        import rhodecode
 
        DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
 
        from_encoding = DEFAULT_ENCODING
 

	
 
    try:
 
        return unicode(str_)
 
    except UnicodeDecodeError:
 
        pass
 

	
 
    try:
 
        return unicode(str_, from_encoding)
 
    except UnicodeDecodeError:
 
        pass
 

	
 
    try:
 
        import chardet
 
        encoding = chardet.detect(str_)['encoding']
 
        if encoding is None:
 
            raise Exception()
 
        return str_.decode(encoding)
 
    except (ImportError, UnicodeDecodeError, Exception):
 
        return unicode(str_, from_encoding, 'replace')
 

	
 

	
 
def safe_str(unicode_, to_encoding=None):
 
    """
 
    safe str function. Does few trick to turn unicode_ into string
 

	
 
    In case of UnicodeEncodeError we try to return it with encoding detected
 
    by chardet library if it fails fallback to string with errors replaced
 

	
 
    :param unicode_: unicode to encode
 
    :rtype: str
 
    :returns: str object
 
    """
 

	
 
    # if it's not basestr cast to str
 
    if not isinstance(unicode_, basestring):
 
        return str(unicode_)
 

	
 
    if isinstance(unicode_, str):
 
        return unicode_
 

	
 
    if not to_encoding:
 
        import rhodecode
 
        DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
 
        to_encoding = DEFAULT_ENCODING
 

	
 
    try:
 
        return unicode_.encode(to_encoding)
 
    except UnicodeEncodeError:
 
        pass
 

	
 
    try:
 
        import chardet
 
        encoding = chardet.detect(unicode_)['encoding']
 
        print encoding
 
        if encoding is None:
 
            raise UnicodeEncodeError()
 

	
 
        return unicode_.encode(encoding)
 
    except (ImportError, UnicodeEncodeError):
 
        return unicode_.encode(to_encoding, 'replace')
 

	
 
    return safe_str
 

	
 

	
 
def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
 
    """
 
    Custom engine_from_config functions that makes sure we use NullPool for
 
    file based sqlite databases. This prevents errors on sqlite. This only
 
    applies to sqlalchemy versions < 0.7.0
 

	
 
    """
 
    import sqlalchemy
 
    from sqlalchemy import engine_from_config as efc
 
    import logging
 

	
 
    if int(sqlalchemy.__version__.split('.')[1]) < 7:
 

	
 
        # This solution should work for sqlalchemy < 0.7.0, and should use
 
        # proxy=TimerProxy() for execution time profiling
 

	
 
        from sqlalchemy.pool import NullPool
 
        url = configuration[prefix + 'url']
 

	
 
        if url.startswith('sqlite'):
 
            kwargs.update({'poolclass': NullPool})
 
        return efc(configuration, prefix, **kwargs)
 
    else:
 
        import time
 
        from sqlalchemy import event
 
        from sqlalchemy.engine import Engine
 

	
 
        log = logging.getLogger('sqlalchemy.engine')
 
        BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
 
        engine = efc(configuration, prefix, **kwargs)
 

	
 
        def color_sql(sql):
 
            COLOR_SEQ = "\033[1;%dm"
 
            COLOR_SQL = YELLOW
 
            normal = '\x1b[0m'
 
            return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
 

	
 
        if configuration['debug']:
 
            #attach events only for debug configuration
 

	
 
            def before_cursor_execute(conn, cursor, statement,
 
                                    parameters, context, executemany):
 
                context._query_start_time = time.time()
 
                log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
 

	
 

	
 
            def after_cursor_execute(conn, cursor, statement,
 
                                    parameters, context, executemany):
 
                total = time.time() - context._query_start_time
 
                log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
 

	
 
            event.listen(engine, "before_cursor_execute",
 
                         before_cursor_execute)
 
            event.listen(engine, "after_cursor_execute",
 
                         after_cursor_execute)
 

	
 
    return engine
 

	
 

	
 
def age(curdate):
 
    """
 
    turns a datetime into an age string.
 

	
 
    :param curdate: datetime object
 
    :rtype: unicode
 
    :returns: unicode words describing age
 
    """
 

	
 
    from datetime import datetime
 
    from webhelpers.date import time_ago_in_words
 

	
 
    _ = lambda s: s
 

	
 
    if not curdate:
 
        return ''
 

	
 
    agescales = [(_(u"year"), 3600 * 24 * 365),
 
                 (_(u"month"), 3600 * 24 * 30),
 
                 (_(u"day"), 3600 * 24),
 
                 (_(u"hour"), 3600),
 
                 (_(u"minute"), 60),
 
                 (_(u"second"), 1), ]
 

	
 
    age = datetime.now() - curdate
 
    age_seconds = (age.days * agescales[2][1]) + age.seconds
 
    pos = 1
 
    for scale in agescales:
 
        if scale[1] <= age_seconds:
 
            if pos == 6:
 
                pos = 5
 
            return '%s %s' % (time_ago_in_words(curdate,
 
                                                agescales[pos][0]), _('ago'))
 
        pos += 1
 

	
 
    return _(u'just now')
 

	
 

	
 
def uri_filter(uri):
 
    """
 
    Removes user:password from given url string
 

	
 
    :param uri:
 
    :rtype: unicode
 
    :returns: filtered list of strings
 
    """
 
    if not uri:
 
        return ''
 

	
 
    proto = ''
 

	
 
    for pat in ('https://', 'http://'):
 
        if uri.startswith(pat):
 
            uri = uri[len(pat):]
 
            proto = pat
 
            break
 

	
 
    # remove passwords and username
 
    uri = uri[uri.find('@') + 1:]
 

	
 
    # get the port
 
    cred_pos = uri.find(':')
 
    if cred_pos == -1:
 
        host, port = uri, None
 
    else:
 
        host, port = uri[:cred_pos], uri[cred_pos + 1:]
 

	
 
    return filter(None, [proto, host, port])
 

	
 

	
 
def credentials_filter(uri):
 
    """
 
    Returns a url with removed credentials
 

	
 
    :param uri:
 
    """
 

	
 
    uri = uri_filter(uri)
 
    #check if we have port
 
    if len(uri) > 2 and uri[2]:
 
        uri[2] = ':' + uri[2]
 

	
 
    return ''.join(uri)
 

	
 

	
 
def get_changeset_safe(repo, rev):
 
    """
 
    Safe version of get_changeset if this changeset doesn't exists for a
 
    repo it returns a Dummy one instead
 

	
 
    :param repo:
 
    :param rev:
 
    """
 
    from rhodecode.lib.vcs.backends.base import BaseRepository
 
    from rhodecode.lib.vcs.exceptions import RepositoryError
 
    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:
 
        from rhodecode.lib.utils import EmptyChangeset
 
        cs = EmptyChangeset(requested_revision=rev)
 
    return cs
 

	
 

	
 
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())
rhodecode/lib/vcs/backends/git/changeset.py
Show inline comments
 
@@ -51,389 +51,389 @@ class GitChangeset(BaseChangeset):
 

	
 
    @LazyProperty
 
    def author(self):
 
        return safe_unicode(self._commit.committer)
 

	
 
    @LazyProperty
 
    def date(self):
 
        return date_fromtimestamp(self._commit.commit_time,
 
                                  self._commit.commit_timezone)
 

	
 
    @LazyProperty
 
    def status(self):
 
        """
 
        Returns modified, added, removed, deleted files for current changeset
 
        """
 
        return self.changed, self.added, self.removed
 

	
 
    @LazyProperty
 
    def branch(self):
 

	
 
        heads = self.repository._heads(reverse=False)
 

	
 
        ref = heads.get(self.raw_id)
 
        if ref:
 
            return safe_unicode(ref)
 

	
 
    def _fix_path(self, path):
 
        """
 
        Paths are stored without trailing slash so we need to get rid off it if
 
        needed.
 
        """
 
        if path.endswith('/'):
 
            path = path.rstrip('/')
 
        return path
 

	
 
    def _get_id_for_path(self, path):
 

	
 
        # FIXME: Please, spare a couple of minutes and make those codes cleaner;
 
        if not path in self._paths:
 
            path = path.strip('/')
 
            # set root tree
 
            tree = self.repository._repo[self._commit.tree]
 
            if path == '':
 
                self._paths[''] = tree.id
 
                return tree.id
 
            splitted = path.split('/')
 
            dirs, name = splitted[:-1], splitted[-1]
 
            curdir = ''
 

	
 
            # initially extract things from root dir
 
            for item, stat, id in tree.iteritems():
 
                if curdir:
 
                    name = '/'.join((curdir, item))
 
                else:
 
                    name = item
 
                self._paths[name] = id
 
                self._stat_modes[name] = stat
 

	
 
            for dir in dirs:
 
                if curdir:
 
                    curdir = '/'.join((curdir, dir))
 
                else:
 
                    curdir = dir
 
                dir_id = None
 
                for item, stat, id in tree.iteritems():
 
                    if dir == item:
 
                        dir_id = id
 
                if dir_id:
 
                    # Update tree
 
                    tree = self.repository._repo[dir_id]
 
                    if not isinstance(tree, objects.Tree):
 
                        raise ChangesetError('%s is not a directory' % curdir)
 
                else:
 
                    raise ChangesetError('%s have not been found' % curdir)
 

	
 
                # cache all items from the given traversed tree
 
                for item, stat, id in tree.iteritems():
 
                    if curdir:
 
                        name = '/'.join((curdir, item))
 
                    else:
 
                        name = item
 
                    self._paths[name] = id
 
                    self._stat_modes[name] = stat
 
            if not path in self._paths:
 
                raise NodeDoesNotExistError("There is no file nor directory "
 
                    "at the given path %r at revision %r"
 
                    % (path, self.short_id))
 
        return self._paths[path]
 

	
 
    def _get_kind(self, path):
 
        id = self._get_id_for_path(path)
 
        obj = self.repository._repo[id]
 
        if isinstance(obj, objects.Blob):
 
            return NodeKind.FILE
 
        elif isinstance(obj, objects.Tree):
 
            return NodeKind.DIR
 

	
 
    def _get_file_nodes(self):
 
        return chain(*(t[2] for t in self.walk()))
 

	
 
    @LazyProperty
 
    def parents(self):
 
        """
 
        Returns list of parents changesets.
 
        """
 
        return [self.repository.get_changeset(parent)
 
            for parent in self._commit.parents]
 

	
 
    def next(self, branch=None):
 

	
 
        if branch and self.branch != branch:
 
            raise VCSError('Branch option used on changeset not belonging '
 
                           'to that branch')
 

	
 
        def _next(changeset, branch):
 
            try:
 
                next_ = changeset.revision + 1
 
                next_rev = changeset.repository.revisions[next_]
 
            except IndexError:
 
                raise ChangesetDoesNotExistError
 
            cs = changeset.repository.get_changeset(next_rev)
 

	
 
            if branch and branch != cs.branch:
 
                return _next(cs, branch)
 

	
 
            return cs
 

	
 
        return _next(self, branch)
 

	
 
    def prev(self, branch=None):
 
        if branch and self.branch != branch:
 
            raise VCSError('Branch option used on changeset not belonging '
 
                           'to that branch')
 

	
 
        def _prev(changeset, branch):
 
            try:
 
                prev_ = changeset.revision - 1
 
                if prev_ < 0:
 
                    raise IndexError
 
                prev_rev = changeset.repository.revisions[prev_]
 
            except IndexError:
 
                raise ChangesetDoesNotExistError
 

	
 
            cs = changeset.repository.get_changeset(prev_rev)
 

	
 
            if branch and branch != cs.branch:
 
                return _prev(cs, branch)
 

	
 
            return cs
 

	
 
        return _prev(self, branch)
 

	
 
    def get_file_mode(self, path):
 
        """
 
        Returns stat mode of the file at the given ``path``.
 
        """
 
        # ensure path is traversed
 
        self._get_id_for_path(path)
 
        return self._stat_modes[path]
 

	
 
    def get_file_content(self, path):
 
        """
 
        Returns content of the file at given ``path``.
 
        """
 
        id = self._get_id_for_path(path)
 
        blob = self.repository._repo[id]
 
        return blob.as_pretty_string()
 

	
 
    def get_file_size(self, path):
 
        """
 
        Returns size of the file at given ``path``.
 
        """
 
        id = self._get_id_for_path(path)
 
        blob = self.repository._repo[id]
 
        return blob.raw_length()
 

	
 
    def get_file_changeset(self, path):
 
        """
 
        Returns last commit of the file at the given ``path``.
 
        """
 
        node = self.get_node(path)
 
        return node.history[0]
 

	
 
    def get_file_history(self, path):
 
        """
 
        Returns history of file as reversed list of ``Changeset`` objects for
 
        which file at given ``path`` has been modified.
 

	
 
        TODO: This function now uses os underlying 'git' and 'grep' commands
 
        which is generally not good. Should be replaced with algorithm
 
        iterating commits.
 
        """
 
        cmd = 'log --pretty="format: %%H" --name-status -p %s -- "%s"' % (
 
        cmd = 'log --pretty="format: %%H" -s -p %s -- "%s"' % (
 
                  self.id, path
 
               )
 
        so, se = self.repository.run_git_command(cmd)
 
        ids = re.findall(r'\w{40}', so)
 
        ids = re.findall(r'[0-9a-fA-F]{40}', so)
 
        return [self.repository.get_changeset(id) for id in ids]
 

	
 
    def get_file_annotate(self, path):
 
        """
 
        Returns a list of three element tuples with lineno,changeset and line
 

	
 
        TODO: This function now uses os underlying 'git' command which is
 
        generally not good. Should be replaced with algorithm iterating
 
        commits.
 
        """
 
        cmd = 'blame -l --root -r %s -- "%s"' % (self.id, path)
 
        # -l     ==> outputs long shas (and we need all 40 characters)
 
        # --root ==> doesn't put '^' character for bounderies
 
        # -r sha ==> blames for the given revision
 
        so, se = self.repository.run_git_command(cmd)
 
        annotate = []
 
        for i, blame_line in enumerate(so.split('\n')[:-1]):
 
            ln_no = i + 1
 
            id, line = re.split(r' \(.+?\) ', blame_line, 1)
 
            annotate.append((ln_no, self.repository.get_changeset(id), line))
 
        return annotate
 

	
 
    def fill_archive(self, stream=None, kind='tgz', prefix=None,
 
                     subrepos=False):
 
        """
 
        Fills up given stream.
 

	
 
        :param stream: file like object.
 
        :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
 
            Default: ``tgz``.
 
        :param prefix: name of root directory in archive.
 
            Default is repository name and changeset's raw_id joined with dash
 
            (``repo-tip.<KIND>``).
 
        :param subrepos: include subrepos in this archive.
 

	
 
        :raise ImproperArchiveTypeError: If given kind is wrong.
 
        :raise VcsError: If given stream is None
 

	
 
        """
 
        allowed_kinds = settings.ARCHIVE_SPECS.keys()
 
        if kind not in allowed_kinds:
 
            raise ImproperArchiveTypeError('Archive kind not supported use one'
 
                'of %s', allowed_kinds)
 

	
 
        if prefix is None:
 
            prefix = '%s-%s' % (self.repository.name, self.short_id)
 
        elif prefix.startswith('/'):
 
            raise VCSError("Prefix cannot start with leading slash")
 
        elif prefix.strip() == '':
 
            raise VCSError("Prefix cannot be empty")
 

	
 
        if kind == 'zip':
 
            frmt = 'zip'
 
        else:
 
            frmt = 'tar'
 
        cmd = 'git archive --format=%s --prefix=%s/ %s' % (frmt, prefix,
 
            self.raw_id)
 
        if kind == 'tgz':
 
            cmd += ' | gzip -9'
 
        elif kind == 'tbz2':
 
            cmd += ' | bzip2 -9'
 

	
 
        if stream is None:
 
            raise VCSError('You need to pass in a valid stream for filling'
 
                           ' with archival data')
 
        popen = Popen(cmd, stdout=PIPE, stderr=PIPE, shell=True,
 
            cwd=self.repository.path)
 

	
 
        buffer_size = 1024 * 8
 
        chunk = popen.stdout.read(buffer_size)
 
        while chunk:
 
            stream.write(chunk)
 
            chunk = popen.stdout.read(buffer_size)
 
        # Make sure all descriptors would be read
 
        popen.communicate()
 

	
 
    def get_nodes(self, path):
 
        if self._get_kind(path) != NodeKind.DIR:
 
            raise ChangesetError("Directory does not exist for revision %r at "
 
                " %r" % (self.revision, path))
 
        path = self._fix_path(path)
 
        id = self._get_id_for_path(path)
 
        tree = self.repository._repo[id]
 
        dirnodes = []
 
        filenodes = []
 
        als = self.repository.alias
 
        for name, stat, id in tree.iteritems():
 
            if objects.S_ISGITLINK(stat):
 
                dirnodes.append(SubModuleNode(name, url=None, changeset=id,
 
                                              alias=als))
 
                continue
 

	
 
            obj = self.repository._repo.get_object(id)
 
            if path != '':
 
                obj_path = '/'.join((path, name))
 
            else:
 
                obj_path = name
 
            if obj_path not in self._stat_modes:
 
                self._stat_modes[obj_path] = stat
 
            if isinstance(obj, objects.Tree):
 
                dirnodes.append(DirNode(obj_path, changeset=self))
 
            elif isinstance(obj, objects.Blob):
 
                filenodes.append(FileNode(obj_path, changeset=self, mode=stat))
 
            else:
 
                raise ChangesetError("Requested object should be Tree "
 
                                     "or Blob, is %r" % type(obj))
 
        nodes = dirnodes + filenodes
 
        for node in nodes:
 
            if not node.path in self.nodes:
 
                self.nodes[node.path] = node
 
        nodes.sort()
 
        return nodes
 

	
 
    def get_node(self, path):
 
        if isinstance(path, unicode):
 
            path = path.encode('utf-8')
 
        path = self._fix_path(path)
 
        if not path in self.nodes:
 
            try:
 
                id_ = self._get_id_for_path(path)
 
            except ChangesetError:
 
                raise NodeDoesNotExistError("Cannot find one of parents' "
 
                    "directories for a given path: %s" % path)
 

	
 
            als = self.repository.alias
 
            _GL = lambda m: m and objects.S_ISGITLINK(m)
 
            if _GL(self._stat_modes.get(path)):
 
                node = SubModuleNode(path, url=None, changeset=id_, alias=als)
 
            else:
 
                obj = self.repository._repo.get_object(id_)
 

	
 
                if isinstance(obj, objects.Tree):
 
                    if path == '':
 
                        node = RootNode(changeset=self)
 
                    else:
 
                        node = DirNode(path, changeset=self)
 
                    node._tree = obj
 
                elif isinstance(obj, objects.Blob):
 
                    node = FileNode(path, changeset=self)
 
                    node._blob = obj
 
                else:
 
                    raise NodeDoesNotExistError("There is no file nor directory "
 
                        "at the given path %r at revision %r"
 
                        % (path, self.short_id))
 
            # cache node
 
            self.nodes[path] = node
 
        return self.nodes[path]
 

	
 
    @LazyProperty
 
    def affected_files(self):
 
        """
 
        Get's a fast accessible file changes for given changeset
 
        """
 

	
 
        return self.added + self.changed
 

	
 
    @LazyProperty
 
    def _diff_name_status(self):
 
        output = []
 
        for parent in self.parents:
 
            cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id)
 
            so, se = self.repository.run_git_command(cmd)
 
            output.append(so.strip())
 
        return '\n'.join(output)
 

	
 
    def _get_paths_for_status(self, status):
 
        """
 
        Returns sorted list of paths for given ``status``.
 

	
 
        :param status: one of: *added*, *modified* or *deleted*
 
        """
 
        paths = set()
 
        char = status[0].upper()
 
        for line in self._diff_name_status.splitlines():
 
            if not line:
 
                continue
 

	
 
            if line.startswith(char):
 
                splitted = line.split(char, 1)
 
                if not len(splitted) == 2:
 
                    raise VCSError("Couldn't parse diff result:\n%s\n\n and "
 
                        "particularly that line: %s" % (self._diff_name_status,
 
                        line))
 
                _path = splitted[1].strip()
 
                paths.add(_path)
 
        return sorted(paths)
 

	
 
    @LazyProperty
 
    def added(self):
 
        """
 
        Returns list of added ``FileNode`` objects.
 
        """
rhodecode/lib/vcs/backends/hg/changeset.py
Show inline comments
 
import os
 
import posixpath
 

	
 
from rhodecode.lib.vcs.backends.base import BaseChangeset
 
from rhodecode.lib.vcs.conf import settings
 
from rhodecode.lib.vcs.exceptions import  ChangesetDoesNotExistError, \
 
    ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError
 
from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \
 
    ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \
 
    RemovedFileNodesGenerator, RootNode, SubModuleNode
 

	
 
from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp
 
from rhodecode.lib.vcs.utils.lazy import LazyProperty
 
from rhodecode.lib.vcs.utils.paths import get_dirs_for_path
 

	
 
from ...utils.hgcompat import archival, hex
 

	
 

	
 
class MercurialChangeset(BaseChangeset):
 
    """
 
    Represents state of the repository at the single revision.
 
    """
 

	
 
    def __init__(self, repository, revision):
 
        self.repository = repository
 
        self.raw_id = revision
 
        self._ctx = repository._repo[revision]
 
        self.revision = self._ctx._rev
 
        self.nodes = {}
 

	
 
    @LazyProperty
 
    def tags(self):
 
        return map(safe_unicode, self._ctx.tags())
 

	
 
    @LazyProperty
 
    def branch(self):
 
        return  safe_unicode(self._ctx.branch())
 

	
 
    @LazyProperty
 
    def bookmarks(self):
 
        return map(safe_unicode, self._ctx.bookmarks())
 

	
 
    @LazyProperty
 
    def message(self):
 
        return safe_unicode(self._ctx.description())
 

	
 
    @LazyProperty
 
    def author(self):
 
        return safe_unicode(self._ctx.user())
 

	
 
    @LazyProperty
 
    def date(self):
 
        return date_fromtimestamp(*self._ctx.date())
 

	
 
    @LazyProperty
 
    def status(self):
 
        """
 
        Returns modified, added, removed, deleted files for current changeset
 
        """
 
        return self.repository._repo.status(self._ctx.p1().node(),
 
                                            self._ctx.node())
 

	
 
    @LazyProperty
 
    def _file_paths(self):
 
        return list(self._ctx)
 

	
 
    @LazyProperty
 
    def _dir_paths(self):
 
        p = list(set(get_dirs_for_path(*self._file_paths)))
 
        p.insert(0, '')
 
        return p
 

	
 
    @LazyProperty
 
    def _paths(self):
 
        return self._dir_paths + self._file_paths
 

	
 
    @LazyProperty
 
    def id(self):
 
        if self.last:
 
            return u'tip'
 
        return self.short_id
 

	
 
    @LazyProperty
 
    def short_id(self):
 
        return self.raw_id[:12]
 

	
 
    @LazyProperty
 
    def parents(self):
 
        """
 
        Returns list of parents changesets.
 
        """
 
        return [self.repository.get_changeset(parent.rev())
 
                for parent in self._ctx.parents() if parent.rev() >= 0]
 

	
 
    def next(self, branch=None):
 

	
 
        if branch and self.branch != branch:
 
            raise VCSError('Branch option used on changeset not belonging '
 
                           'to that branch')
 

	
 
        def _next(changeset, branch):
 
            try:
 
                next_ = changeset.revision + 1
 
                next_rev = changeset.repository.revisions[next_]
 
            except IndexError:
 
                raise ChangesetDoesNotExistError
 
            cs = changeset.repository.get_changeset(next_rev)
 

	
 
            if branch and branch != cs.branch:
 
                return _next(cs, branch)
 

	
 
            return cs
 

	
 
        return _next(self, branch)
 

	
 
    def prev(self, branch=None):
 
        if branch and self.branch != branch:
 
            raise VCSError('Branch option used on changeset not belonging '
 
                           'to that branch')
 

	
 
        def _prev(changeset, branch):
 
            try:
 
                prev_ = changeset.revision - 1
 
                if prev_ < 0:
 
                    raise IndexError
 
                prev_rev = changeset.repository.revisions[prev_]
 
            except IndexError:
 
                raise ChangesetDoesNotExistError
 

	
 
            cs = changeset.repository.get_changeset(prev_rev)
 

	
 
            if branch and branch != cs.branch:
 
                return _prev(cs, branch)
 

	
 
            return cs
 

	
 
        return _prev(self, branch)
 

	
 
    def _fix_path(self, path):
 
        """
 
        Paths are stored without trailing slash so we need to get rid off it if
 
        needed. Also mercurial keeps filenodes as str so we need to decode
 
        from unicode to str
 
        """
 
        if path.endswith('/'):
 
            path = path.rstrip('/')
 

	
 
        return safe_str(path)
 

	
 
    def _get_kind(self, path):
 
        path = self._fix_path(path)
 
        if path in self._file_paths:
 
            return NodeKind.FILE
 
        elif path in self._dir_paths:
 
            return NodeKind.DIR
 
        else:
 
            raise ChangesetError("Node does not exist at the given path %r"
 
                % (path))
 

	
 
    def _get_filectx(self, path):
 
        path = self._fix_path(path)
 
        if self._get_kind(path) != NodeKind.FILE:
 
            raise ChangesetError("File does not exist for revision %r at "
 
                " %r" % (self.revision, path))
 
        return self._ctx.filectx(path)
 

	
 
    def _extract_submodules(self):
 
        """
 
        returns a dictionary with submodule information from substate file
 
        of hg repository
 
        """
 
        return self._ctx.substate
 

	
 
    def get_file_mode(self, path):
 
        """
 
        Returns stat mode of the file at the given ``path``.
 
        """
 
        fctx = self._get_filectx(path)
 
        if 'x' in fctx.flags():
 
            return 0100755
 
        else:
 
            return 0100644
 

	
 
    def get_file_content(self, path):
 
        """
 
        Returns content of the file at given ``path``.
 
        """
 
        fctx = self._get_filectx(path)
 
        return fctx.data()
 

	
 
    def get_file_size(self, path):
 
        """
 
        Returns size of the file at given ``path``.
 
        """
 
        fctx = self._get_filectx(path)
 
        return fctx.size()
 

	
 
    def get_file_changeset(self, path):
 
        """
 
        Returns last commit of the file at the given ``path``.
 
        """
 
        node = self.get_node(path)
 
        return node.history[0]
 

	
 
    def get_file_history(self, path):
 
        """
 
        Returns history of file as reversed list of ``Changeset`` objects for
 
        which file at given ``path`` has been modified.
 
        """
 
        fctx = self._get_filectx(path)
 
        nodes = [fctx.filectx(x).node() for x in fctx.filelog()]
 
        changesets = [self.repository.get_changeset(hex(node))
 
            for node in reversed(nodes)]
 
        return changesets
 

	
 
    def get_file_annotate(self, path):
 
        """
 
        Returns a list of three element tuples with lineno,changeset and line
 
        """
 
        fctx = self._get_filectx(path)
 
        annotate = []
 
        for i, annotate_data in enumerate(fctx.annotate()):
 
            ln_no = i + 1
 
            annotate.append((ln_no, self.repository\
 
                             .get_changeset(hex(annotate_data[0].node())),
 
                             annotate_data[1],))
 

	
 
        return annotate
 

	
 
    def fill_archive(self, stream=None, kind='tgz', prefix=None,
 
                     subrepos=False):
 
        """
 
        Fills up given stream.
 

	
 
        :param stream: file like object.
 
        :param kind: one of following: ``zip``, ``tgz`` or ``tbz2``.
 
            Default: ``tgz``.
 
        :param prefix: name of root directory in archive.
 
            Default is repository name and changeset's raw_id joined with dash
 
            (``repo-tip.<KIND>``).
 
        :param subrepos: include subrepos in this archive.
 

	
 
        :raise ImproperArchiveTypeError: If given kind is wrong.
 
        :raise VcsError: If given stream is None
 
        """
 

	
 
        allowed_kinds = settings.ARCHIVE_SPECS.keys()
 
        if kind not in allowed_kinds:
 
            raise ImproperArchiveTypeError('Archive kind not supported use one'
 
                'of %s', allowed_kinds)
 

	
 
        if stream is None:
 
            raise VCSError('You need to pass in a valid stream for filling'
 
                           ' with archival data')
 

	
 
        if prefix is None:
 
            prefix = '%s-%s' % (self.repository.name, self.short_id)
 
        elif prefix.startswith('/'):
 
            raise VCSError("Prefix cannot start with leading slash")
 
        elif prefix.strip() == '':
 
            raise VCSError("Prefix cannot be empty")
 

	
 
        archival.archive(self.repository._repo, stream, self.raw_id,
 
                         kind, prefix=prefix, subrepos=subrepos)
 

	
 
        #stream.close()
 

	
 
        if stream.closed and hasattr(stream, 'name'):
 
            stream = open(stream.name, 'rb')
 
        elif hasattr(stream, 'mode') and 'r' not in stream.mode:
 
            stream = open(stream.name, 'rb')
 
        else:
 
            stream.seek(0)
 

	
 
    def get_nodes(self, path):
 
        """
 
        Returns combined ``DirNode`` and ``FileNode`` objects list representing
 
        state of changeset at the given ``path``. If node at the given ``path``
 
        is not instance of ``DirNode``, ChangesetError would be raised.
 
        """
 

	
 
        if self._get_kind(path) != NodeKind.DIR:
 
            raise ChangesetError("Directory does not exist for revision %r at "
 
                " %r" % (self.revision, path))
 
        path = self._fix_path(path)
 

	
 
        filenodes = [FileNode(f, changeset=self) for f in self._file_paths
 
            if os.path.dirname(f) == path]
 
        dirs = path == '' and '' or [d for d in self._dir_paths
 
            if d and posixpath.dirname(d) == path]
 
        dirnodes = [DirNode(d, changeset=self) for d in dirs
 
            if os.path.dirname(d) == path]
 

	
 
        als = self.repository.alias
 
        for k, vals in self._extract_submodules().iteritems():
 
            #vals = url,rev,type
 
            loc = vals[0]
 
            cs = vals[1]
 
            dirnodes.append(SubModuleNode(k, url=loc, changeset=cs,
 
                                          alias=als))
 
        nodes = dirnodes + filenodes
 
        # cache nodes
 
        for node in nodes:
 
            self.nodes[node.path] = node
 
        nodes.sort()
 

	
 
        return nodes
 

	
 
    def get_node(self, path):
 
        """
 
        Returns ``Node`` object from the given ``path``. If there is no node at
 
        the given ``path``, ``ChangesetError`` would be raised.
 
        """
 

	
 
        path = self._fix_path(path)
 

	
 
        if not path in self.nodes:
 
            if path in self._file_paths:
 
                node = FileNode(path, changeset=self)
 
            elif path in self._dir_paths or path in self._dir_paths:
 
                if path == '':
 
                    node = RootNode(changeset=self)
 
                else:
 
                    node = DirNode(path, changeset=self)
 
            else:
 
                raise NodeDoesNotExistError("There is no file nor directory "
 
                    "at the given path: %r at revision %r"
 
                    % (path, self.short_id))
 
            # cache node
 
            self.nodes[path] = node
 
        return self.nodes[path]
 

	
 
    @LazyProperty
 
    def affected_files(self):
 
        """
 
        Get's a fast accessible file changes for given changeset
 
        """
 
        return self._ctx.files()
 

	
 
    @property
 
    def added(self):
 
        """
 
        Returns list of added ``FileNode`` objects.
 
        """
 
        return AddedFileNodesGenerator([n for n in self.status[1]], self)
 

	
 
    @property
 
    def changed(self):
 
        """
 
        Returns list of modified ``FileNode`` objects.
 
        """
 
        return ChangedFileNodesGenerator([n for n in  self.status[0]], self)
 

	
 
    @property
 
    def removed(self):
 
        """
 
        Returns list of removed ``FileNode`` objects.
 
        """
 
        return RemovedFileNodesGenerator([n for n in self.status[2]], self)
rhodecode/lib/vcs/utils/__init__.py
Show inline comments
 
"""
 
This module provides some useful tools for ``vcs`` like annotate/diff html
 
output. It also includes some internal helpers.
 
"""
 
import sys
 
import time
 
import datetime
 

	
 

	
 
def makedate():
 
    lt = time.localtime()
 
    if lt[8] == 1 and time.daylight:
 
        tz = time.altzone
 
    else:
 
        tz = time.timezone
 
    return time.mktime(lt), tz
 

	
 

	
 
def date_fromtimestamp(unixts, tzoffset=0):
 
    """
 
    Makes a local datetime object out of unix timestamp
 

	
 
    :param unixts:
 
    :param tzoffset:
 
    """
 

	
 
    return datetime.datetime.fromtimestamp(float(unixts))
 

	
 

	
 
def safe_unicode(str_, from_encoding=None):
 
    """
 
    safe unicode function. Does few trick to turn str_ into unicode
 

	
 
    In case of UnicodeDecode error we try to return it with encoding detected
 
    by chardet library if it fails fallback to unicode with errors replaced
 

	
 
    :param str_: string to decode
 
    :rtype: unicode
 
    :returns: unicode object
 
    """
 
    if isinstance(str_, unicode):
 
        return str_
 
    if not from_encoding:
 
        import rhodecode
 
        DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8')
 
        from_encoding = DEFAULT_ENCODING
 
    try:
 
        return unicode(str_)
 
    except UnicodeDecodeError:
 
        pass
 

	
 
    try:
 
        return unicode(str_, from_encoding)
 
    except UnicodeDecodeError:
 
        pass
 

	
 
    try:
 
        import chardet
 
        encoding = chardet.detect(str_)['encoding']
 
        if encoding is None:
 
            raise Exception()
 
        return str_.decode(encoding)
 
    except (ImportError, UnicodeDecodeError, Exception):
 
        return unicode(str_, from_encoding, 'replace')
 

	
 

	
 
def safe_str(unicode_, to_encoding=None):
 
    """
 
    safe str function. Does few trick to turn unicode_ into string
 

	
 
    In case of UnicodeEncodeError we try to return it with encoding detected
 
    by chardet library if it fails fallback to string with errors replaced
 

	
 
    :param unicode_: unicode to encode
 
    :rtype: str
 
    :returns: str object
 
    """
 

	
 
    if isinstance(unicode_, str):
 
        return unicode_
 
    if not to_encoding:
 
        import rhodecode
 
        DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding', 'utf8')
 
        to_encoding = DEFAULT_ENCODING
 
    try:
 
        return unicode_.encode(to_encoding)
 
    except UnicodeEncodeError:
 
        pass
 

	
 
    try:
 
        import chardet
 
        encoding = chardet.detect(unicode_)['encoding']
 
        print encoding
 
        if encoding is None:
 
            raise UnicodeEncodeError()
 

	
 
        return unicode_.encode(encoding)
 
    except (ImportError, UnicodeEncodeError):
 
        return unicode_.encode(to_encoding, 'replace')
 

	
 
    return safe_str
 

	
 

	
 
def author_email(author):
 
    """
 
    returns email address of given author.
 
    If any of <,> sign are found, it fallbacks to regex findall()
 
    and returns first found result or empty string
 

	
 
    Regex taken from http://www.regular-expressions.info/email.html
 
    """
 
    import re
 
    r = author.find('>')
 
    l = author.find('<')
 

	
 
    if l == -1 or r == -1:
 
        # fallback to regex match of email out of a string
 
        email_re = re.compile(r"""[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!"""
 
                              r"""#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z"""
 
                              r"""0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]"""
 
                              r"""*[a-z0-9])?""", re.IGNORECASE)
 
        m = re.findall(email_re, author)
 
        return m[0] if m else ''
 

	
 
    return author[l + 1:r].strip()
 

	
 

	
 
def author_name(author):
 
    """
 
    get name of author, or else username.
 
    It'll try to find an email in the author string and just cut it off
 
    to get the username
 
    """
 

	
 
    if not '@' in author:
 
        return author
 
    else:
 
        return author.replace(author_email(author), '').replace('<', '')\
 
            .replace('>', '').strip()
rhodecode/model/db.py
Show inline comments
 
@@ -458,385 +458,385 @@ class UsersGroup(Base, BaseModel):
 
            )
 
        return q.scalar()
 

	
 
    @classmethod
 
    def get(cls, users_group_id, cache=False):
 
        users_group = cls.query()
 
        if cache:
 
            users_group = users_group.options(FromCache("sql_cache_short",
 
                                    "get_users_group_%s" % users_group_id))
 
        return users_group.get(users_group_id)
 

	
 

	
 
class UsersGroupMember(Base, BaseModel):
 
    __tablename__ = 'users_groups_members'
 
    __table_args__ = (
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8'},
 
    )
 

	
 
    users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
 
    users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
 
    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
 

	
 
    user = relationship('User', lazy='joined')
 
    users_group = relationship('UsersGroup')
 

	
 
    def __init__(self, gr_id='', u_id=''):
 
        self.users_group_id = gr_id
 
        self.user_id = u_id
 

	
 

	
 
class Repository(Base, BaseModel):
 
    __tablename__ = 'repositories'
 
    __table_args__ = (
 
        UniqueConstraint('repo_name'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8'},
 
    )
 

	
 
    repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
 
    repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
 
    clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
 
    repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
 
    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
 
    private = Column("private", Boolean(), nullable=True, unique=None, default=None)
 
    enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
 
    enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
 
    description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
 
    created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
 

	
 
    fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
 
    group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
 

	
 
    user = relationship('User')
 
    fork = relationship('Repository', remote_side=repo_id)
 
    group = relationship('RepoGroup')
 
    repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
 
    users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
 
    stats = relationship('Statistics', cascade='all', uselist=False)
 

	
 
    followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
 

	
 
    logs = relationship('UserLog')
 

	
 
    def __unicode__(self):
 
        return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id,
 
                                   self.repo_name)
 

	
 
    @classmethod
 
    def url_sep(cls):
 
        return '/'
 

	
 
    @classmethod
 
    def get_by_repo_name(cls, repo_name):
 
        q = Session.query(cls).filter(cls.repo_name == repo_name)
 
        q = q.options(joinedload(Repository.fork))\
 
                .options(joinedload(Repository.user))\
 
                .options(joinedload(Repository.group))
 
        return q.scalar()
 

	
 
    @classmethod
 
    def get_repo_forks(cls, repo_id):
 
        return cls.query().filter(Repository.fork_id == repo_id)
 

	
 
    @classmethod
 
    def base_path(cls):
 
        """
 
        Returns base path when all repos are stored
 

	
 
        :param cls:
 
        """
 
        q = Session.query(RhodeCodeUi)\
 
            .filter(RhodeCodeUi.ui_key == cls.url_sep())
 
        q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
 
        return q.one().ui_value
 

	
 
    @property
 
    def just_name(self):
 
        return self.repo_name.split(Repository.url_sep())[-1]
 

	
 
    @property
 
    def groups_with_parents(self):
 
        groups = []
 
        if self.group is None:
 
            return groups
 

	
 
        cur_gr = self.group
 
        groups.insert(0, cur_gr)
 
        while 1:
 
            gr = getattr(cur_gr, 'parent_group', None)
 
            cur_gr = cur_gr.parent_group
 
            if gr is None:
 
                break
 
            groups.insert(0, gr)
 

	
 
        return groups
 

	
 
    @property
 
    def groups_and_repo(self):
 
        return self.groups_with_parents, self.just_name
 

	
 
    @LazyProperty
 
    def repo_path(self):
 
        """
 
        Returns base full path for that repository means where it actually
 
        exists on a filesystem
 
        """
 
        q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
 
                                              Repository.url_sep())
 
        q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
 
        return q.one().ui_value
 

	
 
    @property
 
    def repo_full_path(self):
 
        p = [self.repo_path]
 
        # we need to split the name by / since this is how we store the
 
        # names in the database, but that eventually needs to be converted
 
        # into a valid system path
 
        p += self.repo_name.split(Repository.url_sep())
 
        return os.path.join(*p)
 

	
 
    def get_new_name(self, repo_name):
 
        """
 
        returns new full repository name based on assigned group and new new
 

	
 
        :param group_name:
 
        """
 
        path_prefix = self.group.full_path_splitted if self.group else []
 
        return Repository.url_sep().join(path_prefix + [repo_name])
 

	
 
    @property
 
    def _ui(self):
 
        """
 
        Creates an db based ui object for this repository
 
        """
 
        from mercurial import ui
 
        from mercurial import config
 
        baseui = ui.ui()
 

	
 
        #clean the baseui object
 
        baseui._ocfg = config.config()
 
        baseui._ucfg = config.config()
 
        baseui._tcfg = config.config()
 

	
 
        ret = RhodeCodeUi.query()\
 
            .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
 

	
 
        hg_ui = ret
 
        for ui_ in hg_ui:
 
            if ui_.ui_active:
 
                log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
 
                          ui_.ui_key, ui_.ui_value)
 
                baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
 

	
 
        return baseui
 

	
 
    @classmethod
 
    def is_valid(cls, repo_name):
 
        """
 
        returns True if given repo name is a valid filesystem repository
 

	
 
        :param cls:
 
        :param repo_name:
 
        """
 
        from rhodecode.lib.utils import is_valid_repo
 

	
 
        return is_valid_repo(repo_name, cls.base_path())
 

	
 
    #==========================================================================
 
    # SCM PROPERTIES
 
    #==========================================================================
 

	
 
    def get_changeset(self, rev):
 
    def get_changeset(self, rev=None):
 
        return get_changeset_safe(self.scm_instance, rev)
 

	
 
    @property
 
    def tip(self):
 
        return self.get_changeset('tip')
 

	
 
    @property
 
    def author(self):
 
        return self.tip.author
 

	
 
    @property
 
    def last_change(self):
 
        return self.scm_instance.last_change
 

	
 
    def comments(self, revisions=None):
 
        """
 
        Returns comments for this repository grouped by revisions
 

	
 
        :param revisions: filter query by revisions only
 
        """
 
        cmts = ChangesetComment.query()\
 
            .filter(ChangesetComment.repo == self)
 
        if revisions:
 
            cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
 
        grouped = defaultdict(list)
 
        for cmt in cmts.all():
 
            grouped[cmt.revision].append(cmt)
 
        return grouped
 

	
 
    def statuses(self, revisions=None):
 
        """
 
        Returns statuses for this repository
 

	
 
        :param revisions: list of revisions to get statuses for
 
        :type revisions: list
 
        """
 

	
 
        statuses = ChangesetStatus.query()\
 
            .filter(ChangesetStatus.repo == self)
 
        if revisions:
 
            statuses = statuses.filter(ChangesetStatus.revision.in_(revisions))
 
        grouped = {}
 
        for stat in statuses.all():
 
            grouped[stat.revision] = [str(stat.status), stat.status_lbl]
 
        return grouped
 

	
 
    #==========================================================================
 
    # SCM CACHE INSTANCE
 
    #==========================================================================
 

	
 
    @property
 
    def invalidate(self):
 
        return CacheInvalidation.invalidate(self.repo_name)
 

	
 
    def set_invalidate(self):
 
        """
 
        set a cache for invalidation for this instance
 
        """
 
        CacheInvalidation.set_invalidate(self.repo_name)
 

	
 
    @LazyProperty
 
    def scm_instance(self):
 
        return self.__get_instance()
 

	
 
    @property
 
    def scm_instance_cached(self):
 
        @cache_region('long_term')
 
        def _c(repo_name):
 
            return self.__get_instance()
 
        rn = self.repo_name
 
        log.debug('Getting cached instance of repo')
 
        inv = self.invalidate
 
        if inv is not None:
 
            region_invalidate(_c, None, rn)
 
            # update our cache
 
            CacheInvalidation.set_valid(inv.cache_key)
 
        return _c(rn)
 

	
 
    def __get_instance(self):
 
        repo_full_path = self.repo_full_path
 
        try:
 
            alias = get_scm(repo_full_path)[0]
 
            log.debug('Creating instance of %s repository' % alias)
 
            backend = get_backend(alias)
 
        except VCSError:
 
            log.error(traceback.format_exc())
 
            log.error('Perhaps this repository is in db and not in '
 
                      'filesystem run rescan repositories with '
 
                      '"destroy old data " option from admin panel')
 
            return
 

	
 
        if alias == 'hg':
 

	
 
            repo = backend(safe_str(repo_full_path), create=False,
 
                           baseui=self._ui)
 
            # skip hidden web repository
 
            if repo._get_hidden():
 
                return
 
        else:
 
            repo = backend(repo_full_path, create=False)
 

	
 
        return repo
 

	
 

	
 
class RepoGroup(Base, BaseModel):
 
    __tablename__ = 'groups'
 
    __table_args__ = (
 
        UniqueConstraint('group_name', 'group_parent_id'),
 
        CheckConstraint('group_id != group_parent_id'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8'},
 
    )
 
    __mapper_args__ = {'order_by': 'group_name'}
 

	
 
    group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
 
    group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
 
    group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
 
    group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
 

	
 
    repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
 
    users_group_to_perm = relationship('UsersGroupRepoGroupToPerm', cascade='all')
 

	
 
    parent_group = relationship('RepoGroup', remote_side=group_id)
 

	
 
    def __init__(self, group_name='', parent_group=None):
 
        self.group_name = group_name
 
        self.parent_group = parent_group
 

	
 
    def __unicode__(self):
 
        return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
 
                                  self.group_name)
 

	
 
    @classmethod
 
    def groups_choices(cls):
 
        from webhelpers.html import literal as _literal
 
        repo_groups = [('', '')]
 
        sep = ' &raquo; '
 
        _name = lambda k: _literal(sep.join(k))
 

	
 
        repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
 
                              for x in cls.query().all()])
 

	
 
        repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
 
        return repo_groups
 

	
 
    @classmethod
 
    def url_sep(cls):
 
        return '/'
 

	
 
    @classmethod
 
    def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
 
        if case_insensitive:
 
            gr = cls.query()\
 
                .filter(cls.group_name.ilike(group_name))
 
        else:
 
            gr = cls.query()\
 
                .filter(cls.group_name == group_name)
 
        if cache:
 
            gr = gr.options(FromCache(
 
                            "sql_cache_short",
 
                            "get_group_%s" % _hash_key(group_name)
 
                            )
 
            )
 
        return gr.scalar()
 

	
 
    @property
 
    def parents(self):
 
        parents_recursion_limit = 5
 
        groups = []
 
        if self.parent_group is None:
 
            return groups
 
        cur_gr = self.parent_group
 
        groups.insert(0, cur_gr)
 
        cnt = 0
 
        while 1:
 
            cnt += 1
 
            gr = getattr(cur_gr, 'parent_group', None)
 
            cur_gr = cur_gr.parent_group
 
            if gr is None:
 
                break
 
            if cnt == parents_recursion_limit:
 
                # this will prevent accidental infinit loops
 
                log.error('group nested more than %s' %
 
                          parents_recursion_limit)
 
                break
 

	
 
            groups.insert(0, gr)
 
        return groups
 

	
 
    @property
 
    def children(self):
 
        return RepoGroup.query().filter(RepoGroup.parent_group == self)
 
@@ -1108,249 +1108,250 @@ class CacheInvalidation(Base, BaseModel)
 
        return u"<%s('%s:%s')>" % (self.__class__.__name__,
 
                                  self.cache_id, self.cache_key)
 
    @classmethod
 
    def clear_cache(cls):
 
        cls.query().delete()
 

	
 
    @classmethod
 
    def _get_key(cls, key):
 
        """
 
        Wrapper for generating a key, together with a prefix
 

	
 
        :param key:
 
        """
 
        import rhodecode
 
        prefix = ''
 
        iid = rhodecode.CONFIG.get('instance_id')
 
        if iid:
 
            prefix = iid
 
        return "%s%s" % (prefix, key), prefix, key.rstrip('_README')
 

	
 
    @classmethod
 
    def get_by_key(cls, key):
 
        return cls.query().filter(cls.cache_key == key).scalar()
 

	
 
    @classmethod
 
    def _get_or_create_key(cls, key, prefix, org_key):
 
        inv_obj = Session.query(cls).filter(cls.cache_key == key).scalar()
 
        if not inv_obj:
 
            try:
 
                inv_obj = CacheInvalidation(key, org_key)
 
                Session.add(inv_obj)
 
                Session.commit()
 
            except Exception:
 
                log.error(traceback.format_exc())
 
                Session.rollback()
 
        return inv_obj
 

	
 
    @classmethod
 
    def invalidate(cls, key):
 
        """
 
        Returns Invalidation object if this given key should be invalidated
 
        None otherwise. `cache_active = False` means that this cache
 
        state is not valid and needs to be invalidated
 

	
 
        :param key:
 
        """
 

	
 
        key, _prefix, _org_key = cls._get_key(key)
 
        inv = cls._get_or_create_key(key, _prefix, _org_key)
 

	
 
        if inv and inv.cache_active is False:
 
            return inv
 

	
 
    @classmethod
 
    def set_invalidate(cls, key):
 
        """
 
        Mark this Cache key for invalidation
 

	
 
        :param key:
 
        """
 

	
 
        key, _prefix, _org_key = cls._get_key(key)
 
        inv_objs = Session.query(cls).filter(cls.cache_args == _org_key).all()
 
        log.debug('marking %s key[s] %s for invalidation' % (len(inv_objs),
 
                                                             _org_key))
 
        try:
 
            for inv_obj in inv_objs:
 
                if inv_obj:
 
                    inv_obj.cache_active = False
 

	
 
                Session.add(inv_obj)
 
            Session.commit()
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            Session.rollback()
 

	
 
    @classmethod
 
    def set_valid(cls, key):
 
        """
 
        Mark this cache key as active and currently cached
 

	
 
        :param key:
 
        """
 
        inv_obj = cls.get_by_key(key)
 
        inv_obj.cache_active = True
 
        Session.add(inv_obj)
 
        Session.commit()
 

	
 

	
 
class ChangesetComment(Base, BaseModel):
 
    __tablename__ = 'changeset_comments'
 
    __table_args__ = (
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8'},
 
    )
 
    comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
 
    repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
 
    revision = Column('revision', String(40), nullable=False)
 
    line_no = Column('line_no', Unicode(10), nullable=True)
 
    f_path = Column('f_path', Unicode(1000), nullable=True)
 
    user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
 
    text = Column('text', Unicode(25000), nullable=False)
 
    modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
 

	
 
    author = relationship('User', lazy='joined')
 
    repo = relationship('Repository')
 

	
 
    @classmethod
 
    def get_users(cls, revision):
 
        """
 
        Returns user associated with this changesetComment. ie those
 
        who actually commented
 

	
 
        :param cls:
 
        :param revision:
 
        """
 
        return Session.query(User)\
 
                .filter(cls.revision == revision)\
 
                .join(ChangesetComment.author).all()
 

	
 

	
 
class ChangesetStatus(Base, BaseModel):
 
    __tablename__ = 'changeset_statuses'
 
    __table_args__ = (
 
        UniqueConstraint('repo_id', 'revision'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8'}
 
    )
 

	
 
    STATUSES = [
 
        ('not_reviewed', _("Not Reviewed")),  # (no icon) and default
 
        ('approved', _("Approved")),
 
        ('rejected', _("Rejected")),
 
        ('under_review', _("Under Review")),
 
    ]
 
    DEFAULT = STATUSES[0][0]
 

	
 
    changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
 
    repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
 
    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
 
    revision = Column('revision', String(40), nullable=False)
 
    status = Column('status', String(128), nullable=False, default=DEFAULT)
 
    modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
 

	
 
    author = relationship('User', lazy='joined')
 
    repo = relationship('Repository')
 

	
 
    @property
 
    def status_lbl(self):
 
        return dict(self.STATUSES).get(self.status)
 

	
 

	
 
class ChangesetStatusHistory(Base, BaseModel):
 
    __tablename__ = 'changeset_statuses_history'
 
    __table_args__ = (
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8'}
 
    )
 
    #TODO: check if sqla has a nice history table implementation
 
    changeset_status_id = Column('changeset_status_id', Integer(), ForeignKey('changeset_statuses.changeset_status_id'), nullable=False, primary_key=True)
 
    user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
 
    status = Column('status', String(128), nullable=False)
 
    modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
 

	
 

	
 
class Notification(Base, BaseModel):
 
    __tablename__ = 'notifications'
 
    __table_args__ = (
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8'},
 
    )
 

	
 
    TYPE_CHANGESET_COMMENT = u'cs_comment'
 
    TYPE_MESSAGE = u'message'
 
    TYPE_MENTION = u'mention'
 
    TYPE_REGISTRATION = u'registration'
 
    TYPE_PULL_REQUEST = u'pull_request'
 

	
 
    notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
 
    subject = Column('subject', Unicode(512), nullable=True)
 
    body = Column('body', Unicode(50000), nullable=True)
 
    created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
 
    created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
 
    type_ = Column('type', Unicode(256))
 

	
 
    created_by_user = relationship('User')
 
    notifications_to_users = relationship('UserNotification', lazy='joined',
 
                                          cascade="all, delete, delete-orphan")
 

	
 
    @property
 
    def recipients(self):
 
        return [x.user for x in UserNotification.query()\
 
                .filter(UserNotification.notification == self).all()]
 
                .filter(UserNotification.notification == self)\
 
                .order_by(UserNotification.user).all()]
 

	
 
    @classmethod
 
    def create(cls, created_by, subject, body, recipients, type_=None):
 
        if type_ is None:
 
            type_ = Notification.TYPE_MESSAGE
 

	
 
        notification = cls()
 
        notification.created_by_user = created_by
 
        notification.subject = subject
 
        notification.body = body
 
        notification.type_ = type_
 
        notification.created_on = datetime.datetime.now()
 

	
 
        for u in recipients:
 
            assoc = UserNotification()
 
            assoc.notification = notification
 
            u.notifications.append(assoc)
 
        Session.add(notification)
 
        return notification
 

	
 
    @property
 
    def description(self):
 
        from rhodecode.model.notification import NotificationModel
 
        return NotificationModel().make_description(self)
 

	
 

	
 
class UserNotification(Base, BaseModel):
 
    __tablename__ = 'user_to_notification'
 
    __table_args__ = (
 
        UniqueConstraint('user_id', 'notification_id'),
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8'}
 
    )
 
    user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
 
    notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
 
    read = Column('read', Boolean, default=False)
 
    sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
 

	
 
    user = relationship('User', lazy="joined")
 
    notification = relationship('Notification', lazy="joined",
 
                                order_by=lambda: Notification.created_on.desc(),)
 

	
 
    def mark_as_read(self):
 
        self.read = True
 
        Session.add(self)
 

	
 

	
 
class DbMigrateVersion(Base, BaseModel):
 
    __tablename__ = 'db_migrate_version'
 
    __table_args__ = (
 
        {'extend_existing': True, 'mysql_engine': 'InnoDB',
 
         'mysql_charset': 'utf8'},
 
    )
 
    repository_id = Column('repository_id', String(250), primary_key=True)
 
    repository_path = Column('repository_path', Text)
 
    version = Column('version', Integer)
rhodecode/model/user.py
Show inline comments
 
@@ -36,557 +36,554 @@ from rhodecode.model import BaseModel
 
from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \
 
    UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \
 
    Notification, RepoGroup, UserRepoGroupToPerm, UsersGroup,\
 
    UsersGroupRepoGroupToPerm
 
from rhodecode.lib.exceptions import DefaultUserException, \
 
    UserOwnsReposException
 

	
 
from sqlalchemy.exc import DatabaseError
 

	
 
from sqlalchemy.orm import joinedload
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
PERM_WEIGHTS = {
 
    'repository.none': 0,
 
    'repository.read': 1,
 
    'repository.write': 3,
 
    'repository.admin': 4,
 
    'group.none': 0,
 
    'group.read': 1,
 
    'group.write': 3,
 
    'group.admin': 4,
 
}
 

	
 

	
 
class UserModel(BaseModel):
 

	
 
    def __get_user(self, user):
 
        return self._get_instance(User, user, callback=User.get_by_username)
 

	
 
    def __get_perm(self, permission):
 
        return self._get_instance(Permission, permission,
 
                                  callback=Permission.get_by_key)
 

	
 
    def get(self, user_id, cache=False):
 
        user = self.sa.query(User)
 
        if cache:
 
            user = user.options(FromCache("sql_cache_short",
 
                                          "get_user_%s" % user_id))
 
        return user.get(user_id)
 

	
 
    def get_user(self, user):
 
        return self.__get_user(user)
 

	
 
    def get_by_username(self, username, cache=False, case_insensitive=False):
 

	
 
        if case_insensitive:
 
            user = self.sa.query(User).filter(User.username.ilike(username))
 
        else:
 
            user = self.sa.query(User)\
 
                .filter(User.username == username)
 
        if cache:
 
            user = user.options(FromCache("sql_cache_short",
 
                                          "get_user_%s" % username))
 
        return user.scalar()
 

	
 
    def get_by_api_key(self, api_key, cache=False):
 
        return User.get_by_api_key(api_key, cache)
 

	
 
    def create(self, form_data):
 
        try:
 
            new_user = User()
 
            for k, v in form_data.items():
 
                setattr(new_user, k, v)
 

	
 
            new_user.api_key = generate_api_key(form_data['username'])
 
            self.sa.add(new_user)
 
            return new_user
 
        except:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    def create_or_update(self, username, password, email, name, lastname,
 
                         active=True, admin=False, ldap_dn=None):
 
        """
 
        Creates a new instance if not found, or updates current one
 

	
 
        :param username:
 
        :param password:
 
        :param email:
 
        :param active:
 
        :param name:
 
        :param lastname:
 
        :param active:
 
        :param admin:
 
        :param ldap_dn:
 
        """
 

	
 
        from rhodecode.lib.auth import get_crypt_password
 

	
 
        log.debug('Checking for %s account in RhodeCode database' % username)
 
        user = User.get_by_username(username, case_insensitive=True)
 
        if user is None:
 
            log.debug('creating new user %s' % username)
 
            new_user = User()
 
        else:
 
            log.debug('updating user %s' % username)
 
            new_user = user
 

	
 
        try:
 
            new_user.username = username
 
            new_user.admin = admin
 
            new_user.password = get_crypt_password(password)
 
            new_user.api_key = generate_api_key(username)
 
            new_user.email = email
 
            new_user.active = active
 
            new_user.ldap_dn = safe_unicode(ldap_dn) if ldap_dn else None
 
            new_user.name = name
 
            new_user.lastname = lastname
 
            self.sa.add(new_user)
 
            return new_user
 
        except (DatabaseError,):
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    def create_for_container_auth(self, username, attrs):
 
        """
 
        Creates the given user if it's not already in the database
 

	
 
        :param username:
 
        :param attrs:
 
        """
 
        if self.get_by_username(username, case_insensitive=True) is None:
 

	
 
            # autogenerate email for container account without one
 
            generate_email = lambda usr: '%s@container_auth.account' % usr
 

	
 
            try:
 
                new_user = User()
 
                new_user.username = username
 
                new_user.password = None
 
                new_user.api_key = generate_api_key(username)
 
                new_user.email = attrs['email']
 
                new_user.active = attrs.get('active', True)
 
                new_user.name = attrs['name'] or generate_email(username)
 
                new_user.lastname = attrs['lastname']
 

	
 
                self.sa.add(new_user)
 
                return new_user
 
            except (DatabaseError,):
 
                log.error(traceback.format_exc())
 
                self.sa.rollback()
 
                raise
 
        log.debug('User %s already exists. Skipping creation of account'
 
                  ' for container auth.', username)
 
        return None
 

	
 
    def create_ldap(self, username, password, user_dn, attrs):
 
        """
 
        Checks if user is in database, if not creates this user marked
 
        as ldap user
 

	
 
        :param username:
 
        :param password:
 
        :param user_dn:
 
        :param attrs:
 
        """
 
        from rhodecode.lib.auth import get_crypt_password
 
        log.debug('Checking for such ldap account in RhodeCode database')
 
        if self.get_by_username(username, case_insensitive=True) is None:
 

	
 
            # autogenerate email for ldap account without one
 
            generate_email = lambda usr: '%s@ldap.account' % usr
 

	
 
            try:
 
                new_user = User()
 
                username = username.lower()
 
                # add ldap account always lowercase
 
                new_user.username = username
 
                new_user.password = get_crypt_password(password)
 
                new_user.api_key = generate_api_key(username)
 
                new_user.email = attrs['email'] or generate_email(username)
 
                new_user.active = attrs.get('active', True)
 
                new_user.ldap_dn = safe_unicode(user_dn)
 
                new_user.name = attrs['name']
 
                new_user.lastname = attrs['lastname']
 

	
 
                self.sa.add(new_user)
 
                return new_user
 
            except (DatabaseError,):
 
                log.error(traceback.format_exc())
 
                self.sa.rollback()
 
                raise
 
        log.debug('this %s user exists skipping creation of ldap account',
 
                  username)
 
        return None
 

	
 
    def create_registration(self, form_data):
 
        from rhodecode.model.notification import NotificationModel
 

	
 
        try:
 
            new_user = User()
 
            for k, v in form_data.items():
 
                if k != 'admin':
 
                    setattr(new_user, k, v)
 
            form_data['admin'] = False
 
            new_user = self.create(form_data)
 

	
 
            self.sa.add(new_user)
 
            self.sa.flush()
 

	
 
            # notification to admins
 
            subject = _('new user registration')
 
            body = ('New user registration\n'
 
                    '---------------------\n'
 
                    '- Username: %s\n'
 
                    '- Full Name: %s\n'
 
                    '- Email: %s\n')
 
            body = body % (new_user.username, new_user.full_name,
 
                           new_user.email)
 
            edit_url = url('edit_user', id=new_user.user_id, qualified=True)
 
            kw = {'registered_user_url': edit_url}
 
            NotificationModel().create(created_by=new_user, subject=subject,
 
                                       body=body, recipients=None,
 
                                       type_=Notification.TYPE_REGISTRATION,
 
                                       email_kwargs=kw)
 

	
 
        except:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    def update(self, user_id, form_data):
 
        try:
 
            user = self.get(user_id, cache=False)
 
            if user.username == 'default':
 
                raise DefaultUserException(
 
                                _("You can't Edit this user since it's"
 
                                  " crucial for entire application"))
 

	
 
            for k, v in form_data.items():
 
                if k == 'new_password' and v != '':
 
                    user.password = v
 
                    user.api_key = generate_api_key(user.username)
 
                else:
 
                    setattr(user, k, v)
 

	
 
            self.sa.add(user)
 
        except:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    def update_my_account(self, user_id, form_data):
 
        try:
 
            user = self.get(user_id, cache=False)
 
            if user.username == 'default':
 
                raise DefaultUserException(
 
                                _("You can't Edit this user since it's"
 
                                  " crucial for entire application"))
 
            for k, v in form_data.items():
 
                if k == 'new_password' and v != '':
 
                    user.password = v
 
                    user.api_key = generate_api_key(user.username)
 
                else:
 
                    if k not in ['admin', 'active']:
 
                        setattr(user, k, v)
 

	
 
            self.sa.add(user)
 
        except:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    def delete(self, user):
 
        user = self.__get_user(user)
 

	
 
        try:
 
            if user.username == 'default':
 
                raise DefaultUserException(
 
                    _(u"You can't remove this user since it's"
 
                      " crucial for entire application")
 
                )
 
            if user.repositories:
 
                repos = [x.repo_name for x in user.repositories]
 
                raise UserOwnsReposException(
 
                    _(u'user "%s" still owns %s repositories and cannot be '
 
                      'removed. Switch owners or remove those repositories. %s')
 
                    % (user.username, len(repos), ', '.join(repos))
 
                )
 
            self.sa.delete(user)
 
        except:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
    def reset_password_link(self, data):
 
        from rhodecode.lib.celerylib import tasks, run_task
 
        run_task(tasks.send_password_link, data['email'])
 

	
 
    def reset_password(self, data):
 
        from rhodecode.lib.celerylib import tasks, run_task
 
        run_task(tasks.reset_user_password, data['email'])
 

	
 
    def fill_data(self, auth_user, user_id=None, api_key=None):
 
        """
 
        Fetches auth_user by user_id,or api_key if present.
 
        Fills auth_user attributes with those taken from database.
 
        Additionally set's is_authenitated if lookup fails
 
        present in database
 

	
 
        :param auth_user: instance of user to set attributes
 
        :param user_id: user id to fetch by
 
        :param api_key: api key to fetch by
 
        """
 
        if user_id is None and api_key is None:
 
            raise Exception('You need to pass user_id or api_key')
 

	
 
        try:
 
            if api_key:
 
                dbuser = self.get_by_api_key(api_key)
 
            else:
 
                dbuser = self.get(user_id)
 

	
 
            if dbuser is not None and dbuser.active:
 
                log.debug('filling %s data' % dbuser)
 
                for k, v in dbuser.get_dict().items():
 
                    setattr(auth_user, k, v)
 
            else:
 
                return False
 

	
 
        except:
 
            log.error(traceback.format_exc())
 
            auth_user.is_authenticated = False
 
            return False
 

	
 
        return True
 

	
 
    def fill_perms(self, user):
 
        """
 
        Fills user permission attribute with permissions taken from database
 
        works for permissions given for repositories, and for permissions that
 
        are granted to groups
 

	
 
        :param user: user instance to fill his perms
 
        """
 
        RK = 'repositories'
 
        GK = 'repositories_groups'
 
        GLOBAL = 'global'
 
        user.permissions[RK] = {}
 
        user.permissions[GK] = {}
 
        user.permissions[GLOBAL] = set()
 

	
 
        #======================================================================
 
        # fetch default permissions
 
        #======================================================================
 
        default_user = User.get_by_username('default', cache=True)
 
        default_user_id = default_user.user_id
 

	
 
        default_repo_perms = Permission.get_default_perms(default_user_id)
 
        default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
 

	
 
        if user.is_admin:
 
            #==================================================================
 
            # admin user have all default rights for repositories
 
            # and groups set to admin
 
            #==================================================================
 
            user.permissions[GLOBAL].add('hg.admin')
 

	
 
            # repositories
 
            for perm in default_repo_perms:
 
                r_k = perm.UserRepoToPerm.repository.repo_name
 
                p = 'repository.admin'
 
                user.permissions[RK][r_k] = p
 

	
 
            # repositories groups
 
            for perm in default_repo_groups_perms:
 
                rg_k = perm.UserRepoGroupToPerm.group.group_name
 
                p = 'group.admin'
 
                user.permissions[GK][rg_k] = p
 
            return user
 

	
 
        #==================================================================
 
        # set default permissions first for repositories and groups
 
        #==================================================================
 
        uid = user.user_id
 

	
 
        # default global permissions
 
        default_global_perms = self.sa.query(UserToPerm)\
 
            .filter(UserToPerm.user_id == default_user_id)
 

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

	
 
        # defaults for repositories, taken from default user
 
        for perm in default_repo_perms:
 
            r_k = perm.UserRepoToPerm.repository.repo_name
 
            if perm.Repository.private and not (perm.Repository.user_id == uid):
 
                # disable defaults for private repos,
 
                p = 'repository.none'
 
            elif perm.Repository.user_id == uid:
 
                # set admin if owner
 
                p = 'repository.admin'
 
            else:
 
                p = perm.Permission.permission_name
 

	
 
            user.permissions[RK][r_k] = p
 

	
 
        # defaults for repositories groups taken from default user permission
 
        # on given group
 
        for perm in default_repo_groups_perms:
 
            rg_k = perm.UserRepoGroupToPerm.group.group_name
 
            p = perm.Permission.permission_name
 
            user.permissions[GK][rg_k] = p
 

	
 
        #==================================================================
 
        # overwrite defaults with user permissions if any found
 
        #==================================================================
 

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

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

	
 
        # 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
 

	
 
        # USER GROUP
 
        #==================================================================
 
        # check if user is part of user groups for this repository and
 
        # fill in (or replace with higher) permissions
 
        #==================================================================
 

	
 
        # users group global
 
        user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\
 
            .options(joinedload(UsersGroupToPerm.permission))\
 
            .join((UsersGroupMember, UsersGroupToPerm.users_group_id ==
 
                   UsersGroupMember.users_group_id))\
 
            .filter(UsersGroupMember.user_id == uid).all()
 

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

	
 
        # 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
 
            if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
 
                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]:
 
                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
 
        user_repo_group_perms_from_users_groups = \
 
         self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\
 
         .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\
 
         .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\
 
         .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\
 
         .filter(UsersGroupMember.user_id == uid)\
 
         .all()
 

	
 
        for perm in user_repo_group_perms_from_users_groups:
 
            g_k = perm.UsersGroupRepoGroupToPerm.group.group_name
 
            print perm, g_k
 
            p = perm.Permission.permission_name
 
            cur_perm = user.permissions[GK][g_k]
 
            # overwrite permission only if it's greater than permission
 
            # given from other sources
 
            if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]:
 
                user.permissions[GK][g_k] = p
 

	
 
        return user
 

	
 
    def has_perm(self, user, perm):
 
        if not isinstance(perm, Permission):
 
            raise Exception('perm needs to be an instance of Permission class '
 
                            'got %s instead' % type(perm))
 

	
 
        user = self.__get_user(user)
 

	
 
        return UserToPerm.query().filter(UserToPerm.user == user)\
 
            .filter(UserToPerm.permission == perm).scalar() is not None
 

	
 
    def grant_perm(self, user, perm):
 
        """
 
        Grant user global permissions
 

	
 
        :param user:
 
        :param perm:
 
        """
 
        user = self.__get_user(user)
 
        perm = self.__get_perm(perm)
 
        # if this permission is already granted skip it
 
        _perm = UserToPerm.query()\
 
            .filter(UserToPerm.user == user)\
 
            .filter(UserToPerm.permission == perm)\
 
            .scalar()
 
        if _perm:
 
            return
 
        new = UserToPerm()
 
        new.user = user
 
        new.permission = perm
 
        self.sa.add(new)
 

	
 
    def revoke_perm(self, user, perm):
 
        """
 
        Revoke users global permissions
 

	
 
        :param user:
 
        :param perm:
 
        """
 
        user = self.__get_user(user)
 
        perm = self.__get_perm(perm)
 

	
 
        obj = UserToPerm.query()\
 
                .filter(UserToPerm.user == user)\
 
                .filter(UserToPerm.permission == perm)\
 
                .scalar()
 
        if obj:
 
            self.sa.delete(obj)
rhodecode/public/css/style.css
Show inline comments
 
@@ -2356,438 +2356,442 @@ h3.files_location {
 
		scroll 3px;
 
	height: 16px;
 
	padding-left: 20px;
 
	margin-top: 7px;
 
	text-align: left;
 
}
 
 
#graph {
 
	overflow: hidden;
 
}
 
 
#graph_nodes {
 
	float: left;
 
	margin-right: -6px;
 
	margin-top: 0px;
 
}
 
 
#graph_content {
 
	width: 80%;
 
	float: left;
 
}
 
 
#graph_content .container_header {
 
	border-bottom: 1px solid #DDD;
 
	padding: 10px;
 
	height: 25px;
 
}
 
 
#graph_content #rev_range_container {
 
	padding: 7px 20px;
 
	float: left;
 
}
 
 
#graph_content .container {
 
	border-bottom: 1px solid #DDD;
 
	height: 56px;
 
	overflow: hidden;
 
}
 
 
#graph_content .container .right {
 
	float: right;
 
	width: 23%;
 
	text-align: right;
 
}
 
 
#graph_content .container .left {
 
	float: left;
 
	width: 25%;
 
	padding-left: 5px;
 
}
 
 
#graph_content .container .mid {
 
	float: left;
 
	width: 49%;
 
}
 
 
 
#graph_content .container .left .date {
 
	color: #666;
 
	padding-left: 22px;
 
	font-size: 10px;
 
}
 
 
#graph_content .container .left .author {
 
	height: 22px;
 
}
 
 
#graph_content .container .left .author .user {
 
	color: #444444;
 
	float: left;
 
	margin-left: -4px;
 
	margin-top: 4px;
 
}
 
 
#graph_content .container .mid .message {
 
	white-space: pre-wrap;
 
}
 
 
#graph_content .container .mid .message a:hover{
 
	text-decoration: none;
 
}
 
#content #graph_content .message .revision-link,
 
#changeset_content .container .message .revision-link
 
 {
 
	color:#3F6F9F;
 
    font-weight: bold !important;
 
}
 
 
#content #graph_content .message .issue-tracker-link,
 
#changeset_content .container .message .issue-tracker-link{
 
    color:#3F6F9F;
 
    font-weight: bold !important;
 
}
 
 
.right .changeset-status-container{
 
    padding-right: 5px;
 
    margin-top:1px;
 
    float:right;
 
    height:14px;
 
}
 
.code-header .changeset-status-container{
 
	float:left;
 
	padding:2px 0px 0px 2px;
 
}
 
.right .changeset-status-container .changeset-status-lbl{
 
	color: rgb(136, 136, 136);
 
    float: left;
 
    padding: 0px 4px 0px 0px;	
 
}
 
.code-header .changeset-status-container .changeset-status-lbl{
 
    float: left;
 
    padding: 0px 4px 0px 0px;   
 
}
 
.right .changeset-status-container .changeset-status-ico{
 
    float: left;
 
}
 
.code-header .changeset-status-container .changeset-status-ico, .container .changeset-status-ico{
 
    float: left;
 
}
 
.right .comments-container{
 
	padding-right: 5px;
 
	margin-top:1px;
 
	float:right;
 
	height:14px;
 
}
 
 
.right .comments-cnt{
 
    float: left;
 
    color: rgb(136, 136, 136); 
 
    padding-right: 2px; 
 
}
 
 
.right .changes{
 
	clear: both;
 
}
 
 
.right .changes .changed_total {
 
	display: block;
 
	float: right;
 
	text-align: center;
 
	min-width: 45px;
 
	cursor: pointer;
 
	color: #444444;
 
	background: #FEA;
 
	-webkit-border-radius: 0px 0px 0px 6px;
 
	-moz-border-radius: 0px 0px 0px 6px;
 
	border-radius: 0px 0px 0px 6px;
 
	padding: 1px;
 
}
 
 
.right .changes .added,.changed,.removed {
 
	display: block;
 
	padding: 1px;
 
	color: #444444;
 
	float: right;
 
	text-align: center;
 
	min-width: 15px;
 
}
 
 
.right .changes .added {
 
	background: #CFC;
 
}
 
 
.right .changes .changed {
 
	background: #FEA;
 
}
 
 
.right .changes .removed {
 
	background: #FAA;
 
}
 
 
.right .merge {
 
  padding: 1px 3px 1px 3px;
 
  background-color: #fca062;
 
  font-size: 10px;
 
  font-weight: bold;
 
  color: #ffffff;
 
  text-transform: uppercase;
 
  white-space: nowrap;
 
  -webkit-border-radius: 3px;
 
  -moz-border-radius: 3px;
 
  border-radius: 3px;
 
  margin-right: 2px;
 
}
 
 
.right .parent {
 
	color: #666666;
 
	clear:both;
 
}
 
.right .logtags{
 
	padding: 2px 2px 2px 2px;
 
}
 
.right .logtags .branchtag,.right .logtags .tagtag,.right .logtags .booktag{
 
    margin: 0px 2px;
 
}
 
 
.right .logtags .branchtag,.logtags .branchtag {
 
  padding: 1px 3px 1px 3px;
 
  background-color: #bfbfbf;
 
  font-size: 10px;
 
  font-weight: bold;
 
  color: #ffffff;
 
  text-transform: uppercase;
 
  white-space: nowrap;
 
  -webkit-border-radius: 3px;
 
  -moz-border-radius: 3px;
 
  border-radius: 3px;
 
}
 
.right .logtags .branchtag a:hover,.logtags .branchtag a{
 
	color: #ffffff;
 
}
 
.right .logtags .branchtag a:hover,.logtags .branchtag a:hover{
 
	text-decoration: none;
 
	color: #ffffff;
 
}
 
.right .logtags .tagtag,.logtags .tagtag {
 
  padding: 1px 3px 1px 3px;
 
  background-color: #62cffc;
 
  font-size: 10px;
 
  font-weight: bold;
 
  color: #ffffff;
 
  text-transform: uppercase;
 
  white-space: nowrap;
 
  -webkit-border-radius: 3px;
 
  -moz-border-radius: 3px;
 
  border-radius: 3px;
 
}
 
.right .logtags .tagtag a:hover,.logtags .tagtag a{
 
	color: #ffffff;
 
}
 
.right .logtags .tagtag a:hover,.logtags .tagtag a:hover{
 
    text-decoration: none;
 
    color: #ffffff;
 
}
 
.right .logbooks .bookbook,.logbooks .bookbook {
 
  padding: 1px 3px 2px;
 
.right .logbooks .bookbook,.logbooks .bookbook,.right .logtags .bookbook,.logtags .bookbook {
 
  padding: 1px 3px 1px 3px;
 
  background-color: #46A546;
 
  font-size: 9.75px;
 
  font-size: 10px;
 
  font-weight: bold;
 
  color: #ffffff;
 
  text-transform: uppercase;
 
  white-space: nowrap;
 
  -webkit-border-radius: 3px;
 
  -moz-border-radius: 3px;
 
  border-radius: 3px;
 
}
 
.right .logbooks .bookbook,.logbooks .bookbook a{
 
.right .logbooks .bookbook,.logbooks .bookbook a,.right .logtags .bookbook,.logtags .bookbook a{
 
	color: #ffffff;
 
}
 
.right .logbooks .bookbook,.logbooks .bookbook a:hover{
 
.right .logbooks .bookbook,.logbooks .bookbook a:hover,.right .logtags .bookbook,.logtags .bookbook a:hover{
 
    text-decoration: none;
 
    color: #ffffff;
 
}
 
div.browserblock {
 
	overflow: hidden;
 
	border: 1px solid #ccc;
 
	background: #f8f8f8;
 
	font-size: 100%;
 
	line-height: 125%;
 
	padding: 0;
 
    -webkit-border-radius: 6px 6px 0px 0px;
 
    -moz-border-radius: 6px 6px 0px 0px;
 
    border-radius: 6px 6px 0px 0px;	
 
}
 
 
div.browserblock .browser-header {
 
	background: #FFF;
 
	padding: 10px 0px 15px 0px;
 
	width: 100%;
 
}
 
 
div.browserblock .browser-nav {
 
	float: left
 
}
 
 
div.browserblock .browser-branch {
 
	float: left;
 
}
 
 
div.browserblock .browser-branch label {
 
	color: #4A4A4A;
 
	vertical-align: text-top;
 
}
 
 
div.browserblock .browser-header span {
 
	margin-left: 5px;
 
	font-weight: 700;
 
}
 
 
div.browserblock .browser-search {
 
	clear: both;
 
	padding: 8px 8px 0px 5px;
 
	height: 20px;
 
}
 
 
div.browserblock #node_filter_box {
 
	
 
}
 
 
div.browserblock .search_activate {
 
	float: left
 
}
 
 
div.browserblock .add_node {
 
	float: left;
 
	padding-left: 5px;
 
}
 
 
div.browserblock .search_activate a:hover,div.browserblock .add_node a:hover
 
	{
 
	text-decoration: none !important;
 
}
 
 
div.browserblock .browser-body {
 
	background: #EEE;
 
	border-top: 1px solid #CCC;
 
}
 
 
table.code-browser {
 
	border-collapse: collapse;
 
	width: 100%;
 
}
 
 
table.code-browser tr {
 
	margin: 3px;
 
}
 
 
table.code-browser thead th {
 
	background-color: #EEE;
 
	height: 20px;
 
	font-size: 1.1em;
 
	font-weight: 700;
 
	text-align: left;
 
	padding-left: 10px;
 
}
 
 
table.code-browser tbody td {
 
	padding-left: 10px;
 
	height: 20px;
 
}
 
 
table.code-browser .browser-file {
 
	background: url("../images/icons/document_16.png") no-repeat scroll 3px;
 
	height: 16px;
 
	padding-left: 20px;
 
	text-align: left;
 
}
 
.diffblock .changeset_header {
 
    height: 16px;
 
}
 
.diffblock .changeset_file {
 
	background: url("../images/icons/file.png") no-repeat scroll 3px;
 
	text-align: left;
 
	float: left;
 
	padding: 2px 0px 2px 22px;
 
}
 
.diffblock .diff-menu-wrapper{
 
	float: left;
 
}
 
 
.diffblock .diff-menu{
 
    position: absolute;
 
    background: none repeat scroll 0 0 #FFFFFF;
 
    border-color: #003367 #666666 #666666;
 
    border-right: 1px solid #666666;
 
    border-style: solid solid solid;
 
    border-width: 1px;
 
    box-shadow: 2px 8px 4px rgba(0, 0, 0, 0.2);
 
    margin-top:5px;
 
    margin-left:1px;
 
    
 
}
 
.diffblock .diff-actions {
 
    padding: 2px 0px 0px 2px;
 
    float: left;
 
}
 
.diffblock  .diff-menu ul li {
 
	padding: 0px 0px 0px 0px !important;
 
}
 
.diffblock  .diff-menu ul li a{
 
	display: block;
 
	padding: 3px 8px 3px 8px !important;
 
}
 
.diffblock  .diff-menu ul li a:hover{
 
    text-decoration: none;
 
    background-color: #EEEEEE;
 
}
 
table.code-browser .browser-dir {
 
	background: url("../images/icons/folder_16.png") no-repeat scroll 3px;
 
	height: 16px;
 
	padding-left: 20px;
 
	text-align: left;
 
}
 
 
table.code-browser .submodule-dir {
 
    background: url("../images/icons/disconnect.png") no-repeat scroll 3px;
 
    height: 16px;
 
    padding-left: 20px;
 
    text-align: left;
 
}
 
 
 
.box .search {
 
	clear: both;
 
	overflow: hidden;
 
	margin: 0;
 
	padding: 0 20px 10px;
 
}
 
 
.box .search div.search_path {
 
	background: none repeat scroll 0 0 #EEE;
 
	border: 1px solid #CCC;
 
	color: blue;
 
	margin-bottom: 10px;
 
	padding: 10px 0;
 
}
 
 
.box .search div.search_path div.link {
 
	font-weight: 700;
 
	margin-left: 25px;
 
}
 
 
.box .search div.search_path div.link a {
 
	color: #003367;
 
	cursor: pointer;
 
	text-decoration: none;
 
}
 
 
#path_unlock {
 
	color: red;
 
	font-size: 1.2em;
 
	padding-left: 4px;
 
}
 
 
.info_box span {
 
	margin-left: 3px;
 
	margin-right: 3px;
 
}
 
 
.info_box .rev {
 
	color: #003367;
 
	font-size: 1.6em;
rhodecode/templates/changelog/changelog.html
Show inline comments
 
## -*- coding: utf-8 -*-
 

	
 
<%inherit file="/base/base.html"/>
 

	
 
<%def name="title()">
 
${c.repo_name} ${_('Changelog')} - ${c.rhodecode_name}
 
</%def>
 

	
 
<%def name="breadcrumbs_links()">
 
    ${h.link_to(u'Home',h.url('/'))}
 
    &raquo;
 
    ${h.link_to(c.repo_name,h.url('summary_home',repo_name=c.repo_name))}
 
    &raquo;
 
    ${_('Changelog')} - ${_('showing ')} ${c.size if c.size <= c.total_cs else c.total_cs} ${_('out of')} ${c.total_cs} ${_('revisions')}
 
</%def>
 

	
 
<%def name="page_nav()">
 
	${self.menu('changelog')}
 
</%def>
 

	
 
<%def name="main()">
 
<div class="box">
 
    <!-- box / title -->
 
    <div class="title">
 
        ${self.breadcrumbs()}
 
    </div>
 
    <div class="table">
 
		% if c.pagination:
 
			<div id="graph">
 
				<div id="graph_nodes">
 
					<canvas id="graph_canvas"></canvas>
 
				</div>
 
				<div id="graph_content">
 
                    <div class="info_box" style="clear: both;padding: 10px 6px;vertical-align: right;text-align: right;"><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}" class="ui-btn small">${_('Open new pull request')}</a></div>
 
					<div class="container_header">
 
				        ${h.form(h.url.current(),method='get')}
 
				        <div class="info_box" style="float:left">
 
				          ${h.submit('set',_('Show'),class_="ui-btn")}
 
				          ${h.text('size',size=1,value=c.size)}
 
				          ${_('revisions')}
 
				        </div>
 
				        ${h.end_form()}
 
					<div id="rev_range_container" style="display:none"></div>
 
                    <div style="float:right">${h.select('branch_filter',c.branch_name,c.branch_filters)}</div>
 
					</div>
 

	
 
				%for cnt,cs in enumerate(c.pagination):
 
					<div id="chg_${cnt+1}" class="container ${'tablerow%s' % (cnt%2)}">
 
						<div class="left">
 
							<div>
 
							${h.checkbox(cs.short_id,class_="changeset_range")}
 
							<span class="tooltip" title="${h.age(cs.date)}"><a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}"><span class="changeset_id">${cs.revision}:<span class="changeset_hash">${h.short_id(cs.raw_id)}</span></span></a></span>
 
							</div>
 
							<div class="author">
 
								<div class="gravatar">
 
									<img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/>
 
								</div>
 
								<div title="${cs.author}" class="user">${h.person(cs.author)}</div>
 
							</div>
 
                            <div class="date">${cs.date}</div>
 
						</div>
 
						<div class="mid">
 
                            <div class="message">${h.urlify_commit(h.wrap_paragraphs(cs.message),c.repo_name,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</div>
 
                            <div class="expand"><span class="expandtext">&darr; ${_('show more')} &darr;</span></div>
 
						</div>
 
						<div class="right">
 
									<div class="changes">
 
                                        <div id="${cs.raw_id}"  style="float:right;" class="changed_total tooltip" title="${_('Affected number of files, click to show more details')}">${len(cs.affected_files)}</div>
 
                                        <div class="comments-container">
 
                                        %if len(c.comments.get(cs.raw_id,[])) > 0:
 
                                            <div class="comments-cnt" title="${('comments')}">
 
                                              <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id,anchor='comment-%s' % c.comments[cs.raw_id][0].comment_id)}">
 
                                               <div class="comments-cnt">${len(c.comments[cs.raw_id])}</div>
 
                                               <img src="${h.url('/images/icons/comments.png')}">
 
                                              </a>
 
                                            </div>
 
                                        %endif
 
                                        </div>
 
                                        <div class="changeset-status-container">
 
                                            %if c.statuses.get(cs.raw_id):
 
                                              <div title="${_('Changeset status')}" class="changeset-status-lbl">${c.statuses.get(cs.raw_id)[1]}</div>
 
                                              <div class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses.get(cs.raw_id)[0])}" /></div>
 
                                            %endif
 
                                        </div>
 
									</div>
 
								   %if cs.parents:
 
									%for p_cs in reversed(cs.parents):
 
										<div class="parent">${_('Parent')}
 
											<span class="changeset_id">${p_cs.revision}:<span class="changeset_hash">${h.link_to(h.short_id(p_cs.raw_id),
 
											h.url('changeset_home',repo_name=c.repo_name,revision=p_cs.raw_id),title=p_cs.message)}</span></span>
 
										</div>
 
									%endfor
 
								   %else:
 
                                        <div class="parent">${_('No parents')}</div>
 
                                   %endif
 

	
 
								<span class="logtags">
 
									%if len(cs.parents)>1:
 
									<span class="merge">${_('merge')}</span>
 
									%endif
 
									%if cs.branch:
 
									<span class="branchtag" title="${'%s %s' % (_('branch'),cs.branch)}">
 
									   ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
 
									   ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
 
                                    </span>
 
									%endif
 
                                    %if h.is_hg(c.rhodecode_repo):
 
                                      %for book in cs.bookmarks:
 
                                      <span class="bookbook" title="${'%s %s' % (_('bookmark'),book)}">
 
                                         ${h.link_to(h.shorter(book),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}
 
                                      </span>
 
                                      %endfor
 
									%endif
 
									%for tag in cs.tags:
 
										<span class="tagtag"  title="${'%s %s' % (_('tag'),tag)}">
 
										${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))}</span>
 
									%endfor
 
								</span>
 
						</div>
 
					</div>
 

	
 
				%endfor
 
				<div class="pagination-wh pagination-left">
 
					${c.pagination.pager('$link_previous ~2~ $link_next')}
 
				</div>
 
				</div>
 
			</div>
 

	
 
			<script type="text/javascript" src="${h.url('/js/graph.js')}"></script>
 
			<script type="text/javascript">
 
				YAHOO.util.Event.onDOMReady(function(){
 

	
 
                    //Monitor range checkboxes and build a link to changesets
 
                    //ranges
 
                    var checkboxes = YUD.getElementsByClassName('changeset_range');
 
                    var url_tmpl = "${h.url('changeset_home',repo_name=c.repo_name,revision='__REVRANGE__')}";
 
                    YUE.on(checkboxes,'click',function(e){
 
                        var checked_checkboxes = [];
 
                        for (pos in checkboxes){
 
                            if(checkboxes[pos].checked){
 
                                checked_checkboxes.push(checkboxes[pos]);
 
                            }
 
                        }
 
                        if(checked_checkboxes.length>1){
 
                        	var rev_end = checked_checkboxes[0].name;
 
                        	var rev_start = checked_checkboxes[checked_checkboxes.length-1].name;
 

	
 
                            var url = url_tmpl.replace('__REVRANGE__',
 
                            		rev_start+'...'+rev_end);
 

	
 
                        var link = "<a href="+url+">${_('Show selected changes __S -> __E')}</a>"
 
                        link = link.replace('__S',rev_start);
 
                        link = link.replace('__E',rev_end);
 
                        YUD.get('rev_range_container').innerHTML = link;
 
                        YUD.setStyle('rev_range_container','display','');
 
                        }
 
                        else{
 
                        	YUD.setStyle('rev_range_container','display','none');
 

	
 
                        }
 
                    });
 

	
 
                    var msgs = YUQ('.message');
 
                    // get first element height
 
                    var el = YUQ('#graph_content .container')[0];
 
                    var row_h = el.clientHeight;
 
                    for(var i=0;i<msgs.length;i++){
 
                    	var m = msgs[i];
 

	
 
                    	var h = m.clientHeight;
 
                    	var pad = YUD.getStyle(m,'padding');
 
                    	if(h > row_h){
 
                    		var offset = row_h - (h+12);
 
                    		YUD.setStyle(m.nextElementSibling,'display','block');
 
                    		YUD.setStyle(m.nextElementSibling,'margin-top',offset+'px');
 
                    	};
 
                    }
 
                    YUE.on(YUQ('.expand'),'click',function(e){
 
                    	var elem = e.currentTarget.parentNode.parentNode;
 
                    	YUD.setStyle(e.currentTarget,'display','none');
 
                    	YUD.setStyle(elem,'height','auto');
 

	
 
                    	//redraw the graph, max_w and jsdata are global vars
 
                        set_canvas(max_w);
 

	
 
                        var r = new BranchRenderer();
 
                        r.render(jsdata,max_w);
 

	
 
                    })
 

	
 
                    // Fetch changeset details
 
                    YUE.on(YUD.getElementsByClassName('changed_total'),'click',function(e){
 
                    	var id = e.currentTarget.id
 
                    	var url = "${h.url('changelog_details',repo_name=c.repo_name,cs='__CS__')}"
 
                    	var url = url.replace('__CS__',id);
 
                    	ypjax(url,id,function(){tooltip_activate()});
 
                    });
 

	
 
                    // change branch filter
 
                    YUE.on(YUD.get('branch_filter'),'change',function(e){
 
                    	var selected_branch = e.currentTarget.options[e.currentTarget.selectedIndex].value;
 
                    	var url_main = "${h.url('changelog_home',repo_name=c.repo_name)}";
 
                    	var url = "${h.url('changelog_home',repo_name=c.repo_name,branch='__BRANCH__')}";
 
                    	var url = url.replace('__BRANCH__',selected_branch);
 
                    	if(selected_branch != ''){
 
                    		window.location = url;
 
                    	}else{
 
                    		window.location = url_main;
 
                    	}
 

	
 
                    });
 

	
 
					function set_canvas(heads) {
 
						var c = document.getElementById('graph_nodes');
 
						var t = document.getElementById('graph_content');
 
						canvas = document.getElementById('graph_canvas');
 
						var div_h = t.clientHeight;
 
						c.style.height=div_h+'px';
 
						canvas.setAttribute('height',div_h);
 
						c.style.height=max_w+'px';
 
						canvas.setAttribute('width',max_w);
 
					};
 
					var heads = 1;
 
					var max_heads = 0;
 
					var jsdata = ${c.jsdata|n};
 

	
 
					for( var i=0;i<jsdata.length;i++){
 
					    var m = Math.max.apply(Math, jsdata[i][1]);
 
					    if (m>max_heads){
 
					        max_heads = m;
 
					    }
 
					}
 
					var max_w = Math.max(100,max_heads*25);
 
					set_canvas(max_w);
 

	
 
					var r = new BranchRenderer();
 
					r.render(jsdata,max_w);
 

	
 
				});
 
			</script>
 
		%else:
 
			${_('There are no changes yet')}
 
		%endif
 
    </div>
 
</div>
 
</%def>
rhodecode/tests/__init__.py
Show inline comments
 
"""Pylons application test package
 

	
 
This package assumes the Pylons environment is already loaded, such as
 
when this script is imported from the `nosetests --with-pylons=test.ini`
 
command.
 

	
 
This module initializes the application via ``websetup`` (`paster
 
setup-app`) and provides the base testing objects.
 
"""
 
import os
 
import time
 
import logging
 
from os.path import join as jn
 

	
 
from unittest import TestCase
 
from tempfile import _RandomNameSequence
 

	
 
from paste.deploy import loadapp
 
from paste.script.appinstall import SetupCommand
 
from pylons import config, url
 
from routes.util import URLGenerator
 
from webtest import TestApp
 

	
 
from rhodecode import is_windows
 
from rhodecode.model.meta import Session
 
from rhodecode.model.db import User
 

	
 
import pylons.test
 

	
 
os.environ['TZ'] = 'UTC'
 
if not is_windows:
 
time.tzset()
 

	
 
log = logging.getLogger(__name__)
 

	
 
__all__ = [
 
    'environ', 'url', 'TestController', 'TESTS_TMP_PATH', 'HG_REPO',
 
    'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO', 'HG_FORK', 'GIT_FORK',
 
    'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
 
    'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
 
    'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL'
 
]
 

	
 
# Invoke websetup with the current config file
 
# SetupCommand('setup-app').run([config_file])
 

	
 
##RUNNING DESIRED TESTS
 
# nosetests -x rhodecode.tests.functional.test_admin_settings:TestSettingsController.test_my_account
 
# nosetests --pdb --pdb-failures
 
environ = {}
 

	
 
#SOME GLOBALS FOR TESTS
 

	
 
TESTS_TMP_PATH = jn('/', 'tmp', 'rc_test_%s' % _RandomNameSequence().next())
 
TEST_USER_ADMIN_LOGIN = 'test_admin'
 
TEST_USER_ADMIN_PASS = 'test12'
 
TEST_USER_ADMIN_EMAIL = 'test_admin@mail.com'
 

	
 
TEST_USER_REGULAR_LOGIN = 'test_regular'
 
TEST_USER_REGULAR_PASS = 'test12'
 
TEST_USER_REGULAR_EMAIL = 'test_regular@mail.com'
 

	
 
TEST_USER_REGULAR2_LOGIN = 'test_regular2'
 
TEST_USER_REGULAR2_PASS = 'test12'
 
TEST_USER_REGULAR2_EMAIL = 'test_regular2@mail.com'
 

	
 
HG_REPO = 'vcs_test_hg'
 
GIT_REPO = 'vcs_test_git'
 

	
 
NEW_HG_REPO = 'vcs_test_hg_new'
 
NEW_GIT_REPO = 'vcs_test_git_new'
 

	
 
HG_FORK = 'vcs_test_hg_fork'
 
GIT_FORK = 'vcs_test_git_fork'
 

	
 

	
 
class TestController(TestCase):
 

	
 
    def __init__(self, *args, **kwargs):
 
        wsgiapp = pylons.test.pylonsapp
 
        config = wsgiapp.config
 

	
 
        self.app = TestApp(wsgiapp)
 
        url._push_object(URLGenerator(config['routes.map'], environ))
 
        self.Session = Session
 
        self.index_location = config['app_conf']['index_dir']
 
        TestCase.__init__(self, *args, **kwargs)
 

	
 
    def log_user(self, username=TEST_USER_ADMIN_LOGIN,
 
                 password=TEST_USER_ADMIN_PASS):
 
        self._logged_username = username
 
        response = self.app.post(url(controller='login', action='index'),
 
                                 {'username':username,
 
                                  'password':password})
 

	
 
        if 'invalid user name' in response.body:
 
            self.fail('could not login using %s %s' % (username, password))
 

	
 
        self.assertEqual(response.status, '302 Found')
 
        ses = response.session['rhodecode_user']
 
        self.assertEqual(ses.get('username'), username)
 
        response = response.follow()
 
        self.assertEqual(ses.get('is_authenticated'), True)
 

	
 
        return response.session['rhodecode_user']
 

	
 
    def _get_logged_user(self):
 
        return User.get_by_username(self._logged_username)
 

	
 
    def checkSessionFlash(self, response, msg):
 
        self.assertTrue('flash' in response.session)
 
        self.assertTrue(msg in response.session['flash'][0][1])
rhodecode/tests/functional/test_files.py
Show inline comments
 
from rhodecode.tests import *
 

	
 
ARCHIVE_SPECS = {
 
    '.tar.bz2': ('application/x-bzip2', 'tbz2', ''),
 
    '.tar.gz': ('application/x-gzip', 'tgz', ''),
 
    '.zip': ('application/zip', 'zip', ''),
 
}
 

	
 

	
 
class TestFilesController(TestController):
 

	
 
    def test_index(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='files', action='index',
 
                                    repo_name=HG_REPO,
 
                                    revision='tip',
 
                                    f_path='/'))
 
        # Test response...
 
        response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/docs">docs</a>')
 
        response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/tests">tests</a>')
 
        response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/vcs">vcs</a>')
 
        response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/.hgignore">.hgignore</a>')
 
        response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/27cd5cce30c96924232dffcd24178a07ffeb5dfc/MANIFEST.in">MANIFEST.in</a>')
 

	
 
    def test_index_revision(self):
 
        self.log_user()
 

	
 
        response = self.app.get(
 
            url(controller='files', action='index',
 
                repo_name=HG_REPO,
 
                revision='7ba66bec8d6dbba14a2155be32408c435c5f4492',
 
                f_path='/')
 
        )
 

	
 
        #Test response...
 

	
 
        response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/docs">docs</a>')
 
        response.mustcontain('<a class="browser-dir ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/tests">tests</a>')
 
        response.mustcontain('<a class="browser-file ypjax-link" href="/vcs_test_hg/files/7ba66bec8d6dbba14a2155be32408c435c5f4492/README.rst">README.rst</a>')
 
        response.mustcontain('1.1 KiB')
 
        response.mustcontain('text/x-python')
 

	
 
    def test_index_different_branch(self):
 
        self.log_user()
 

	
 
        response = self.app.get(url(controller='files', action='index',
 
                                    repo_name=HG_REPO,
 
                                    revision='97e8b885c04894463c51898e14387d80c30ed1ee',
 
                                    f_path='/'))
 

	
 
        response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: git</a></span>""")
 

	
 
    def test_index_paging(self):
 
        self.log_user()
 

	
 
        for r in [(73, 'a066b25d5df7016b45a41b7e2a78c33b57adc235'),
 
                  (92, 'cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e'),
 
                  (109, '75feb4c33e81186c87eac740cee2447330288412'),
 
                  (1, '3d8f361e72ab303da48d799ff1ac40d5ac37c67e'),
 
                  (0, 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]:
 

	
 
            response = self.app.get(url(controller='files', action='index',
 
                                    repo_name=HG_REPO,
 
                                    revision=r[1],
 
                                    f_path='/'))
 

	
 
            response.mustcontain("""@ r%s:%s""" % (r[0], r[1][:12]))
 

	
 
    def test_file_source(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='files', action='index',
 
                                    repo_name=HG_REPO,
 
                                    revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
 
                                    f_path='vcs/nodes.py'))
 

	
 
        #test or history
 
        response.mustcontain("""<optgroup label="Changesets">
 
<option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
 
<option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
 
<option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
 
<option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
 
<option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
 
<option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
 
<option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
 
<option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
 
<option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
 
<option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
 
<option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
 
<option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
 
<option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
 
<option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
 
<option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
 
<option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
 
<option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
 
<option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
 
<option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
 
<option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
 
<option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
 
<option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
 
<option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
 
<option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
 
<option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
 
<option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
 
<option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
 
<option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
 
<option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
 
<option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
 
<option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
 
<option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
 
<option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
 
</optgroup>
 
<optgroup label="Branches">
 
<option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
 
<option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
 
<option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
 
</optgroup>
 
<optgroup label="Tags">
 
<option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
 
<option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
 
<option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
 
<option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
 
<option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
 
</optgroup>
 
""")
 

	
 
        response.mustcontain("""<div class="commit">merge</div>""")
 

	
 
        response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
 

	
 
    def test_file_annotation(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='files', action='index',
 
                                    repo_name=HG_REPO,
 
                                    revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
 
                                    f_path='vcs/nodes.py',
 
                                    annotate=True))
 

	
 

	
 
        response.mustcontain("""<optgroup label="Changesets">
 
<option value="8911406ad776fdd3d0b9932a2e89677e57405a48">r167:8911406ad776 (default)</option>
 
<option value="aa957ed78c35a1541f508d2ec90e501b0a9e3167">r165:aa957ed78c35 (default)</option>
 
<option value="48e11b73e94c0db33e736eaeea692f990cb0b5f1">r140:48e11b73e94c (default)</option>
 
<option value="adf3cbf483298563b968a6c673cd5bde5f7d5eea">r126:adf3cbf48329 (default)</option>
 
<option value="6249fd0fb2cfb1411e764129f598e2cf0de79a6f">r113:6249fd0fb2cf (git)</option>
 
<option value="75feb4c33e81186c87eac740cee2447330288412">r109:75feb4c33e81 (default)</option>
 
<option value="9a4dc232ecdc763ef2e98ae2238cfcbba4f6ad8d">r108:9a4dc232ecdc (default)</option>
 
<option value="595cce4efa21fda2f2e4eeb4fe5f2a6befe6fa2d">r107:595cce4efa21 (default)</option>
 
<option value="4a8bd421fbc2dfbfb70d85a3fe064075ab2c49da">r104:4a8bd421fbc2 (default)</option>
 
<option value="57be63fc8f85e65a0106a53187f7316f8c487ffa">r102:57be63fc8f85 (default)</option>
 
<option value="5530bd87f7e2e124a64d07cb2654c997682128be">r101:5530bd87f7e2 (git)</option>
 
<option value="e516008b1c93f142263dc4b7961787cbad654ce1">r99:e516008b1c93 (default)</option>
 
<option value="41f43fc74b8b285984554532eb105ac3be5c434f">r93:41f43fc74b8b (default)</option>
 
<option value="cc66b61b8455b264a7a8a2d8ddc80fcfc58c221e">r92:cc66b61b8455 (default)</option>
 
<option value="73ab5b616b3271b0518682fb4988ce421de8099f">r91:73ab5b616b32 (default)</option>
 
<option value="e0da75f308c0f18f98e9ce6257626009fdda2b39">r82:e0da75f308c0 (default)</option>
 
<option value="fb2e41e0f0810be4d7103bc2a4c7be16ee3ec611">r81:fb2e41e0f081 (default)</option>
 
<option value="602ae2f5e7ade70b3b66a58cdd9e3e613dc8a028">r76:602ae2f5e7ad (default)</option>
 
<option value="a066b25d5df7016b45a41b7e2a78c33b57adc235">r73:a066b25d5df7 (default)</option>
 
<option value="637a933c905958ce5151f154147c25c1c7b68832">r61:637a933c9059 (web)</option>
 
<option value="0c21004effeb8ce2d2d5b4a8baf6afa8394b6fbc">r60:0c21004effeb (web)</option>
 
<option value="a1f39c56d3f1d52d5fb5920370a2a2716cd9a444">r59:a1f39c56d3f1 (web)</option>
 
<option value="97d32df05c715a3bbf936bf3cc4e32fb77fe1a7f">r58:97d32df05c71 (web)</option>
 
<option value="08eaf14517718dccea4b67755a93368341aca919">r57:08eaf1451771 (web)</option>
 
<option value="22f71ad265265a53238359c883aa976e725aa07d">r56:22f71ad26526 (web)</option>
 
<option value="97501f02b7b4330924b647755663a2d90a5e638d">r49:97501f02b7b4 (web)</option>
 
<option value="86ede6754f2b27309452bb11f997386ae01d0e5a">r47:86ede6754f2b (web)</option>
 
<option value="014c40c0203c423dc19ecf94644f7cac9d4cdce0">r45:014c40c0203c (web)</option>
 
<option value="ee87846a61c12153b51543bf860e1026c6d3dcba">r30:ee87846a61c1 (default)</option>
 
<option value="9bb326a04ae5d98d437dece54be04f830cf1edd9">r26:9bb326a04ae5 (default)</option>
 
<option value="536c1a19428381cfea92ac44985304f6a8049569">r24:536c1a194283 (default)</option>
 
<option value="dc5d2c0661b61928834a785d3e64a3f80d3aad9c">r8:dc5d2c0661b6 (default)</option>
 
<option value="3803844fdbd3b711175fc3da9bdacfcd6d29a6fb">r7:3803844fdbd3 (default)</option>
 
</optgroup>
 
<optgroup label="Branches">
 
<option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">default</option>
 
<option value="97e8b885c04894463c51898e14387d80c30ed1ee">git</option>
 
<option value="2e6a2bf9356ca56df08807f4ad86d480da72a8f4">web</option>
 
</optgroup>
 
<optgroup label="Tags">
 
<option selected="selected" value="27cd5cce30c96924232dffcd24178a07ffeb5dfc">tip</option>
 
<option value="fd4bdb5e9b2a29b4393a4ac6caef48c17ee1a200">0.1.4</option>
 
<option value="17544fbfcd33ffb439e2b728b5d526b1ef30bfcf">0.1.3</option>
 
<option value="a7e60bff65d57ac3a1a1ce3b12a70f8a9e8a7720">0.1.2</option>
 
<option value="eb3a60fc964309c1a318b8dfe26aa2d1586c85ae">0.1.1</option>
 
</optgroup>""")
 

	
 
        response.mustcontain("""<span style="text-transform: uppercase;"><a href="#">branch: default</a></span>""")
 

	
 
    def test_archival(self):
 
        self.log_user()
 

	
 
        for arch_ext, info in ARCHIVE_SPECS.items():
 
            short = '27cd5cce30c9%s' % arch_ext
 
            fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
 
            filename = '%s-%s' % (HG_REPO, fname)
 

	
 
            response = self.app.get(url(controller='files', action='archivefile',
 
            filename = '%s-%s' % (HG_REPO, short)
 
            response = self.app.get(url(controller='files', 
 
                                        action='archivefile',
 
                                        repo_name=HG_REPO,
 
                                        fname=fname))
 

	
 
            assert response.status == '200 OK', 'wrong response code'
 
            assert response.response._headers.items() == [('Pragma', 'no-cache'),
 
            self.assertEqual(response.status, '200 OK')
 
            self.assertEqual(response.response._headers.items(),
 
             [('Pragma', 'no-cache'),
 
                                                  ('Cache-Control', 'no-cache'),
 
                                                  ('Content-Type', '%s; charset=utf-8' % info[0]),
 
                                                  ('Content-Disposition', 'attachment; filename=%s' % filename), ], 'wrong headers'
 
              ('Content-Disposition', 'attachment; filename=%s' % filename),
 
             ]
 
            )
 

	
 
    def test_archival_wrong_ext(self):
 
        self.log_user()
 

	
 
        for arch_ext in ['tar', 'rar', 'x', '..ax', '.zipz']:
 
            fname = '27cd5cce30c96924232dffcd24178a07ffeb5dfc%s' % arch_ext
 

	
 
            response = self.app.get(url(controller='files', action='archivefile',
 
                                        repo_name=HG_REPO,
 
                                        fname=fname))
 
            assert 'Unknown archive type' in response.body
 

	
 
            response.mustcontain('Unknown archive type')
 

	
 
    def test_archival_wrong_revision(self):
 
        self.log_user()
 

	
 
        for rev in ['00x000000', 'tar', 'wrong', '@##$@$424213232', '232dffcd']:
 
            fname = '%s.zip' % rev
 

	
 
            response = self.app.get(url(controller='files', action='archivefile',
 
                                        repo_name=HG_REPO,
 
                                        fname=fname))
 
            assert 'Unknown revision' in response.body
 
            response.mustcontain('Unknown revision')
 

	
 
    #==========================================================================
 
    # RAW FILE
 
    #==========================================================================
 
    def test_raw_file_ok(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='files', action='rawfile',
 
                                    repo_name=HG_REPO,
 
                                    revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
 
                                    f_path='vcs/nodes.py'))
 

	
 
        assert response.content_disposition == "attachment; filename=nodes.py"
 
        assert response.content_type == "text/x-python"
 
        self.assertEqual(response.content_disposition, "attachment; filename=nodes.py")
 
        self.assertEqual(response.content_type, "text/x-python")
 

	
 
    def test_raw_file_wrong_cs(self):
 
        self.log_user()
 
        rev = u'ERRORce30c96924232dffcd24178a07ffeb5dfc'
 
        f_path = 'vcs/nodes.py'
 

	
 
        response = self.app.get(url(controller='files', action='rawfile',
 
                                    repo_name=HG_REPO,
 
                                    revision=rev,
 
                                    f_path=f_path))
 

	
 
        msg = """Revision %r does not exist for this repository""" % (rev)
 
        self.checkSessionFlash(response, msg)
 

	
 
        msg = """%s""" % (HG_REPO)
 
        self.checkSessionFlash(response, msg)
 

	
 
    def test_raw_file_wrong_f_path(self):
 
        self.log_user()
 
        rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
 
        f_path = 'vcs/ERRORnodes.py'
 
        response = self.app.get(url(controller='files', action='rawfile',
 
                                    repo_name=HG_REPO,
 
                                    revision=rev,
 
                                    f_path=f_path))
 

	
 
        msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
 
        self.checkSessionFlash(response, msg)
 

	
 
    #==========================================================================
 
    # RAW RESPONSE - PLAIN
 
    #==========================================================================
 
    def test_raw_ok(self):
 
        self.log_user()
 
        response = self.app.get(url(controller='files', action='raw',
 
                                    repo_name=HG_REPO,
 
                                    revision='27cd5cce30c96924232dffcd24178a07ffeb5dfc',
 
                                    f_path='vcs/nodes.py'))
 

	
 
        assert response.content_type == "text/plain"
 
        self.assertEqual(response.content_type, "text/plain")
 

	
 
    def test_raw_wrong_cs(self):
 
        self.log_user()
 
        rev = u'ERRORcce30c96924232dffcd24178a07ffeb5dfc'
 
        f_path = 'vcs/nodes.py'
 

	
 
        response = self.app.get(url(controller='files', action='raw',
 
                                    repo_name=HG_REPO,
 
                                    revision=rev,
 
                                    f_path=f_path))
 
        msg = """Revision %r does not exist for this repository""" % (rev)
 
        self.checkSessionFlash(response, msg)
 

	
 
        msg = """%s""" % (HG_REPO)
 
        self.checkSessionFlash(response, msg)
 

	
 
    def test_raw_wrong_f_path(self):
 
        self.log_user()
 
        rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
 
        f_path = 'vcs/ERRORnodes.py'
 
        response = self.app.get(url(controller='files', action='raw',
 
                                    repo_name=HG_REPO,
 
                                    revision=rev,
 
                                    f_path=f_path))
 
        msg = "There is no file nor directory at the given path: %r at revision %r" % (f_path, rev[:12])
 
        self.checkSessionFlash(response, msg)
 

	
 
    def test_ajaxed_files_list(self):
 
        self.log_user()
 
        rev = '27cd5cce30c96924232dffcd24178a07ffeb5dfc'
 
        response = self.app.get(
 
            url('files_nodelist_home', repo_name=HG_REPO,f_path='/',revision=rev),
 
            extra_environ={'HTTP_X_PARTIAL_XHR': '1'},
 
        )
 
        response.mustcontain("vcs/web/simplevcs/views/repository.py")
rhodecode/tests/functional/test_login.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
from rhodecode.tests import *
 
from rhodecode.model.db import User, Notification
 
from rhodecode.lib.utils2 import generate_api_key
 
from rhodecode.lib.auth import check_password
 
from rhodecode.model.meta import Session
 

	
 

	
 
class TestLoginController(TestController):
 

	
 
    def tearDown(self):
 
        for n in Notification.query().all():
 
            Session.delete(n)
 

	
 
        Session.commit()
 
        self.assertEqual(Notification.query().all(), [])
 

	
 
    def test_index(self):
 
        response = self.app.get(url(controller='login', action='index'))
 
        self.assertEqual(response.status, '200 OK')
 
        # Test response...
 

	
 
    def test_login_admin_ok(self):
 
        response = self.app.post(url(controller='login', action='index'),
 
                                 {'username':'test_admin',
 
                                  'password':'test12'})
 
        self.assertEqual(response.status, '302 Found')
 
        self.assertEqual(response.session['rhodecode_user'].get('username') ,
 
                         'test_admin')
 
        response = response.follow()
 
        self.assertTrue('%s repository' % HG_REPO in response.body)
 

	
 
    def test_login_regular_ok(self):
 
        response = self.app.post(url(controller='login', action='index'),
 
                                 {'username':'test_regular',
 
                                  'password':'test12'})
 

	
 
        self.assertEqual(response.status, '302 Found')
 
        self.assertEqual(response.session['rhodecode_user'].get('username') ,
 
                         'test_regular')
 
        response = response.follow()
 
        self.assertTrue('%s repository' % HG_REPO in response.body)
 
        self.assertTrue('<a title="Admin" href="/_admin">' not in response.body)
 

	
 
    def test_login_ok_came_from(self):
 
        test_came_from = '/_admin/users'
 
        response = self.app.post(url(controller='login', action='index',
 
                                     came_from=test_came_from),
 
                                 {'username':'test_admin',
 
                                  'password':'test12'})
 
        self.assertEqual(response.status, '302 Found')
 
        response = response.follow()
 

	
 
        self.assertEqual(response.status, '200 OK')
 
        self.assertTrue('Users administration' in response.body)
 

	
 

	
 
    def test_login_short_password(self):
 
        response = self.app.post(url(controller='login', action='index'),
 
                                 {'username':'test_admin',
 
                                  'password':'as'})
 
        self.assertEqual(response.status, '200 OK')
 

	
 
        self.assertTrue('Enter 3 characters or more' in response.body)
 

	
 
    def test_login_wrong_username_password(self):
 
        response = self.app.post(url(controller='login', action='index'),
 
                                 {'username':'error',
 
                                  'password':'test12'})
 
        self.assertEqual(response.status , '200 OK')
 

	
 
        self.assertTrue('invalid user name' in response.body)
 
        self.assertTrue('invalid password' in response.body)
 

	
 
    #==========================================================================
 
    # REGISTRATIONS
 
    #==========================================================================
 
    def test_register(self):
 
        response = self.app.get(url(controller='login', action='register'))
 
        self.assertTrue('Sign Up to RhodeCode' in response.body)
 

	
 
    def test_register_err_same_username(self):
 
        response = self.app.post(url(controller='login', action='register'),
 
                                            {'username':'test_admin',
 
                                             'password':'test12',
 
                                             'password_confirmation':'test12',
 
                                             'email':'goodmail@domain.com',
 
                                             'name':'test',
 
                                             'lastname':'test'})
 

	
 
        self.assertEqual(response.status , '200 OK')
 
        self.assertTrue('This username already exists' in response.body)
 

	
 
    def test_register_err_same_email(self):
 
        response = self.app.post(url(controller='login', action='register'),
 
                                            {'username':'test_admin_0',
 
                                             'password':'test12',
 
                                             'password_confirmation':'test12',
 
                                             'email':'test_admin@mail.com',
 
                                             'name':'test',
 
                                             'lastname':'test'})
 

	
 
        self.assertEqual(response.status , '200 OK')
 
        assert 'This e-mail address is already taken' in response.body
 
        response.mustcontain('This e-mail address is already taken')
 

	
 
    def test_register_err_same_email_case_sensitive(self):
 
        response = self.app.post(url(controller='login', action='register'),
 
                                            {'username':'test_admin_1',
 
                                             'password':'test12',
 
                                             'password_confirmation':'test12',
 
                                             'email':'TesT_Admin@mail.COM',
 
                                             'name':'test',
 
                                             'lastname':'test'})
 
        self.assertEqual(response.status , '200 OK')
 
        assert 'This e-mail address is already taken' in response.body
 
        response.mustcontain('This e-mail address is already taken')
 

	
 
    def test_register_err_wrong_data(self):
 
        response = self.app.post(url(controller='login', action='register'),
 
                                            {'username':'xs',
 
                                             'password':'test',
 
                                             'password_confirmation':'test',
 
                                             'email':'goodmailm',
 
                                             'name':'test',
 
                                             'lastname':'test'})
 
        self.assertEqual(response.status , '200 OK')
 
        assert 'An email address must contain a single @' in response.body
 
        assert 'Enter a value 6 characters long or more' in response.body
 

	
 
        response.mustcontain('An email address must contain a single @')
 
        response.mustcontain('Enter a value 6 characters long or more')
 

	
 
    def test_register_err_username(self):
 
        response = self.app.post(url(controller='login', action='register'),
 
                                            {'username':'error user',
 
                                             'password':'test12',
 
                                             'password_confirmation':'test12',
 
                                             'email':'goodmailm',
 
                                             'name':'test',
 
                                             'lastname':'test'})
 

	
 
        self.assertEqual(response.status , '200 OK')
 
        assert 'An email address must contain a single @' in response.body
 
        assert ('Username may only contain '
 
        response.mustcontain('An email address must contain a single @')
 
        response.mustcontain('Username may only contain '
 
                'alphanumeric characters underscores, '
 
                'periods or dashes and must begin with '
 
                'alphanumeric character') in response.body
 
                'alphanumeric character')
 

	
 
    def test_register_err_case_sensitive(self):
 
        response = self.app.post(url(controller='login', action='register'),
 
                                            {'username':'Test_Admin',
 
                                             'password':'test12',
 
                                             'password_confirmation':'test12',
 
                                             'email':'goodmailm',
 
                                             'name':'test',
 
                                             'lastname':'test'})
 

	
 
        self.assertEqual(response.status , '200 OK')
 
        self.assertTrue('An email address must contain a single @' in response.body)
 
        self.assertTrue('This username already exists' in response.body)
 

	
 

	
 

	
 
    def test_register_special_chars(self):
 
        response = self.app.post(url(controller='login', action='register'),
 
                                            {'username':'xxxaxn',
 
                                             'password':'ąćźżąśśśś',
 
                                             'password_confirmation':'ąćźżąśśśś',
 
                                             'email':'goodmailm@test.plx',
 
                                             'name':'test',
 
                                             'lastname':'test'})
 

	
 
        self.assertEqual(response.status , '200 OK')
 
        self.assertTrue('Invalid characters in password' in response.body)
 

	
 

	
 
    def test_register_password_mismatch(self):
 
        response = self.app.post(url(controller='login', action='register'),
 
                                            {'username':'xs',
 
                                             'password':'123qwe',
 
                                             'password_confirmation':'qwe123',
 
                                             'email':'goodmailm@test.plxa',
 
                                             'name':'test',
 
                                             'lastname':'test'})
 

	
 
        self.assertEqual(response.status , '200 OK')
 
        assert 'Passwords do not match' in response.body
 
        response.mustcontain('Passwords do not match')
 

	
 
    def test_register_ok(self):
 
        username = 'test_regular4'
 
        password = 'qweqwe'
 
        email = 'marcin@test.com'
 
        name = 'testname'
 
        lastname = 'testlastname'
 

	
 
        response = self.app.post(url(controller='login', action='register'),
 
                                            {'username':username,
 
                                             'password':password,
 
                                             'password_confirmation':password,
 
                                             'email':email,
 
                                             'name':name,
 
                                             'lastname':lastname})
 
                                             'lastname':lastname,
 
                                             'admin':True}) # This should be overriden
 
        self.assertEqual(response.status , '302 Found')
 
        assert 'You have successfully registered into rhodecode' in response.session['flash'][0], 'No flash message about user registration'
 
        self.checkSessionFlash(response, 'You have successfully registered into rhodecode')
 

	
 
        ret = self.Session.query(User).filter(User.username == 'test_regular4').one()
 
        assert ret.username == username , 'field mismatch %s %s' % (ret.username, username)
 
        assert check_password(password, ret.password) == True , 'password mismatch'
 
        assert ret.email == email , 'field mismatch %s %s' % (ret.email, email)
 
        assert ret.name == name , 'field mismatch %s %s' % (ret.name, name)
 
        assert ret.lastname == lastname , 'field mismatch %s %s' % (ret.lastname, lastname)
 

	
 
        self.assertEqual(ret.username, username)
 
        self.assertEqual(check_password(password, ret.password), True)
 
        self.assertEqual(ret.email, email)
 
        self.assertEqual(ret.name, name)
 
        self.assertEqual(ret.lastname, lastname)
 
        self.assertNotEqual(ret.api_key, None)
 
        self.assertEqual(ret.admin, False)
 

	
 
    def test_forgot_password_wrong_mail(self):
 
        response = self.app.post(url(controller='login', action='password_reset'),
 
                                            {'email':'marcin@wrongmail.org', })
 
        response = self.app.post(
 
                        url(controller='login', action='password_reset'),
 
                            {'email': 'marcin@wrongmail.org',}
 
        )
 

	
 
        assert "This e-mail address doesn't exist" in response.body, 'Missing error message about wrong email'
 
        response.mustcontain("This e-mail address doesn't exist")
 

	
 
    def test_forgot_password(self):
 
        response = self.app.get(url(controller='login',
 
                                    action='password_reset'))
 
        self.assertEqual(response.status , '200 OK')
 

	
 
        username = 'test_password_reset_1'
 
        password = 'qweqwe'
 
        email = 'marcin@python-works.com'
 
        name = 'passwd'
 
        lastname = 'reset'
 

	
 
        new = User()
 
        new.username = username
 
        new.password = password
 
        new.email = email
 
        new.name = name
 
        new.lastname = lastname
 
        new.api_key = generate_api_key(username)
 
        self.Session.add(new)
 
        self.Session.commit()
 

	
 
        response = self.app.post(url(controller='login',
 
                                     action='password_reset'),
 
                                 {'email':email, })
 

	
 
        self.checkSessionFlash(response, 'Your password reset link was sent')
 

	
 
        response = response.follow()
 

	
 
        # BAD KEY
 

	
 
        key = "bad"
 
        response = self.app.get(url(controller='login',
 
                                    action='password_reset_confirmation',
 
                                    key=key))
 
        self.assertEqual(response.status, '302 Found')
 
        self.assertTrue(response.location.endswith(url('reset_password')))
 

	
 
        # GOOD KEY
 

	
 
        key = User.get_by_username(username).api_key
 
        response = self.app.get(url(controller='login',
 
                                    action='password_reset_confirmation',
 
                                    key=key))
 
        self.assertEqual(response.status, '302 Found')
 
        self.assertTrue(response.location.endswith(url('login_home')))
 

	
 
        self.checkSessionFlash(response,
 
                               ('Your password reset was successful, '
 
                                'new password has been sent to your email'))
 

	
 
        response = response.follow()
rhodecode/tests/test_models.py
Show inline comments
 
import os
 
import unittest
 
from rhodecode.tests import *
 

	
 
from rhodecode.model.repos_group import ReposGroupModel
 
from rhodecode.model.repo import RepoModel
 
from rhodecode.model.db import RepoGroup, User, Notification, UserNotification, \
 
    UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm
 
    UsersGroup, UsersGroupMember, Permission, UsersGroupRepoGroupToPerm,\
 
    Repository
 
from sqlalchemy.exc import IntegrityError
 
from rhodecode.model.user import UserModel
 

	
 
from rhodecode.model.meta import Session
 
from rhodecode.model.notification import NotificationModel
 
from rhodecode.model.users_group import UsersGroupModel
 
from rhodecode.lib.auth import AuthUser
 

	
 

	
 
def _make_group(path, desc='desc', parent_id=None,
 
                 skip_if_exists=False):
 

	
 
    gr = RepoGroup.get_by_group_name(path)
 
    if gr and skip_if_exists:
 
        return gr
 

	
 
    gr = ReposGroupModel().create(path, desc, parent_id)
 
    return gr
 

	
 

	
 
class TestReposGroups(unittest.TestCase):
 

	
 
    def setUp(self):
 
        self.g1 = _make_group('test1', skip_if_exists=True)
 
        Session.commit()
 
        self.g2 = _make_group('test2', skip_if_exists=True)
 
        Session.commit()
 
        self.g3 = _make_group('test3', skip_if_exists=True)
 
        Session.commit()
 

	
 
    def tearDown(self):
 
        print 'out'
 

	
 
    def __check_path(self, *path):
 
        """
 
        Checks the path for existance !
 
        """
 
        path = [TESTS_TMP_PATH] + list(path)
 
        path = os.path.join(*path)
 
        return os.path.isdir(path)
 

	
 
    def _check_folders(self):
 
        print os.listdir(TESTS_TMP_PATH)
 

	
 
    def __delete_group(self, id_):
 
        ReposGroupModel().delete(id_)
 

	
 
    def __update_group(self, id_, path, desc='desc', parent_id=None):
 
        form_data = dict(
 
            group_name=path,
 
            group_description=desc,
 
            group_parent_id=parent_id,
 
            perms_updates=[],
 
            perms_new=[]
 
        )
 
        gr = ReposGroupModel().update(id_, form_data)
 
        return gr
 

	
 
    def test_create_group(self):
 
        g = _make_group('newGroup')
 
        self.assertEqual(g.full_path, 'newGroup')
 

	
 
        self.assertTrue(self.__check_path('newGroup'))
 

	
 
    def test_create_same_name_group(self):
 
        self.assertRaises(IntegrityError, lambda:_make_group('newGroup'))
 
        Session.rollback()
 

	
 
    def test_same_subgroup(self):
 
        sg1 = _make_group('sub1', parent_id=self.g1.group_id)
 
        self.assertEqual(sg1.parent_group, self.g1)
 
        self.assertEqual(sg1.full_path, 'test1/sub1')
 
        self.assertTrue(self.__check_path('test1', 'sub1'))
 

	
 
        ssg1 = _make_group('subsub1', parent_id=sg1.group_id)
 
        self.assertEqual(ssg1.parent_group, sg1)
 
        self.assertEqual(ssg1.full_path, 'test1/sub1/subsub1')
 
        self.assertTrue(self.__check_path('test1', 'sub1', 'subsub1'))
 

	
 
    def test_remove_group(self):
 
        sg1 = _make_group('deleteme')
 
        self.__delete_group(sg1.group_id)
 

	
 
        self.assertEqual(RepoGroup.get(sg1.group_id), None)
 
        self.assertFalse(self.__check_path('deteteme'))
 

	
 
        sg1 = _make_group('deleteme', parent_id=self.g1.group_id)
 
        self.__delete_group(sg1.group_id)
 

	
 
        self.assertEqual(RepoGroup.get(sg1.group_id), None)
 
        self.assertFalse(self.__check_path('test1', 'deteteme'))
 

	
 
    def test_rename_single_group(self):
 
        sg1 = _make_group('initial')
 

	
 
        new_sg1 = self.__update_group(sg1.group_id, 'after')
 
        self.assertTrue(self.__check_path('after'))
 
        self.assertEqual(RepoGroup.get_by_group_name('initial'), None)
 

	
 
    def test_update_group_parent(self):
 

	
 
        sg1 = _make_group('initial', parent_id=self.g1.group_id)
 

	
 
        new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g1.group_id)
 
        self.assertTrue(self.__check_path('test1', 'after'))
 
        self.assertEqual(RepoGroup.get_by_group_name('test1/initial'), None)
 

	
 
        new_sg1 = self.__update_group(sg1.group_id, 'after', parent_id=self.g3.group_id)
 
        self.assertTrue(self.__check_path('test3', 'after'))
 
        self.assertEqual(RepoGroup.get_by_group_name('test3/initial'), None)
 

	
 
        new_sg1 = self.__update_group(sg1.group_id, 'hello')
 
        self.assertTrue(self.__check_path('hello'))
 

	
 
        self.assertEqual(RepoGroup.get_by_group_name('hello'), new_sg1)
 

	
 
    def test_subgrouping_with_repo(self):
 

	
 
        g1 = _make_group('g1')
 
        g2 = _make_group('g2')
 

	
 
        # create new repo
 
        form_data = dict(repo_name='john',
 
                         repo_name_full='john',
 
                         fork_name=None,
 
                         description=None,
 
                         repo_group=None,
 
                         private=False,
 
                         repo_type='hg',
 
                         clone_uri=None)
 
        cur_user = User.get_by_username(TEST_USER_ADMIN_LOGIN)
 
        r = RepoModel().create(form_data, cur_user)
 

	
 
        self.assertEqual(r.repo_name, 'john')
 

	
 
        # put repo into group
 
        form_data = form_data
 
        form_data['repo_group'] = g1.group_id
 
        form_data['perms_new'] = []
 
        form_data['perms_updates'] = []
 
        RepoModel().update(r.repo_name, form_data)
 
        self.assertEqual(r.repo_name, 'g1/john')
 

	
 
        self.__update_group(g1.group_id, 'g1', parent_id=g2.group_id)
 
        self.assertTrue(self.__check_path('g2', 'g1'))
 

	
 
        # test repo
 
        self.assertEqual(r.repo_name, os.path.join('g2', 'g1', r.just_name))
 

	
 
        self.assertEqual(r.repo_name, RepoGroup.url_sep().join(['g2', 'g1', r.just_name]))
 

	
 
    def test_move_to_root(self):
 
        g1 = _make_group('t11')
 
        Session.commit()
 
        g2 = _make_group('t22',parent_id=g1.group_id)
 
        Session.commit()
 

	
 
        self.assertEqual(g2.full_path,'t11/t22')
 
        self.assertTrue(self.__check_path('t11', 't22'))
 

	
 
        g2 = self.__update_group(g2.group_id, 'g22', parent_id=None)
 
        Session.commit()
 

	
 
        self.assertEqual(g2.group_name,'g22')
 
        # we moved out group from t1 to '' so it's full path should be 'g2'
 
        self.assertEqual(g2.full_path,'g22')
 
        self.assertFalse(self.__check_path('t11', 't22'))
 
        self.assertTrue(self.__check_path('g22'))
 

	
 

	
 
class TestUser(unittest.TestCase):
 
    def __init__(self, methodName='runTest'):
 
        Session.remove()
 
        super(TestUser, self).__init__(methodName=methodName)
 

	
 
    def test_create_and_remove(self):
 
        usr = UserModel().create_or_update(username=u'test_user', password=u'qweqwe',
 
                                     email=u'u232@rhodecode.org',
 
                                     name=u'u1', lastname=u'u1')
 
        Session.commit()
 
        self.assertEqual(User.get_by_username(u'test_user'), usr)
 

	
 
        # make users group
 
        users_group = UsersGroupModel().create('some_example_group')
 
        Session.commit()
 

	
 
        UsersGroupModel().add_user_to_group(users_group, usr)
 
        Session.commit()
 

	
 
        self.assertEqual(UsersGroup.get(users_group.users_group_id), users_group)
 
        self.assertEqual(UsersGroupMember.query().count(), 1)
 
        UserModel().delete(usr.user_id)
 
        Session.commit()
 

	
 
        self.assertEqual(UsersGroupMember.query().all(), [])
 

	
 

	
 
class TestNotifications(unittest.TestCase):
 

	
 
    def __init__(self, methodName='runTest'):
 
        Session.remove()
 
        self.u1 = UserModel().create_or_update(username=u'u1',
 
                                        password=u'qweqwe',
 
                                        email=u'u1@rhodecode.org',
 
                                        name=u'u1', lastname=u'u1')
 
        Session.commit()
 
        self.u1 = self.u1.user_id
 

	
 
        self.u2 = UserModel().create_or_update(username=u'u2',
 
                                        password=u'qweqwe',
 
                                        email=u'u2@rhodecode.org',
 
                                        name=u'u2', lastname=u'u3')
 
        Session.commit()
 
        self.u2 = self.u2.user_id
 

	
 
        self.u3 = UserModel().create_or_update(username=u'u3',
 
                                        password=u'qweqwe',
 
                                        email=u'u3@rhodecode.org',
 
                                        name=u'u3', lastname=u'u3')
 
        Session.commit()
 
        self.u3 = self.u3.user_id
 

	
 
        super(TestNotifications, self).__init__(methodName=methodName)
 

	
 
    def _clean_notifications(self):
 
        for n in Notification.query().all():
 
            Session.delete(n)
 

	
 
        Session.commit()
 
        self.assertEqual(Notification.query().all(), [])
 

	
 
    def tearDown(self):
 
        self._clean_notifications()
 

	
 
    def test_create_notification(self):
 
        self.assertEqual([], Notification.query().all())
 
        self.assertEqual([], UserNotification.query().all())
 

	
 
        usrs = [self.u1, self.u2]
 
        notification = NotificationModel().create(created_by=self.u1,
 
                                           subject=u'subj', body=u'hi there',
 
                                           recipients=usrs)
 
        Session.commit()
 
        u1 = User.get(self.u1)
 
        u2 = User.get(self.u2)
 
        u3 = User.get(self.u3)
 
        notifications = Notification.query().all()
 
        self.assertEqual(len(notifications), 1)
 

	
 
        unotification = UserNotification.query()\
 
            .filter(UserNotification.notification == notification).all()
 

	
 
        self.assertEqual(notifications[0].recipients, [u1, u2])
 
        self.assertEqual(notification.notification_id,
 
                         notifications[0].notification_id)
 
        self.assertEqual(len(unotification), len(usrs))
 
        self.assertEqual([x.user.user_id for x in unotification], usrs)
 

	
 
    def test_user_notifications(self):
 
        self.assertEqual([], Notification.query().all())
 
        self.assertEqual([], UserNotification.query().all())
 

	
 
        notification1 = NotificationModel().create(created_by=self.u1,
 
                                            subject=u'subj', body=u'hi there1',
 
                                            recipients=[self.u3])
 
        Session.commit()
 
        notification2 = NotificationModel().create(created_by=self.u1,
 
                                            subject=u'subj', body=u'hi there2',
 
                                            recipients=[self.u3])
 
        Session.commit()
 
        u3 = Session.query(User).get(self.u3)
 

	
 
        self.assertEqual(sorted([x.notification for x in u3.notifications]),
 
                         sorted([notification2, notification1]))
 

	
 
    def test_delete_notifications(self):
 
        self.assertEqual([], Notification.query().all())
 
        self.assertEqual([], UserNotification.query().all())
 

	
 
        notification = NotificationModel().create(created_by=self.u1,
 
                                           subject=u'title', body=u'hi there3',
 
                                    recipients=[self.u3, self.u1, self.u2])
 
        Session.commit()
 
        notifications = Notification.query().all()
 
        self.assertTrue(notification in notifications)
 

	
 
        Notification.delete(notification.notification_id)
 
        Session.commit()
 

	
 
        notifications = Notification.query().all()
 
        self.assertFalse(notification in notifications)
 

	
 
        un = UserNotification.query().filter(UserNotification.notification
 
                                             == notification).all()
 
        self.assertEqual(un, [])
 

	
 
    def test_delete_association(self):
 

	
 
        self.assertEqual([], Notification.query().all())
 
        self.assertEqual([], UserNotification.query().all())
 

	
 
        notification = NotificationModel().create(created_by=self.u1,
 
                                           subject=u'title', body=u'hi there3',
 
                                    recipients=[self.u3, self.u1, self.u2])
 
        Session.commit()
 

	
 
        unotification = UserNotification.query()\
 
                            .filter(UserNotification.notification ==
 
                                    notification)\
 
                            .filter(UserNotification.user_id == self.u3)\
 
                            .scalar()
 

	
 
        self.assertEqual(unotification.user_id, self.u3)
 

	
 
        NotificationModel().delete(self.u3,
 
                                   notification.notification_id)
 
        Session.commit()
 

	
 
        u3notification = UserNotification.query()\
 
                            .filter(UserNotification.notification ==
 
                                    notification)\
 
                            .filter(UserNotification.user_id == self.u3)\
 
                            .scalar()
 

	
 
        self.assertEqual(u3notification, None)
 

	
 
        # notification object is still there
 
        self.assertEqual(Notification.query().all(), [notification])
 

	
 
        #u1 and u2 still have assignments
 
        u1notification = UserNotification.query()\
 
                            .filter(UserNotification.notification ==
 
                                    notification)\
 
                            .filter(UserNotification.user_id == self.u1)\
 
                            .scalar()
 
        self.assertNotEqual(u1notification, None)
 
        u2notification = UserNotification.query()\
 
                            .filter(UserNotification.notification ==
 
                                    notification)\
 
                            .filter(UserNotification.user_id == self.u2)\
 
                            .scalar()
 
        self.assertNotEqual(u2notification, None)
 
@@ -431,285 +431,285 @@ class TestPermissions(unittest.TestCase)
 
            email=u'u1@rhodecode.org', name=u'u1', lastname=u'u1'
 
        )
 
        self.u2 = UserModel().create_or_update(
 
            username=u'u2', password=u'qweqwe',
 
            email=u'u2@rhodecode.org', name=u'u2', lastname=u'u2'
 
        )
 
        self.anon = User.get_by_username('default')
 
        self.a1 = UserModel().create_or_update(
 
            username=u'a1', password=u'qweqwe',
 
            email=u'a1@rhodecode.org', name=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.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 !
 
        u1_auth = AuthUser(user_id=self.a1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
 
                         perms['repositories'][HG_REPO])
 

	
 
    def test_default_group_perms(self):
 
        self.g1 = _make_group('test1', skip_if_exists=True)
 
        self.g2 = _make_group('test2', skip_if_exists=True)
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        perms = {
 
            'repositories_groups': {u'test1': 'group.read', u'test2': 'group.read'},
 
            '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])
 
        self.assertEqual(u1_auth.permissions['repositories_groups'],
 
                         perms['repositories_groups'])
 

	
 
    def test_default_admin_group_perms(self):
 
        self.g1 = _make_group('test1', skip_if_exists=True)
 
        self.g2 = _make_group('test2', skip_if_exists=True)
 
        a1_auth = AuthUser(user_id=self.a1.user_id)
 
        perms = {
 
            'repositories_groups': {u'test1': 'group.admin', u'test2': 'group.admin'},
 
            'global': set(['hg.admin']),
 
            'repositories': {u'vcs_test_hg': 'repository.admin'}
 
        }
 

	
 
        self.assertEqual(a1_auth.permissions['repositories'][HG_REPO],
 
                         perms['repositories'][HG_REPO])
 
        self.assertEqual(a1_auth.permissions['repositories_groups'],
 
                         perms['repositories_groups'])
 

	
 
    def test_propagated_permission_from_users_group(self):
 
        # make group
 
        self.ug1 = UsersGroupModel().create('G1')
 
        # add user to group
 
        UsersGroupModel().add_user_to_group(self.ug1, self.u1)
 

	
 
        # set permission to lower
 
        new_perm = 'repository.none'
 
        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)
 

	
 
        # grant perm for group this should override permission from user
 
        new_perm = 'repository.write'
 
        RepoModel().grant_users_group_permission(repo=HG_REPO,
 
                                                 group_name=self.ug1,
 
                                                 perm=new_perm)
 
        # check perms
 
        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],
 
                         new_perm)
 
        self.assertEqual(u1_auth.permissions['repositories_groups'],
 
                         perms['repositories_groups'])
 

	
 
    def test_propagated_permission_from_users_group_lower_weight(self):
 
        # make group
 
        self.ug1 = UsersGroupModel().create('G1')
 
        # add user to group
 
        UsersGroupModel().add_user_to_group(self.ug1, self.u1)
 

	
 
        # set permission to lower
 
        new_perm_h = 'repository.write'
 
        RepoModel().grant_user_permission(repo=HG_REPO, user=self.u1,
 
                                          perm=new_perm_h)
 
        Session.commit()
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
 
                         new_perm_h)
 

	
 
        # grant perm for group this should NOT override permission from user
 
        # since it's lower than granted
 
        new_perm_l = 'repository.read'
 
        RepoModel().grant_users_group_permission(repo=HG_REPO,
 
                                                 group_name=self.ug1,
 
                                                 perm=new_perm_l)
 
        # check perms
 
        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.write'}
 
        }
 
        self.assertEqual(u1_auth.permissions['repositories'][HG_REPO],
 
                         new_perm_h)
 
        self.assertEqual(u1_auth.permissions['repositories_groups'],
 
                         perms['repositories_groups'])
 

	
 
    def test_repo_in_group_permissions(self):
 
        self.g1 = _make_group('group1', skip_if_exists=True)
 
        self.g2 = _make_group('group2', skip_if_exists=True)
 
        Session.commit()
 
        # both perms should be read !
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories_groups'],
 
                         {u'group1': u'group.read', u'group2': u'group.read'})
 

	
 
        a1_auth = AuthUser(user_id=self.anon.user_id)
 
        self.assertEqual(a1_auth.permissions['repositories_groups'],
 
                 {u'group1': u'group.read', u'group2': u'group.read'})
 

	
 
        #Change perms to none for both groups
 
        ReposGroupModel().grant_user_permission(repos_group=self.g1,
 
                                                user=self.anon,
 
                                                perm='group.none')
 
        ReposGroupModel().grant_user_permission(repos_group=self.g2,
 
                                                user=self.anon,
 
                                                perm='group.none')
 

	
 

	
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories_groups'],
 
                 {u'group1': u'group.none', u'group2': u'group.none'})
 

	
 
        a1_auth = AuthUser(user_id=self.anon.user_id)
 
        self.assertEqual(a1_auth.permissions['repositories_groups'],
 
                 {u'group1': u'group.none', u'group2': u'group.none'})
 

	
 
        # add repo to group
 
        form_data = {
 
            'repo_name':HG_REPO,
 
            'repo_name_full':os.path.join(self.g1.group_name,HG_REPO),
 
            'repo_name_full':RepoGroup.url_sep().join([self.g1.group_name,HG_REPO]),
 
            'repo_type':'hg',
 
            'clone_uri':'',
 
            'repo_group':self.g1.group_id,
 
            'description':'desc',
 
            'private':False
 
        }
 
        self.test_repo = RepoModel().create(form_data, cur_user=self.u1)
 
        Session.commit()
 

	
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories_groups'],
 
                 {u'group1': u'group.none', u'group2': u'group.none'})
 

	
 
        a1_auth = AuthUser(user_id=self.anon.user_id)
 
        self.assertEqual(a1_auth.permissions['repositories_groups'],
 
                 {u'group1': u'group.none', u'group2': u'group.none'})
 

	
 
        #grant permission for u2 !
 
        ReposGroupModel().grant_user_permission(repos_group=self.g1,
 
                                                user=self.u2,
 
                                                perm='group.read')
 
        ReposGroupModel().grant_user_permission(repos_group=self.g2,
 
                                                user=self.u2,
 
                                                perm='group.read')
 
        Session.commit()
 
        self.assertNotEqual(self.u1, self.u2)
 
        #u1 and anon should have not change perms while u2 should !
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories_groups'],
 
                 {u'group1': u'group.none', u'group2': u'group.none'})
 

	
 
        u2_auth = AuthUser(user_id=self.u2.user_id)
 
        self.assertEqual(u2_auth.permissions['repositories_groups'],
 
                 {u'group1': u'group.read', u'group2': u'group.read'})
 

	
 
        a1_auth = AuthUser(user_id=self.anon.user_id)
 
        self.assertEqual(a1_auth.permissions['repositories_groups'],
 
                 {u'group1': u'group.none', u'group2': u'group.none'})
 

	
 
    def test_repo_group_user_as_user_group_member(self):
 
        # create Group1
 
        self.g1 = _make_group('group1', skip_if_exists=True)
 
        Session.commit()
 
        a1_auth = AuthUser(user_id=self.anon.user_id)
 

	
 
        self.assertEqual(a1_auth.permissions['repositories_groups'],
 
                         {u'group1': u'group.read'})
 

	
 
        # set default permission to none
 
        ReposGroupModel().grant_user_permission(repos_group=self.g1,
 
                                                user=self.anon,
 
                                                perm='group.none')
 
        # make group
 
        self.ug1 = UsersGroupModel().create('G1')
 
        # add user to group
 
        UsersGroupModel().add_user_to_group(self.ug1, self.u1)
 
        Session.commit()
 

	
 
        # check if user is in the group
 
        membrs = [x.user_id for x in UsersGroupModel().get(self.ug1.users_group_id).members]
 
        self.assertEqual(membrs, [self.u1.user_id])
 
        # add some user to that group
 

	
 
        # check his permissions
 
        a1_auth = AuthUser(user_id=self.anon.user_id)
 
        self.assertEqual(a1_auth.permissions['repositories_groups'],
 
                         {u'group1': u'group.none'})
 

	
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories_groups'],
 
                         {u'group1': u'group.none'})
 

	
 
        # grant ug1 read permissions for
 
        ReposGroupModel().grant_users_group_permission(repos_group=self.g1,
 
                                                       group_name=self.ug1,
 
                                                       perm='group.read')
 
        Session.commit()
 
        # check if the
 
        obj = Session.query(UsersGroupRepoGroupToPerm)\
 
            .filter(UsersGroupRepoGroupToPerm.group == self.g1)\
 
            .filter(UsersGroupRepoGroupToPerm.users_group == self.ug1)\
 
            .scalar()
 
        self.assertEqual(obj.permission.permission_name, 'group.read')
 

	
 
        a1_auth = AuthUser(user_id=self.anon.user_id)
 

	
 
        self.assertEqual(a1_auth.permissions['repositories_groups'],
 
                         {u'group1': u'group.none'})
 

	
 
        u1_auth = AuthUser(user_id=self.u1.user_id)
 
        self.assertEqual(u1_auth.permissions['repositories_groups'],
 
                         {u'group1': u'group.read'})
0 comments (0 inline, 0 general)