diff --git a/.hgignore b/.hgignore --- a/.hgignore +++ b/.hgignore @@ -6,6 +6,7 @@ syntax: glob *.egg syntax: regexp +^rcextensions ^build ^docs/build/ ^docs/_build/ diff --git a/README.rst b/README.rst --- a/README.rst +++ b/README.rst @@ -15,10 +15,11 @@ RhodeCode is similar in some respects to 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 is powered by a vcs_ library that Lukasz Balcerzak and I created to -handle multiple different version control systems. +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 `Semantic Versioning `_ +RhodeCode uses `PEP386 versioning http://www.python.org/dev/peps/pep-0386/`_ Installation ------------ @@ -99,7 +100,7 @@ RhodeCode Features - 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 -- Async tasks for speed and performance using celery_ (works without them too) +- 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 diff --git a/development.ini b/development.ini --- a/development.ini +++ b/development.ini @@ -93,6 +93,11 @@ issue_prefix = # ## all running rhodecode instances. Leave empty if you don't use it instance_id = +## alternative return HTTP header for failed authentication. Default HTTP +## response is 401 HTTPUnauthorized. Currently HG clients have troubles with +## handling that. Set this variable to 403 to return HTTPForbidden +auth_ret_code = + #################################### ### CELERY CONFIG #### #################################### @@ -171,6 +176,7 @@ beaker.cache.sql_cache_long.key_length = beaker.session.type = file beaker.session.key = rhodecode +# secure cookie requires AES python libraries #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu #beaker.session.validate_key = 9712sds2212c--zxc123 beaker.session.timeout = 36000 @@ -207,13 +213,13 @@ logview.pylons.util = #eee sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode sqlalchemy.db1.echo = false sqlalchemy.db1.pool_recycle = 3600 -sqlalchemy.convert_unicode = true +sqlalchemy.db1.convert_unicode = true ################################ ### LOGGING CONFIGURATION #### ################################ [loggers] -keys = root, routes, rhodecode, sqlalchemy, beaker, templates +keys = root, routes, rhodecode, sqlalchemy, beaker, templates, whoosh_indexer [handlers] keys = console, console_sql @@ -259,6 +265,12 @@ handlers = console_sql qualname = sqlalchemy.engine propagate = 0 +[logger_whoosh_indexer] +level = DEBUG +handlers = +qualname = whoosh_indexer +propagate = 1 + ############## ## HANDLERS ## ############## diff --git a/docs/api/api.rst b/docs/api/api.rst --- a/docs/api/api.rst +++ b/docs/api/api.rst @@ -27,7 +27,7 @@ API ACCESS All clients are required to send JSON-RPC spec JSON data:: { - "id:, + "id:"", "api_key":"", "method":"", "args":{"":""} @@ -50,9 +50,9 @@ Simply provide RhodeCode API will return always a JSON-RPC response:: { - "id":, - "result": "", - "error": null + "id":, # matching id sent by request + "result": ""|null, # JSON formatted result, null if any errors + "error": "null"| # JSON formatted error (if any) } All responses from API will be `HTTP/1.0 200 OK`, if there's an error while @@ -72,6 +72,7 @@ belonging to user with admin rights INPUT:: + id : api_key : "" method : "pull" args : { @@ -94,6 +95,7 @@ rights. INPUT:: + id : api_key : "" method : "get_user" args : { @@ -111,7 +113,15 @@ OUTPUT:: "email" : "", "active" : "", "admin" :  "", - "ldap_dn" : "" + "ldap_dn" : "", + "last_login": "", + "permissions": { + "global": ["hg.create.repository", + "repository.read", + "hg.register.manual_activate"], + "repositories": {"repo1": "repository.none"}, + "repositories_groups": {"Group1": "group.read"} + }, } error: null @@ -126,6 +136,7 @@ belonging to user with admin rights. INPUT:: + id : api_key : "" method : "get_users" args : { } @@ -141,7 +152,8 @@ OUTPUT:: "email" : "", "active" : "", "admin" :  "", - "ldap_dn" : "" + "ldap_dn" : "", + "last_login": "", }, … ] @@ -157,6 +169,7 @@ be executed only using api_key belonging INPUT:: + id : api_key : "" method : "create_user" args : { @@ -188,6 +201,7 @@ be executed only using api_key belonging INPUT:: + id : api_key : "" method : "update_user" args : { @@ -220,6 +234,7 @@ belonging to user with admin rights. INPUT:: + id : api_key : "" method : "get_users_group" args : { @@ -258,6 +273,7 @@ api_key belonging to user with admin rig INPUT:: + id : api_key : "" method : "get_users_groups" args : { } @@ -296,6 +312,7 @@ belonging to user with admin rights INPUT:: + id : api_key : "" method : "create_users_group" args: { @@ -322,6 +339,7 @@ belonging to user with admin rights INPUT:: + id : api_key : "" method : "add_user_users_group" args: { @@ -350,6 +368,7 @@ using api_key belonging to user with adm INPUT:: + id : api_key : "" method : "remove_user_from_users_group" args: { @@ -370,12 +389,14 @@ OUTPUT:: get_repo -------- -Gets an existing repository by it's name or repository_id. This command can +Gets an existing repository by it's name or repository_id. Members will return +either users_group or user associated to that repository. This command can be executed only using api_key belonging to user with admin rights. INPUT:: + id : api_key : "" method : "get_repo" args: { @@ -391,7 +412,9 @@ OUTPUT:: "type" : "", "description" : "", "members" : [ - { "id" : "", + { + "type": "user", + "id" : "", "username" : "", "firstname": "", "lastname" : "", @@ -402,7 +425,8 @@ OUTPUT:: "permission" : "repository.(read|write|admin)" }, … - { + { + "type": "users_group", "id" : "", "name" : "", "active": "", @@ -423,6 +447,7 @@ belonging to user with admin rights INPUT:: + id : api_key : "" method : "get_repos" args: { } @@ -452,6 +477,7 @@ with admin rights INPUT:: + id : api_key : "" method : "get_repo_nodes" args: { @@ -485,6 +511,7 @@ and create "baz" repository with "bar" a INPUT:: + id : api_key : "" method : "create_repo" args: { @@ -514,6 +541,7 @@ belonging to user with admin rights. INPUT:: + id : api_key : "" method : "delete_repo" args: { @@ -538,6 +566,7 @@ with admin rights. INPUT:: + id : api_key : "" method : "grant_user_permission" args: { @@ -563,6 +592,7 @@ only using api_key belonging to user wit INPUT:: + id : api_key : "" method : "revoke_user_permission" args: { @@ -588,6 +618,7 @@ api_key belonging to user with admin rig INPUT:: + id : api_key : "" method : "grant_users_group_permission" args: { @@ -612,6 +643,7 @@ executed only using api_key belonging to INPUT:: + id : api_key : "" method : "revoke_users_group_permission" args: { diff --git a/docs/changelog.rst b/docs/changelog.rst --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -5,7 +5,45 @@ Changelog ========= +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**) ---------------------- diff --git a/docs/index.rst b/docs/index.rst --- a/docs/index.rst +++ b/docs/index.rst @@ -23,7 +23,8 @@ Users Guide usage/git_support usage/statistics usage/backup - + usage/debugging + **Develop** .. toctree:: diff --git a/docs/setup.rst b/docs/setup.rst --- a/docs/setup.rst +++ b/docs/setup.rst @@ -20,9 +20,10 @@ following command to do this:: Next, you need to create the databases used by RhodeCode. I recommend that you -use sqlite (default) or postgresql. If you choose a database other than the +use postgresql or sqlite (default). If you choose a database other than the default ensure you properly adjust the db url in your production.ini -configuration file to use this other database. Create the databases by running +configuration file to use this other database. RhodeCode currently supports +postgresql, sqlite and mysql databases. Create the database by running the following command:: paster setup-app production.ini @@ -57,15 +58,18 @@ You are now ready to use RhodeCode, to r - In the admin panel you can toggle ldap, anonymous, permissions settings. As well as edit more advanced options on users and repositories -Try copying your own mercurial repository into the "root" directory you are -using, then from within the RhodeCode web application choose Admin > -repositories. Then choose Add New Repository. Add the repository you copied -into the root. Test that you can browse your repository from within RhodeCode -and then try cloning your repository from RhodeCode with:: +Optionally users can create `rcextensions` package that extends RhodeCode +functionality. To do this simply execute:: + + paster make-rcext production.ini - hg clone http://127.0.0.1:5000/ +This will create `rcextensions` package in the same place that your `ini` file +lives. With `rcextensions` it's possible to add additional mapping for whoosh, +stats and add additional code into the push/pull/create repo hooks. For example +for sending signals to build-bots such as jenkins. +Please see the `__init__.py` file inside `rcextensions` package +for more details. -where *repository name* is replaced by the name of your repository. Using RhodeCode with SSH ------------------------ diff --git a/docs/upgrade.rst b/docs/upgrade.rst --- a/docs/upgrade.rst +++ b/docs/upgrade.rst @@ -47,6 +47,9 @@ This will upgrade the schema and update and will always recheck the settings of the application, if there are no new options that need to be set. +.. note:: + If you're using Celery, make sure you restart all instances of it after + upgrade. .. _virtualenv: http://pypi.python.org/pypi/virtualenv .. _python: http://www.python.org/ diff --git a/docs/usage/debugging.rst b/docs/usage/debugging.rst new file mode 100644 --- /dev/null +++ b/docs/usage/debugging.rst @@ -0,0 +1,30 @@ +.. _debugging: + +=================== +Debugging RhodeCode +=================== + +If you encountered problems with RhodeCode here are some instructions how to +possibly debug them. + +** First make sure you're using the latest version available.** + +enable detailed debug +--------------------- + +RhodeCode uses standard python logging modules to log it's output. +By default only loggers with INFO level are displayed. To enable full output +change `level = DEBUG` for all logging handlers in currently used .ini file. +This change will allow to see much more detailed output in the logfile or +console. This generally helps a lot to track issues. + + +enable interactive debug mode +----------------------------- + +To enable interactive debug mode simply comment out `set debug = false` in +.ini file, this will trigger and interactive debugger each time there an +error in browser, or send a http link if error occured in the backend. This +is a great tool for fast debugging as you get a handy python console right +in the web view. ** NEVER ENABLE THIS ON PRODUCTION ** the interactive console +can be a serious security threat to you system. diff --git a/docs/usage/general.rst b/docs/usage/general.rst --- a/docs/usage/general.rst +++ b/docs/usage/general.rst @@ -71,6 +71,11 @@ RhodeCode will send mails on user regist on errors the mails will have a detailed traceback of error. +Mails are also sent for code comments. If someone comments on a changeset +mail is sent to all participants, the person who commited the changeset +(if present in RhodeCode), and to all people mentioned with @mention system. + + Trending source files --------------------- diff --git a/init.d/rhodecode-daemon4 b/init.d/rhodecode-daemon4 new file mode 100644 --- /dev/null +++ b/init.d/rhodecode-daemon4 @@ -0,0 +1,70 @@ +#!/bin/bash +########################################### +#### THIS IS AN ARCH LINUX RC.D SCRIPT #### +########################################### + +. /etc/rc.conf +. /etc/rc.d/functions + +DAEMON=rhodecode +APP_HOMEDIR="/srv" +APP_PATH="$APP_HOMEDIR/$DAEMON" +CONF_NAME="production.ini" +LOG_FILE="/var/log/$DAEMON.log" +PID_FILE="/run/daemons/$DAEMON" +APPL=/usr/bin/paster +RUN_AS="*****" + +ARGS="serve --daemon \ +--user=$RUN_AS \ +--group=$RUN_AS \ +--pid-file=$PID_FILE \ +--log-file=$LOG_FILE \ +$APP_PATH/$CONF_NAME" + +[ -r /etc/conf.d/$DAEMON ] && . /etc/conf.d/$DAEMON + +if [[ -r $PID_FILE ]]; then + read -r PID < "$PID_FILE" + if [[ $PID && ! -d /proc/$PID ]]; then + unset PID + rm_daemon $DAEMON + fi +fi + +case "$1" in +start) + stat_busy "Starting $DAEMON" + export HOME=$APP_PATH + [ -z "$PID" ] && $APPL $ARGS &>/dev/null + if [ $? = 0 ]; then + add_daemon $DAEMON + stat_done + else + stat_fail + exit 1 + fi + ;; +stop) + stat_busy "Stopping $DAEMON" + [ -n "$PID" ] && kill $PID &>/dev/null + if [ $? = 0 ]; then + rm_daemon $DAEMON + stat_done + else + stat_fail + exit 1 + fi + ;; +restart) + $0 stop + sleep 1 + $0 start + ;; +status) + stat_busy "Checking $name status"; + ck_status $name + ;; +*) + echo "usage: $0 {start|stop|restart|status}" +esac \ No newline at end of file diff --git a/production.ini b/production.ini --- a/production.ini +++ b/production.ini @@ -93,6 +93,11 @@ issue_prefix = # ## all running rhodecode instances. Leave empty if you don't use it instance_id = +## alternative return HTTP header for failed authentication. Default HTTP +## response is 401 HTTPUnauthorized. Currently HG clients have troubles with +## handling that. Set this variable to 403 to return HTTPForbidden +auth_ret_code = + #################################### ### CELERY CONFIG #### #################################### @@ -208,13 +213,13 @@ logview.pylons.util = #eee sqlalchemy.db1.url = postgresql://postgres:qwe@localhost/rhodecode sqlalchemy.db1.echo = false sqlalchemy.db1.pool_recycle = 3600 -sqlalchemy.convert_unicode = true +sqlalchemy.db1.convert_unicode = true ################################ ### LOGGING CONFIGURATION #### ################################ [loggers] -keys = root, routes, rhodecode, sqlalchemy, beaker, templates +keys = root, routes, rhodecode, sqlalchemy, beaker, templates, whoosh_indexer [handlers] keys = console, console_sql @@ -260,6 +265,12 @@ handlers = console_sql qualname = sqlalchemy.engine propagate = 0 +[logger_whoosh_indexer] +level = DEBUG +handlers = +qualname = whoosh_indexer +propagate = 1 + ############## ## HANDLERS ## ############## diff --git a/requires.txt b/requires.txt --- a/requires.txt +++ b/requires.txt @@ -1,17 +1,17 @@ Pylons==1.0.0 Beaker==1.6.3 -WebHelpers>=1.2 +WebHelpers==1.3 formencode==1.2.4 -SQLAlchemy==0.7.4 -Mako==0.5.0 +SQLAlchemy==0.7.6 +Mako==0.6.2 pygments>=1.4 whoosh>=2.3.0,<2.4 celery>=2.2.5,<2.3 babel python-dateutil>=1.5.0,<2.0.0 -dulwich>=0.8.0,<0.9.0 +https://github.com/jelmer/dulwich/tarball/master webob==1.0.8 markdown==2.1.1 docutils==0.8.1 py-bcrypt -mercurial>=2.1,<2.2 \ No newline at end of file +mercurial>=2.1,<2.2 diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -4,7 +4,7 @@ ~~~~~~~~~~~~~~~~~~ RhodeCode, a web based repository management based on pylons - versioning implementation: http://semver.org/ + versioning implementation: http://www.python.org/dev/peps/pep-0386/ :created_on: Apr 9, 2010 :author: marcink @@ -26,8 +26,18 @@ import sys import platform -VERSION = (1, 3, 3) -__version__ = '.'.join((str(each) for each in VERSION[:4])) +VERSION = (1, 3, 4) + +try: + from rhodecode.lib import get_current_revision + _rev = get_current_revision() + if _rev: + VERSION += ('dev%s' % _rev[0],) +except ImportError: + pass + +__version__ = ('.'.join((str(each) for each in VERSION[:3])) + + '.'.join(VERSION[3:])) __dbversion__ = 5 # defines current db version for migrations __platform__ = platform.system() __license__ = 'GPLv3' @@ -39,16 +49,16 @@ PLATFORM_OTHERS = ('Linux', 'Darwin', 'F requirements = [ "Pylons==1.0.0", "Beaker==1.6.3", - "WebHelpers>=1.2", + "WebHelpers==1.3", "formencode==1.2.4", - "SQLAlchemy==0.7.4", - "Mako==0.5.0", + "SQLAlchemy==0.7.6", + "Mako==0.6.2", "pygments>=1.4", "whoosh>=2.3.0,<2.4", "celery>=2.2.5,<2.3", "babel", "python-dateutil>=1.5.0,<2.0.0", - "dulwich>=0.8.0,<0.9.0", + "dulwich>=0.8.4,<0.9.0", "webob==1.0.8", "markdown==2.1.1", "docutils==0.8.1", @@ -65,17 +75,6 @@ else: requirements.append("mercurial>=2.1,<2.2") -try: - from rhodecode.lib import get_current_revision - _rev = get_current_revision(quiet=True) -except ImportError: - # this is needed when doing some setup.py operations - _rev = False - -if len(VERSION) > 3 and _rev: - __version__ += ' [rev:%s]' % _rev[0] - - def get_version(): """Returns shorter version (digit parts only) as string.""" @@ -90,3 +89,6 @@ CELERY_ON = False # link to config for pylons CONFIG = {} + +# Linked module for extensions +EXTENSIONS = {} diff --git a/rhodecode/config/conf.py b/rhodecode/config/conf.py new file mode 100644 --- /dev/null +++ b/rhodecode/config/conf.py @@ -0,0 +1,58 @@ +# -*- coding: utf-8 -*- +""" + package.rhodecode.config.conf + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + Various config settings for RhodeCode + + :created_on: Mar 7, 2012 + :author: marcink + :copyright: (C) 2009-2010 Marcin Kuzminski + :license: , see LICENSE_FILE for more details. +""" +from rhodecode import EXTENSIONS + +from rhodecode.lib.utils2 import __get_lem + + +# language map is also used by whoosh indexer, which for those specified +# extensions will index it's content +LANGUAGES_EXTENSIONS_MAP = __get_lem() + +#============================================================================== +# WHOOSH INDEX EXTENSIONS +#============================================================================== +# EXTENSIONS WE WANT TO INDEX CONTENT OFF USING WHOOSH +INDEX_EXTENSIONS = LANGUAGES_EXTENSIONS_MAP.keys() + +# list of readme files to search in file tree and display in summary +# attached weights defines the search order lower is first +ALL_READMES = [ + ('readme', 0), ('README', 0), ('Readme', 0), + ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1), + ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2), + ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2), + ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2), +] + +# extension together with weights to search lower is first +RST_EXTS = [ + ('', 0), ('.rst', 1), ('.rest', 1), + ('.RST', 2), ('.REST', 2), + ('.txt', 3), ('.TXT', 3) +] + +MARKDOWN_EXTS = [ + ('.md', 1), ('.MD', 1), + ('.mkdn', 2), ('.MKDN', 2), + ('.mdown', 3), ('.MDOWN', 3), + ('.markdown', 4), ('.MARKDOWN', 4) +] + +PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)] + +ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS + +DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" + +DATE_FORMAT = "%Y-%m-%d" diff --git a/rhodecode/config/deployment.ini_tmpl b/rhodecode/config/deployment.ini_tmpl --- a/rhodecode/config/deployment.ini_tmpl +++ b/rhodecode/config/deployment.ini_tmpl @@ -93,6 +93,11 @@ issue_prefix = # ## all running rhodecode instances. Leave empty if you don't use it instance_id = +## alternative return HTTP header for failed authentication. Default HTTP +## response is 401 HTTPUnauthorized. Currently HG clients have troubles with +## handling that. Set this variable to 403 to return HTTPForbidden +auth_ret_code = + #################################### ### CELERY CONFIG #### #################################### @@ -218,13 +223,13 @@ sqlalchemy.db1.url = sqlite:///%(here)s/ sqlalchemy.db1.echo = false sqlalchemy.db1.pool_recycle = 3600 -sqlalchemy.convert_unicode = true +sqlalchemy.db1.convert_unicode = true ################################ ### LOGGING CONFIGURATION #### ################################ [loggers] -keys = root, routes, rhodecode, sqlalchemy, beaker, templates +keys = root, routes, rhodecode, sqlalchemy, beaker, templates, whoosh_indexer [handlers] keys = console, console_sql @@ -270,6 +275,12 @@ handlers = console_sql qualname = sqlalchemy.engine propagate = 0 +[logger_whoosh_indexer] +level = DEBUG +handlers = +qualname = whoosh_indexer +propagate = 1 + ############## ## HANDLERS ## ############## diff --git a/rhodecode/config/environment.py b/rhodecode/config/environment.py --- a/rhodecode/config/environment.py +++ b/rhodecode/config/environment.py @@ -2,21 +2,24 @@ import os import logging +import rhodecode from mako.lookup import TemplateLookup from pylons.configuration import PylonsConfig from pylons.error import handle_mako_error -import rhodecode +# don't remove this import it does magic for celery +from rhodecode.lib import celerypylons + import rhodecode.lib.app_globals as app_globals -import rhodecode.lib.helpers from rhodecode.config.routing import make_map -# don't remove this import it does magic for celery -from rhodecode.lib import celerypylons, str2bool -from rhodecode.lib import engine_from_config + +from rhodecode.lib import helpers from rhodecode.lib.auth import set_available_permissions -from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config +from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config,\ + load_rcextensions +from rhodecode.lib.utils2 import engine_from_config, str2bool from rhodecode.model import init_model from rhodecode.model.scm import ScmModel @@ -24,17 +27,20 @@ log = logging.getLogger(__name__) def load_environment(global_conf, app_conf, initial=False): - """Configure the Pylons environment via the ``pylons.config`` + """ + Configure the Pylons environment via the ``pylons.config`` object """ config = PylonsConfig() # Pylons paths root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - paths = dict(root=root, - controllers=os.path.join(root, 'controllers'), - static_files=os.path.join(root, 'public'), - templates=[os.path.join(root, 'templates')]) + paths = dict( + root=root, + controllers=os.path.join(root, 'controllers'), + static_files=os.path.join(root, 'public'), + templates=[os.path.join(root, 'templates')] + ) # Initialize config with the basic options config.init_app(global_conf, app_conf, package='rhodecode', paths=paths) @@ -44,8 +50,11 @@ def load_environment(global_conf, app_co config['routes.map'] = make_map(config) config['pylons.app_globals'] = app_globals.Globals(config) - config['pylons.h'] = rhodecode.lib.helpers + config['pylons.h'] = helpers rhodecode.CONFIG = config + + load_rcextensions(root_path=config['here']) + # Setup cache object as early as possible import pylons pylons.cache._push_object(config['pylons.app_globals'].cache) diff --git a/rhodecode/config/rcextensions/__init__.py b/rhodecode/config/rcextensions/__init__.py new file mode 100644 --- /dev/null +++ b/rhodecode/config/rcextensions/__init__.py @@ -0,0 +1,84 @@ +# Additional mappings that are not present in the pygments lexers +# used for building stats +# format is {'ext':'Name'} eg. {'py':'Python'} +# NOTE: that this will overide any mappings in LANGUAGES_EXTENSIONS_MAP +# build by pygments +EXTRA_MAPPINGS = {} + +#============================================================================== +# WHOOSH INDEX EXTENSIONS +#============================================================================== +# if INDEX_EXTENSIONS is [] it'll use pygments lexers extensions by default. +# To set your own just add to this list extensions to index with content +INDEX_EXTENSIONS = [] + +# additional extensions for indexing besides the default from pygments +# those get's added to INDEX_EXTENSIONS +EXTRA_INDEX_EXTENSIONS = [] + + +#============================================================================== +# POST CREATE REPOSITORY HOOK +#============================================================================== +# this function will be executed after each repository is created +def _crhook(*args, **kwargs): + """ + Post create repository HOOK + kwargs available: + :param repo_name: + :param repo_type: + :param description: + :param private: + :param created_on: + :param enable_downloads: + :param repo_id: + :param user_id: + :param enable_statistics: + :param clone_uri: + :param fork_id: + :param group_id: + :param created_by: + """ + return 0 +CREATE_REPO_HOOK = _crhook + + +#============================================================================== +# POST PUSH HOOK +#============================================================================== + +# this function will be executed after each push it's runned after the build-in +# hook that rhodecode uses for logging pushes +def _pushhook(*args, **kwargs): + """ + Post push hook + kwargs available: + + :param username: name of user who pushed + :param ip: ip of who pushed + :param action: pull + :param repository: repository name + :param pushed_revs: generator of pushed revisions + """ + return 0 +PUSH_HOOK = _pushhook + + +#============================================================================== +# POST PULL HOOK +#============================================================================== + +# this function will be executed after each push it's runned after the build-in +# hook that rhodecode uses for logging pushes +def _pullhook(*args, **kwargs): + """ + Post pull hook + kwargs available:: + + :param username: name of user who pulled + :param ip: ip of who pushed + :param action: pull + :param repository: repository name + """ + return 0 +PULL_HOOK = _pullhook diff --git a/rhodecode/config/rcextensions/make_rcextensions.py b/rhodecode/config/rcextensions/make_rcextensions.py new file mode 100644 --- /dev/null +++ b/rhodecode/config/rcextensions/make_rcextensions.py @@ -0,0 +1,79 @@ +# -*- 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 + :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 . +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 diff --git a/rhodecode/controllers/admin/repos.py b/rhodecode/controllers/admin/repos.py --- a/rhodecode/controllers/admin/repos.py +++ b/rhodecode/controllers/admin/repos.py @@ -284,7 +284,6 @@ class ReposController(BaseController): :param repo_name: """ - try: RepoModel().revoke_user_permission(repo=repo_name, user=request.POST['user_id']) diff --git a/rhodecode/controllers/admin/users.py b/rhodecode/controllers/admin/users.py --- a/rhodecode/controllers/admin/users.py +++ b/rhodecode/controllers/admin/users.py @@ -145,11 +145,12 @@ class UsersController(BaseController): user_model = UserModel() try: user_model.delete(id) + Session.commit() h.flash(_('successfully deleted user'), category='success') - Session.commit() except (UserOwnsReposException, DefaultUserException), e: - h.flash(str(e), category='warning') + h.flash(e, category='warning') except Exception: + log.error(traceback.format_exc()) h.flash(_('An error occurred during deletion of user'), category='error') return redirect(url('users')) diff --git a/rhodecode/controllers/admin/users_groups.py b/rhodecode/controllers/admin/users_groups.py --- a/rhodecode/controllers/admin/users_groups.py +++ b/rhodecode/controllers/admin/users_groups.py @@ -32,8 +32,9 @@ from pylons import request, session, tmp from pylons.controllers.util import abort, redirect from pylons.i18n.translation import _ +from rhodecode.lib import helpers as h from rhodecode.lib.exceptions import UsersGroupsAssignedException -from rhodecode.lib import helpers as h, safe_unicode +from rhodecode.lib.utils2 import safe_unicode from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator from rhodecode.lib.base import BaseController, render diff --git a/rhodecode/controllers/api/__init__.py b/rhodecode/controllers/api/__init__.py --- a/rhodecode/controllers/api/__init__.py +++ b/rhodecode/controllers/api/__init__.py @@ -233,10 +233,10 @@ class JSONRPCController(WSGIController): try: return json.dumps(response) except TypeError, e: - log.debug('Error encoding response: %s' % e) + log.error('API FAILED. Error encoding response: %s' % e) return json.dumps( dict( - self._req_id, + id=self._req_id, result=None, error="Error encoding response" ) diff --git a/rhodecode/controllers/api/api.py b/rhodecode/controllers/api/api.py --- a/rhodecode/controllers/api/api.py +++ b/rhodecode/controllers/api/api.py @@ -30,16 +30,15 @@ import logging from rhodecode.controllers.api import JSONRPCController, JSONRPCError from rhodecode.lib.auth import HasPermissionAllDecorator, \ - HasPermissionAnyDecorator, PasswordGenerator + HasPermissionAnyDecorator, PasswordGenerator, AuthUser from rhodecode.model.meta import Session from rhodecode.model.scm import ScmModel -from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository +from rhodecode.model.db import User, UsersGroup, Repository from rhodecode.model.repo import RepoModel from rhodecode.model.user import UserModel from rhodecode.model.users_group import UsersGroupModel -from rhodecode.model.repos_group import ReposGroupModel - +from rhodecode.lib.utils import map_groups log = logging.getLogger(__name__) @@ -100,7 +99,9 @@ class ApiController(JSONRPCController): email=user.email, active=user.active, admin=user.admin, - ldap_dn=user.ldap_dn + ldap_dn=user.ldap_dn, + last_login=user.last_login, + permissions=AuthUser(user_id=user.user_id).permissions ) @HasPermissionAllDecorator('hg.admin') @@ -122,7 +123,8 @@ class ApiController(JSONRPCController): email=user.email, active=user.active, admin=user.admin, - ldap_dn=user.ldap_dn + ldap_dn=user.ldap_dn, + last_login=user.last_login, ) ) return result @@ -282,7 +284,7 @@ class ApiController(JSONRPCController): @HasPermissionAllDecorator('hg.admin') def add_user_to_users_group(self, apiuser, group_name, username): """" - Add a user to a group + Add a user to a users group :param apiuser: :param group_name: @@ -360,7 +362,7 @@ class ApiController(JSONRPCController): user = user.user members.append( dict( - type_="user", + type="user", id=user.user_id, username=user.username, firstname=user.name, @@ -377,7 +379,7 @@ class ApiController(JSONRPCController): users_group = users_group.users_group members.append( dict( - type_="users_group", + type="users_group", id=users_group.users_group_id, name=users_group.users_group_name, active=users_group.users_group_active, @@ -464,15 +466,10 @@ class ApiController(JSONRPCController): if Repository.get_by_repo_name(repo_name): raise JSONRPCError("repo %s already exist" % repo_name) - groups = repo_name.split('/') + groups = repo_name.split(Repository.url_sep()) real_name = groups[-1] - groups = groups[:-1] - parent_id = None - for g in groups: - group = RepoGroup.get_by_group_name(g) - if not group: - group = ReposGroupModel().create(g, '', parent_id) - parent_id = group.group_id + # create structure of groups + group = map_groups(repo_name) repo = RepoModel().create( dict( @@ -481,7 +478,7 @@ class ApiController(JSONRPCController): description=description, private=private, repo_type=repo_type, - repo_group=parent_id, + repo_group=group.group_id if group else None, clone_uri=clone_uri ), owner diff --git a/rhodecode/controllers/branches.py b/rhodecode/controllers/branches.py --- a/rhodecode/controllers/branches.py +++ b/rhodecode/controllers/branches.py @@ -31,7 +31,7 @@ import binascii from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseRepoController, render from rhodecode.lib.compat import OrderedDict -from rhodecode.lib import safe_unicode +from rhodecode.lib.utils2 import safe_unicode log = logging.getLogger(__name__) diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py --- a/rhodecode/controllers/changeset.py +++ b/rhodecode/controllers/changeset.py @@ -51,13 +51,18 @@ from rhodecode.lib.diffs import wrapped_ log = logging.getLogger(__name__) -def anchor_url(revision, path): +def _update_with_GET(params, GET): + for k in ['diff1', 'diff2', 'diff']: + params[k] += GET.getall(k) + + +def anchor_url(revision, path, GET): fid = h.FID(revision, path) - return h.url.current(anchor=fid, **dict(request.GET)) + return h.url.current(anchor=fid, **dict(GET)) def get_ignore_ws(fid, GET): - ig_ws_global = request.GET.get('ignorews') + ig_ws_global = GET.get('ignorews') ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid)) if ig_ws: try: @@ -67,12 +72,13 @@ def get_ignore_ws(fid, GET): return ig_ws_global -def _ignorews_url(fileid=None): - +def _ignorews_url(GET, fileid=None): + fileid = str(fileid) if fileid else None params = defaultdict(list) + _update_with_GET(params, GET) lbl = _('show white space') - ig_ws = get_ignore_ws(fileid, request.GET) - ln_ctx = get_line_ctx(fileid, request.GET) + ig_ws = get_ignore_ws(fileid, GET) + ln_ctx = get_line_ctx(fileid, GET) # global option if fileid is None: if ig_ws is None: @@ -98,7 +104,7 @@ def _ignorews_url(fileid=None): def get_line_ctx(fid, GET): - ln_ctx_global = request.GET.get('context') + ln_ctx_global = GET.get('context') ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid)) if ln_ctx: @@ -112,16 +118,19 @@ def get_line_ctx(fid, GET): return -def _context_url(fileid=None): +def _context_url(GET, fileid=None): """ Generates url for context lines :param fileid: """ - ig_ws = get_ignore_ws(fileid, request.GET) - ln_ctx = (get_line_ctx(fileid, request.GET) or 3) * 2 + + fileid = str(fileid) if fileid else None + ig_ws = get_ignore_ws(fileid, GET) + ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2 params = defaultdict(list) + _update_with_GET(params, GET) # global option if fileid is None: @@ -162,7 +171,7 @@ class ChangesetController(BaseRepoContro c.anchor_url = anchor_url c.ignorews_url = _ignorews_url c.context_url = _context_url - + limit_off = request.GET.get('fulldiff') #get ranges of revisions if preset rev_range = revision.split('...')[:2] enable_comments = True @@ -220,7 +229,7 @@ class ChangesetController(BaseRepoContro ign_whitespace_lcl = get_ignore_ws(fid, request.GET) lim = self.cut_off_limit if cumulative_diff > self.cut_off_limit: - lim = -1 + lim = -1 if limit_off is None else None size, cs1, cs2, diff, st = wrapped_diff( filenode_old=None, filenode_new=node, @@ -251,7 +260,7 @@ class ChangesetController(BaseRepoContro ign_whitespace_lcl = get_ignore_ws(fid, request.GET) lim = self.cut_off_limit if cumulative_diff > self.cut_off_limit: - lim = -1 + lim = -1 if limit_off is None else None size, cs1, cs2, diff, st = wrapped_diff( filenode_old=filenode_old, filenode_new=node, diff --git a/rhodecode/controllers/feed.py b/rhodecode/controllers/feed.py --- a/rhodecode/controllers/feed.py +++ b/rhodecode/controllers/feed.py @@ -28,7 +28,7 @@ import logging from pylons import url, response, tmpl_context as c from pylons.i18n.translation import _ -from rhodecode.lib import safe_unicode +from rhodecode.lib.utils2 import safe_unicode from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator from rhodecode.lib.base import BaseRepoController diff --git a/rhodecode/controllers/files.py b/rhodecode/controllers/files.py --- a/rhodecode/controllers/files.py +++ b/rhodecode/controllers/files.py @@ -32,24 +32,26 @@ from pylons.i18n.translation import _ from pylons.controllers.util import redirect from pylons.decorators import jsonify -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.lib import diffs +from rhodecode.lib import helpers as h from rhodecode.lib.compat import OrderedDict -from rhodecode.lib import convert_line_endings, detect_mode, safe_str +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 import diffs -import rhodecode.lib.helpers as h +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.controllers.changeset import anchor_url, _ignorews_url,\ _context_url, get_line_ctx, get_ignore_ws -from rhodecode.lib.diffs import wrapped_diff -from rhodecode.model.scm import ScmModel + log = logging.getLogger(__name__) @@ -447,7 +449,7 @@ class FilesController(BaseRepoController ign_whitespace_lcl = get_ignore_ws(fid, request.GET) lim = request.GET.get('fulldiff') or self.cut_off_limit - _, cs1, cs2, diff, st = wrapped_diff(filenode_old=node1, + _, cs1, cs2, diff, st = diffs.wrapped_diff(filenode_old=node1, filenode_new=node2, cut_off_limit=lim, ignore_whitespace=ign_whitespace_lcl, diff --git a/rhodecode/controllers/home.py b/rhodecode/controllers/home.py --- a/rhodecode/controllers/home.py +++ b/rhodecode/controllers/home.py @@ -44,7 +44,7 @@ class HomeController(BaseController): def index(self): c.repos_list = self.scm_model.get_repos() c.groups = self.scm_model.get_repos_groups() - + c.group = None return render('/index.html') def repo_switcher(self): diff --git a/rhodecode/controllers/summary.py b/rhodecode/controllers/summary.py --- a/rhodecode/controllers/summary.py +++ b/rhodecode/controllers/summary.py @@ -26,6 +26,7 @@ import traceback import calendar import logging +import urllib from time import mktime from datetime import timedelta, date from urlparse import urlparse @@ -39,15 +40,15 @@ 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 import ALL_READMES, ALL_EXTS +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, \ - LANGUAGES_EXTENSIONS_MAP +from rhodecode.lib.celerylib.tasks import get_commits_stats from rhodecode.lib.helpers import RepoPage from rhodecode.lib.compat import json, OrderedDict @@ -91,34 +92,37 @@ class SummaryController(BaseRepoControll 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': parsed_url.path + 'path': decoded_path } + uri = uri_tmpl % uri_dict # generate another clone url by id - uri_dict.update({'path': '/_%s' % c.dbrepo.repo_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]: + for name, hash_ in c.rhodecode_repo.tags.items()[:10]: try: - c.repo_tags[name] = c.rhodecode_repo.get_changeset(hash) + c.repo_tags[name] = c.rhodecode_repo.get_changeset(hash_) except ChangesetError: - c.repo_tags[name] = EmptyChangeset(hash) + c.repo_tags[name] = EmptyChangeset(hash_) c.repo_branches = OrderedDict() - for name, hash in c.rhodecode_repo.branches.items()[:10]: + for name, hash_ in c.rhodecode_repo.branches.items()[:10]: try: - c.repo_branches[name] = c.rhodecode_repo.get_changeset(hash) + c.repo_branches[name] = c.rhodecode_repo.get_changeset(hash_) except ChangesetError: - c.repo_branches[name] = EmptyChangeset(hash) + c.repo_branches[name] = EmptyChangeset(hash_) td = date.today() + timedelta(days=1) td_1m = td - timedelta(days=calendar.mdays[td.month]) @@ -175,7 +179,7 @@ class SummaryController(BaseRepoControll 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_repo) + c.readme_data, c.readme_file = self.__get_readme_data(c.rhodecode_db_repo) return render('summary/summary.html') def __get_readme_data(self, repo): @@ -206,7 +210,7 @@ class SummaryController(BaseRepoControll return readme_data, readme_file - key = repo.name + '_README' + key = repo.repo_name + '_README' inv = CacheInvalidation.invalidate(key) if inv is not None: region_invalidate(_get_readme_from_cache, None, key) diff --git a/rhodecode/lib/__init__.py b/rhodecode/lib/__init__.py --- a/rhodecode/lib/__init__.py +++ b/rhodecode/lib/__init__.py @@ -1,432 +1,4 @@ -# -*- coding: utf-8 -*- -""" - rhodecode.lib.__init__ - ~~~~~~~~~~~~~~~~~~~~~~~ - - Some simple helper functions - - :created_on: Jan 5, 2011 - :author: marcink - :copyright: (C) 2011-2012 Marcin Kuzminski - :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 . - import os -import re -from rhodecode.lib.vcs.utils.lazy import LazyProperty - - -def __get_lem(): - 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) - -# language map is also used by whoosh indexer, which for those specified -# extensions will index it's content -LANGUAGES_EXTENSIONS_MAP = __get_lem() - -# Additional mappings that are not present in the pygments lexers -# NOTE: that this will overide any mappings in LANGUAGES_EXTENSIONS_MAP -ADDITIONAL_MAPPINGS = {'xaml': 'XAML'} - -LANGUAGES_EXTENSIONS_MAP.update(ADDITIONAL_MAPPINGS) - -# list of readme files to search in file tree and display in summary -# attached weights defines the search order lower is first -ALL_READMES = [ - ('readme', 0), ('README', 0), ('Readme', 0), - ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1), - ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2), - ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2), - ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2), -] - -# extension together with weights to search lower is first -RST_EXTS = [ - ('', 0), ('.rst', 1), ('.rest', 1), - ('.RST', 2), ('.REST', 2), - ('.txt', 3), ('.TXT', 3) -] - -MARKDOWN_EXTS = [ - ('.md', 1), ('.MD', 1), - ('.mkdn', 2), ('.MKDN', 2), - ('.mdown', 3), ('.MDOWN', 3), - ('.markdown', 4), ('.MARKDOWN', 4) -] - -PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)] - -ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS - - -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)|(?>>>> 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 def get_current_revision(quiet=False): @@ -450,16 +22,3 @@ def get_current_revision(quiet=False): print ("Cannot retrieve rhodecode's revision. Original error " "was: %s" % err) return None - - -def extract_mentioned_users(s): - """ - Returns unique usernames from given string s that have @mention - - :param s: string to get mentions - """ - usrs = {} - for username in re.findall(r'(?:^@|\s@)(\w+)', s): - usrs[username] = username - - return sorted(usrs.keys()) diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -43,7 +43,7 @@ if __platform__ in PLATFORM_WIN: if __platform__ in PLATFORM_OTHERS: import bcrypt -from rhodecode.lib import str2bool, safe_unicode +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 @@ -521,8 +521,7 @@ class PermsDecorator(object): self.user = cls.rhodecode_user self.user_perms = self.user.permissions log.debug('checking %s permissions %s for %s %s', - self.__class__.__name__, self.required_perms, cls, - self.user) + self.__class__.__name__, self.required_perms, cls, self.user) if self.check_permissions(): log.debug('Permission granted for %s %s' % (cls, self.user)) @@ -604,6 +603,7 @@ class HasRepoPermissionAnyDecorator(Perm user_perms = set([self.user_perms['repositories'][repo_name]]) except KeyError: return False + if self.required_perms.intersection(user_perms): return True return False @@ -655,29 +655,37 @@ class PermsFunction(object): for perm in perms: if perm not in available_perms: - raise Exception("'%s' permission in not defined" % perm) + raise Exception("'%s' permission is not defined" % perm) self.required_perms = set(perms) self.user_perms = None - self.granted_for = '' self.repo_name = None + self.group_name = None def __call__(self, check_Location=''): user = request.user - log.debug('checking %s %s %s', self.__class__.__name__, - self.required_perms, user) + cls_name = self.__class__.__name__ + check_scope = { + 'HasPermissionAll': '', + 'HasPermissionAny': '', + 'HasRepoPermissionAll': 'repo:%s' % self.repo_name, + 'HasRepoPermissionAny': 'repo:%s' % self.repo_name, + 'HasReposGroupPermissionAll': 'group:%s' % self.group_name, + 'HasReposGroupPermissionAny': 'group:%s' % self.group_name, + }.get(cls_name, '?') + log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name, + self.required_perms, user, check_scope, + check_Location or 'unspecified location') if not user: log.debug('Empty request user') return False self.user_perms = user.permissions - self.granted_for = user - if self.check_permissions(): - log.debug('Permission granted %s @ %s', self.granted_for, + log.debug('Permission granted for user: %s @ %s', user, check_Location or 'unspecified location') return True else: - log.debug('Permission denied for %s @ %s', self.granted_for, + log.debug('Permission denied for user: %s @ %s', user, check_Location or 'unspecified location') return False @@ -701,7 +709,6 @@ class HasPermissionAny(PermsFunction): class HasRepoPermissionAll(PermsFunction): - def __call__(self, repo_name=None, check_Location=''): self.repo_name = repo_name return super(HasRepoPermissionAll, self).__call__(check_Location) @@ -711,19 +718,17 @@ class HasRepoPermissionAll(PermsFunction self.repo_name = get_repo_slug(request) try: - self.user_perms = set( + self._user_perms = set( [self.user_perms['repositories'][self.repo_name]] ) except KeyError: return False - self.granted_for = self.repo_name - if self.required_perms.issubset(self.user_perms): + if self.required_perms.issubset(self._user_perms): return True return False class HasRepoPermissionAny(PermsFunction): - def __call__(self, repo_name=None, check_Location=''): self.repo_name = repo_name return super(HasRepoPermissionAny, self).__call__(check_Location) @@ -733,13 +738,12 @@ class HasRepoPermissionAny(PermsFunction self.repo_name = get_repo_slug(request) try: - self.user_perms = set( + self._user_perms = set( [self.user_perms['repositories'][self.repo_name]] ) except KeyError: return False - self.granted_for = self.repo_name - if self.required_perms.intersection(self.user_perms): + if self.required_perms.intersection(self._user_perms): return True return False @@ -751,13 +755,12 @@ class HasReposGroupPermissionAny(PermsFu def check_permissions(self): try: - self.user_perms = set( + self._user_perms = set( [self.user_perms['repositories_groups'][self.group_name]] ) except KeyError: return False - self.granted_for = self.repo_name - if self.required_perms.intersection(self.user_perms): + if self.required_perms.intersection(self._user_perms): return True return False @@ -769,13 +772,12 @@ class HasReposGroupPermissionAll(PermsFu def check_permissions(self): try: - self.user_perms = set( + self._user_perms = set( [self.user_perms['repositories_groups'][self.group_name]] ) except KeyError: return False - self.granted_for = self.repo_name - if self.required_perms.issubset(self.user_perms): + if self.required_perms.issubset(self._user_perms): return True return False @@ -788,12 +790,16 @@ class HasPermissionAnyMiddleware(object) self.required_perms = set(perms) def __call__(self, user, repo_name): + # repo_name MUST be unicode, since we handle keys in permission + # dict by unicode + repo_name = safe_unicode(repo_name) usr = AuthUser(user.user_id) try: self.user_perms = set([usr.permissions['repositories'][repo_name]]) - except: + except Exception: + log.error('Exception while accessing permissions %s' % + traceback.format_exc()) self.user_perms = set() - self.granted_for = '' self.username = user.username self.repo_name = repo_name return self.check_permissions() @@ -803,7 +809,13 @@ class HasPermissionAnyMiddleware(object) 'permissions %s for user:%s repository:%s', self.user_perms, self.username, self.repo_name) if self.required_perms.intersection(self.user_perms): - log.debug('permission granted') + log.debug('permission granted for user:%s on repo:%s' % ( + self.username, self.repo_name + ) + ) return True - log.debug('permission denied') + log.debug('permission denied for user:%s on repo:%s' % ( + self.username, self.repo_name + ) + ) return False diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -7,6 +7,8 @@ import time import traceback from paste.auth.basic import AuthBasicAuthenticator +from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden +from paste.httpheaders import WWW_AUTHENTICATE from pylons import config, tmpl_context as c, request, session, url from pylons.controllers import WSGIController @@ -15,7 +17,7 @@ from pylons.templating import render_mak from rhodecode import __version__, BACKENDS -from rhodecode.lib import str2bool, safe_unicode +from rhodecode.lib.utils2 import str2bool, safe_unicode from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\ HasPermissionAnyMiddleware, CookieStoreWrapper from rhodecode.lib.utils import get_repo_slug, invalidate_cache @@ -28,6 +30,22 @@ from rhodecode.model.scm import ScmModel log = logging.getLogger(__name__) +class BasicAuth(AuthBasicAuthenticator): + + def __init__(self, realm, authfunc, auth_http_code=None): + self.realm = realm + self.authfunc = authfunc + self._rc_auth_http_code = auth_http_code + + def build_authentication(self): + head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm) + if self._rc_auth_http_code and self._rc_auth_http_code == '403': + # return 403 if alternative http return code is specified in + # RhodeCode config + return HTTPForbidden(headers=head) + return HTTPUnauthorized(headers=head) + + class BaseVCSController(object): def __init__(self, application, config): @@ -36,7 +54,8 @@ class BaseVCSController(object): # base path of repo locations self.basepath = self.config['base_path'] #authenticate this mercurial request using authfunc - self.authenticate = AuthBasicAuthenticator('', authfunc) + self.authenticate = BasicAuth('', authfunc, + config.get('auth_ret_code')) self.ipaddr = '0.0.0.0' def _handle_request(self, environ, start_response): diff --git a/rhodecode/lib/caching_query.py b/rhodecode/lib/caching_query.py --- a/rhodecode/lib/caching_query.py +++ b/rhodecode/lib/caching_query.py @@ -24,7 +24,7 @@ from beaker.exceptions import BeakerExce from sqlalchemy.orm.interfaces import MapperOption from sqlalchemy.orm.query import Query from sqlalchemy.sql import visitors -from rhodecode.lib import safe_str +from rhodecode.lib.utils2 import safe_str class CachingQuery(Query): diff --git a/rhodecode/lib/celerylib/__init__.py b/rhodecode/lib/celerylib/__init__.py --- a/rhodecode/lib/celerylib/__init__.py +++ b/rhodecode/lib/celerylib/__init__.py @@ -36,7 +36,7 @@ from decorator import decorator from rhodecode.lib.vcs.utils.lazy import LazyProperty from rhodecode import CELERY_ON -from rhodecode.lib import str2bool, safe_str +from rhodecode.lib.utils2 import str2bool, safe_str from rhodecode.lib.pidlock import DaemonLock, LockHeld from rhodecode.model import init_model from rhodecode.model import meta diff --git a/rhodecode/lib/celerylib/tasks.py b/rhodecode/lib/celerylib/tasks.py --- a/rhodecode/lib/celerylib/tasks.py +++ b/rhodecode/lib/celerylib/tasks.py @@ -40,7 +40,7 @@ from pylons.i18n.translation import _ from rhodecode.lib.vcs import get_backend from rhodecode import CELERY_ON -from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str +from rhodecode.lib.utils2 import safe_str from rhodecode.lib.celerylib import run_task, locked_task, dbsession, \ str2bool, __get_lockkey, LockHeld, DaemonLock, get_session from rhodecode.lib.helpers import person @@ -147,6 +147,7 @@ def get_commits_stats(repo_name, ts_min_ last_rev, last_rev + parse_limit) ) for cs in repo[last_rev:last_rev + parse_limit]: + log.debug('parsing %s' % cs) last_cs = cs # remember last parsed changeset k = lmktime([cs.date.timetuple()[0], cs.date.timetuple()[1], cs.date.timetuple()[2], 0, 0, 0, 0, 0, 0]) @@ -233,10 +234,10 @@ def get_commits_stats(repo_name, ts_min_ lock.release() return False - #final release + # final release lock.release() - #execute another task if celery is enabled + # execute another task if celery is enabled if len(repo.revisions) > 1 and CELERY_ON: run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y) return True @@ -327,7 +328,7 @@ def send_email(recipients, subject, body DBS = get_session() email_config = config - subject = "%s %s" % (email_config.get('email_prefix'), subject) + subject = "%s %s" % (email_config.get('email_prefix', ''), subject) if not recipients: # if recipients are not defined we send to email_config + all admins admins = [u.email for u in User.query() @@ -395,6 +396,7 @@ def create_repo_fork(form_data, cur_user DBS.commit() def __get_codes_stats(repo_name): + from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP repo = Repository.get_by_repo_name(repo_name).scm_instance tip = repo.get_changeset() diff --git a/rhodecode/lib/celerypylons/commands.py b/rhodecode/lib/celerypylons/commands.py --- a/rhodecode/lib/celerypylons/commands.py +++ b/rhodecode/lib/celerypylons/commands.py @@ -1,9 +1,9 @@ import rhodecode -from rhodecode.lib.utils import BasePasterCommand, Command +from rhodecode.lib.utils import BasePasterCommand, Command, load_rcextensions from celery.app import app_or_default from celery.bin import camqadm, celerybeat, celeryd, celeryev -from rhodecode.lib import str2bool +from rhodecode.lib.utils2 import str2bool __all__ = ['CeleryDaemonCommand', 'CeleryBeatCommand', 'CAMQPAdminCommand', 'CeleryEventCommand'] @@ -39,9 +39,11 @@ class CeleryCommand(BasePasterCommand): raise Exception('Please enable celery_on in .ini config ' 'file before running celeryd') rhodecode.CELERY_ON = CELERY_ON + load_rcextensions(config['here']) cmd = self.celery_command(app_or_default()) return cmd.run(**vars(self.options)) + class CeleryDaemonCommand(CeleryCommand): """Start the celery worker @@ -82,6 +84,7 @@ class CAMQPAdminCommand(CeleryCommand): parser = Command.standard_parser(quiet=True) celery_command = camqadm.AMQPAdminCommand + class CeleryEventCommand(CeleryCommand): """Celery event command. diff --git a/rhodecode/lib/compat.py b/rhodecode/lib/compat.py --- a/rhodecode/lib/compat.py +++ b/rhodecode/lib/compat.py @@ -25,16 +25,93 @@ # along with this program. If not, see . import os +import datetime +import functools +import decimal from rhodecode import __platform__, PLATFORM_WIN #============================================================================== # json #============================================================================== + + +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 isinstance(obj, OrderedDict): + return obj.as_dict() + elif hasattr(obj, '__json__'): + if callable(obj.__json__): + return obj.__json__() + else: + return obj.__json__ + else: + raise NotImplementedError + try: import 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) except ImportError: import simplejson as json + def extended_encode(obj): + try: + return _obj_dump(obj) + except NotImplementedError: + pass + raise TypeError("%r is not JSON serializable" % (obj,)) + json.dumps = functools.partial(json.dumps, default=extended_encode) + #============================================================================== # izip_longest @@ -44,11 +121,11 @@ try: except ImportError: import itertools - def izip_longest(*args, **kwds): # noqa + def izip_longest(*args, **kwds): fillvalue = kwds.get("fillvalue") def sentinel(counter=([fillvalue] * (len(args) - 1)).pop): - yield counter() # yields the fillvalue, or raises IndexError + yield counter() # yields the fillvalue, or raises IndexError fillers = itertools.repeat(fillvalue) iters = [itertools.chain(it, sentinel(), fillers) diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -376,7 +376,7 @@ class DbManage(object): if not self.tests and not test_repo_path: path = raw_input( - 'Enter a valid path to store repositories. ' + 'Enter a valid absolute path to store repositories. ' 'All repositories in that path will be added automatically:' ) else: @@ -388,8 +388,12 @@ class DbManage(object): path_ok = False log.error('Given path %s is not a valid directory' % path) + elif not os.path.isabs(path): + path_ok = False + log.error('Given path %s is not an absolute path' % path) + # check write access - if not os.access(path, os.W_OK) and path_ok: + elif not os.access(path, os.W_OK) and path_ok: path_ok = False log.error('No write permission to given path %s' % path) diff --git a/rhodecode/lib/dbmigrate/migrate/exceptions.py b/rhodecode/lib/dbmigrate/migrate/exceptions.py --- a/rhodecode/lib/dbmigrate/migrate/exceptions.py +++ b/rhodecode/lib/dbmigrate/migrate/exceptions.py @@ -71,9 +71,6 @@ class InvalidScriptError(ScriptError): """Invalid script error.""" -class InvalidVersionError(Error): - """Invalid version error.""" - # migrate.changeset class NotSupportedError(Error): diff --git a/rhodecode/lib/dbmigrate/schema/db_1_2_0.py b/rhodecode/lib/dbmigrate/schema/db_1_2_0.py --- a/rhodecode/lib/dbmigrate/schema/db_1_2_0.py +++ b/rhodecode/lib/dbmigrate/schema/db_1_2_0.py @@ -39,7 +39,7 @@ from rhodecode.lib.vcs.utils.helpers imp from rhodecode.lib.vcs.exceptions import VCSError from rhodecode.lib.vcs.utils.lazy import LazyProperty -from rhodecode.lib import str2bool, safe_str, get_changeset_safe, \ +from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \ generate_api_key, safe_unicode from rhodecode.lib.exceptions import UsersGroupsAssignedException from rhodecode.lib.compat import json @@ -717,7 +717,7 @@ class Repository(Base, BaseModel): return repo -class RepoGroup(Base, BaseModel): +class Group(Base, BaseModel): __tablename__ = 'groups' __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'), CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},) @@ -728,8 +728,7 @@ class RepoGroup(Base, BaseModel): 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) - parent_group = relationship('RepoGroup', remote_side=group_id) - + parent_group = relationship('Group', remote_side=group_id) def __init__(self, group_name='', parent_group=None): self.group_name = group_name diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -39,12 +39,20 @@ from webhelpers.html.tags import _set_in from rhodecode.lib.annotate import annotate_highlight from rhodecode.lib.utils import repo_name_slug -from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe +from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \ + get_changeset_safe from rhodecode.lib.markup_renderer import MarkupRenderer log = logging.getLogger(__name__) +def shorter(text, size=20): + postfix = '...' + if len(text) > size: + return text[:size - len(postfix)] + postfix + return text + + def _reset(name, value=None, id=NotGiven, type="reset", **attrs): """ Reset button @@ -67,7 +75,7 @@ def FID(raw_id, path): :param path: """ - return 'C-%s-%s' % (short_id(raw_id), md5(path).hexdigest()[:12]) + return 'C-%s-%s' % (short_id(raw_id), md5(safe_str(path)).hexdigest()[:12]) def get_token(): @@ -86,6 +94,7 @@ def get_token(): session.save() return session[token_key] + class _GetError(object): """Get error from form_errors, and represent it as span wrapped error message @@ -101,6 +110,7 @@ class _GetError(object): get_error = _GetError() + class _ToolTip(object): def __call__(self, tooltip_title, trim_at=50): @@ -112,6 +122,7 @@ class _ToolTip(object): return escape(tooltip_title) tooltip = _ToolTip() + class _FilesBreadCrumbs(object): def __call__(self, repo_name, rev, paths): @@ -136,8 +147,10 @@ class _FilesBreadCrumbs(object): files_breadcrumbs = _FilesBreadCrumbs() + class CodeHtmlFormatter(HtmlFormatter): - """My code Html Formatter for source codes + """ + My code Html Formatter for source codes """ def wrap(self, source, outfile): @@ -319,7 +332,7 @@ flash = _Flash() # SCM FILTERS available via h. #============================================================================== from rhodecode.lib.vcs.utils import author_name, author_email -from rhodecode.lib import credentials_filter, age as _age +from rhodecode.lib.utils2 import credentials_filter, age as _age from rhodecode.model.db import User age = lambda x: _age(x) @@ -759,10 +772,10 @@ def fancy_file_stats(stats): d_v = d if d > 0 else '' def cgen(l_type): - mapping = {'tr': 'top-right-rounded-corner', - 'tl': 'top-left-rounded-corner', - 'br': 'bottom-right-rounded-corner', - 'bl': 'bottom-left-rounded-corner'} + mapping = {'tr': 'top-right-rounded-corner-mid', + 'tl': 'top-left-rounded-corner-mid', + 'br': 'bottom-right-rounded-corner-mid', + 'bl': 'bottom-left-rounded-corner-mid'} map_getter = lambda x: mapping[x] if l_type == 'a' and d_v: @@ -801,6 +814,12 @@ def urlify_text(text_): def urlify_changesets(text_, repository): + """ + Extract revision ids from changeset and make link from them + + :param text_: + :param repository: + """ import re URL_PAT = re.compile(r'([0-9a-fA-F]{12,})') @@ -839,8 +858,8 @@ def urlify_commit(text_, repository=None import re import traceback - # urlify changesets - text_ = urlify_changesets(text_, repository) + def escaper(string): + return string.replace('<', '<').replace('>', '>') def linkify_others(t, l): urls = re.compile(r'(\)',) @@ -852,6 +871,11 @@ def urlify_commit(text_, repository=None links.append(e) return ''.join(links) + + + # urlify changesets - extrac revisions and make link out of them + text_ = urlify_changesets(escaper(text_), repository) + try: conf = config['app_conf'] diff --git a/rhodecode/lib/hooks.py b/rhodecode/lib/hooks.py --- a/rhodecode/lib/hooks.py +++ b/rhodecode/lib/hooks.py @@ -27,9 +27,10 @@ import sys from mercurial.scmutil import revrange from mercurial.node import nullrev - +from rhodecode import EXTENSIONS from rhodecode.lib import helpers as h from rhodecode.lib.utils import action_logger +from inspect import isfunction def repo_size(ui, repo, hooktype=None, **kwargs): @@ -78,14 +79,19 @@ def log_pull_action(ui, repo, **kwargs): :param repo: """ - extra_params = dict(repo.ui.configitems('rhodecode_extras')) - username = extra_params['username'] - repository = extra_params['repository'] + extras = dict(repo.ui.configitems('rhodecode_extras')) + username = extras['username'] + repository = extras['repository'] action = 'pull' - action_logger(username, action, repository, extra_params['ip'], - commit=True) + action_logger(username, action, repository, extras['ip'], commit=True) + # extension hook call + callback = getattr(EXTENSIONS, 'PULL_HOOK', None) + if isfunction(callback): + kw = {} + kw.update(extras) + callback(**kw) return 0 @@ -97,10 +103,10 @@ def log_push_action(ui, repo, **kwargs): :param repo: """ - extra_params = dict(repo.ui.configitems('rhodecode_extras')) - username = extra_params['username'] - repository = extra_params['repository'] - action = extra_params['action'] + ':%s' + extras = dict(repo.ui.configitems('rhodecode_extras')) + username = extras['username'] + repository = extras['repository'] + action = extras['action'] + ':%s' node = kwargs['node'] def get_revs(repo, rev_opt): @@ -119,16 +125,22 @@ def log_push_action(ui, repo, **kwargs): action = action % ','.join(revs) - action_logger(username, action, repository, extra_params['ip'], - commit=True) + action_logger(username, action, repository, extras['ip'], commit=True) + # extension hook call + callback = getattr(EXTENSIONS, 'PUSH_HOOK', None) + if isfunction(callback): + kw = {'pushed_revs': revs} + kw.update(extras) + callback(**kw) return 0 def log_create_repository(repository_dict, created_by, **kwargs): """ Post create repository Hook. This is a dummy function for admins to re-use - if needed + if needed. It's taken from rhodecode-extensions module and executed + if present :param repository: dict dump of repository object :param created_by: username who created repository @@ -151,5 +163,12 @@ def log_create_repository(repository_dic """ + callback = getattr(EXTENSIONS, 'CREATE_REPO_HOOK', None) + if isfunction(callback): + kw = {} + kw.update(repository_dict) + kw.update({'created_by': created_by}) + kw.update(kwargs) + return callback(**kw) return 0 diff --git a/rhodecode/lib/indexers/__init__.py b/rhodecode/lib/indexers/__init__.py --- a/rhodecode/lib/indexers/__init__.py +++ b/rhodecode/lib/indexers/__init__.py @@ -25,6 +25,7 @@ import os import sys import traceback +import logging from os.path import dirname as dn, join as jn #to get the rhodecode import @@ -46,11 +47,9 @@ from rhodecode.model import init_model from rhodecode.model.scm import ScmModel from rhodecode.model.repo import RepoModel from rhodecode.config.environment import load_environment -from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, LazyProperty -from rhodecode.lib.utils import BasePasterCommand, Command, add_cache - -# EXTENSIONS WE WANT TO INDEX CONTENT OFF -INDEX_EXTENSIONS = LANGUAGES_EXTENSIONS_MAP.keys() +from rhodecode.lib.utils2 import LazyProperty +from rhodecode.lib.utils import BasePasterCommand, Command, add_cache,\ + load_rcextensions # CUSTOM ANALYZER wordsplit + lowercase filter ANALYZER = RegexTokenizer(expression=r"\w+") | LowercaseFilter() @@ -84,18 +83,17 @@ class MakeIndex(BasePasterCommand): parser = Command.standard_parser(verbose=True) def command(self): - + logging.config.fileConfig(self.path_to_ini_file) from pylons import config add_cache(config) engine = engine_from_config(config, 'sqlalchemy.db1.') init_model(engine) - index_location = config['index_dir'] repo_location = self.options.repo_location \ if self.options.repo_location else RepoModel().repos_path repo_list = map(strip, self.options.repo_list.split(',')) \ if self.options.repo_list else None - + load_rcextensions(config['here']) #====================================================================== # WHOOSH DAEMON #====================================================================== @@ -105,7 +103,7 @@ class MakeIndex(BasePasterCommand): l = DaemonLock(file_=jn(dn(dn(index_location)), 'make_index.lock')) WhooshIndexingDaemon(index_location=index_location, repo_location=repo_location, - repo_list=repo_list)\ + repo_list=repo_list,)\ .run(full_index=self.options.full_index) l.release() except LockHeld: diff --git a/rhodecode/lib/indexers/daemon.py b/rhodecode/lib/indexers/daemon.py --- a/rhodecode/lib/indexers/daemon.py +++ b/rhodecode/lib/indexers/daemon.py @@ -38,34 +38,17 @@ from os.path import join as jn project_path = dn(dn(dn(dn(os.path.realpath(__file__))))) sys.path.append(project_path) - +from rhodecode.config.conf import INDEX_EXTENSIONS from rhodecode.model.scm import ScmModel -from rhodecode.lib import safe_unicode -from rhodecode.lib.indexers import INDEX_EXTENSIONS, SCHEMA, IDX_NAME +from rhodecode.lib.utils2 import safe_unicode +from rhodecode.lib.indexers import SCHEMA, IDX_NAME from rhodecode.lib.vcs.exceptions import ChangesetError, RepositoryError, \ NodeDoesNotExistError from whoosh.index import create_in, open_dir - -log = logging.getLogger('whooshIndexer') -# create logger -log.setLevel(logging.DEBUG) -log.propagate = False -# create console handler and set level to debug -ch = logging.StreamHandler() -ch.setLevel(logging.DEBUG) - -# create formatter -formatter = logging.Formatter("%(asctime)s - %(name)s -" - " %(levelname)s - %(message)s") - -# add formatter to ch -ch.setFormatter(formatter) - -# add ch to logger -log.addHandler(ch) +log = logging.getLogger('whoosh_indexer') class WhooshIndexingDaemon(object): @@ -103,7 +86,8 @@ class WhooshIndexingDaemon(object): self.initial = True def get_paths(self, repo): - """recursive walk in root dir and return a set of all path in that dir + """ + recursive walk in root dir and return a set of all path in that dir based on repository walk function """ index_paths_ = set() @@ -127,32 +111,39 @@ class WhooshIndexingDaemon(object): return mktime(node.last_changeset.date.timetuple()) def add_doc(self, writer, path, repo, repo_name): - """Adding doc to writer this function itself fetches data from - the instance of vcs backend""" - node = self.get_node(repo, path) + """ + Adding doc to writer this function itself fetches data from + the instance of vcs backend + """ - #we just index the content of chosen files, and skip binary files + node = self.get_node(repo, path) + indexed = indexed_w_content = 0 + # we just index the content of chosen files, and skip binary files if node.extension in INDEX_EXTENSIONS and not node.is_binary: - u_content = node.content if not isinstance(u_content, unicode): log.warning(' >> %s Could not get this content as unicode ' - 'replacing with empty content', path) + 'replacing with empty content' % path) u_content = u'' else: log.debug(' >> %s [WITH CONTENT]' % path) + indexed_w_content += 1 else: log.debug(' >> %s' % path) - #just index file name without it's content + # just index file name without it's content u_content = u'' + indexed += 1 - writer.add_document(owner=unicode(repo.contact), - repository=safe_unicode(repo_name), - path=safe_unicode(path), - content=u_content, - modtime=self.get_node_mtime(node), - extension=node.extension) + writer.add_document( + owner=unicode(repo.contact), + repository=safe_unicode(repo_name), + path=safe_unicode(path), + content=u_content, + modtime=self.get_node_mtime(node), + extension=node.extension + ) + return indexed, indexed_w_content def build_index(self): if os.path.exists(self.index_location): @@ -164,19 +155,25 @@ class WhooshIndexingDaemon(object): idx = create_in(self.index_location, SCHEMA, indexname=IDX_NAME) writer = idx.writer() - + log.debug('BUILDIN INDEX FOR EXTENSIONS %s' % INDEX_EXTENSIONS) for repo_name, repo in self.repo_paths.items(): log.debug('building index @ %s' % repo.path) - + i_cnt = iwc_cnt = 0 for idx_path in self.get_paths(repo): - self.add_doc(writer, idx_path, repo, repo_name) + i, iwc = self.add_doc(writer, idx_path, repo, repo_name) + i_cnt += i + iwc_cnt += iwc + log.debug('added %s files %s with content for repo %s' % ( + i_cnt + iwc_cnt, iwc_cnt, repo.path) + ) log.debug('>> COMMITING CHANGES <<') writer.commit(merge=True) log.debug('>>> FINISHED BUILDING INDEX <<<') def update_index(self): - log.debug('STARTING INCREMENTAL INDEXING UPDATE') + log.debug('STARTING INCREMENTAL INDEXING UPDATE FOR EXTENSIONS %s' % + INDEX_EXTENSIONS) idx = open_dir(self.index_location, indexname=self.indexname) # The set of all paths in the index @@ -215,14 +212,19 @@ class WhooshIndexingDaemon(object): # Loop over the files in the filesystem # Assume we have a function that gathers the filenames of the # documents to be indexed + ri_cnt = riwc_cnt = 0 for repo_name, repo in self.repo_paths.items(): for path in self.get_paths(repo): if path in to_index or path not in indexed_paths: # This is either a file that's changed, or a new file # that wasn't indexed before. So index it! - self.add_doc(writer, path, repo, repo_name) + i, iwc = self.add_doc(writer, path, repo, repo_name) log.debug('re indexing %s' % path) - + ri_cnt += i + riwc_cnt += iwc + log.debug('added %s files %s with content for repo %s' % ( + ri_cnt + riwc_cnt, riwc_cnt, repo.path) + ) log.debug('>> COMMITING CHANGES <<') writer.commit(merge=True) log.debug('>>> FINISHED REBUILDING INDEX <<<') diff --git a/rhodecode/lib/markup_renderer.py b/rhodecode/lib/markup_renderer.py --- a/rhodecode/lib/markup_renderer.py +++ b/rhodecode/lib/markup_renderer.py @@ -27,7 +27,7 @@ import re import logging -from rhodecode.lib import safe_unicode +from rhodecode.lib.utils2 import safe_unicode log = logging.getLogger(__name__) diff --git a/rhodecode/lib/middleware/https_fixup.py b/rhodecode/lib/middleware/https_fixup.py --- a/rhodecode/lib/middleware/https_fixup.py +++ b/rhodecode/lib/middleware/https_fixup.py @@ -23,7 +23,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from rhodecode.lib import str2bool +from rhodecode.lib.utils2 import str2bool class HttpsFixup(object): diff --git a/rhodecode/lib/middleware/simplegit.py b/rhodecode/lib/middleware/simplegit.py --- a/rhodecode/lib/middleware/simplegit.py +++ b/rhodecode/lib/middleware/simplegit.py @@ -65,11 +65,11 @@ dulserver.DEFAULT_HANDLERS = { } from dulwich.repo import Repo -from dulwich.web import HTTPGitApplication +from dulwich.web import make_wsgi_chain from paste.httpheaders import REMOTE_USER, AUTH_TYPE -from rhodecode.lib import safe_str +from rhodecode.lib.utils2 import safe_str from rhodecode.lib.base import BaseVCSController from rhodecode.lib.auth import get_container_username from rhodecode.lib.utils import is_valid_repo @@ -86,7 +86,9 @@ GIT_PROTO_PAT = re.compile(r'^/(.+)/(inf def is_git(environ): path_info = environ['PATH_INFO'] isgit_path = GIT_PROTO_PAT.match(path_info) - log.debug('is a git path %s pathinfo : %s' % (isgit_path, path_info)) + log.debug('pathinfo: %s detected as GIT %s' % ( + path_info, isgit_path != None) + ) return isgit_path @@ -113,6 +115,10 @@ class SimpleGit(BaseVCSController): except: return HTTPInternalServerError()(environ, start_response) + # quick check if that dir exists... + if is_valid_repo(repo_name, self.basepath) is False: + return HTTPNotFound()(environ, start_response) + #====================================================================== # GET ACTION PULL or PUSH #====================================================================== @@ -121,7 +127,6 @@ class SimpleGit(BaseVCSController): #====================================================================== # CHECK ANONYMOUS PERMISSION #====================================================================== - if action in ['pull', 'push']: anonymous_user = self.__get_user('default') username = anonymous_user.username @@ -177,13 +182,9 @@ class SimpleGit(BaseVCSController): #=================================================================== # GIT REQUEST HANDLING #=================================================================== - repo_path = safe_str(os.path.join(self.basepath, repo_name)) + repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) log.debug('Repository path is %s' % repo_path) - # quick check if that dir exists... - if is_valid_repo(repo_name, self.basepath) is False: - return HTTPNotFound()(environ, start_response) - try: #invalidate cache on push if action == 'push': @@ -204,7 +205,7 @@ class SimpleGit(BaseVCSController): """ _d = {'/' + repo_name: Repo(repo_path)} backend = dulserver.DictBackend(_d) - gitserve = HTTPGitApplication(backend) + gitserve = make_wsgi_chain(backend) return gitserve diff --git a/rhodecode/lib/middleware/simplehg.py b/rhodecode/lib/middleware/simplehg.py --- a/rhodecode/lib/middleware/simplehg.py +++ b/rhodecode/lib/middleware/simplehg.py @@ -27,13 +27,14 @@ import os import logging import traceback +import urllib from mercurial.error import RepoError from mercurial.hgweb import hgweb_mod from paste.httpheaders import REMOTE_USER, AUTH_TYPE -from rhodecode.lib import safe_str +from rhodecode.lib.utils2 import safe_str from rhodecode.lib.base import BaseVCSController from rhodecode.lib.auth import get_container_username from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections @@ -45,13 +46,21 @@ log = logging.getLogger(__name__) def is_mercurial(environ): - """Returns True if request's target is mercurial server - header + """ + Returns True if request's target is mercurial server - header ``HTTP_ACCEPT`` of such request would start with ``application/mercurial``. """ http_accept = environ.get('HTTP_ACCEPT') + path_info = environ['PATH_INFO'] if http_accept and http_accept.startswith('application/mercurial'): - return True - return False + ishg_path = True + else: + ishg_path = False + + log.debug('pathinfo: %s detected as HG %s' % ( + path_info, ishg_path) + ) + return ishg_path class SimpleHg(BaseVCSController): @@ -76,16 +85,20 @@ class SimpleHg(BaseVCSController): except: return HTTPInternalServerError()(environ, start_response) + # quick check if that dir exists... + if is_valid_repo(repo_name, self.basepath) is False: + return HTTPNotFound()(environ, start_response) + #====================================================================== # GET ACTION PULL or PUSH #====================================================================== action = self.__get_action(environ) + #====================================================================== # CHECK ANONYMOUS PERMISSION #====================================================================== if action in ['pull', 'push']: anonymous_user = self.__get_user('default') - username = anonymous_user.username anonymous_perm = self._check_permission(action, anonymous_user, repo_name) @@ -132,30 +145,28 @@ class SimpleHg(BaseVCSController): start_response) #check permissions for this repository - perm = self._check_permission(action, user, - repo_name) + perm = self._check_permission(action, user, repo_name) if perm is not True: return HTTPForbidden()(environ, start_response) - extras = {'ip': ipaddr, - 'username': username, - 'action': action, - 'repository': repo_name} + # extras are injected into mercurial UI object and later available + # in hg hooks executed by rhodecode + extras = { + 'ip': ipaddr, + 'username': username, + 'action': action, + 'repository': repo_name + } #====================================================================== # MERCURIAL REQUEST HANDLING #====================================================================== - - repo_path = safe_str(os.path.join(self.basepath, repo_name)) + repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) log.debug('Repository path is %s' % repo_path) baseui = make_ui('db') self.__inject_extras(repo_path, baseui, extras) - # quick check if that dir exists... - if is_valid_repo(repo_name, self.basepath) is False: - return HTTPNotFound()(environ, start_response) - try: # invalidate cache on push if action == 'push': diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -51,9 +51,12 @@ from rhodecode.lib.caching_query import from rhodecode.model import meta from rhodecode.model.db import Repository, User, RhodeCodeUi, \ - UserLog, RepoGroup, RhodeCodeSetting, UserRepoGroupToPerm + 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__) @@ -61,7 +64,8 @@ REMOVED_REPO_PAT = re.compile(r'rm__\d{8 def recursive_replace(str_, replace=' '): - """Recursive replace of given sign to just one instance + """ + Recursive replace of given sign to just one instance :param str_: given string :param replace: char to find and replace multiple instances @@ -79,7 +83,8 @@ def recursive_replace(str_, replace=' ') def repo_name_slug(value): - """Return slug of name of repository + """ + Return slug of name of repository This function is called on each creation/modification of repository to prevent bad names in repo """ @@ -154,7 +159,10 @@ def action_logger(user, action, repo, ip user_log.user_ip = ipaddr sa.add(user_log) - log.info('Adding user %s, action %s on %s' % (user_obj, action, repo)) + log.info( + 'Adding user %s, action %s on %s' % (user_obj, action, + safe_unicode(repo)) + ) if commit: sa.commit() except: @@ -198,12 +206,13 @@ def get_repos(path, recursive=False): 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(base_path, repo_name) + full_path = os.path.join(safe_str(base_path), safe_str(repo_name)) try: get_scm(full_path) @@ -219,7 +228,7 @@ def is_valid_repos_group(repos_group_nam :param repo_name: :param base_path: """ - full_path = os.path.join(base_path, repos_group_name) + 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): @@ -258,7 +267,8 @@ ui_sections = ['alias', 'auth', def make_ui(read_from='file', path=None, checkpaths=True): - """A function that will read python rc files or database + """ + 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 @@ -371,15 +381,16 @@ class EmptyChangeset(BaseChangeset): return 0 -def map_groups(groups): +def map_groups(path): """ - Checks for groups existence, and creates groups structures. - It returns last group in structure + Given a full path to a repository, create all nested groups that this + repo is inside. This function creates parent-child relationships between + groups and creates default perms for all new groups. - :param groups: list of groups structure + :param paths: full path to repository """ sa = meta.Session - + groups = path.split(Repository.url_sep()) parent = None group = None @@ -391,22 +402,18 @@ def map_groups(groups): group = RepoGroup.get_by_group_name(group_name) desc = '%s group' % group_name -# # WTF that doesn't work !? -# if group is None: -# group = rgm.create(group_name, desc, parent, just_db=True) -# sa.commit() - # skip folders that are now removed repos if REMOVED_REPO_PAT.match(group_name): break if group is None: - log.debug('creating group level: %s group_name: %s' % (lvl, group_name)) + log.debug('creating group level: %s group_name: %s' % (lvl, + group_name)) group = RepoGroup(group_name, parent) group.group_description = desc sa.add(group) rgm._create_default_perms(group) - sa.commit() + sa.flush() parent = group return group @@ -429,7 +436,7 @@ def repo2db_mapper(initial_repo_list, re added = [] for name, repo in initial_repo_list.items(): - group = map_groups(name.split(Repository.url_sep())) + group = map_groups(name) if not rm.get_by_repo_name(name, cache=False): log.info('repository %s not found creating default' % name) added.append(name) @@ -446,13 +453,19 @@ def repo2db_mapper(initial_repo_list, re sa.commit() removed = [] if remove_obsolete: - #remove from database those repositories that are not in the filesystem + # remove from database those repositories that are not in the filesystem for repo in sa.query(Repository).all(): if repo.repo_name not in initial_repo_list.keys(): + log.debug("Removing non existing repository found in db %s" % + repo.repo_name) removed.append(repo.repo_name) sa.delete(repo) sa.commit() + # clear cache keys + log.debug("Clearing cache keys now...") + CacheInvalidation.clear_cache() + sa.commit() return added, removed @@ -484,6 +497,30 @@ def add_cache(settings): beaker.cache.cache_regions[region] = region_settings +def load_rcextensions(root_path): + import rhodecode + from rhodecode.config import conf + + path = os.path.join(root_path, 'rcextensions', '__init__.py') + if os.path.isfile(path): + rcext = create_module('rc', path) + EXT = rhodecode.EXTENSIONS = rcext + log.debug('Found rcextensions now loading %s...' % rcext) + + # Additional mappings that are not present in the pygments lexers + conf.LANGUAGES_EXTENSIONS_MAP.update(getattr(EXT, 'EXTRA_MAPPINGS', {})) + + #OVERRIDE OUR EXTENSIONS FROM RC-EXTENSIONS (if present) + + if getattr(EXT, 'INDEX_EXTENSIONS', []) != []: + log.debug('settings custom INDEX_EXTENSIONS') + conf.INDEX_EXTENSIONS = getattr(EXT, 'INDEX_EXTENSIONS', []) + + #ADDITIONAL MAPPINGS + log.debug('adding extra into INDEX_EXTENSIONS') + conf.INDEX_EXTENSIONS.extend(getattr(EXT, 'EXTRA_INDEX_EXTENSIONS', [])) + + #============================================================================== # TEST FUNCTIONS AND CREATORS #============================================================================== @@ -624,6 +661,6 @@ class BasePasterCommand(Command): """ from pylons import config as pylonsconfig - path_to_ini_file = os.path.realpath(conf) - conf = paste.deploy.appconfig('config:' + path_to_ini_file) + self.path_to_ini_file = os.path.realpath(conf) + conf = paste.deploy.appconfig('config:' + self.path_to_ini_file) pylonsconfig.init_app(conf.global_conf, conf.local_conf) diff --git a/rhodecode/lib/utils2.py b/rhodecode/lib/utils2.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/utils2.py @@ -0,0 +1,405 @@ +# -*- coding: utf-8 -*- +""" + rhodecode.lib.utils + ~~~~~~~~~~~~~~~~~~~ + + Some simple helper functions + + :created_on: Jan 5, 2011 + :author: marcink + :copyright: (C) 2011-2012 Marcin Kuzminski + :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 . + +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)|(?>>>> 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 + + +def extract_mentioned_users(s): + """ + Returns unique usernames from given string s that have @mention + + :param s: string to get mentions + """ + usrs = {} + for username in re.findall(r'(?:^@|\s@)(\w+)', s): + usrs[username] = username + + return sorted(usrs.keys()) diff --git a/rhodecode/lib/vcs/backends/git/changeset.py b/rhodecode/lib/vcs/backends/git/changeset.py --- a/rhodecode/lib/vcs/backends/git/changeset.py +++ b/rhodecode/lib/vcs/backends/git/changeset.py @@ -68,19 +68,24 @@ class GitChangeset(BaseChangeset): def branch(self): # TODO: Cache as we walk (id <-> branch name mapping) refs = self.repository._repo.get_refs() - heads = [(key[len('refs/heads/'):], val) for key, val in refs.items() - if key.startswith('refs/heads/')] + heads = {} + for key, val in refs.items(): + for ref_key in ['refs/heads/', 'refs/remotes/origin/']: + if key.startswith(ref_key): + n = key[len(ref_key):] + if n not in ['HEAD']: + heads[n] = val - for name, id in heads: + for name, id in heads.iteritems(): walker = self.repository._repo.object_store.get_graph_walker([id]) while True: - id = walker.next() - if not id: + id_ = walker.next() + if not id_: break - if id == self.id: + if id_ == self.id: return safe_unicode(name) raise ChangesetError("This should not happen... Have you manually " - "change id of the changeset?") + "change id of the changeset?") def _fix_path(self, path): """ @@ -92,6 +97,7 @@ class GitChangeset(BaseChangeset): 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('/') @@ -103,24 +109,23 @@ class GitChangeset(BaseChangeset): 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 - #if curdir in self._paths: - ## This path have been already traversed - ## Update tree and continue - #tree = self.repository._repo[self._paths[curdir]] - #continue dir_id = None for item, stat, id in tree.iteritems(): - if curdir: - item_path = '/'.join((curdir, item)) - else: - item_path = item - self._paths[item_path] = id - self._stat_modes[item_path] = stat if dir == item: dir_id = id if dir_id: @@ -130,13 +135,16 @@ class GitChangeset(BaseChangeset): raise ChangesetError('%s is not a directory' % curdir) else: raise ChangesetError('%s have not been found' % curdir) - 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 + + # 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" diff --git a/rhodecode/model/__init__.py b/rhodecode/model/__init__.py --- a/rhodecode/model/__init__.py +++ b/rhodecode/model/__init__.py @@ -85,14 +85,14 @@ class BaseModel(object): if isinstance(instance, cls): return instance - elif isinstance(instance, int) or str(instance).isdigit(): + elif isinstance(instance, (int, long)) or str(instance).isdigit(): return cls.get(instance) else: if instance: if callback is None: raise Exception( - 'given object must be int or Instance of %s got %s, ' - 'no callback provided' % (cls, type(instance)) + 'given object must be int, long or Instance of %s ' + 'got %s, no callback provided' % (cls, type(instance)) ) else: return callback(instance) diff --git a/rhodecode/model/comment.py b/rhodecode/model/comment.py --- a/rhodecode/model/comment.py +++ b/rhodecode/model/comment.py @@ -29,7 +29,7 @@ import traceback from pylons.i18n.translation import _ from sqlalchemy.util.compat import defaultdict -from rhodecode.lib import extract_mentioned_users +from rhodecode.lib.utils2 import extract_mentioned_users from rhodecode.lib import helpers as h from rhodecode.model import BaseModel from rhodecode.model.db import ChangesetComment, User, Repository, Notification @@ -63,6 +63,7 @@ class ChangesetCommentsModel(BaseModel): :param f_path: :param line_no: """ + if text: repo = Repository.get(repo_id) cs = repo.scm_instance.get_changeset(revision) @@ -78,7 +79,6 @@ class ChangesetCommentsModel(BaseModel): self.sa.add(comment) self.sa.flush() - # make notification line = '' if line_no: diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -39,7 +39,8 @@ from rhodecode.lib.vcs.utils.helpers imp from rhodecode.lib.vcs.exceptions import VCSError from rhodecode.lib.vcs.utils.lazy import LazyProperty -from rhodecode.lib import str2bool, safe_str, get_changeset_safe, safe_unicode +from rhodecode.lib.utils2 import str2bool, safe_str, get_changeset_safe, \ + safe_unicode from rhodecode.lib.compat import json from rhodecode.lib.caching_query import FromCache @@ -145,12 +146,18 @@ class BaseModel(object): obj = cls.query().get(id_) Session.delete(obj) + def __repr__(self): + if hasattr(self, '__unicode__'): + # python repr needs to return str + return safe_str(self.__unicode__()) + return '' % (self.__class__.__name__) class RhodeCodeSetting(Base, BaseModel): __tablename__ = 'rhodecode_settings' __table_args__ = ( UniqueConstraint('app_settings_name'), - {'extend_existing': True} + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} ) app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) @@ -181,8 +188,8 @@ class RhodeCodeSetting(Base, BaseModel): """ self._app_settings_value = safe_unicode(val) - def __repr__(self): - return "<%s('%s:%s')>" % ( + def __unicode__(self): + return u"<%s('%s:%s')>" % ( self.__class__.__name__, self.app_settings_name, self.app_settings_value ) @@ -224,7 +231,8 @@ class RhodeCodeUi(Base, BaseModel): __tablename__ = 'rhodecode_ui' __table_args__ = ( UniqueConstraint('ui_key'), - {'extend_existing': True} + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} ) HOOK_UPDATE = 'changegroup.update' @@ -274,7 +282,8 @@ class User(Base, BaseModel): __tablename__ = 'users' __table_args__ = ( UniqueConstraint('username'), UniqueConstraint('email'), - {'extend_existing': True} + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} ) user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) @@ -294,10 +303,15 @@ class User(Base, BaseModel): repositories = relationship('Repository') user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all') repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all') + repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all') group_member = relationship('UsersGroupMember', cascade='all') - notifications = relationship('UserNotification',) + notifications = relationship('UserNotification', cascade='all') + # notifications assigned to this user + user_created_notifications = relationship('Notification', cascade='all') + # comments created by this user + user_comments = relationship('ChangesetComment', cascade='all') @hybrid_property def email(self): @@ -328,8 +342,8 @@ class User(Base, BaseModel): def is_admin(self): return self.admin - def __repr__(self): - return "<%s('id:%s:%s')>" % (self.__class__.__name__, + def __unicode__(self): + return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.user_id, self.username) @classmethod @@ -376,6 +390,9 @@ class User(Base, BaseModel): def __json__(self): return dict( + user_id=self.user_id, + first_name=self.name, + last_name=self.lastname, email=self.email, full_name=self.full_name, full_name_or_username=self.full_name_or_username, @@ -386,7 +403,10 @@ class User(Base, BaseModel): class UserLog(Base, BaseModel): __tablename__ = 'user_logs' - __table_args__ = {'extend_existing': True} + __table_args__ = ( + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'}, + ) user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True) @@ -405,7 +425,10 @@ class UserLog(Base, BaseModel): class UsersGroup(Base, BaseModel): __tablename__ = 'users_groups' - __table_args__ = {'extend_existing': True} + __table_args__ = ( + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'}, + ) users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None) @@ -413,9 +436,10 @@ class UsersGroup(Base, BaseModel): members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined") users_group_to_perm = relationship('UsersGroupToPerm', cascade='all') + users_group_repo_to_perm = relationship('UsersGroupRepoToPerm', cascade='all') - def __repr__(self): - return '' % (self.users_group_name) + def __unicode__(self): + return u'' % (self.users_group_name) @classmethod def get_by_group_name(cls, group_name, cache=False, @@ -443,7 +467,10 @@ class UsersGroup(Base, BaseModel): class UsersGroupMember(Base, BaseModel): __tablename__ = 'users_groups_members' - __table_args__ = {'extend_existing': True} + __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) @@ -461,7 +488,8 @@ class Repository(Base, BaseModel): __tablename__ = 'repositories' __table_args__ = ( UniqueConstraint('repo_name'), - {'extend_existing': True}, + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'}, ) repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) @@ -489,9 +517,9 @@ class Repository(Base, BaseModel): logs = relationship('UserLog') - def __repr__(self): - return "<%s('%s:%s')>" % (self.__class__.__name__, - self.repo_id, self.repo_name) + def __unicode__(self): + return u"<%s('%s:%s')>" % (self.__class__.__name__,self.repo_id, + self.repo_name) @classmethod def url_sep(cls): @@ -710,7 +738,8 @@ class RepoGroup(Base, BaseModel): __table_args__ = ( UniqueConstraint('group_name', 'group_parent_id'), CheckConstraint('group_id != group_parent_id'), - {'extend_existing': True}, + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'}, ) __mapper_args__ = {'order_by': 'group_name'} @@ -728,8 +757,8 @@ class RepoGroup(Base, BaseModel): self.group_name = group_name self.parent_group = parent_group - def __repr__(self): - return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id, + def __unicode__(self): + return u"<%s('%s:%s')>" % (self.__class__.__name__, self.group_id, self.group_name) @classmethod @@ -837,13 +866,16 @@ class RepoGroup(Base, BaseModel): class Permission(Base, BaseModel): __tablename__ = 'permissions' - __table_args__ = {'extend_existing': True} + __table_args__ = ( + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'}, + ) permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) - def __repr__(self): - return "<%s('%s:%s')>" % ( + def __unicode__(self): + return u"<%s('%s:%s')>" % ( self.__class__.__name__, self.permission_id, self.permission_name ) @@ -874,7 +906,8 @@ class UserRepoToPerm(Base, BaseModel): __tablename__ = 'repo_to_perm' __table_args__ = ( UniqueConstraint('user_id', 'repository_id', 'permission_id'), - {'extend_existing': True} + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} ) repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) @@ -894,15 +927,16 @@ class UserRepoToPerm(Base, BaseModel): Session.add(n) return n - def __repr__(self): - return ' %s >' % (self.user, self.repository) + def __unicode__(self): + return u' %s >' % (self.user, self.repository) class UserToPerm(Base, BaseModel): __tablename__ = 'user_to_perm' __table_args__ = ( UniqueConstraint('user_id', 'permission_id'), - {'extend_existing': True} + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} ) user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None) @@ -916,7 +950,8 @@ class UsersGroupRepoToPerm(Base, BaseMod __tablename__ = 'users_group_repo_to_perm' __table_args__ = ( UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), - {'extend_existing': True} + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} ) users_group_to_perm_id = Column("users_group_to_perm_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) @@ -936,15 +971,16 @@ class UsersGroupRepoToPerm(Base, BaseMod Session.add(n) return n - def __repr__(self): - return ' %s >' % (self.users_group, self.repository) + def __unicode__(self): + return u' %s >' % (self.users_group, self.repository) class UsersGroupToPerm(Base, BaseModel): __tablename__ = 'users_group_to_perm' __table_args__ = ( UniqueConstraint('users_group_id', 'permission_id',), - {'extend_existing': True} + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} ) users_group_to_perm_id = Column("users_group_to_perm_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) @@ -958,7 +994,8 @@ class UserRepoGroupToPerm(Base, BaseMode __tablename__ = 'user_repo_group_to_perm' __table_args__ = ( UniqueConstraint('user_id', 'group_id', 'permission_id'), - {'extend_existing': True} + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} ) group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) @@ -975,7 +1012,8 @@ class UsersGroupRepoGroupToPerm(Base, Ba __tablename__ = 'users_group_repo_group_to_perm' __table_args__ = ( UniqueConstraint('users_group_id', 'group_id'), - {'extend_existing': True} + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} ) users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) @@ -990,7 +1028,11 @@ class UsersGroupRepoGroupToPerm(Base, Ba class Statistics(Base, BaseModel): __tablename__ = 'statistics' - __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing': True}) + __table_args__ = ( + UniqueConstraint('repository_id'), + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} + ) stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None) stat_on_revision = Column("stat_on_revision", Integer(), nullable=False) @@ -1006,7 +1048,8 @@ class UserFollowing(Base, BaseModel): __table_args__ = ( UniqueConstraint('user_id', 'follows_repository_id'), UniqueConstraint('user_id', 'follows_user_id'), - {'extend_existing': True} + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'} ) user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) @@ -1027,7 +1070,11 @@ class UserFollowing(Base, BaseModel): class CacheInvalidation(Base, BaseModel): __tablename__ = 'cache_invalidation' - __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing': True}) + __table_args__ = ( + UniqueConstraint('cache_key'), + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'}, + ) cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) @@ -1038,14 +1085,17 @@ class CacheInvalidation(Base, BaseModel) self.cache_args = cache_args self.cache_active = False - def __repr__(self): - return "<%s('%s:%s')>" % (self.__class__.__name__, + def __unicode__(self): + 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 + Wrapper for generating a key, together with a prefix :param key: """ @@ -1054,13 +1104,26 @@ class CacheInvalidation(Base, BaseModel) iid = rhodecode.CONFIG.get('instance_id') if iid: prefix = iid - return "%s%s" % (prefix, key) + 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 @@ -1069,10 +1132,12 @@ class CacheInvalidation(Base, BaseModel) :param key: """ - return cls.query()\ - .filter(CacheInvalidation.cache_key == key)\ - .filter(CacheInvalidation.cache_active == False)\ - .scalar() + + 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): @@ -1082,17 +1147,16 @@ class CacheInvalidation(Base, BaseModel) :param key: """ - log.debug('marking %s for invalidation' % key) - inv_obj = Session.query(cls)\ - .filter(cls.cache_key == key).scalar() - if inv_obj: - inv_obj.cache_active = False - else: - log.debug('cache key not found in invalidation db -> creating one') - inv_obj = CacheInvalidation(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 - try: - Session.add(inv_obj) + Session.add(inv_obj) Session.commit() except Exception: log.error(traceback.format_exc()) @@ -1113,7 +1177,10 @@ class CacheInvalidation(Base, BaseModel) class ChangesetComment(Base, BaseModel): __tablename__ = 'changeset_comments' - __table_args__ = ({'extend_existing': True},) + __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) @@ -1142,7 +1209,10 @@ class ChangesetComment(Base, BaseModel): class Notification(Base, BaseModel): __tablename__ = 'notifications' - __table_args__ = ({'extend_existing': True},) + __table_args__ = ( + {'extend_existing': True, 'mysql_engine':'InnoDB', + 'mysql_charset': 'utf8'}, + ) TYPE_CHANGESET_COMMENT = u'cs_comment' TYPE_MESSAGE = u'message' @@ -1194,7 +1264,8 @@ class UserNotification(Base, BaseModel): __tablename__ = 'user_to_notification' __table_args__ = ( UniqueConstraint('user_id', 'notification_id'), - {'extend_existing': True} + {'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) @@ -1212,7 +1283,10 @@ class UserNotification(Base, BaseModel): class DbMigrateVersion(Base, BaseModel): __tablename__ = 'db_migrate_version' - __table_args__ = {'extend_existing': True} + __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) diff --git a/rhodecode/model/notification.py b/rhodecode/model/notification.py --- a/rhodecode/model/notification.py +++ b/rhodecode/model/notification.py @@ -32,6 +32,7 @@ import datetime from pylons.i18n.translation import _ import rhodecode +from rhodecode.config.conf import DATETIME_FORMAT from rhodecode.lib import helpers as h from rhodecode.model import BaseModel from rhodecode.model.db import Notification, User, UserNotification @@ -47,11 +48,11 @@ class NotificationModel(BaseModel): def __get_notification(self, notification): if isinstance(notification, Notification): return notification - elif isinstance(notification, int): + elif isinstance(notification, (int, long)): return Notification.get(notification) else: if notification: - raise Exception('notification must be int or Instance' + raise Exception('notification must be int, long or Instance' ' of Notification got %s' % type(notification)) def create(self, created_by, subject, body, recipients=None, @@ -111,6 +112,7 @@ class NotificationModel(BaseModel): kwargs.update(email_kwargs) email_body_html = EmailNotificationModel()\ .get_email_tmpl(type_, **kwargs) + run_task(tasks.send_email, rec.email, email_subject, email_body, email_body_html) @@ -176,14 +178,13 @@ class NotificationModel(BaseModel): notification.TYPE_REGISTRATION: _('registered in RhodeCode') } - DATETIME_FORMAT = "%Y-%m-%d %H:%M:%S" - tmpl = "%(user)s %(action)s %(when)s" if show_age: when = h.age(notification.created_on) else: DTF = lambda d: datetime.datetime.strftime(d, DATETIME_FORMAT) when = DTF(notification.created_on) + data = dict( user=notification.created_by_user.username, action=_map[notification.type_], when=when, diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -29,15 +29,15 @@ import traceback from datetime import datetime from rhodecode.lib.vcs.backends import get_backend - -from rhodecode.lib import LazyProperty -from rhodecode.lib import safe_str, safe_unicode +from rhodecode.lib.compat import json +from rhodecode.lib.utils2 import LazyProperty, safe_str, safe_unicode from rhodecode.lib.caching_query import FromCache from rhodecode.lib.hooks import log_create_repository from rhodecode.model import BaseModel from rhodecode.model.db import Repository, UserRepoToPerm, User, Permission, \ Statistics, UsersGroup, UsersGroupRepoToPerm, RhodeCodeUi, RepoGroup +from rhodecode.lib import helpers as h log = logging.getLogger(__name__) @@ -95,25 +95,28 @@ class RepoModel(BaseModel): return repo.scalar() def get_users_js(self): - users = self.sa.query(User).filter(User.active == True).all() - u_tmpl = '''{id:%s, fname:"%s", lname:"%s", nname:"%s"},''' - users_array = '[%s]' % '\n'.join([u_tmpl % (u.user_id, u.name, - u.lastname, u.username) - for u in users]) - return users_array + return json.dumps([ + { + 'id': u.user_id, + 'fname': u.name, + 'lname': u.lastname, + 'nname': u.username, + 'gravatar_lnk': h.gravatar_url(u.email, 14) + } for u in users] + ) def get_users_groups_js(self): users_groups = self.sa.query(UsersGroup)\ .filter(UsersGroup.users_group_active == True).all() - g_tmpl = '''{id:%s, grname:"%s",grmembers:"%s"},''' - - users_groups_array = '[%s]' % '\n'.join([g_tmpl % \ - (gr.users_group_id, gr.users_group_name, - len(gr.members)) - for gr in users_groups]) - return users_groups_array + return json.dumps([ + { + 'id': gr.users_group_id, + 'grname': gr.users_group_name, + 'grmembers': len(gr.members), + } for gr in users_groups] + ) def _get_defaults(self, repo_name): """ @@ -346,6 +349,7 @@ class RepoModel(BaseModel): :param repo: Instance of Repository, repository_id, or repository name :param user: Instance of User, user_id or username """ + user = self.__get_user(user) repo = self.__get_repo(repo) diff --git a/rhodecode/model/repos_group.py b/rhodecode/model/repos_group.py --- a/rhodecode/model/repos_group.py +++ b/rhodecode/model/repos_group.py @@ -28,7 +28,7 @@ import logging import traceback import shutil -from rhodecode.lib import LazyProperty +from rhodecode.lib.utils2 import LazyProperty from rhodecode.model import BaseModel from rhodecode.model.db import RepoGroup, RhodeCodeUi, UserRepoGroupToPerm, \ diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -35,7 +35,7 @@ from rhodecode.lib.vcs.nodes import File from rhodecode import BACKENDS from rhodecode.lib import helpers as h -from rhodecode.lib import safe_str +from rhodecode.lib.utils2 import safe_str from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \ action_logger, EmptyChangeset, REMOVED_REPO_PAT @@ -235,13 +235,13 @@ class ScmModel(BaseModel): return group_iter def mark_for_invalidation(self, repo_name): - """Puts cache invalidation task into db for + """ + Puts cache invalidation task into db for further global cache invalidation :param repo_name: this repo that should invalidation take place """ CacheInvalidation.set_invalidate(repo_name) - CacheInvalidation.set_invalidate(repo_name + "_README") def toggle_following_repo(self, follow_repo_id, user_id): diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -29,18 +29,19 @@ import traceback from pylons import url from pylons.i18n.translation import _ -from rhodecode.lib import safe_unicode +from rhodecode.lib.utils2 import safe_unicode, generate_api_key from rhodecode.lib.caching_query import FromCache from rhodecode.model import BaseModel from rhodecode.model.db import User, UserRepoToPerm, Repository, Permission, \ UserToPerm, UsersGroupRepoToPerm, UsersGroupToPerm, UsersGroupMember, \ - Notification, RepoGroup, UserRepoGroupToPerm, UsersGroup + Notification, RepoGroup, UserRepoGroupToPerm, UsersGroup,\ + UsersGroupRepoGroupToPerm from rhodecode.lib.exceptions import DefaultUserException, \ UserOwnsReposException from sqlalchemy.exc import DatabaseError -from rhodecode.lib import generate_api_key + from sqlalchemy.orm import joinedload log = logging.getLogger(__name__) @@ -298,14 +299,16 @@ class UserModel(BaseModel): try: if user.username == 'default': raise DefaultUserException( - _("You can't remove this user since it's" - " crucial for entire application")) + _(u"You can't remove this user since it's" + " crucial for entire application") + ) if user.repositories: - raise UserOwnsReposException(_('This user still owns %s ' - 'repositories and cannot be ' - 'removed. Switch owners or ' - 'remove those repositories') \ - % 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()) @@ -409,7 +412,7 @@ class UserModel(BaseModel): for perm in default_global_perms: user.permissions[GLOBAL].add(perm.permission.permission_name) - # default for repositories + # 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): @@ -423,17 +426,18 @@ class UserModel(BaseModel): user.permissions[RK][r_k] = p - # default for repositories groups + # 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 default with user permissions if any + # overwrite defaults with user permissions if any found #================================================================== - # user global + # user global permissions user_perms = self.sa.query(UserToPerm)\ .options(joinedload(UserToPerm.permission))\ .filter(UserToPerm.user_id == uid).all() @@ -441,7 +445,7 @@ class UserModel(BaseModel): for perm in user_perms: user.permissions[GLOBAL].add(perm.permission.permission_name) - # user repositories + # user explicit permissions for repositories user_repo_perms = \ self.sa.query(UserRepoToPerm, Permission, Repository)\ .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\ @@ -459,8 +463,8 @@ class UserModel(BaseModel): user.permissions[RK][r_k] = p #================================================================== - # check if user is part of groups for this repository and fill in - # (or replace with higher) permissions + # check if user is part of user groups for this repository and + # fill in (or replace with higher) permissions #================================================================== # users group global @@ -473,7 +477,7 @@ class UserModel(BaseModel): for perm in user_perms_from_users_groups: user.permissions[GLOBAL].add(perm.permission.permission_name) - # users group repositories + # 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))\ @@ -495,12 +499,12 @@ class UserModel(BaseModel): # get access for this user for repos group and override defaults #================================================================== - # user repositories groups + # 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(UserRepoToPerm.user_id == uid)\ + .filter(UserRepoGroupToPerm.user_id == uid)\ .all() for perm in user_repo_groups_perms: @@ -510,6 +514,30 @@ class UserModel(BaseModel): if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: user.permissions[GK][rg_k] = p + #================================================================== + # 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): diff --git a/rhodecode/public/css/style.css b/rhodecode/public/css/style.css --- a/rhodecode/public/css/style.css +++ b/rhodecode/public/css/style.css @@ -185,6 +185,41 @@ div.options a { border-bottom-right-radius: 8px; } +.top-left-rounded-corner-mid { + -webkit-border-top-left-radius: 4px; + -khtml-border-radius-topleft: 4px; + -moz-border-radius-topleft: 4px; + border-top-left-radius: 4px; +} + +.top-right-rounded-corner-mid { + -webkit-border-top-right-radius: 4px; + -khtml-border-radius-topright: 4px; + -moz-border-radius-topright: 4px; + border-top-right-radius: 4px; +} + +.bottom-left-rounded-corner-mid { + -webkit-border-bottom-left-radius: 4px; + -khtml-border-radius-bottomleft: 4px; + -moz-border-radius-bottomleft: 4px; + border-bottom-left-radius: 4px; +} + +.bottom-right-rounded-corner-mid { + -webkit-border-bottom-right-radius: 4px; + -khtml-border-radius-bottomright: 4px; + -moz-border-radius-bottomright: 4px; + border-bottom-right-radius: 4px; +} + +.help-block { + color: #999999; + display: block; + margin-bottom: 0; + margin-top: 5px; +} + #header { margin: 0; padding: 0 10px; @@ -197,18 +232,16 @@ div.options a { -moz-border-radius: 0px 0px 8px 8px; border-radius: 0px 0px 8px 8px; height: 37px; - background-color: #eedc94; + background-color: #003B76; background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), - to(#eedc94) ); + background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) ); background-image: -moz-linear-gradient(top, #003b76, #00376e); background-image: -ms-linear-gradient(top, #003b76, #00376e); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); background-image: -webkit-linear-gradient(top, #003b76, #00376e); background-image: -o-linear-gradient(top, #003b76, #00376e); background-image: linear-gradient(top, #003b76, #00376e); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', - endColorstr='#00376e', GradientType=0 ); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76',endColorstr='#00376e', GradientType=0 ); box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); } @@ -260,9 +293,9 @@ div.options a { min-height: 44px; clear: both; position: relative; - background-color: #eedc94; + background-color: #003B76; background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1),to(#eedc94) ); + background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) ); background-image: -moz-linear-gradient(top, #003b76, #00376e); background-image: -ms-linear-gradient(top, #003b76, #00376e); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76),color-stop(100%, #00376e) ); @@ -289,6 +322,14 @@ div.options a { -moz-border-radius: 0px 0px 0px 0px; border-radius: 0px 0px 0px 0px; } + +.ie7 #header #header-inner.hover, +.ie8 #header #header-inner.hover, +.ie9 #header #header-inner.hover +{ + z-index: auto !important; +} + #header #header-inner #home a { height: 40px; width: 46px; @@ -997,9 +1038,9 @@ tbody .yui-dt-editable { cursor: pointer #content div.box div.title { clear: both; overflow: hidden; - background-color: #eedc94; + background-color: #003B76; background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94) ); + background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) ); background-image: -moz-linear-gradient(top, #003b76, #00376e); background-image: -ms-linear-gradient(top, #003b76, #00376e); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); @@ -1758,33 +1799,21 @@ div.form div.fields div.field div.button } #footer div#footer-inner { - background-color: #eedc94; background-repeat : repeat-x; - background-image : -khtml-gradient( linear, left top, left bottom, - from( #fceec1), to( #eedc94)); background-image : -moz-linear-gradient( - top, #003b76, #00376e); background-image : -ms-linear-gradient( top, - #003b76, #00376e); background-image : -webkit-gradient( linear, left - top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e)); + background-color: #003B76; + background-repeat : repeat-x; + background-image : -khtml-gradient( linear, left top, left bottom, from(#003B76), to(#00376E)); + background-image : -moz-linear-gradient(top, #003b76, #00376e); + background-image : -ms-linear-gradient( top, #003b76, #00376e); + background-image : -webkit-gradient( linear, left top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e)); background-image : -webkit-linear-gradient( top, #003b76, #00376e)); background-image : -o-linear-gradient( top, #003b76, #00376e)); - background-image : linear-gradient( top, #003b76, #00376e); filter : - progid : DXImageTransform.Microsoft.gradient ( startColorstr = - '#003b76', endColorstr = '#00376e', GradientType = 0); + background-image : linear-gradient( top, #003b76, #00376e); + filter :progid : DXImageTransform.Microsoft.gradient ( startColorstr = '#003b76', endColorstr = '#00376e', GradientType = 0); box-shadow: 0 2px 2px rgba(0, 0, 0, 0.6); -webkit-border-radius: 4px 4px 4px 4px; -khtml-border-radius: 4px 4px 4px 4px; -moz-border-radius: 4px 4px 4px 4px; border-radius: 4px 4px 4px 4px; - background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), - to(#eedc94) ); - background-image: -moz-linear-gradient(top, #003b76, #00376e); - background-image: -ms-linear-gradient(top, #003b76, #00376e); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); - background-image: -webkit-linear-gradient(top, #003b76, #00376e); - background-image: -o-linear-gradient(top, #003b76, #00376e); - background-image: linear-gradient(top, #003b76, #00376e); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', - endColorstr='#00376e', GradientType=0 ); } #footer div#footer-inner p { @@ -1808,30 +1837,18 @@ div.form div.fields div.field div.button clear: both; overflow: hidden; position: relative; - background-color: #eedc94; background-repeat : repeat-x; - background-image : -khtml-gradient( linear, left top, left bottom, - from( #fceec1), to( #eedc94)); background-image : -moz-linear-gradient( - top, #003b76, #00376e); background-image : -ms-linear-gradient( top, - #003b76, #00376e); background-image : -webkit-gradient( linear, left - top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e)); + background-color: #003B76; + background-repeat : repeat-x; + background-image : -khtml-gradient( linear, left top, left bottom, from(#003B76), to(#00376E)); + background-image : -moz-linear-gradient( top, #003b76, #00376e); + background-image : -ms-linear-gradient( top, #003b76, #00376e); + background-image : -webkit-gradient( linear, left top, left bottom, color-stop( 0%, #003b76), color-stop( 100%, #00376e)); background-image : -webkit-linear-gradient( top, #003b76, #00376e)); background-image : -o-linear-gradient( top, #003b76, #00376e)); - background-image : linear-gradient( top, #003b76, #00376e); filter : - progid : DXImageTransform.Microsoft.gradient ( startColorstr = - '#003b76', endColorstr = '#00376e', GradientType = 0); + background-image : linear-gradient( top, #003b76, #00376e); + filter : progid : DXImageTransform.Microsoft.gradient ( startColorstr = '#003b76', endColorstr = '#00376e', GradientType = 0); margin: 0 auto; padding: 0; - background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), - to(#eedc94) ); - background-image: -moz-linear-gradient(top, #003b76, #00376e); - background-image: -ms-linear-gradient(top, #003b76, #00376e); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); - background-image: -webkit-linear-gradient(top, #003b76, #00376e); - background-image: -o-linear-gradient(top, #003b76, #00376e); - background-image: linear-gradient(top, #003b76, #00376e); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', - endColorstr='#00376e', GradientType=0 ); } #login div.inner { @@ -1908,16 +1925,14 @@ div.form div.fields div.field div.button width: 278px; background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), - to(#eedc94) ); + background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) ); background-image: -moz-linear-gradient(top, #003b76, #00376e); background-image: -ms-linear-gradient(top, #003b76, #00376e); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); background-image: -webkit-linear-gradient(top, #003b76, #00376e); background-image: -o-linear-gradient(top, #003b76, #00376e); background-image: linear-gradient(top, #003b76, #00376e); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', - endColorstr='#00376e', GradientType=0 ); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#003b76', endColorstr='#00376e', GradientType=0 ); z-index: 999; -webkit-border-radius: 0px 0px 4px 4px; @@ -2060,10 +2075,9 @@ div.form div.fields div.field div.button clear: both; overflow: hidden; position: relative; - background-color: #eedc94; + background-color: #003B76; background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), - to(#eedc94) ); + background-image: -khtml-gradient(linear, left top, left bottom, from(#003B76), to(#00376E) ); background-image: -moz-linear-gradient(top, #003b76, #00376e); background-image: -ms-linear-gradient(top, #003b76, #00376e); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #003b76), color-stop(100%, #00376e) ); @@ -2794,12 +2808,12 @@ table.code-browser .browser-dir { } .ac .yui-ac { - position: relative; + position: inherit; font-size: 100%; } .ac .perm_ac { - width: 15em; + width: 20em; } .ac .yui-ac-input { @@ -2809,16 +2823,15 @@ table.code-browser .browser-dir { .ac .yui-ac-container { position: absolute; top: 1.6em; - width: 100%; + width: auto; } .ac .yui-ac-content { position: absolute; - width: 100%; border: 1px solid gray; background: #fff; - overflow: hidden; z-index: 9050; + } .ac .yui-ac-shadow { @@ -2827,7 +2840,7 @@ table.code-browser .browser-dir { background: #000; -moz-opacity: 0.1px; opacity: .10; - filter: alpha(opacity = 10); + filter: alpha(opacity = 10); z-index: 9049; margin: .3em; } @@ -2836,6 +2849,7 @@ table.code-browser .browser-dir { width: 100%; margin: 0; padding: 0; + z-index: 9050; } .ac .yui-ac-content li { @@ -2843,15 +2857,28 @@ table.code-browser .browser-dir { white-space: nowrap; margin: 0; padding: 2px 5px; + height: 18px; + z-index: 9050; + display: block; + width: auto !important; +} + +.ac .yui-ac-content li .ac-container-wrap{ + width: auto; } .ac .yui-ac-content li.yui-ac-prehighlight { background: #B3D4FF; + z-index: 9050; } .ac .yui-ac-content li.yui-ac-highlight { background: #556CB5; color: #FFF; + z-index: 9050; +} +.ac .yui-ac-bd{ + z-index: 9050; } .follow { @@ -3006,17 +3033,14 @@ table.code-browser .browser-dir { .error_msg { background-color: #c43c35; background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), - to(#c43c35) ); + background-image: -khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35) ); background-image: -moz-linear-gradient(top, #ee5f5b, #c43c35); background-image: -ms-linear-gradient(top, #ee5f5b, #c43c35); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), - color-stop(100%, #c43c35) ); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35) ); background-image: -webkit-linear-gradient(top, #ee5f5b, #c43c35); background-image: -o-linear-gradient(top, #ee5f5b, #c43c35); background-image: linear-gradient(top, #ee5f5b, #c43c35); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', - endColorstr='#c43c35', GradientType=0 ); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b',endColorstr='#c43c35', GradientType=0 ); border-color: #c43c35 #c43c35 #882a25; } @@ -3024,51 +3048,42 @@ table.code-browser .browser-dir { color: #404040 !important; background-color: #eedc94; background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), - to(#eedc94) ); + background-image: -khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94) ); background-image: -moz-linear-gradient(top, #fceec1, #eedc94); background-image: -ms-linear-gradient(top, #fceec1, #eedc94); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), - color-stop(100%, #eedc94) ); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94) ); background-image: -webkit-linear-gradient(top, #fceec1, #eedc94); background-image: -o-linear-gradient(top, #fceec1, #eedc94); background-image: linear-gradient(top, #fceec1, #eedc94); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', - endColorstr='#eedc94', GradientType=0 ); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0 ); border-color: #eedc94 #eedc94 #e4c652; } .success_msg { background-color: #57a957; background-repeat: repeat-x !important; - background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), - to(#57a957) ); + background-image: -khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957) ); background-image: -moz-linear-gradient(top, #62c462, #57a957); background-image: -ms-linear-gradient(top, #62c462, #57a957); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), - color-stop(100%, #57a957) ); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957) ); background-image: -webkit-linear-gradient(top, #62c462, #57a957); background-image: -o-linear-gradient(top, #62c462, #57a957); background-image: linear-gradient(top, #62c462, #57a957); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', - endColorstr='#57a957', GradientType=0 ); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0 ); border-color: #57a957 #57a957 #3d773d; } .notice_msg { background-color: #339bb9; background-repeat: repeat-x; - background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), - to(#339bb9) ); + background-image: -khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9) ); background-image: -moz-linear-gradient(top, #5bc0de, #339bb9); background-image: -ms-linear-gradient(top, #5bc0de, #339bb9); - background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), - color-stop(100%, #339bb9) ); + background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9) ); background-image: -webkit-linear-gradient(top, #5bc0de, #339bb9); background-image: -o-linear-gradient(top, #5bc0de, #339bb9); background-image: linear-gradient(top, #5bc0de, #339bb9); - filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', - endColorstr='#339bb9', GradientType=0 ); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0 ); border-color: #339bb9 #339bb9 #22697d; } @@ -3096,8 +3111,7 @@ table.code-browser .browser-dir { } #msg_close { - background: transparent url("../icons/cross_grey_small.png") no-repeat - scroll 0 0; + background: transparent url("../icons/cross_grey_small.png") no-repeat scroll 0 0; cursor: pointer; height: 16px; position: absolute; @@ -3105,7 +3119,12 @@ table.code-browser .browser-dir { top: 5px; width: 16px; } - +div#legend_data{ + padding-left:10px; +} +div#legend_container table{ + border: none !important; +} div#legend_container table,div#legend_choices table { width: auto !important; } @@ -4115,6 +4134,56 @@ form.comment-inline-form { padding:5px 0px 5px 38px; } +/**** + PERMS +*****/ +#perms .perms_section_head { + padding:10px 10px 10px 0px; + font-size:16px; + font-weight: bold; +} + +#perms .perm_tag{ + padding: 1px 3px 1px 3px; + font-size: 10px; + font-weight: bold; + text-transform: uppercase; + white-space: nowrap; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; +} + +#perms .perm_tag.admin{ + background-color: #B94A48; + color: #ffffff; +} + +#perms .perm_tag.write{ + background-color: #B94A48; + color: #ffffff; +} + +#perms .perm_tag.read{ + background-color: #468847; + color: #ffffff; +} + +#perms .perm_tag.none{ + background-color: #bfbfbf; + color: #ffffff; +} + +.perm-gravatar{ + vertical-align:middle; + padding:2px; +} +.perm-gravatar-ac{ + vertical-align:middle; + padding:2px; + width: 14px; + height: 14px; +} /***************************************************************************** DIFFS CSS diff --git a/rhodecode/public/js/rhodecode.js b/rhodecode/public/js/rhodecode.js --- a/rhodecode/public/js/rhodecode.js +++ b/rhodecode/public/js/rhodecode.js @@ -609,6 +609,178 @@ var deleteNotification = function(url, n }; +/** MEMBERS AUTOCOMPLETE WIDGET **/ + +var MembersAutoComplete = function (users_list, groups_list, group_lbl, members_lbl) { + var myUsers = users_list; + var myGroups = groups_list; + + // Define a custom search function for the DataSource of users + var matchUsers = function (sQuery) { + // Case insensitive matching + var query = sQuery.toLowerCase(); + var i = 0; + var l = myUsers.length; + var matches = []; + + // Match against each name of each contact + for (; i < l; i++) { + contact = myUsers[i]; + if ((contact.fname.toLowerCase().indexOf(query) > -1) || (contact.lname.toLowerCase().indexOf(query) > -1) || (contact.nname && (contact.nname.toLowerCase().indexOf(query) > -1))) { + matches[matches.length] = contact; + } + } + return matches; + }; + + // Define a custom search function for the DataSource of usersGroups + var matchGroups = function (sQuery) { + // Case insensitive matching + var query = sQuery.toLowerCase(); + var i = 0; + var l = myGroups.length; + var matches = []; + + // Match against each name of each contact + for (; i < l; i++) { + matched_group = myGroups[i]; + if (matched_group.grname.toLowerCase().indexOf(query) > -1) { + matches[matches.length] = matched_group; + } + } + return matches; + }; + + //match all + var matchAll = function (sQuery) { + u = matchUsers(sQuery); + g = matchGroups(sQuery); + return u.concat(g); + }; + + // DataScheme for members + var memberDS = new YAHOO.util.FunctionDataSource(matchAll); + memberDS.responseSchema = { + fields: ["id", "fname", "lname", "nname", "grname", "grmembers", "gravatar_lnk"] + }; + + // DataScheme for owner + var ownerDS = new YAHOO.util.FunctionDataSource(matchUsers); + ownerDS.responseSchema = { + fields: ["id", "fname", "lname", "nname", "gravatar_lnk"] + }; + + // Instantiate AutoComplete for perms + var membersAC = new YAHOO.widget.AutoComplete("perm_new_member_name", "perm_container", memberDS); + membersAC.useShadow = false; + membersAC.resultTypeList = false; + + // Instantiate AutoComplete for owner + var ownerAC = new YAHOO.widget.AutoComplete("user", "owner_container", ownerDS); + ownerAC.useShadow = false; + ownerAC.resultTypeList = false; + + + // Helper highlight function for the formatter + var highlightMatch = function (full, snippet, matchindex) { + return full.substring(0, matchindex) + + "" + + full.substr(matchindex, snippet.length) + + "" + full.substring(matchindex + snippet.length); + }; + + // Custom formatter to highlight the matching letters + var custom_formatter = function (oResultData, sQuery, sResultMatch) { + var query = sQuery.toLowerCase(); + var _gravatar = function(res, em, group){ + if (group !== undefined){ + em = '/images/icons/group.png' + } + tmpl = '
{1}
' + return tmpl.format(em,res) + } + // group + if (oResultData.grname != undefined) { + var grname = oResultData.grname; + var grmembers = oResultData.grmembers; + var grnameMatchIndex = grname.toLowerCase().indexOf(query); + var grprefix = "{0}: ".format(group_lbl); + var grsuffix = " (" + grmembers + " )"; + var grsuffix = " ({0} {1})".format(grmembers, members_lbl); + + if (grnameMatchIndex > -1) { + return _gravatar(grprefix + highlightMatch(grname, query, grnameMatchIndex) + grsuffix,null,true); + } + return _gravatar(grprefix + oResultData.grname + grsuffix, null,true); + // Users + } else if (oResultData.fname != undefined) { + var fname = oResultData.fname, + lname = oResultData.lname, + nname = oResultData.nname || "", + // Guard against null value + fnameMatchIndex = fname.toLowerCase().indexOf(query), + lnameMatchIndex = lname.toLowerCase().indexOf(query), + nnameMatchIndex = nname.toLowerCase().indexOf(query), + displayfname, displaylname, displaynname; + + if (fnameMatchIndex > -1) { + displayfname = highlightMatch(fname, query, fnameMatchIndex); + } else { + displayfname = fname; + } + + if (lnameMatchIndex > -1) { + displaylname = highlightMatch(lname, query, lnameMatchIndex); + } else { + displaylname = lname; + } + + if (nnameMatchIndex > -1) { + displaynname = "(" + highlightMatch(nname, query, nnameMatchIndex) + ")"; + } else { + displaynname = nname ? "(" + nname + ")" : ""; + } + + return _gravatar(displayfname + " " + displaylname + " " + displaynname, oResultData.gravatar_lnk); + } else { + return ''; + } + }; + membersAC.formatResult = custom_formatter; + ownerAC.formatResult = custom_formatter; + + var myHandler = function (sType, aArgs) { + + var myAC = aArgs[0]; // reference back to the AC instance + var elLI = aArgs[1]; // reference to the selected LI element + var oData = aArgs[2]; // object literal of selected item's result data + //fill the autocomplete with value + if (oData.nname != undefined) { + //users + myAC.getInputEl().value = oData.nname; + YUD.get('perm_new_member_type').value = 'user'; + } else { + //groups + myAC.getInputEl().value = oData.grname; + YUD.get('perm_new_member_type').value = 'users_group'; + } + }; + + membersAC.itemSelectEvent.subscribe(myHandler); + if(ownerAC.itemSelectEvent){ + ownerAC.itemSelectEvent.subscribe(myHandler); + } + + return { + memberDS: memberDS, + ownerDS: ownerDS, + membersAC: membersAC, + ownerAC: ownerAC, + }; +} + + + /** * QUICK REPO MENU */ @@ -700,6 +872,19 @@ var nameSort = function(a, b, desc, fiel return compState; }; +var permNameSort = function(a, b, desc, field) { + var a_ = fromHTML(a.getData(field)); + var b_ = fromHTML(b.getData(field)); + // extract name from table + + a_ = a_.children[0].innerHTML; + b_ = b_.children[0].innerHTML; + + var comp = YAHOO.util.Sort.compare; + var compState = comp(a_, b_, desc); + return compState; +}; + var groupNameSort = function(a, b, desc, field) { var a_ = fromHTML(a.getData(field)); var b_ = fromHTML(b.getData(field)); diff --git a/rhodecode/templates/_data_table/_dt_elements.html b/rhodecode/templates/_data_table/_dt_elements.html --- a/rhodecode/templates/_data_table/_dt_elements.html +++ b/rhodecode/templates/_data_table/_dt_elements.html @@ -40,7 +40,14 @@ -<%def name="repo_name(name,rtype,private,fork_of)"> +<%def name="repo_name(name,rtype,private,fork_of,short_name=False, admin=False)"> + <% + def get_name(name,short_name=short_name): + if short_name: + return name.split('/')[-1] + else: + return name + %>
##TYPE OF REPO %if h.is_hg(rtype): @@ -57,7 +64,11 @@ %endif ##NAME - ${h.link_to(name,h.url('summary_home',repo_name=name),class_="repo_name")} + %if admin: + ${h.link_to(get_name(name),h.url('edit_repo',repo_name=name),class_="repo_name")} + %else: + ${h.link_to(get_name(name),h.url('summary_home',repo_name=name),class_="repo_name")} + %endif %if fork_of: ${_('fork')} diff --git a/rhodecode/templates/admin/repos/repo_add_base.html b/rhodecode/templates/admin/repos/repo_add_base.html --- a/rhodecode/templates/admin/repos/repo_add_base.html +++ b/rhodecode/templates/admin/repos/repo_add_base.html @@ -21,6 +21,7 @@
${h.text('clone_uri',class_="small")} + ${_('Optional http[s] url from which repository should be cloned.')}
@@ -28,7 +29,8 @@
- ${h.select('repo_group','',c.repo_groups,class_="medium")} + ${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")} + ${_('Optional select a group to put this repository into.')}
@@ -37,6 +39,7 @@
${h.select('repo_type','hg',c.backends,class_="small")} + ${_('Type of repository to create.')}
@@ -44,15 +47,17 @@
- ${h.textarea('description',cols=23,rows=5)} + ${h.textarea('description')} + ${_('Keep it short and to the point. Use a README file for longer descriptions.')}
- +
${h.checkbox('private',value="True")} + ${_('Private repositories are only visible to people explicitly added as collaborators.')}
diff --git a/rhodecode/templates/admin/repos/repo_edit.html b/rhodecode/templates/admin/repos/repo_edit.html --- a/rhodecode/templates/admin/repos/repo_edit.html +++ b/rhodecode/templates/admin/repos/repo_edit.html @@ -41,6 +41,7 @@
${h.text('clone_uri',class_="medium")} + ${_('Optional http[s] url from which repository should be cloned.')}
@@ -49,6 +50,7 @@
${h.select('repo_group','',c.repo_groups,class_="medium")} + ${_('Optional select a group to put this repository into.')}
@@ -64,16 +66,18 @@
- ${h.textarea('description',cols=23,rows=5)} + ${h.textarea('description')} + ${_('Keep it short and to the point. Use a README file for longer descriptions.')}
- +
${h.checkbox('private',value="True")} + ${_('Private repositories are only visible to people explicitly added as collaborators.')}
@@ -82,6 +86,7 @@
${h.checkbox('enable_statistics',value="True")} + ${_('Enable statistics window on summary page.')}
@@ -90,15 +95,17 @@
${h.checkbox('enable_downloads',value="True")} + ${_('Enable download menu on summary page.')}
-
+
${h.text('user',class_='yui-ac-input')} + ${_('Change owner of this repository.')}
diff --git a/rhodecode/templates/admin/repos/repo_edit_perms.html b/rhodecode/templates/admin/repos/repo_edit_perms.html --- a/rhodecode/templates/admin/repos/repo_edit_perms.html +++ b/rhodecode/templates/admin/repos/repo_edit_perms.html @@ -25,7 +25,7 @@ ${h.radio('u_perm_%s' % r2p.user.username,'repository.write')} ${h.radio('u_perm_%s' % r2p.user.username,'repository.admin')} - ${r2p.user.username} + ${r2p.user.username} %if r2p.user.username !='default': @@ -46,7 +46,7 @@ ${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'repository.write')} ${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'repository.admin')} - ${g2p.users_group.users_group_name} + ${g2p.users_group.users_group_name} @@ -117,165 +117,12 @@ YUE.onDOMReady(function () { YUD.setStyle('add_perm', 'opacity', '0.6'); YUD.setStyle('add_perm', 'cursor', 'default'); }); + MembersAutoComplete( + ${c.users_array|n}, + ${c.users_groups_array|n}, + "${_('Group')}", + "${_('members')}" + ); }); -YAHOO.example.FnMultipleFields = function () { - var myUsers = ${c.users_array|n}; - var myGroups = ${c.users_groups_array|n}; - - // Define a custom search function for the DataSource of users - var matchUsers = function (sQuery) { - // Case insensitive matching - var query = sQuery.toLowerCase(); - var i = 0; - var l = myUsers.length; - var matches = []; - - // Match against each name of each contact - for (; i < l; i++) { - contact = myUsers[i]; - if ((contact.fname.toLowerCase().indexOf(query) > -1) || (contact.lname.toLowerCase().indexOf(query) > -1) || (contact.nname && (contact.nname.toLowerCase().indexOf(query) > -1))) { - matches[matches.length] = contact; - } - } - return matches; - }; - - // Define a custom search function for the DataSource of usersGroups - var matchGroups = function (sQuery) { - // Case insensitive matching - var query = sQuery.toLowerCase(); - var i = 0; - var l = myGroups.length; - var matches = []; - - // Match against each name of each contact - for (; i < l; i++) { - matched_group = myGroups[i]; - if (matched_group.grname.toLowerCase().indexOf(query) > -1) { - matches[matches.length] = matched_group; - } - } - return matches; - }; - - //match all - var matchAll = function (sQuery) { - u = matchUsers(sQuery); - g = matchGroups(sQuery); - return u.concat(g); - }; - - // DataScheme for members - var memberDS = new YAHOO.util.FunctionDataSource(matchAll); - memberDS.responseSchema = { - fields: ["id", "fname", "lname", "nname", "grname", "grmembers"] - }; - - // DataScheme for owner - var ownerDS = new YAHOO.util.FunctionDataSource(matchUsers); - ownerDS.responseSchema = { - fields: ["id", "fname", "lname", "nname"] - }; - - // Instantiate AutoComplete for perms - var membersAC = new YAHOO.widget.AutoComplete("perm_new_member_name", "perm_container", memberDS); - membersAC.useShadow = false; - membersAC.resultTypeList = false; - - // Instantiate AutoComplete for owner - var ownerAC = new YAHOO.widget.AutoComplete("user", "owner_container", ownerDS); - ownerAC.useShadow = false; - ownerAC.resultTypeList = false; - - - // Helper highlight function for the formatter - var highlightMatch = function (full, snippet, matchindex) { - return full.substring(0, matchindex) + "" + full.substr(matchindex, snippet.length) + "" + full.substring(matchindex + snippet.length); - }; - - // Custom formatter to highlight the matching letters - var custom_formatter = function (oResultData, sQuery, sResultMatch) { - var query = sQuery.toLowerCase(); - - if (oResultData.grname != undefined) { - var grname = oResultData.grname; - var grmembers = oResultData.grmembers; - var grnameMatchIndex = grname.toLowerCase().indexOf(query); - var grprefix = "${_('Group')}: "; - var grsuffix = " (" + grmembers + " ${_('members')})"; - - if (grnameMatchIndex > -1) { - return grprefix + highlightMatch(grname, query, grnameMatchIndex) + grsuffix; - } - - return grprefix + oResultData.grname + grsuffix; - } else if (oResultData.fname != undefined) { - - var fname = oResultData.fname, - lname = oResultData.lname, - nname = oResultData.nname || "", - // Guard against null value - fnameMatchIndex = fname.toLowerCase().indexOf(query), - lnameMatchIndex = lname.toLowerCase().indexOf(query), - nnameMatchIndex = nname.toLowerCase().indexOf(query), - displayfname, displaylname, displaynname; - - if (fnameMatchIndex > -1) { - displayfname = highlightMatch(fname, query, fnameMatchIndex); - } else { - displayfname = fname; - } - - if (lnameMatchIndex > -1) { - displaylname = highlightMatch(lname, query, lnameMatchIndex); - } else { - displaylname = lname; - } - - if (nnameMatchIndex > -1) { - displaynname = "(" + highlightMatch(nname, query, nnameMatchIndex) + ")"; - } else { - displaynname = nname ? "(" + nname + ")" : ""; - } - - return displayfname + " " + displaylname + " " + displaynname; - } else { - return ''; - } - }; - membersAC.formatResult = custom_formatter; - ownerAC.formatResult = custom_formatter; - - var myHandler = function (sType, aArgs) { - - var myAC = aArgs[0]; // reference back to the AC instance - var elLI = aArgs[1]; // reference to the selected LI element - var oData = aArgs[2]; // object literal of selected item's result data - //fill the autocomplete with value - if (oData.nname != undefined) { - //users - myAC.getInputEl().value = oData.nname; - YUD.get('perm_new_member_type').value = 'user'; - } else { - //groups - myAC.getInputEl().value = oData.grname; - YUD.get('perm_new_member_type').value = 'users_group'; - } - - }; - - membersAC.itemSelectEvent.subscribe(myHandler); - if(ownerAC.itemSelectEvent){ - ownerAC.itemSelectEvent.subscribe(myHandler); - } - - return { - memberDS: memberDS, - ownerDS: ownerDS, - membersAC: membersAC, - ownerAC: ownerAC, - }; -}(); - diff --git a/rhodecode/templates/admin/repos/repos.html b/rhodecode/templates/admin/repos/repos.html --- a/rhodecode/templates/admin/repos/repos.html +++ b/rhodecode/templates/admin/repos/repos.html @@ -48,7 +48,7 @@ ${dt.quick_menu(repo['name'])} - ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],repo['dbrepo_fork'].get('repo_name'))} + ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],repo['dbrepo_fork'].get('repo_name'), admin=True)} ##DESCRIPTION diff --git a/rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html b/rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html --- a/rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html +++ b/rhodecode/templates/admin/repos_groups/repos_group_edit_perms.html @@ -15,7 +15,7 @@ ${h.radio('u_perm_%s' % r2p.user.username,'group.write')} ${h.radio('u_perm_%s' % r2p.user.username,'group.admin')} - ${r2p.user.username} + ${r2p.user.username} %if r2p.user.username !='default': @@ -35,7 +35,7 @@ ${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'group.write')} ${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'group.admin')} - ${g2p.users_group.users_group_name} + ${g2p.users_group.users_group_name} @@ -68,7 +68,7 @@ diff --git a/rhodecode/templates/admin/repos_groups/repos_groups.html b/rhodecode/templates/admin/repos_groups/repos_groups.html --- a/rhodecode/templates/admin/repos_groups/repos_groups.html +++ b/rhodecode/templates/admin/repos_groups/repos_groups.html @@ -17,5 +17,5 @@ ${self.menu('admin')} <%def name="main()"> - <%include file="/index_base.html" args="parent=self"/> + <%include file="/index_base.html" args="parent=self,short_repo_names=True"/> diff --git a/rhodecode/templates/admin/repos_groups/repos_groups_show.html b/rhodecode/templates/admin/repos_groups/repos_groups_show.html --- a/rhodecode/templates/admin/repos_groups/repos_groups_show.html +++ b/rhodecode/templates/admin/repos_groups/repos_groups_show.html @@ -44,7 +44,7 @@
${_('Repositories group')} - ${h.link_to(h.literal(' » '.join([g.name for g in gr.parents+[gr]])),url('edit_repos_group',id=gr.group_id))} + ${h.link_to(h.literal(' » '.join(map(h.safe_unicode,[g.name for g in gr.parents+[gr]]))),url('edit_repos_group',id=gr.group_id))}
${gr.group_description} diff --git a/rhodecode/templates/admin/users/user_edit_my_account.html b/rhodecode/templates/admin/users/user_edit_my_account.html --- a/rhodecode/templates/admin/users/user_edit_my_account.html +++ b/rhodecode/templates/admin/users/user_edit_my_account.html @@ -113,52 +113,45 @@
- ${_('My repositories')} + ${_('My repos')} / ${_('My permissions')}
%if h.HasPermissionAny('hg.admin','hg.create.repository')(): %endif
-
- +
+
+
+ - - + + + + <%namespace name="dt" file="/_data_table/_dt_elements.html"/> %if c.user_repos: %for repo in c.user_repos: - - + ##QUICK MENU + + ##REPO NAME AND ICONS + + ##LAST REVISION +
${_('Name')}${_('revision')}${_('action')}${_('Revision')}${_('Action')}${_('Action')}
- %if h.is_hg(repo['dbrepo']['repo_type']): - ${_('Mercurial repository')} - %elif h.is_git(repo['dbrepo']['repo_type']): - ${_('Git repository')} - %else: - - %endif - %if repo['dbrepo']['private']: - ${_('private')} - %else: - ${_('public')} - %endif - - ${h.link_to(repo['name'], h.url('summary_home',repo_name=repo['name']),class_="repo_name")} - %if repo['dbrepo_fork']: - - ${_('public')} - %endif - ${("r%s:%s") % (repo['rev'],h.short_id(repo['tip']))} + ${dt.quick_menu(repo['name'])} + + ${dt.repo_name(repo['name'],repo['dbrepo']['repo_type'],repo['dbrepo']['private'],repo['dbrepo_fork'].get('repo_name'))} + + ${dt.revision(repo['name'],repo['rev'],repo['tip'],repo['author'],repo['last_msg'])} + ${_('private')} ${h.form(url('repo_settings_delete', repo_name=repo['name']),method='delete')} @@ -177,14 +170,144 @@ %endif
+
+
+
diff --git a/rhodecode/templates/admin/users_groups/users_groups.html b/rhodecode/templates/admin/users_groups/users_groups.html --- a/rhodecode/templates/admin/users_groups/users_groups.html +++ b/rhodecode/templates/admin/users_groups/users_groups.html @@ -37,7 +37,7 @@ %for cnt,u_group in enumerate(c.users_groups_list): ${h.link_to(u_group.users_group_name,h.url('edit_users_group', id=u_group.users_group_id))} - ${len(u_group.members)} + ${len(u_group.members)} ${h.bool2icon(u_group.users_group_active)} ${h.form(url('users_group', id=u_group.users_group_id),method='delete')} diff --git a/rhodecode/templates/base/root.html b/rhodecode/templates/base/root.html --- a/rhodecode/templates/base/root.html +++ b/rhodecode/templates/base/root.html @@ -1,5 +1,5 @@ ## -*- coding: utf-8 -*- - + ${self.title()} @@ -37,7 +37,7 @@ ## JAVASCRIPT ## <%def name="js()"> - @@ -130,6 +130,17 @@ ${self.js()} - ${next.body()} + ## IE hacks + + + + + ${next.body()} diff --git a/rhodecode/templates/changelog/changelog.html b/rhodecode/templates/changelog/changelog.html --- a/rhodecode/templates/changelog/changelog.html +++ b/rhodecode/templates/changelog/changelog.html @@ -93,11 +93,11 @@ %endif %if h.is_hg(c.rhodecode_repo) and cs.branch: - ${h.link_to(cs.branch,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))} + ${h.link_to(h.shorter(cs.branch),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))} %endif %for tag in cs.tags: - ${h.link_to(tag,h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))} + ${h.link_to(h.shorter(tag),h.url('files_home',repo_name=c.repo_name,revision=cs.raw_id))} %endfor diff --git a/rhodecode/templates/changeset/changeset.html b/rhodecode/templates/changeset/changeset.html --- a/rhodecode/templates/changeset/changeset.html +++ b/rhodecode/templates/changeset/changeset.html @@ -36,8 +36,8 @@
- ${c.ignorews_url()} - ${c.context_url()} + ${c.ignorews_url(request.GET)} + ${c.context_url(request.GET)}
${len(c.comments)} comment(s) (${c.inline_cnt} ${_('inline')})
@@ -91,14 +91,14 @@ - ${_('%s files affected with %s additions and %s deletions:') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)} + ${_('%s files affected with %s insertions and %s deletions:') % (len(c.changeset.affected_files),c.lines_added,c.lines_deleted)}
%for change,filenode,diff,cs1,cs2,stat in c.changes:
%if change != 'removed': - ${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path)+"_target")} + ${h.link_to(h.safe_unicode(filenode.path),c.anchor_url(filenode.changeset.raw_id,filenode.path,request.GET)+"_target")} %else: ${h.link_to(h.safe_unicode(filenode.path),h.url.current(anchor=h.FID('',filenode.path)))} %endif diff --git a/rhodecode/templates/changeset/diff_block.html b/rhodecode/templates/changeset/diff_block.html --- a/rhodecode/templates/changeset/diff_block.html +++ b/rhodecode/templates/changeset/diff_block.html @@ -7,7 +7,7 @@ %for change,filenode,diff,cs1,cs2,stat in changes: %if change !='removed': -
+
@@ -19,8 +19,8 @@ - ${c.ignorews_url(h.FID(filenode.changeset.raw_id,filenode.path))} - ${c.context_url(h.FID(filenode.changeset.raw_id,filenode.path))} + ${c.ignorews_url(request.GET, h.FID(filenode.changeset.raw_id,filenode.path))} + ${c.context_url(request.GET, h.FID(filenode.changeset.raw_id,filenode.path))}