Changeset - dacdea9fda2a
[Not reviewed]
development.ini
Show inline comments
 
################################################################################
 
################################################################################
 
# Kallithea - Development config:                                              #
 
# listening on *:5000                                                          #
 
# sqlite and kallithea.db                                                      #
 
# initial_repo_scan = true                                                     #
 
# set debug = true                                                             #
 
# verbose and colorful logging                                                 #
 
#                                                                              #
 
# The %(here)s variable will be replaced with the parent directory of this file#
 
################################################################################
 
################################################################################
 

	
 
[DEFAULT]
 
debug = true
 
pdebug = false
 

	
 
################################################################################
 
## Uncomment and replace with the address which should receive                ##
 
## any error reports after application crash                                  ##
 
## Additionally those settings will be used by Kallithea mailing system       ##
 
################################################################################
 
#email_to = admin@localhost
 
#error_email_from = paste_error@localhost
 
#app_email_from = kallithea-noreply@localhost
 
#error_message =
 
#email_prefix = [Kallithea]
 

	
 
#smtp_server = mail.server.com
 
#smtp_username =
 
#smtp_password =
 
#smtp_port =
 
#smtp_use_tls = false
 
#smtp_use_ssl = true
 
## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
 
#smtp_auth =
 

	
 
[server:main]
 
## PASTE ##
 
#use = egg:Paste#http
 
## nr of worker threads to spawn
 
#threadpool_workers = 5
 
## max request before thread respawn
 
#threadpool_max_requests = 10
 
## option to use threads of process
 
#use_threadpool = true
 

	
 
## WAITRESS ##
 
use = egg:waitress#main
 
## number of worker threads
 
threads = 5
 
## MAX BODY SIZE 100GB
 
max_request_body_size = 107374182400
 
## use poll instead of select, fixes fd limits, may not work on old
 
## windows systems.
 
#asyncore_use_poll = True
 

	
 
## GUNICORN ##
 
#use = egg:gunicorn#main
 
## number of process workers. You must set `instance_id = *` when this option
 
## is set to more than one worker
 
#workers = 1
 
## process name
 
#proc_name = kallithea
 
## type of worker class, one of sync, eventlet, gevent, tornado
 
## recommended for bigger setup is using of of other than sync one
 
#worker_class = sync
 
#max_requests = 1000
 
## ammount of time a worker can handle request before it gets killed and
 
## restarted
 
#timeout = 3600
 

	
 
## UWSGI ##
 
## run with uwsgi --ini-paste-logged <inifile.ini>
 
#[uwsgi]
 
#socket = /tmp/uwsgi.sock
 
#master = true
 
#http = 127.0.0.1:5000
 

	
 
## set as deamon and redirect all output to file
 
#daemonize = ./uwsgi_kallithea.log
 

	
 
## master process PID
 
#pidfile = ./uwsgi_kallithea.pid
 

	
 
## stats server with workers statistics, use uwsgitop
 
## for monitoring, `uwsgitop 127.0.0.1:1717`
 
#stats = 127.0.0.1:1717
 
#memory-report = true
 

	
 
## log 5XX errors
 
#log-5xx = true
 

	
 
## Set the socket listen queue size.
 
#listen = 256
 

	
 
## Gracefully Reload workers after the specified amount of managed requests
 
## (avoid memory leaks).
 
#max-requests = 1000
 

	
 
## enable large buffers
 
#buffer-size=65535
 

	
 
## socket and http timeouts ##
 
#http-timeout=3600
 
#socket-timeout=3600
 

	
 
## Log requests slower than the specified number of milliseconds.
 
#log-slow = 10
 

	
 
## Exit if no app can be loaded.
 
#need-app = true
 

	
 
## Set lazy mode (load apps in workers instead of master).
 
#lazy = true
 

	
 
## scaling ##
 
## set cheaper algorithm to use, if not set default will be used
 
#cheaper-algo = spare
 

	
 
## minimum number of workers to keep at all times
 
#cheaper = 1
 

	
 
## number of workers to spawn at startup
 
#cheaper-initial = 1
 

	
 
## maximum number of workers that can be spawned
 
#workers = 4
 

	
 
## how many workers should be spawned at a time
 
#cheaper-step = 1
 

	
 
## COMMON ##
 
host = 0.0.0.0
 
port = 5000
 

	
 
## prefix middleware for rc
 
#[filter:proxy-prefix]
 
#use = egg:PasteDeploy#prefix
 
#prefix = /<your-prefix>
 

	
 
[app:main]
 
use = egg:kallithea
 
## enable proxy prefix middleware
 
#filter-with = proxy-prefix
 

	
 
full_stack = true
 
static_files = true
 
## Available Languages:
 
## de en fr ja pl pt_BR ru zh_CN zh_TW
 
lang = en
 
cache_dir = %(here)s/data
 
index_dir = %(here)s/data/index
 

	
 
## perform a full repository scan on each server start, this should be
 
## set to false after first startup, to allow faster server restarts.
 
#initial_repo_scan = false
 
initial_repo_scan = true
 

	
 
## uncomment and set this path to use archive download cache
 
archive_cache_dir = %(here)s/tarballcache
 

	
 
## change this to unique ID for security
 
app_instance_uuid = development-not-secret
 

	
 
## cut off limit for large diffs (size in bytes)
 
cut_off_limit = 256000
 

	
 
## use cache version of scm repo everywhere
 
vcs_full_cache = true
 

	
 
## force https in Kallithea, fixes https redirects, assumes it's always https
 
force_https = false
 

	
 
## use Strict-Transport-Security headers
 
use_htsts = false
 

	
 
## number of commits stats will parse on each iteration
 
commit_parse_limit = 25
 

	
 
## path to git executable
 
git_path = git
 

	
 
## git rev filter option, --all is the default filter, if you need to
 
## hide all refs in changelog switch this to --branches --tags
 
#git_rev_filter = --branches --tags
 

	
 
## RSS feed options
 
rss_cut_off_limit = 256000
 
rss_items_per_page = 10
 
rss_include_diff = false
 

	
 
## options for showing and identifying changesets
 
show_sha_length = 12
 
show_revision_number = true
 

	
 
## gist URL alias, used to create nicer urls for gist. This should be an
 
## url that does rewrites to _admin/gists/<gistid>.
 
## example: http://gist.kallithea.server/{gistid}. Empty means use the internal
 
## Kallithea url, ie. http[s]://your.kallithea.server/_admin/gists/<gistid>
 
gist_alias_url =
 

	
 
## white list of API enabled controllers. This allows to add list of
 
## controllers to which access will be enabled by api_key. eg: to enable
 
## api access to raw_files put `FilesController:raw`, to enable access to patches
 
## add `ChangesetController:changeset_patch`. This list should be "," separated
 
## Syntax is <ControllerClass>:<function>. Check debug logs for generated names
 
## Recommended settings below are commented out:
 
api_access_controllers_whitelist =
 
#    ChangesetController:changeset_patch,
 
#    ChangesetController:changeset_raw,
 
#    FilesController:raw,
 
#    FilesController:archivefile
 

	
 
## default encoding used to convert from and to unicode
 
## can be also a comma seperated list of encoding in case of mixed encodings
 
default_encoding = utf8
 

	
 
## issue tracker for Kallithea (leave blank to disable, absent for default)
 
#bugtracker = https://bitbucket.org/conservancy/kallithea/issues
 

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

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

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

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

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

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

	
 
issue_prefix = #
 

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

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

	
 

	
 
## instance-id prefix
 
## a prefix key for this instance used for cache invalidation when running
 
## multiple instances of kallithea, make sure it's globally unique for
 
## all running kallithea 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
 
## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with
 
## handling that. Set this variable to 403 to return HTTPForbidden
 
auth_ret_code =
 

	
 
## locking return code. When repository is locked return this HTTP code. 2XX
 
## codes don't break the transactions while 4XX codes do
 
lock_ret_code = 423
 

	
 
## allows to change the repository location in settings page
 
allow_repo_location_change = True
 

	
 
## allows to setup custom hooks in settings page
 
allow_custom_hooks_settings = True
 

	
 

	
 
####################################
 
###        CELERY CONFIG        ####
 
####################################
 

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

	
 
celery.imports = kallithea.lib.celerylib.tasks
 

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

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

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

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

	
 
####################################
 
###         BEAKER CACHE        ####
 
####################################
 

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

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

	
 
beaker.cache.super_short_term.type=memory
 
beaker.cache.super_short_term.expire=10
 
beaker.cache.super_short_term.key_length = 256
 

	
 
beaker.cache.short_term.type=memory
 
beaker.cache.short_term.expire=60
 
beaker.cache.short_term.key_length = 256
 

	
 
beaker.cache.long_term.type=memory
 
beaker.cache.long_term.expire=36000
 
beaker.cache.long_term.key_length = 256
 

	
 
beaker.cache.sql_cache_short.type=memory
 
beaker.cache.sql_cache_short.expire=10
 
beaker.cache.sql_cache_short.key_length = 256
 

	
 
beaker.cache.sql_cache_med.type=memory
 
beaker.cache.sql_cache_med.expire=360
 
beaker.cache.sql_cache_med.key_length = 256
 

	
 
beaker.cache.sql_cache_long.type=file
 
beaker.cache.sql_cache_long.expire=3600
 
beaker.cache.sql_cache_long.key_length = 256
 

	
 
####################################
 
###       BEAKER SESSION        ####
 
####################################
 
## Type of storage used for the session, current types are
 
## dbm, file, memcached, database, and memory.
 
## The storage uses the Container API
 
## that is also used by the cache system.
 

	
 
## db session ##
 
#beaker.session.type = ext:database
 
#beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
 
#beaker.session.table_name = db_session
 

	
 
## encrypted cookie client side session, good for many instances ##
 
#beaker.session.type = cookie
 

	
 
## file based cookies (default) ##
 
#beaker.session.type = file
 

	
 
beaker.session.key = kallithea
 
beaker.session.secret = development-not-secret
 

	
 
## Secure encrypted cookie. Requires AES and AES python libraries
 
## you must disable beaker.session.secret to use this
 
#beaker.session.encrypt_key = <key_for_encryption>
 
#beaker.session.validate_key = <validation_key>
 

	
 
## sets session as invalid if it haven't been accessed for given amount of time
 
beaker.session.timeout = 2592000
 
beaker.session.httponly = true
 
#beaker.session.cookie_path = /<your-prefix>
 

	
 
## uncomment for https secure cookie
 
beaker.session.secure = false
 

	
 
## auto save the session to not to use .save()
 
beaker.session.auto = False
 

	
 
## default cookie expiration time in seconds `true` expire at browser close ##
 
#beaker.session.cookie_expires = 3600
 

	
 

	
 
############################
 
## ERROR HANDLING SYSTEMS ##
 
############################
 

	
 
####################
 
### [errormator] ###
 
####################
 

	
 
## Errormator is tailored to work with Kallithea, see
 
## http://errormator.com for details how to obtain an account
 
## you must install python package `errormator_client` to make it work
 

	
 
## errormator enabled
 
errormator = false
 

	
 
errormator.server_url = https://api.errormator.com
 
errormator.api_key = YOUR_API_KEY
 

	
 
## TWEAK AMOUNT OF INFO SENT HERE
 

	
 
## enables 404 error logging (default False)
 
errormator.report_404 = false
 

	
 
## time in seconds after request is considered being slow (default 1)
 
errormator.slow_request_time = 1
 

	
 
## record slow requests in application
 
## (needs to be enabled for slow datastore recording and time tracking)
 
errormator.slow_requests = true
 

	
 
## enable hooking to application loggers
 
# errormator.logging = true
 

	
 
## minimum log level for log capture
 
# errormator.logging.level = WARNING
 

	
 
## send logs only from erroneous/slow requests
 
## (saves API quota for intensive logging)
 
errormator.logging_on_error = false
 

	
 
## list of additonal keywords that should be grabbed from environ object
 
## can be string with comma separated list of words in lowercase
 
## (by default client will always send following info:
 
## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
 
## start with HTTP* this list be extended with additional keywords here
 
errormator.environ_keys_whitelist =
 

	
 

	
 
## list of keywords that should be blanked from request object
 
## can be string with comma separated list of words in lowercase
 
## (by default client will always blank keys that contain following words
 
## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
 
## this list be extended with additional keywords set here
 
errormator.request_keys_blacklist =
 

	
 

	
 
## list of namespaces that should be ignores when gathering log entries
 
## can be string with comma separated list of namespaces
 
## (by default the client ignores own entries: errormator_client.client)
 
errormator.log_namespace_blacklist =
 

	
 

	
 
################
 
### [sentry] ###
 
################
 

	
 
## sentry is a alternative open source error aggregator
 
## you must install python packages `sentry` and `raven` to enable
 

	
 
sentry.dsn = YOUR_DNS
 
sentry.servers =
 
sentry.name =
 
sentry.key =
 
sentry.public_key =
 
sentry.secret_key =
 
sentry.project =
 
sentry.site =
 
sentry.include_paths =
 
sentry.exclude_paths =
 

	
 

	
 
################################################################################
 
## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*  ##
 
## Debug mode will enable the interactive debugging tool, allowing ANYONE to  ##
 
## execute malicious code after an exception is raised.                       ##
 
################################################################################
 
#set debug = false
 
set debug = true
 

	
 
##################################
 
###       LOGVIEW CONFIG       ###
 
##################################
 

	
 
logview.sqlalchemy = #faa
 
logview.pylons.templating = #bfb
 
logview.pylons.util = #eee
 

	
 
#########################################################
 
### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG    ###
 
#########################################################
 

	
 
# SQLITE [default]
 
sqlalchemy.db1.url = sqlite:///%(here)s/kallithea.db?timeout=60
 

	
 
# POSTGRESQL
 
# sqlalchemy.db1.url = postgresql://user:pass@localhost/kallithea
 

	
 
# MySQL
 
# sqlalchemy.db1.url = mysql://user:pass@localhost/kallithea
 

	
 
# see sqlalchemy docs for others
 

	
 
sqlalchemy.db1.echo = false
 
sqlalchemy.db1.pool_recycle = 3600
 
sqlalchemy.db1.convert_unicode = true
 

	
 
################################
 
### LOGGING CONFIGURATION   ####
 
################################
 

	
 
[loggers]
 
keys = root, routes, kallithea, sqlalchemy, beaker, templates, whoosh_indexer
 

	
 
[handlers]
 
keys = console, console_sql
 

	
 
[formatters]
 
keys = generic, color_formatter, color_formatter_sql
 

	
 
#############
 
## LOGGERS ##
 
#############
 

	
 
[logger_root]
 
level = NOTSET
 
handlers = console
 

	
 
[logger_routes]
 
level = DEBUG
 
handlers =
 
qualname = routes.middleware
 
## "level = DEBUG" logs the route matched and routing variables.
 
propagate = 1
 

	
 
[logger_beaker]
 
level = DEBUG
 
handlers =
 
qualname = beaker.container
 
propagate = 1
 

	
 
[logger_templates]
 
level = INFO
 
handlers =
 
qualname = pylons.templating
 
propagate = 1
 

	
 
[logger_kallithea]
 
level = DEBUG
 
handlers =
 
qualname = kallithea
 
propagate = 1
 

	
 
[logger_sqlalchemy]
 
level = INFO
 
handlers = console_sql
 
qualname = sqlalchemy.engine
 
propagate = 0
 

	
 
[logger_whoosh_indexer]
 
level = DEBUG
 
handlers =
 
qualname = whoosh_indexer
 
propagate = 1
 

	
 
##############
 
## HANDLERS ##
 
##############
 

	
 
[handler_console]
 
class = StreamHandler
 
args = (sys.stderr,)
 
#level = INFO
 
#formatter = generic
 
level = DEBUG
 
formatter = color_formatter
 

	
 
[handler_console_sql]
 
class = StreamHandler
 
args = (sys.stderr,)
 
#level = WARN
 
#formatter = generic
 
level = DEBUG
 
formatter = color_formatter_sql
 

	
 
################
 
## FORMATTERS ##
 
################
 

	
 
[formatter_generic]
 
format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
 

	
 
[formatter_color_formatter]
 
class=kallithea.lib.colored_formatter.ColorFormatter
 
format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
 

	
 
[formatter_color_formatter_sql]
 
class=kallithea.lib.colored_formatter.ColorFormatterSql
 
format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
kallithea/bin/template.ini.mako
Show inline comments
 
## -*- coding: utf-8 -*-
 
<%text>################################################################################
 
################################################################################
 
# Kallithea - config file generated with kallithea-config                      #
 
################################################################################
 
################################################################################
 
</%text>
 
[DEFAULT]
 
debug = true
 
pdebug = false
 
<%text>
 
################################################################################
 
## Uncomment and replace with the address which should receive                ##
 
## any error reports after application crash                                  ##
 
## Additionally those settings will be used by Kallithea mailing system       ##
 
################################################################################</%text>
 
#email_to = admin@localhost
 
#error_email_from = paste_error@localhost
 
#app_email_from = kallithea-noreply@localhost
 
#error_message =
 
#email_prefix = [Kallithea]
 

	
 
#smtp_server = mail.server.com
 
#smtp_username =
 
#smtp_password =
 
#smtp_port =
 
#smtp_use_tls = false
 
#smtp_use_ssl = true
 
<%text>## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)</%text>
 
#smtp_auth =
 

	
 
[server:main]
 
%if http_server == 'paste':
 
<%text>## PASTE ##</%text>
 
use = egg:Paste#http
 
<%text>## nr of worker threads to spawn</%text>
 
threadpool_workers = 5
 
<%text>## max request before thread respawn</%text>
 
threadpool_max_requests = 10
 
<%text>## option to use threads of process</%text>
 
use_threadpool = true
 
%endif
 
%if http_server == 'waitress':
 
<%text>## WAITRESS ##</%text>
 
use = egg:waitress#main
 
<%text>## number of worker threads</%text>
 
threads = 5
 
<%text>## MAX BODY SIZE 100GB</%text>
 
max_request_body_size = 107374182400
 
<%text>## use poll instead of select, fixes fd limits, may not work on old</%text>
 
<%text>## windows systems.</%text>
 
#asyncore_use_poll = True
 
%endif
 
%if http_server == 'gunicorn':
 
<%text>## GUNICORN ##</%text>
 
use = egg:gunicorn#main
 
<%text>## number of process workers. You must set `instance_id = *` when this option</%text>
 
<%text>## is set to more than one worker</%text>
 
workers = 1
 
<%text>## process name</%text>
 
proc_name = kallithea
 
<%text>## type of worker class, one of sync, eventlet, gevent, tornado</%text>
 
<%text>## recommended for bigger setup is using of of other than sync one</%text>
 
worker_class = sync
 
max_requests = 1000
 
<%text>## ammount of time a worker can handle request before it gets killed and</%text>
 
<%text>## restarted</%text>
 
timeout = 3600
 
%endif
 
%if http_server == 'uwsgi':
 
<%text>## UWSGI ##</%text>
 
<%text>## run with uwsgi --ini-paste-logged <inifile.ini></%text>
 
[uwsgi]
 
socket = /tmp/uwsgi.sock
 
master = true
 
http = 127.0.0.1:5000
 

	
 
<%text>## set as deamon and redirect all output to file</%text>
 
#daemonize = ./uwsgi_kallithea.log
 

	
 
<%text>## master process PID</%text>
 
pidfile = ./uwsgi_kallithea.pid
 

	
 
<%text>## stats server with workers statistics, use uwsgitop</%text>
 
<%text>## for monitoring, `uwsgitop 127.0.0.1:1717`</%text>
 
stats = 127.0.0.1:1717
 
memory-report = true
 

	
 
<%text>## log 5XX errors</%text>
 
log-5xx = true
 

	
 
<%text>## Set the socket listen queue size.</%text>
 
listen = 256
 

	
 
<%text>## Gracefully Reload workers after the specified amount of managed requests</%text>
 
<%text>## (avoid memory leaks).</%text>
 
max-requests = 1000
 

	
 
<%text>## enable large buffers</%text>
 
buffer-size=65535
 

	
 
<%text>## socket and http timeouts ##</%text>
 
http-timeout=3600
 
socket-timeout=3600
 

	
 
<%text>## Log requests slower than the specified number of milliseconds.</%text>
 
log-slow = 10
 

	
 
<%text>## Exit if no app can be loaded.</%text>
 
need-app = true
 

	
 
<%text>## Set lazy mode (load apps in workers instead of master).</%text>
 
lazy = true
 

	
 
<%text>## scaling ##</%text>
 
<%text>## set cheaper algorithm to use, if not set default will be used</%text>
 
cheaper-algo = spare
 

	
 
<%text>## minimum number of workers to keep at all times</%text>
 
cheaper = 1
 

	
 
<%text>## number of workers to spawn at startup</%text>
 
cheaper-initial = 1
 

	
 
<%text>## maximum number of workers that can be spawned</%text>
 
workers = 4
 

	
 
<%text>## how many workers should be spawned at a time</%text>
 
cheaper-step = 1
 
%endif
 
<%text>## COMMON ##</%text>
 
host = ${host}
 
port = ${port}
 

	
 
<%text>## prefix middleware for rc</%text>
 
#[filter:proxy-prefix]
 
#use = egg:PasteDeploy#prefix
 
#prefix = /<your-prefix>
 

	
 
[app:main]
 
use = egg:kallithea
 
<%text>## enable proxy prefix middleware</%text>
 
#filter-with = proxy-prefix
 

	
 
full_stack = true
 
static_files = true
 
<%text>## Available Languages:</%text>
 
<%text>## de en fr ja pl pt_BR ru zh_CN zh_TW</%text>
 
lang = ${lang}
 
cache_dir = ${here}/data
 
index_dir = ${here}/data/index
 

	
 
<%text>## perform a full repository scan on each server start, this should be</%text>
 
<%text>## set to false after first startup, to allow faster server restarts.</%text>
 
initial_repo_scan = false
 

	
 
<%text>## uncomment and set this path to use archive download cache</%text>
 
archive_cache_dir = ${here}/tarballcache
 

	
 
<%text>## change this to unique ID for security</%text>
 
app_instance_uuid = ${uuid()}
 

	
 
<%text>## cut off limit for large diffs (size in bytes)</%text>
 
cut_off_limit = 256000
 

	
 
<%text>## use cache version of scm repo everywhere</%text>
 
vcs_full_cache = true
 

	
 
<%text>## force https in Kallithea, fixes https redirects, assumes it's always https</%text>
 
force_https = false
 

	
 
<%text>## use Strict-Transport-Security headers</%text>
 
use_htsts = false
 

	
 
<%text>## number of commits stats will parse on each iteration</%text>
 
commit_parse_limit = 25
 

	
 
<%text>## path to git executable</%text>
 
git_path = git
 

	
 
<%text>## git rev filter option, --all is the default filter, if you need to</%text>
 
<%text>## hide all refs in changelog switch this to --branches --tags</%text>
 
#git_rev_filter = --branches --tags
 

	
 
<%text>## RSS feed options</%text>
 
rss_cut_off_limit = 256000
 
rss_items_per_page = 10
 
rss_include_diff = false
 

	
 
<%text>## options for showing and identifying changesets</%text>
 
show_sha_length = 12
 
show_revision_number = true
 

	
 
<%text>## gist URL alias, used to create nicer urls for gist. This should be an</%text>
 
<%text>## url that does rewrites to _admin/gists/<gistid>.</%text>
 
<%text>## example: http://gist.kallithea.server/{gistid}. Empty means use the internal</%text>
 
<%text>## Kallithea url, ie. http[s]://your.kallithea.server/_admin/gists/<gistid></%text>
 
gist_alias_url =
 

	
 
<%text>## white list of API enabled controllers. This allows to add list of</%text>
 
<%text>## controllers to which access will be enabled by api_key. eg: to enable</%text>
 
<%text>## api access to raw_files put `FilesController:raw`, to enable access to patches</%text>
 
<%text>## add `ChangesetController:changeset_patch`. This list should be "," separated</%text>
 
<%text>## Syntax is <ControllerClass>:<function>. Check debug logs for generated names</%text>
 
<%text>## Recommended settings below are commented out:</%text>
 
api_access_controllers_whitelist =
 
#    ChangesetController:changeset_patch,
 
#    ChangesetController:changeset_raw,
 
#    FilesController:raw,
 
#    FilesController:archivefile
 

	
 
<%text>## default encoding used to convert from and to unicode</%text>
 
<%text>## can be also a comma seperated list of encoding in case of mixed encodings</%text>
 
default_encoding = utf8
 

	
 
<%text>## issue tracker for Kallithea (leave blank to disable, absent for default)</%text>
 
#bugtracker = https://bitbucket.org/conservancy/kallithea/issues
 

	
 
<%text>## issue tracking mapping for commits messages</%text>
 
<%text>## comment out issue_pat, issue_server, issue_prefix to enable</%text>
 

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

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

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

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

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

	
 
issue_prefix = #
 

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

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

	
 

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

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

	
 
<%text>## locking return code. When repository is locked return this HTTP code. 2XX</%text>
 
<%text>## codes don't break the transactions while 4XX codes do</%text>
 
lock_ret_code = 423
 

	
 
<%text>## allows to change the repository location in settings page</%text>
 
allow_repo_location_change = True
 

	
 
<%text>## allows to setup custom hooks in settings page</%text>
 
allow_custom_hooks_settings = True
 

	
 
<%text>
 
####################################
 
###        CELERY CONFIG        ####
 
####################################
 
</%text>
 
use_celery = false
 
broker.host = localhost
 
broker.vhost = rabbitmqhost
 
broker.port = 5672
 
broker.user = rabbitmq
 
broker.password = qweqwe
 

	
 
celery.imports = kallithea.lib.celerylib.tasks
 

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

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

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

	
 
<%text>## tasks will never be sent to the queue, but executed locally instead.</%text>
 
celery.always.eager = false
 
<%text>
 
####################################
 
###         BEAKER CACHE        ####
 
####################################
 
</%text>
 
beaker.cache.data_dir=${here}/data/cache/data
 
beaker.cache.lock_dir=${here}/data/cache/lock
 

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

	
 
beaker.cache.super_short_term.type=memory
 
beaker.cache.super_short_term.expire=10
 
beaker.cache.super_short_term.key_length = 256
 

	
 
beaker.cache.short_term.type=memory
 
beaker.cache.short_term.expire=60
 
beaker.cache.short_term.key_length = 256
 

	
 
beaker.cache.long_term.type=memory
 
beaker.cache.long_term.expire=36000
 
beaker.cache.long_term.key_length = 256
 

	
 
beaker.cache.sql_cache_short.type=memory
 
beaker.cache.sql_cache_short.expire=10
 
beaker.cache.sql_cache_short.key_length = 256
 

	
 
beaker.cache.sql_cache_med.type=memory
 
beaker.cache.sql_cache_med.expire=360
 
beaker.cache.sql_cache_med.key_length = 256
 

	
 
beaker.cache.sql_cache_long.type=file
 
beaker.cache.sql_cache_long.expire=3600
 
beaker.cache.sql_cache_long.key_length = 256
 
<%text>
 
####################################
 
###       BEAKER SESSION        ####
 
####################################
 
## Type of storage used for the session, current types are
 
## dbm, file, memcached, database, and memory.
 
## The storage uses the Container API
 
## that is also used by the cache system.
 
</%text>
 
<%text>## db session ##</%text>
 
#beaker.session.type = ext:database
 
#beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
 
#beaker.session.table_name = db_session
 

	
 
<%text>## encrypted cookie client side session, good for many instances ##</%text>
 
#beaker.session.type = cookie
 

	
 
<%text>## file based cookies (default) ##</%text>
 
#beaker.session.type = file
 

	
 
beaker.session.key = kallithea
 
beaker.session.secret = ${uuid()}
 

	
 
<%text>## Secure encrypted cookie. Requires AES and AES python libraries</%text>
 
<%text>## you must disable beaker.session.secret to use this</%text>
 
#beaker.session.encrypt_key = <key_for_encryption>
 
#beaker.session.validate_key = <validation_key>
 

	
 
<%text>## sets session as invalid if it haven't been accessed for given amount of time</%text>
 
beaker.session.timeout = 2592000
 
beaker.session.httponly = true
 
#beaker.session.cookie_path = /<your-prefix>
 

	
 
<%text>## uncomment for https secure cookie</%text>
 
beaker.session.secure = false
 

	
 
<%text>## auto save the session to not to use .save()</%text>
 
beaker.session.auto = False
 

	
 
<%text>## default cookie expiration time in seconds `true` expire at browser close ##</%text>
 
#beaker.session.cookie_expires = 3600
 

	
 
%if error_aggregation_service == 'errormator':
 
<%text>
 
############################
 
## ERROR HANDLING SYSTEMS ##
 
############################
 

	
 
####################
 
### [errormator] ###
 
####################
 

	
 
## Errormator is tailored to work with Kallithea, see
 
## http://errormator.com for details how to obtain an account
 
## you must install python package `errormator_client` to make it work
 
</%text>
 
<%text>## errormator enabled</%text>
 
errormator = false
 

	
 
errormator.server_url = https://api.errormator.com
 
errormator.api_key = YOUR_API_KEY
 

	
 
<%text>## TWEAK AMOUNT OF INFO SENT HERE</%text>
 

	
 
<%text>## enables 404 error logging (default False)</%text>
 
errormator.report_404 = false
 

	
 
<%text>## time in seconds after request is considered being slow (default 1)</%text>
 
errormator.slow_request_time = 1
 

	
 
<%text>## record slow requests in application</%text>
 
<%text>## (needs to be enabled for slow datastore recording and time tracking)</%text>
 
errormator.slow_requests = true
 

	
 
<%text>## enable hooking to application loggers</%text>
 
# errormator.logging = true
 

	
 
<%text>## minimum log level for log capture</%text>
 
# errormator.logging.level = WARNING
 

	
 
<%text>## send logs only from erroneous/slow requests</%text>
 
<%text>## (saves API quota for intensive logging)</%text>
 
errormator.logging_on_error = false
 

	
 
<%text>## list of additonal keywords that should be grabbed from environ object</%text>
 
<%text>## can be string with comma separated list of words in lowercase</%text>
 
<%text>## (by default client will always send following info:</%text>
 
<%text>## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that</%text>
 
<%text>## start with HTTP* this list be extended with additional keywords here</%text>
 
errormator.environ_keys_whitelist =
 

	
 

	
 
<%text>## list of keywords that should be blanked from request object</%text>
 
<%text>## can be string with comma separated list of words in lowercase</%text>
 
<%text>## (by default client will always blank keys that contain following words</%text>
 
<%text>## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'</%text>
 
<%text>## this list be extended with additional keywords set here</%text>
 
errormator.request_keys_blacklist =
 

	
 

	
 
<%text>## list of namespaces that should be ignores when gathering log entries</%text>
 
<%text>## can be string with comma separated list of namespaces</%text>
 
<%text>## (by default the client ignores own entries: errormator_client.client)</%text>
 
errormator.log_namespace_blacklist =
 
%elif error_aggregation_service == 'sentry':
 
<%text>
 
################
 
### [sentry] ###
 
################
 

	
 
## sentry is a alternative open source error aggregator
 
## you must install python packages `sentry` and `raven` to enable
 
</%text>
 
sentry.dsn = YOUR_DNS
 
sentry.servers =
 
sentry.name =
 
sentry.key =
 
sentry.public_key =
 
sentry.secret_key =
 
sentry.project =
 
sentry.site =
 
sentry.include_paths =
 
sentry.exclude_paths =
 
%endif
 
<%text>
 
################################################################################
 
## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*  ##
 
## Debug mode will enable the interactive debugging tool, allowing ANYONE to  ##
 
## execute malicious code after an exception is raised.                       ##
 
################################################################################</%text>
 
set debug = false
 
<%text>
 
##################################
 
###       LOGVIEW CONFIG       ###
 
##################################
 
</%text>
 
logview.sqlalchemy = #faa
 
logview.pylons.templating = #bfb
 
logview.pylons.util = #eee
 
<%text>
 
#########################################################
 
### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG    ###
 
#########################################################
 
</%text>
 
%if database_engine == 'sqlite':
 
# SQLITE [default]
 
sqlalchemy.db1.url = sqlite:///${here}/kallithea.db?timeout=60
 
%elif database_engine == 'postgres':
 
# POSTGRESQL
 
sqlalchemy.db1.url = postgresql://user:pass@localhost/kallithea
 
%elif database_engine == 'mysql':
 
# MySQL
 
sqlalchemy.db1.url = mysql://user:pass@localhost/kallithea
 
%endif
 
# see sqlalchemy docs for others
 

	
 
sqlalchemy.db1.echo = false
 
sqlalchemy.db1.pool_recycle = 3600
 
sqlalchemy.db1.convert_unicode = true
 
<%text>
 
################################
 
### LOGGING CONFIGURATION   ####
 
################################
 
</%text>
 
[loggers]
 
keys = root, routes, kallithea, sqlalchemy, beaker, templates, whoosh_indexer
 

	
 
[handlers]
 
keys = console, console_sql
 

	
 
[formatters]
 
keys = generic, color_formatter, color_formatter_sql
 
<%text>
 
#############
 
## LOGGERS ##
 
#############
 
</%text>
 
[logger_root]
 
level = NOTSET
 
handlers = console
 

	
 
[logger_routes]
 
level = DEBUG
 
handlers =
 
qualname = routes.middleware
 
<%text>## "level = DEBUG" logs the route matched and routing variables.</%text>
 
propagate = 1
 

	
 
[logger_beaker]
 
level = DEBUG
 
handlers =
 
qualname = beaker.container
 
propagate = 1
 

	
 
[logger_templates]
 
level = INFO
 
handlers =
 
qualname = pylons.templating
 
propagate = 1
 

	
 
[logger_kallithea]
 
level = DEBUG
 
handlers =
 
qualname = kallithea
 
propagate = 1
 

	
 
[logger_sqlalchemy]
 
level = INFO
 
handlers = console_sql
 
qualname = sqlalchemy.engine
 
propagate = 0
 

	
 
[logger_whoosh_indexer]
 
level = DEBUG
 
handlers =
 
qualname = whoosh_indexer
 
propagate = 1
 
<%text>
 
##############
 
## HANDLERS ##
 
##############
 
</%text>
 
[handler_console]
 
class = StreamHandler
 
args = (sys.stderr,)
 
level = INFO
 
formatter = generic
 

	
 
[handler_console_sql]
 
class = StreamHandler
 
args = (sys.stderr,)
 
level = WARN
 
formatter = generic
 
<%text>
 
################
 
## FORMATTERS ##
 
################
 
</%text>
 
[formatter_generic]
 
format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
 

	
 
[formatter_color_formatter]
 
class=kallithea.lib.colored_formatter.ColorFormatter
 
format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
 

	
 
[formatter_color_formatter_sql]
 
class=kallithea.lib.colored_formatter.ColorFormatterSql
 
format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
kallithea/config/deployment.ini_tmpl
Show inline comments
 
################################################################################
 
################################################################################
 
# Kallithea - Example config                                                   #
 
#                                                                              #
 
# The %(here)s variable will be replaced with the parent directory of this file#
 
################################################################################
 
################################################################################
 

	
 
[DEFAULT]
 
debug = true
 
pdebug = false
 

	
 
################################################################################
 
## Uncomment and replace with the address which should receive                ##
 
## any error reports after application crash                                  ##
 
## Additionally those settings will be used by Kallithea mailing system       ##
 
################################################################################
 
#email_to = admin@localhost
 
#error_email_from = paste_error@localhost
 
#app_email_from = kallithea-noreply@localhost
 
#error_message =
 
#email_prefix = [Kallithea]
 

	
 
#smtp_server = mail.server.com
 
#smtp_username =
 
#smtp_password =
 
#smtp_port =
 
#smtp_use_tls = false
 
#smtp_use_ssl = true
 
## Specify available auth parameters here (e.g. LOGIN PLAIN CRAM-MD5, etc.)
 
#smtp_auth =
 

	
 
[server:main]
 
## PASTE ##
 
#use = egg:Paste#http
 
## nr of worker threads to spawn
 
#threadpool_workers = 5
 
## max request before thread respawn
 
#threadpool_max_requests = 10
 
## option to use threads of process
 
#use_threadpool = true
 

	
 
## WAITRESS ##
 
use = egg:waitress#main
 
## number of worker threads
 
threads = 5
 
## MAX BODY SIZE 100GB
 
max_request_body_size = 107374182400
 
## use poll instead of select, fixes fd limits, may not work on old
 
## windows systems.
 
#asyncore_use_poll = True
 

	
 
## GUNICORN ##
 
#use = egg:gunicorn#main
 
## number of process workers. You must set `instance_id = *` when this option
 
## is set to more than one worker
 
#workers = 1
 
## process name
 
#proc_name = kallithea
 
## type of worker class, one of sync, eventlet, gevent, tornado
 
## recommended for bigger setup is using of of other than sync one
 
#worker_class = sync
 
#max_requests = 1000
 
## ammount of time a worker can handle request before it gets killed and
 
## restarted
 
#timeout = 3600
 

	
 
## UWSGI ##
 
## run with uwsgi --ini-paste-logged <inifile.ini>
 
#[uwsgi]
 
#socket = /tmp/uwsgi.sock
 
#master = true
 
#http = 127.0.0.1:5000
 

	
 
## set as deamon and redirect all output to file
 
#daemonize = ./uwsgi_kallithea.log
 

	
 
## master process PID
 
#pidfile = ./uwsgi_kallithea.pid
 

	
 
## stats server with workers statistics, use uwsgitop
 
## for monitoring, `uwsgitop 127.0.0.1:1717`
 
#stats = 127.0.0.1:1717
 
#memory-report = true
 

	
 
## log 5XX errors
 
#log-5xx = true
 

	
 
## Set the socket listen queue size.
 
#listen = 256
 

	
 
## Gracefully Reload workers after the specified amount of managed requests
 
## (avoid memory leaks).
 
#max-requests = 1000
 

	
 
## enable large buffers
 
#buffer-size=65535
 

	
 
## socket and http timeouts ##
 
#http-timeout=3600
 
#socket-timeout=3600
 

	
 
## Log requests slower than the specified number of milliseconds.
 
#log-slow = 10
 

	
 
## Exit if no app can be loaded.
 
#need-app = true
 

	
 
## Set lazy mode (load apps in workers instead of master).
 
#lazy = true
 

	
 
## scaling ##
 
## set cheaper algorithm to use, if not set default will be used
 
#cheaper-algo = spare
 

	
 
## minimum number of workers to keep at all times
 
#cheaper = 1
 

	
 
## number of workers to spawn at startup
 
#cheaper-initial = 1
 

	
 
## maximum number of workers that can be spawned
 
#workers = 4
 

	
 
## how many workers should be spawned at a time
 
#cheaper-step = 1
 

	
 
## COMMON ##
 
host = 127.0.0.1
 
port = 5000
 

	
 
## prefix middleware for rc
 
#[filter:proxy-prefix]
 
#use = egg:PasteDeploy#prefix
 
#prefix = /<your-prefix>
 

	
 
[app:main]
 
use = egg:kallithea
 
## enable proxy prefix middleware
 
#filter-with = proxy-prefix
 

	
 
full_stack = true
 
static_files = true
 
## Available Languages:
 
## de en fr ja pl pt_BR ru zh_CN zh_TW
 
lang = en
 
cache_dir = %(here)s/data
 
index_dir = %(here)s/data/index
 

	
 
## perform a full repository scan on each server start, this should be
 
## set to false after first startup, to allow faster server restarts.
 
initial_repo_scan = false
 

	
 
## uncomment and set this path to use archive download cache
 
archive_cache_dir = %(here)s/tarballcache
 

	
 
## change this to unique ID for security
 
app_instance_uuid = ${app_instance_uuid}
 

	
 
## cut off limit for large diffs (size in bytes)
 
cut_off_limit = 256000
 

	
 
## use cache version of scm repo everywhere
 
vcs_full_cache = true
 

	
 
## force https in Kallithea, fixes https redirects, assumes it's always https
 
force_https = false
 

	
 
## use Strict-Transport-Security headers
 
use_htsts = false
 

	
 
## number of commits stats will parse on each iteration
 
commit_parse_limit = 25
 

	
 
## path to git executable
 
git_path = git
 

	
 
## git rev filter option, --all is the default filter, if you need to
 
## hide all refs in changelog switch this to --branches --tags
 
#git_rev_filter = --branches --tags
 

	
 
## RSS feed options
 
rss_cut_off_limit = 256000
 
rss_items_per_page = 10
 
rss_include_diff = false
 

	
 
## options for showing and identifying changesets
 
show_sha_length = 12
 
show_revision_number = true
 

	
 
## gist URL alias, used to create nicer urls for gist. This should be an
 
## url that does rewrites to _admin/gists/<gistid>.
 
## example: http://gist.kallithea.server/{gistid}. Empty means use the internal
 
## Kallithea url, ie. http[s]://your.kallithea.server/_admin/gists/<gistid>
 
gist_alias_url =
 

	
 
## white list of API enabled controllers. This allows to add list of
 
## controllers to which access will be enabled by api_key. eg: to enable
 
## api access to raw_files put `FilesController:raw`, to enable access to patches
 
## add `ChangesetController:changeset_patch`. This list should be "," separated
 
## Syntax is <ControllerClass>:<function>. Check debug logs for generated names
 
## Recommended settings below are commented out:
 
api_access_controllers_whitelist =
 
#    ChangesetController:changeset_patch,
 
#    ChangesetController:changeset_raw,
 
#    FilesController:raw,
 
#    FilesController:archivefile
 

	
 
## default encoding used to convert from and to unicode
 
## can be also a comma seperated list of encoding in case of mixed encodings
 
default_encoding = utf8
 

	
 
## issue tracker for Kallithea (leave blank to disable, absent for default)
 
#bugtracker = https://bitbucket.org/conservancy/kallithea/issues
 

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

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

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

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

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

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

	
 
issue_prefix = #
 

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

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

	
 

	
 
## instance-id prefix
 
## a prefix key for this instance used for cache invalidation when running
 
## multiple instances of kallithea, make sure it's globally unique for
 
## all running kallithea 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
 
## response is 401 HTTPUnauthorized. Currently Mercurial clients have trouble with
 
## handling that. Set this variable to 403 to return HTTPForbidden
 
auth_ret_code =
 

	
 
## locking return code. When repository is locked return this HTTP code. 2XX
 
## codes don't break the transactions while 4XX codes do
 
lock_ret_code = 423
 

	
 
## allows to change the repository location in settings page
 
allow_repo_location_change = True
 

	
 
## allows to setup custom hooks in settings page
 
allow_custom_hooks_settings = True
 

	
 

	
 
####################################
 
###        CELERY CONFIG        ####
 
####################################
 

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

	
 
celery.imports = kallithea.lib.celerylib.tasks
 

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

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

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

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

	
 
####################################
 
###         BEAKER CACHE        ####
 
####################################
 

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

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

	
 
beaker.cache.super_short_term.type=memory
 
beaker.cache.super_short_term.expire=10
 
beaker.cache.super_short_term.key_length = 256
 

	
 
beaker.cache.short_term.type=memory
 
beaker.cache.short_term.expire=60
 
beaker.cache.short_term.key_length = 256
 

	
 
beaker.cache.long_term.type=memory
 
beaker.cache.long_term.expire=36000
 
beaker.cache.long_term.key_length = 256
 

	
 
beaker.cache.sql_cache_short.type=memory
 
beaker.cache.sql_cache_short.expire=10
 
beaker.cache.sql_cache_short.key_length = 256
 

	
 
beaker.cache.sql_cache_med.type=memory
 
beaker.cache.sql_cache_med.expire=360
 
beaker.cache.sql_cache_med.key_length = 256
 

	
 
beaker.cache.sql_cache_long.type=file
 
beaker.cache.sql_cache_long.expire=3600
 
beaker.cache.sql_cache_long.key_length = 256
 

	
 
####################################
 
###       BEAKER SESSION        ####
 
####################################
 
## Type of storage used for the session, current types are
 
## dbm, file, memcached, database, and memory.
 
## The storage uses the Container API
 
## that is also used by the cache system.
 

	
 
## db session ##
 
#beaker.session.type = ext:database
 
#beaker.session.sa.url = postgresql://postgres:qwe@localhost/kallithea
 
#beaker.session.table_name = db_session
 

	
 
## encrypted cookie client side session, good for many instances ##
 
#beaker.session.type = cookie
 

	
 
## file based cookies (default) ##
 
#beaker.session.type = file
 

	
 
beaker.session.key = kallithea
 
beaker.session.secret = ${app_instance_uuid}
 

	
 
## Secure encrypted cookie. Requires AES and AES python libraries
 
## you must disable beaker.session.secret to use this
 
#beaker.session.encrypt_key = <key_for_encryption>
 
#beaker.session.validate_key = <validation_key>
 

	
 
## sets session as invalid if it haven't been accessed for given amount of time
 
beaker.session.timeout = 2592000
 
beaker.session.httponly = true
 
#beaker.session.cookie_path = /<your-prefix>
 

	
 
## uncomment for https secure cookie
 
beaker.session.secure = false
 

	
 
## auto save the session to not to use .save()
 
beaker.session.auto = False
 

	
 
## default cookie expiration time in seconds `true` expire at browser close ##
 
#beaker.session.cookie_expires = 3600
 

	
 

	
 
############################
 
## ERROR HANDLING SYSTEMS ##
 
############################
 

	
 
####################
 
### [errormator] ###
 
####################
 

	
 
## Errormator is tailored to work with Kallithea, see
 
## http://errormator.com for details how to obtain an account
 
## you must install python package `errormator_client` to make it work
 

	
 
## errormator enabled
 
errormator = false
 

	
 
errormator.server_url = https://api.errormator.com
 
errormator.api_key = YOUR_API_KEY
 

	
 
## TWEAK AMOUNT OF INFO SENT HERE
 

	
 
## enables 404 error logging (default False)
 
errormator.report_404 = false
 

	
 
## time in seconds after request is considered being slow (default 1)
 
errormator.slow_request_time = 1
 

	
 
## record slow requests in application
 
## (needs to be enabled for slow datastore recording and time tracking)
 
errormator.slow_requests = true
 

	
 
## enable hooking to application loggers
 
# errormator.logging = true
 

	
 
## minimum log level for log capture
 
# errormator.logging.level = WARNING
 

	
 
## send logs only from erroneous/slow requests
 
## (saves API quota for intensive logging)
 
errormator.logging_on_error = false
 

	
 
## list of additonal keywords that should be grabbed from environ object
 
## can be string with comma separated list of words in lowercase
 
## (by default client will always send following info:
 
## 'REMOTE_USER', 'REMOTE_ADDR', 'SERVER_NAME', 'CONTENT_TYPE' + all keys that
 
## start with HTTP* this list be extended with additional keywords here
 
errormator.environ_keys_whitelist =
 

	
 

	
 
## list of keywords that should be blanked from request object
 
## can be string with comma separated list of words in lowercase
 
## (by default client will always blank keys that contain following words
 
## 'password', 'passwd', 'pwd', 'auth_tkt', 'secret', 'csrf'
 
## this list be extended with additional keywords set here
 
errormator.request_keys_blacklist =
 

	
 

	
 
## list of namespaces that should be ignores when gathering log entries
 
## can be string with comma separated list of namespaces
 
## (by default the client ignores own entries: errormator_client.client)
 
errormator.log_namespace_blacklist =
 

	
 

	
 
################
 
### [sentry] ###
 
################
 

	
 
## sentry is a alternative open source error aggregator
 
## you must install python packages `sentry` and `raven` to enable
 

	
 
sentry.dsn = YOUR_DNS
 
sentry.servers =
 
sentry.name =
 
sentry.key =
 
sentry.public_key =
 
sentry.secret_key =
 
sentry.project =
 
sentry.site =
 
sentry.include_paths =
 
sentry.exclude_paths =
 

	
 

	
 
################################################################################
 
## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT*  ##
 
## Debug mode will enable the interactive debugging tool, allowing ANYONE to  ##
 
## execute malicious code after an exception is raised.                       ##
 
################################################################################
 
set debug = false
 

	
 
##################################
 
###       LOGVIEW CONFIG       ###
 
##################################
 

	
 
logview.sqlalchemy = #faa
 
logview.pylons.templating = #bfb
 
logview.pylons.util = #eee
 

	
 
#########################################################
 
### DB CONFIGS - EACH DB WILL HAVE IT'S OWN CONFIG    ###
 
#########################################################
 

	
 
# SQLITE [default]
 
sqlalchemy.db1.url = sqlite:///%(here)s/kallithea.db?timeout=60
 

	
 
# POSTGRESQL
 
# sqlalchemy.db1.url = postgresql://user:pass@localhost/kallithea
 

	
 
# MySQL
 
# sqlalchemy.db1.url = mysql://user:pass@localhost/kallithea
 

	
 
# see sqlalchemy docs for others
 

	
 
sqlalchemy.db1.echo = false
 
sqlalchemy.db1.pool_recycle = 3600
 
sqlalchemy.db1.convert_unicode = true
 

	
 
################################
 
### LOGGING CONFIGURATION   ####
 
################################
 

	
 
[loggers]
 
keys = root, routes, kallithea, sqlalchemy, beaker, templates, whoosh_indexer
 

	
 
[handlers]
 
keys = console, console_sql
 

	
 
[formatters]
 
keys = generic, color_formatter, color_formatter_sql
 

	
 
#############
 
## LOGGERS ##
 
#############
 

	
 
[logger_root]
 
level = NOTSET
 
handlers = console
 

	
 
[logger_routes]
 
level = DEBUG
 
handlers =
 
qualname = routes.middleware
 
## "level = DEBUG" logs the route matched and routing variables.
 
propagate = 1
 

	
 
[logger_beaker]
 
level = DEBUG
 
handlers =
 
qualname = beaker.container
 
propagate = 1
 

	
 
[logger_templates]
 
level = INFO
 
handlers =
 
qualname = pylons.templating
 
propagate = 1
 

	
 
[logger_kallithea]
 
level = DEBUG
 
handlers =
 
qualname = kallithea
 
propagate = 1
 

	
 
[logger_sqlalchemy]
 
level = INFO
 
handlers = console_sql
 
qualname = sqlalchemy.engine
 
propagate = 0
 

	
 
[logger_whoosh_indexer]
 
level = DEBUG
 
handlers =
 
qualname = whoosh_indexer
 
propagate = 1
 

	
 
##############
 
## HANDLERS ##
 
##############
 

	
 
[handler_console]
 
class = StreamHandler
 
args = (sys.stderr,)
 
level = INFO
 
formatter = generic
 

	
 
[handler_console_sql]
 
class = StreamHandler
 
args = (sys.stderr,)
 
level = WARN
 
formatter = generic
 

	
 
################
 
## FORMATTERS ##
 
################
 

	
 
[formatter_generic]
 
format = %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
 

	
 
[formatter_color_formatter]
 
class=kallithea.lib.colored_formatter.ColorFormatter
 
format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
 

	
 
[formatter_color_formatter_sql]
 
class=kallithea.lib.colored_formatter.ColorFormatterSql
 
format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
 
datefmt = %Y-%m-%d %H:%M:%S
kallithea/controllers/search.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.controllers.search
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

	
 
Search controller for Kallithea
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Aug 7, 2010
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import logging
 
import traceback
 
import urllib
 
from pylons.i18n.translation import _
 
from pylons import request, config, tmpl_context as c
 

	
 
from whoosh.index import open_dir, EmptyIndexError
 
from whoosh.qparser import QueryParser, QueryParserError
 
from whoosh.query import Phrase, Prefix
 
from webhelpers.util import update_params
 

	
 
from kallithea.lib.auth import LoginRequired
 
from kallithea.lib.base import BaseRepoController, render
 
from kallithea.lib.indexers import CHGSETS_SCHEMA, SCHEMA, CHGSET_IDX_NAME, \
 
    IDX_NAME, WhooshResultWrapper
 
from kallithea.model.repo import RepoModel
 
from kallithea.lib.utils2 import safe_str, safe_int
 
from kallithea.lib.helpers import Page
 

	
 
log = logging.getLogger(__name__)
 

	
 

	
 
class SearchController(BaseRepoController):
 

	
 
    def __before__(self):
 
        super(SearchController, self).__before__()
 

	
 
    @LoginRequired()
 
    def index(self, repo_name=None):
 
        c.repo_name = repo_name
 
        c.formated_results = []
 
        c.runtime = ''
 
        c.cur_query = request.GET.get('q', None)
 
        c.cur_type = request.GET.get('type', 'content')
 
        c.cur_search = search_type = {'content': 'content',
 
                                      'commit': 'message',
 
                                      'path': 'path',
 
                                      'repository': 'repository'
 
                                      }.get(c.cur_type, 'content')
 

	
 
        index_name = {
 
            'content': IDX_NAME,
 
            'commit': CHGSET_IDX_NAME,
 
            'path': IDX_NAME
 
        }.get(c.cur_type, IDX_NAME)
 

	
 
        schema_defn = {
 
            'content': SCHEMA,
 
            'commit': CHGSETS_SCHEMA,
 
            'path': SCHEMA
 
        }.get(c.cur_type, SCHEMA)
 

	
 
        log.debug('IDX: %s' % index_name)
 
        log.debug('SCHEMA: %s' % schema_defn)
 

	
 
        if c.cur_query:
 
            cur_query = c.cur_query.lower()
 
            log.debug(cur_query)
 

	
 
        if c.cur_query:
 
            p = safe_int(request.GET.get('page', 1), 1)
 
            highlight_items = set()
 
            try:
 
                idx = open_dir(config['app_conf']['index_dir'],
 
                               indexname=index_name)
 
                searcher = idx.searcher()
 

	
 
                qp = QueryParser(search_type, schema=schema_defn)
 
                if c.repo_name:
 
                    cur_query = u'repository:%s %s' % (c.repo_name, cur_query)
 
                try:
 
                    query = qp.parse(unicode(cur_query))
 
                    # extract words for highlight
 
                    if isinstance(query, Phrase):
 
                        highlight_items.update(query.words)
 
                    elif isinstance(query, Prefix):
 
                        highlight_items.add(query.text)
 
                    else:
 
                        for i in query.all_terms():
 
                            if i[0] in ['content', 'message']:
 
                                highlight_items.add(i[1])
 

	
 
                    matcher = query.matcher(searcher)
 

	
 
                    log.debug('query: %s' % query)
 
                    log.debug('hl terms: %s' % highlight_items)
 
                    results = searcher.search(query)
 
                    res_ln = len(results)
 
                    c.runtime = '%s results (%.3f seconds)' % (
 
                        res_ln, results.runtime
 
                    )
 

	
 
                    def url_generator(**kw):
 
                        q = urllib.quote(safe_str(c.cur_query))
 
                        return update_params("?q=%s&type=%s" \
 
                        % (q, safe_str(c.cur_type)), **kw)
 
                    repo_location = RepoModel().repos_path
 
                    c.formated_results = Page(
 
                        WhooshResultWrapper(search_type, searcher, matcher,
 
                                            highlight_items, repo_location),
 
                        page=p,
 
                        item_count=res_ln,
 
                        items_per_page=10,
 
                        url=url_generator
 
                    )
 

	
 
                except QueryParserError:
 
                    c.runtime = _('Invalid search query. Try quoting it.')
 
                searcher.close()
 
            except (EmptyIndexError, IOError):
 
                log.error(traceback.format_exc())
 
                log.error('Empty Index data')
 
                c.runtime = _('There is no index to search in. '
 
                              'Please run whoosh indexer')
 
            except (Exception):
 
                log.error(traceback.format_exc())
 
                c.runtime = _('An error occurred during this search operation')
 
                c.runtime = _('An error occurred during search operation.')
 

	
 
        # Return a rendered template
 
        return render('/search/search.html')
kallithea/lib/middleware/simplegit.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.lib.middleware.simplegit
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

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

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Apr 28, 2010
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 

	
 
"""
 

	
 

	
 
import os
 
import re
 
import logging
 
import traceback
 

	
 
from paste.httpheaders import REMOTE_USER, AUTH_TYPE
 
from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
 
    HTTPNotAcceptable
 
from kallithea.model.db import User, Ui
 

	
 
from kallithea.lib.utils2 import safe_str, fix_PATH, get_server_url,\
 
    _set_extras
 
from kallithea.lib.base import BaseVCSController
 
from kallithea.lib.utils import make_ui, is_valid_repo
 
from kallithea.lib.exceptions import HTTPLockedRC
 
from kallithea.lib.hooks import pre_pull
 
from kallithea.lib import auth_modules
 

	
 
log = logging.getLogger(__name__)
 

	
 

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

	
 

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

	
 

	
 
class SimpleGit(BaseVCSController):
 

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

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

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

	
 
        # quick check if that dir exists...
 
        if not is_valid_repo(repo_name, self.basepath, 'git'):
 
            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
 
            if anonymous_user.active:
 
                # ONLY check permissions if the user is activated
 
                anonymous_perm = self._check_permission(action, anonymous_user,
 
                                                        repo_name, ip_addr)
 
            else:
 
                anonymous_perm = False
 

	
 
            if not anonymous_user.active or not anonymous_perm:
 
                if not anonymous_user.active:
 
                    log.debug('Anonymous access is disabled, running '
 
                              'authentication')
 

	
 
                if not anonymous_perm:
 
                    log.debug('Not enough credentials to access this '
 
                              'repository as anonymous user')
 

	
 
                username = None
 
                #==============================================================
 
                # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
 
                # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
 
                #==============================================================
 

	
 
                # try to auth based on environ, container auth methods
 
                log.debug('Running PRE-AUTH for container based authentication')
 
                pre_auth = auth_modules.authenticate('', '', environ)
 
                if pre_auth and pre_auth.get('username'):
 
                    username = pre_auth['username']
 
                log.debug('PRE-AUTH got %s as username' % username)
 

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

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

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

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

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

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

	
 
        fix_PATH()
 
        log.debug('HOOKS extras is %s' % extras)
 
        baseui = make_ui('db')
 
        self.__inject_extras(repo_path, baseui, extras)
 

	
 
        try:
 
            self._handle_githooks(repo_name, action, baseui, environ)
 
            log.info('%s action on GIT repo "%s" by "%s" from %s' %
 
            log.info('%s action on Git repo "%s" by "%s" from %s' %
 
                     (action, str_repo_name, safe_str(username), ip_addr))
 
            app = self.__make_app(repo_name, repo_path, extras)
 
            return app(environ, start_response)
 
        except HTTPLockedRC, e:
 
            _code = CONFIG.get('lock_ret_code')
 
            log.debug('Repository LOCKED ret code %s!' % (_code))
 
            return e(environ, start_response)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            return HTTPInternalServerError()(environ, start_response)
 
        finally:
 
            # invalidate cache on push
 
            if action == 'push':
 
                self._invalidate_cache(repo_name)
 

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

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

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

	
 
    def __get_repository(self, environ):
 
        """
 
        Gets repository name out of PATH_INFO header
 

	
 
        :param environ: environ where PATH_INFO is stored
 
        """
 
        try:
 
            environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
 
            repo_name = GIT_PROTO_PAT.match(environ['PATH_INFO']).group(1)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            raise
 

	
 
        return repo_name
 

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

	
 
    def __get_action(self, environ):
 
        """
 
        Maps git request commands into a pull or push command.
 

	
 
        :param environ:
 
        """
 
        service = environ['QUERY_STRING'].split('=')
 

	
 
        if len(service) > 1:
 
            service_cmd = service[1]
 
            mapping = {
 
                'git-receive-pack': 'push',
 
                'git-upload-pack': 'pull',
 
            }
 
            op = mapping[service_cmd]
 
            self._git_stored_op = op
 
            return op
 
        else:
 
            # try to fallback to stored variable as we don't know if the last
 
            # operation is pull/push
 
            op = getattr(self, '_git_stored_op', 'pull')
 
        return op
 

	
 
    def _handle_githooks(self, repo_name, action, baseui, environ):
 
        """
 
        Handles pull action, push is handled by post-receive hook
 
        """
 
        from kallithea.lib.hooks import log_pull_action
 
        service = environ['QUERY_STRING'].split('=')
 

	
 
        if len(service) < 2:
 
            return
 

	
 
        from kallithea.model.db import Repository
 
        _repo = Repository.get_by_repo_name(repo_name)
 
        _repo = _repo.scm_instance
 

	
 
        _hooks = dict(baseui.configitems('hooks')) or {}
 
        if action == 'pull':
 
            # stupid git, emulate pre-pull hook !
 
            pre_pull(ui=baseui, repo=_repo._repo)
 
        if action == 'pull' and _hooks.get(Ui.HOOK_PULL):
 
            log_pull_action(ui=baseui, repo=_repo._repo)
 

	
 
    def __inject_extras(self, repo_path, baseui, extras={}):
 
        """
 
        Injects some extra params into baseui instance
 

	
 
        :param baseui: baseui instance
 
        :param extras: dict with extra params to put into baseui
 
        """
 

	
 
        _set_extras(extras)
kallithea/lib/middleware/simplehg.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.lib.middleware.simplehg
 
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 

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

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Apr 28, 2010
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 

	
 
"""
 

	
 

	
 
import os
 
import logging
 
import traceback
 

	
 
from paste.httpheaders import REMOTE_USER, AUTH_TYPE
 
from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError, \
 
    HTTPNotAcceptable
 
from kallithea.model.db import User
 

	
 
from kallithea.lib.utils2 import safe_str, fix_PATH, get_server_url,\
 
    _set_extras
 
from kallithea.lib.base import BaseVCSController
 
from kallithea.lib.utils import make_ui, is_valid_repo, ui_sections
 
from kallithea.lib.vcs.utils.hgcompat import RepoError, hgweb_mod
 
from kallithea.lib.exceptions import HTTPLockedRC
 
from kallithea.lib import auth_modules
 

	
 
log = logging.getLogger(__name__)
 

	
 

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

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

	
 

	
 
class SimpleHg(BaseVCSController):
 

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

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

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

	
 
        # quick check if that dir exists...
 
        if not is_valid_repo(repo_name, self.basepath, 'hg'):
 
            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
 
            if anonymous_user.active:
 
                # ONLY check permissions if the user is activated
 
                anonymous_perm = self._check_permission(action, anonymous_user,
 
                                                        repo_name, ip_addr)
 
            else:
 
                anonymous_perm = False
 

	
 
            if not anonymous_user.active or not anonymous_perm:
 
                if not anonymous_user.active:
 
                    log.debug('Anonymous access is disabled, running '
 
                              'authentication')
 

	
 
                if not anonymous_perm:
 
                    log.debug('Not enough credentials to access this '
 
                              'repository as anonymous user')
 

	
 
                username = None
 
                #==============================================================
 
                # DEFAULT PERM FAILED OR ANONYMOUS ACCESS IS DISABLED SO WE
 
                # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
 
                #==============================================================
 

	
 
                # try to auth based on environ, container auth methods
 
                log.debug('Running PRE-AUTH for container based authentication')
 
                pre_auth = auth_modules.authenticate('', '', environ)
 
                if pre_auth and pre_auth.get('username'):
 
                    username = pre_auth['username']
 
                log.debug('PRE-AUTH got %s as username' % username)
 

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

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

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

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

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

	
 
        fix_PATH()
 
        log.debug('HOOKS extras is %s' % extras)
 
        baseui = make_ui('db')
 
        self.__inject_extras(repo_path, baseui, extras)
 

	
 
        try:
 
            log.info('%s action on HG repo "%s" by "%s" from %s' %
 
            log.info('%s action on Mercurial repo "%s" by "%s" from %s' %
 
                     (action, str_repo_name, safe_str(username), ip_addr))
 
            app = self.__make_app(repo_path, baseui, extras)
 
            return app(environ, start_response)
 
        except RepoError, e:
 
            if str(e).find('not found') != -1:
 
                return HTTPNotFound()(environ, start_response)
 
        except HTTPLockedRC, e:
 
            _code = CONFIG.get('lock_ret_code')
 
            log.debug('Repository LOCKED ret code %s!' % (_code))
 
            return e(environ, start_response)
 
        except Exception:
 
            log.error(traceback.format_exc())
 
            return HTTPInternalServerError()(environ, start_response)
 
        finally:
 
            # invalidate cache on push
 
            if action == 'push':
 
                self._invalidate_cache(repo_name)
 

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

	
 
    def __get_repository(self, environ):
 
        """
 
        Gets repository name out of PATH_INFO header
 

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

	
 
        return repo_name
 

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

	
 
    def __get_action(self, environ):
 
        """
 
        Maps mercurial request commands into a clone,pull or push command.
 
        This should always return a valid command string
 

	
 
        :param environ:
 
        """
 
        mapping = {'changegroup': 'pull',
 
                   'changegroupsubset': 'pull',
 
                   'stream_out': 'pull',
 
                   'listkeys': 'pull',
 
                   'unbundle': 'push',
 
                   'pushkey': 'push', }
 
        for qry in environ['QUERY_STRING'].split('&'):
 
            if qry.startswith('cmd'):
 
                cmd = qry.split('=')[-1]
 
                if cmd in mapping:
 
                    return mapping[cmd]
 

	
 
                return 'pull'
 

	
 
        raise Exception('Unable to detect pull/push action !!'
 
                        'Are you using non standard command or client ?')
 

	
 
    def __inject_extras(self, repo_path, baseui, extras={}):
 
        """
 
        Injects some extra params into baseui instance
 

	
 
        also overwrites global settings with those takes from local hgrc file
 

	
 
        :param baseui: baseui instance
 
        :param extras: dict with extra params to put into baseui
 
        """
 

	
 
        hgrc = os.path.join(repo_path, '.hg', 'hgrc')
 

	
 
        # make our hgweb quiet so it doesn't print output
 
        baseui.setconfig('ui', 'quiet', 'true')
 

	
 
        repoui = make_ui('file', hgrc, False)
 

	
 
        if repoui:
 
            #overwrite our ui instance with the section from hgrc file
 
            for section in ui_sections:
 
                for k, v in repoui.configitems(section):
 
                    baseui.setconfig(section, k, v)
 
        _set_extras(extras)
kallithea/lib/utils.py
Show inline comments
 
# -*- coding: utf-8 -*-
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU General Public License for more details.
 
#
 
# You should have received a copy of the GNU General Public License
 
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
"""
 
kallithea.lib.utils
 
~~~~~~~~~~~~~~~~~~~
 

	
 
Utilities library for Kallithea
 

	
 
This file was forked by the Kallithea project in July 2014.
 
Original author and date, and relevant copyright and licensing information is below:
 
:created_on: Apr 18, 2010
 
:author: marcink
 
:copyright: (c) 2013 RhodeCode GmbH, and others.
 
:license: GPLv3, see LICENSE.md for more details.
 
"""
 

	
 
import os
 
import re
 
import logging
 
import datetime
 
import traceback
 
import paste
 
import beaker
 
import tarfile
 
import shutil
 
import decorator
 
import warnings
 
from os.path import abspath
 
from os.path import dirname as dn, join as jn
 

	
 
from paste.script.command import Command, BadCommand
 

	
 
from webhelpers.text import collapse, remove_formatting, strip_tags
 
from beaker.cache import _cache_decorate
 

	
 
from kallithea import BRAND
 

	
 
from kallithea.lib.vcs.utils.hgcompat import ui, config
 
from kallithea.lib.vcs.utils.helpers import get_scm
 
from kallithea.lib.vcs.exceptions import VCSError
 

	
 
from kallithea.lib.caching_query import FromCache
 

	
 
from kallithea.model import meta
 
from kallithea.model.db import Repository, User, Ui, \
 
    UserLog, RepoGroup, Setting, CacheInvalidation, UserGroup
 
from kallithea.model.meta import Session
 
from kallithea.model.repo_group import RepoGroupModel
 
from kallithea.lib.utils2 import safe_str, safe_unicode, get_current_authuser
 
from kallithea.lib.vcs.utils.fakemod import create_module
 

	
 
log = logging.getLogger(__name__)
 

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

	
 

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

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

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

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

	
 

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

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

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

	
 

	
 
#==============================================================================
 
# PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS
 
#==============================================================================
 
def get_repo_slug(request):
 
    _repo = request.environ['pylons.routes_dict'].get('repo_name')
 
    if _repo:
 
        _repo = _repo.rstrip('/')
 
    return _repo
 

	
 

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

	
 

	
 
def get_user_group_slug(request):
 
    _group = request.environ['pylons.routes_dict'].get('id')
 
    try:
 
        _group = UserGroup.get(_group)
 
        if _group:
 
            _group = _group.users_group_name
 
    except Exception:
 
        log.debug(traceback.format_exc())
 
        #catch all failures here
 
        pass
 

	
 
    return _group
 

	
 

	
 
def _extract_id_from_repo_name(repo_name):
 
    if repo_name.startswith('/'):
 
        repo_name = repo_name.lstrip('/')
 
    by_id_match = re.match(r'^_(\d{1,})', repo_name)
 
    if by_id_match:
 
        return by_id_match.groups()[0]
 

	
 

	
 
def get_repo_by_id(repo_name):
 
    """
 
    Extracts repo_name by id from special urls. Example url is _11/repo_name
 

	
 
    :param repo_name:
 
    :return: repo_name if matched else None
 
    """
 
    try:
 
        _repo_id = _extract_id_from_repo_name(repo_name)
 
        if _repo_id:
 
            from kallithea.model.db import Repository
 
            return Repository.get(_repo_id).repo_name
 
    except Exception:
 
        log.debug('Failed to extract repo_name from URL %s' % (
 
                  traceback.format_exc()))
 
        return
 

	
 

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

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

	
 
    """
 

	
 
    if not sa:
 
        sa = meta.Session()
 
    # if we don't get explicit IP address try to get one from registered user
 
    # in tmpl context var
 
    if not ipaddr:
 
        ipaddr = getattr(get_current_authuser(), 'ip_addr', '')
 

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

	
 
        if getattr(repo, 'repo_id', None):
 
            repo_obj = Repository.get(repo.repo_id)
 
            repo_name = repo_obj.repo_name
 
        elif isinstance(repo, basestring):
 
            repo_name = repo.lstrip('/')
 
            repo_obj = Repository.get_by_repo_name(repo_name)
 
        else:
 
            repo_obj = None
 
            repo_name = ''
 

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

	
 
        user_log.repository = repo_obj
 
        user_log.repository_name = repo_name
 

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

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

	
 

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

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

	
 
    # remove ending slash for better results
 
    path = path.rstrip(os.sep)
 
    log.debug('now scanning in %s location recursive:%s...' % (path, recursive))
 

	
 
    def _get_repos(p):
 
        if not os.access(p, os.R_OK) or not os.access(p, os.X_OK):
 
            log.warning('ignoring repo path without access: %s' % (p,))
 
            return
 
        if not os.access(p, os.W_OK):
 
            log.warning('repo path without write access: %s' % (p,))
 
        for dirpath in os.listdir(p):
 
            if os.path.isfile(os.path.join(p, dirpath)):
 
                continue
 
            cur_path = os.path.join(p, dirpath)
 

	
 
            # skip removed repos
 
            if skip_removed_repos and REMOVED_REPO_PAT.match(dirpath):
 
                continue
 

	
 
            #skip .<somethin> dirs
 
            if dirpath.startswith('.'):
 
                continue
 

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

	
 
    return _get_repos(path)
 

	
 

	
 
def is_valid_repo(repo_name, base_path, scm=None):
 
    """
 
    Returns True if given path is a valid repository False otherwise.
 
    If scm param is given also compare if given scm is the same as expected
 
    from scm parameter
 

	
 
    :param repo_name:
 
    :param base_path:
 
    :param scm:
 

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

	
 
    try:
 
        scm_ = get_scm(full_path)
 
        if scm:
 
            return scm_[0] == scm
 
        return True
 
    except VCSError:
 
        return False
 

	
 

	
 
def is_valid_repo_group(repo_group_name, base_path, skip_path_check=False):
 
    """
 
    Returns True if given path is a repository group False otherwise
 

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

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

	
 
    try:
 
        # we need to check bare git repos at higher level
 
        # since we might match branches/hooks/info/objects or possible
 
        # other things inside bare git repo
 
        get_scm(os.path.dirname(full_path))
 
        return False
 
    except VCSError:
 
        pass
 

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

	
 
    return False
 

	
 

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

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

	
 

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

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

	
 
    baseui = ui.ui()
 

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

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

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

	
 
        hg_ui = ret
 
        for ui_ in hg_ui:
 
            if ui_.ui_active:
 
                ui_val = safe_str(ui_.ui_value)
 
                if ui_.ui_section == 'hooks' and BRAND != 'kallithea' and ui_val.startswith('python:' + BRAND + '.lib.hooks.'):
 
                    ui_val = ui_val.replace('python:' + BRAND + '.lib.hooks.', 'python:kallithea.lib.hooks.')
 
                log.debug('settings ui from db: [%s] %s=%s', ui_.ui_section,
 
                          ui_.ui_key, ui_val)
 
                baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
 
                                 ui_val)
 
            if ui_.ui_key == 'push_ssl':
 
                # force set push_ssl requirement to False, kallithea
 
                # handles that
 
                baseui.setconfig(safe_str(ui_.ui_section), safe_str(ui_.ui_key),
 
                                 False)
 
        if clear_session:
 
            meta.Session.remove()
 
    return baseui
 

	
 

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

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

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

	
 

	
 
def set_vcs_config(config):
 
    """
 
    Patch VCS config with some Kallithea specific stuff
 

	
 
    :param config: kallithea.CONFIG
 
    """
 
    import kallithea
 
    from kallithea.lib.vcs import conf
 
    from kallithea.lib.utils2 import aslist
 
    conf.settings.BACKENDS = {
 
        'hg': 'kallithea.lib.vcs.backends.hg.MercurialRepository',
 
        'git': 'kallithea.lib.vcs.backends.git.GitRepository',
 
    }
 

	
 
    conf.settings.GIT_EXECUTABLE_PATH = config.get('git_path', 'git')
 
    conf.settings.GIT_REV_FILTER = config.get('git_rev_filter', '--all').strip()
 
    conf.settings.DEFAULT_ENCODINGS = aslist(config.get('default_encoding',
 
                                                        'utf8'), sep=',')
 

	
 

	
 
def map_groups(path):
 
    """
 
    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 paths: full path to repository
 
    """
 
    sa = meta.Session()
 
    groups = path.split(Repository.url_sep())
 
    parent = None
 
    group = None
 

	
 
    # last element is repo in nested groups structure
 
    groups = groups[:-1]
 
    rgm = RepoGroupModel(sa)
 
    owner = User.get_first_admin()
 
    for lvl, group_name in enumerate(groups):
 
        group_name = '/'.join(groups[:lvl] + [group_name])
 
        group = RepoGroup.get_by_group_name(group_name)
 
        desc = '%s group' % group_name
 

	
 
        # 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))
 
            group = RepoGroup(group_name, parent)
 
            group.group_description = desc
 
            group.user = owner
 
            sa.add(group)
 
            perm_obj = rgm._create_default_perms(group)
 
            sa.add(perm_obj)
 
            sa.flush()
 

	
 
        parent = group
 
    return group
 

	
 

	
 
def repo2db_mapper(initial_repo_list, remove_obsolete=False,
 
                   install_git_hook=False):
 
    """
 
    maps all repos given in initial_repo_list, non existing repositories
 
    are created, if remove_obsolete is True it also check for db entries
 
    that are not in initial_repo_list and removes them.
 

	
 
    :param initial_repo_list: list of repositories found by scanning methods
 
    :param remove_obsolete: check for obsolete entries in database
 
    :param install_git_hook: if this is True, also check and install githook
 
        for a repo if missing
 
    """
 
    from kallithea.model.repo import RepoModel
 
    from kallithea.model.scm import ScmModel
 
    sa = meta.Session()
 
    repo_model = RepoModel()
 
    user = User.get_first_admin()
 
    added = []
 

	
 
    ##creation defaults
 
    defs = Setting.get_default_repo_settings(strip_prefix=True)
 
    enable_statistics = defs.get('repo_enable_statistics')
 
    enable_locking = defs.get('repo_enable_locking')
 
    enable_downloads = defs.get('repo_enable_downloads')
 
    private = defs.get('repo_private')
 

	
 
    for name, repo in initial_repo_list.items():
 
        group = map_groups(name)
 
        unicode_name = safe_unicode(name)
 
        db_repo = repo_model.get_by_repo_name(unicode_name)
 
        # found repo that is on filesystem not in Kallithea database
 
        if not db_repo:
 
            log.info('repository %s not found, creating now' % name)
 
            added.append(name)
 
            desc = (repo.description
 
                    if repo.description != 'unknown'
 
                    else '%s repository' % name)
 

	
 
            new_repo = repo_model._create_repo(
 
                repo_name=name,
 
                repo_type=repo.alias,
 
                description=desc,
 
                repo_group=getattr(group, 'group_id', None),
 
                owner=user,
 
                enable_locking=enable_locking,
 
                enable_downloads=enable_downloads,
 
                enable_statistics=enable_statistics,
 
                private=private,
 
                state=Repository.STATE_CREATED
 
            )
 
            sa.commit()
 
            # we added that repo just now, and make sure it has githook
 
            # installed, and updated server info
 
            if new_repo.repo_type == 'git':
 
                git_repo = new_repo.scm_instance
 
                ScmModel().install_git_hook(git_repo)
 
                # update repository server-info
 
                log.debug('Running update server info')
 
                git_repo._update_server_info()
 
            new_repo.update_changeset_cache()
 
        elif install_git_hook:
 
            if db_repo.repo_type == 'git':
 
                ScmModel().install_git_hook(db_repo.scm_instance)
 

	
 
    removed = []
 
    if remove_obsolete:
 
        # 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)
 
                try:
 
                    removed.append(repo.repo_name)
 
                    RepoModel(sa).delete(repo, forks='detach', fs_remove=False)
 
                    sa.commit()
 
                except Exception:
 
                    #don't hold further removals on error
 
                    log.error(traceback.format_exc())
 
                    sa.rollback()
 
    return added, removed
 

	
 

	
 
# set cache regions for beaker so celery can utilise it
 
def add_cache(settings):
 
    cache_settings = {'regions': None}
 
    for key in settings.keys():
 
        for prefix in ['beaker.cache.', 'cache.']:
 
            if key.startswith(prefix):
 
                name = key.split(prefix)[1].strip()
 
                cache_settings[name] = settings[key].strip()
 
    if cache_settings['regions']:
 
        for region in cache_settings['regions'].split(','):
 
            region = region.strip()
 
            region_settings = {}
 
            for key, value in cache_settings.items():
 
                if key.startswith(region):
 
                    region_settings[key.split('.')[1]] = value
 
            region_settings['expire'] = int(region_settings.get('expire',
 
                                                                60))
 
            region_settings.setdefault('lock_dir',
 
                                       cache_settings.get('lock_dir'))
 
            region_settings.setdefault('data_dir',
 
                                       cache_settings.get('data_dir'))
 

	
 
            if 'type' not in region_settings:
 
                region_settings['type'] = cache_settings.get('type',
 
                                                             'memory')
 
            beaker.cache.cache_regions[region] = region_settings
 

	
 

	
 
def load_rcextensions(root_path):
 
    import kallithea
 
    from kallithea.config import conf
 

	
 
    path = os.path.join(root_path, 'rcextensions', '__init__.py')
 
    if os.path.isfile(path):
 
        rcext = create_module('rc', path)
 
        EXT = kallithea.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', []))
 

	
 
        # auto check if the module is not missing any data, set to default if is
 
        # this will help autoupdate new feature of rcext module
 
        #from kallithea.config import rcextensions
 
        #for k in dir(rcextensions):
 
        #    if not k.startswith('_') and not hasattr(EXT, k):
 
        #        setattr(EXT, k, getattr(rcextensions, k))
 

	
 

	
 
def get_custom_lexer(extension):
 
    """
 
    returns a custom lexer if it's defined in rcextensions module, or None
 
    if there's no custom lexer defined
 
    """
 
    import kallithea
 
    from pygments import lexers
 
    #check if we didn't define this extension as other lexer
 
    if kallithea.EXTENSIONS and extension in kallithea.EXTENSIONS.EXTRA_LEXERS:
 
        _lexer_name = kallithea.EXTENSIONS.EXTRA_LEXERS[extension]
 
        return lexers.get_lexer_by_name(_lexer_name)
 

	
 

	
 
#==============================================================================
 
# TEST FUNCTIONS AND CREATORS
 
#==============================================================================
 
def create_test_index(repo_location, config, full_index):
 
    """
 
    Makes default test index
 

	
 
    :param config: test config
 
    :param full_index:
 
    """
 

	
 
    from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
 
    from kallithea.lib.pidlock import DaemonLock, LockHeld
 

	
 
    repo_location = repo_location
 

	
 
    index_location = os.path.join(config['app_conf']['index_dir'])
 
    if not os.path.exists(index_location):
 
        os.makedirs(index_location)
 

	
 
    try:
 
        l = DaemonLock(file_=jn(dn(index_location), 'make_index.lock'))
 
        WhooshIndexingDaemon(index_location=index_location,
 
                             repo_location=repo_location)\
 
            .run(full_index=full_index)
 
        l.release()
 
    except LockHeld:
 
        pass
 

	
 

	
 
def create_test_env(repos_test_path, config):
 
    """
 
    Makes a fresh database and
 
    install test repository into tmp dir
 
    """
 
    from kallithea.lib.db_manage import DbManage
 
    from kallithea.tests import HG_REPO, GIT_REPO, TESTS_TMP_PATH
 

	
 
    # PART ONE create db
 
    dbconf = config['sqlalchemy.db1.url']
 
    log.debug('making test db %s' % dbconf)
 

	
 
    # create test dir if it doesn't exist
 
    if not os.path.isdir(repos_test_path):
 
        log.debug('Creating testdir %s' % repos_test_path)
 
        os.makedirs(repos_test_path)
 

	
 
    dbmanage = DbManage(log_sql=True, dbconf=dbconf, root=config['here'],
 
                        tests=True)
 
    dbmanage.create_tables(override=True)
 
    # for tests dynamically set new root paths based on generated content
 
    dbmanage.create_settings(dbmanage.config_prompt(repos_test_path))
 
    dbmanage.create_default_user()
 
    dbmanage.admin_prompt()
 
    dbmanage.create_permissions()
 
    dbmanage.populate_default_permissions()
 
    Session().commit()
 
    # PART TWO make test repo
 
    log.debug('making test vcs repositories')
 

	
 
    idx_path = config['app_conf']['index_dir']
 
    data_path = config['app_conf']['cache_dir']
 

	
 
    #clean index and data
 
    if idx_path and os.path.exists(idx_path):
 
        log.debug('remove %s' % idx_path)
 
        shutil.rmtree(idx_path)
 

	
 
    if data_path and os.path.exists(data_path):
 
        log.debug('remove %s' % data_path)
 
        shutil.rmtree(data_path)
 

	
 
    #CREATE DEFAULT TEST REPOS
 
    cur_dir = dn(dn(abspath(__file__)))
 
    tar = tarfile.open(jn(cur_dir, 'tests', 'fixtures', "vcs_test_hg.tar.gz"))
 
    tar.extractall(jn(TESTS_TMP_PATH, HG_REPO))
 
    tar.close()
 

	
 
    cur_dir = dn(dn(abspath(__file__)))
 
    tar = tarfile.open(jn(cur_dir, 'tests', 'fixtures', "vcs_test_git.tar.gz"))
 
    tar.extractall(jn(TESTS_TMP_PATH, GIT_REPO))
 
    tar.close()
 

	
 
    #LOAD VCS test stuff
 
    from kallithea.tests.vcs import setup_package
 
    setup_package()
 

	
 

	
 
#==============================================================================
 
# PASTER COMMANDS
 
#==============================================================================
 
class BasePasterCommand(Command):
 
    """
 
    Abstract Base Class for paster commands.
 

	
 
    The celery commands are somewhat aggressive about loading
 
    celery.conf, and since our module sets the `CELERY_LOADER`
 
    environment variable to our loader, we have to bootstrap a bit and
 
    make sure we've had a chance to load the pylons config off of the
 
    command line, otherwise everything fails.
 
    """
 
    min_args = 1
 
    min_args_error = "Please provide a paster config file as an argument."
 
    takes_config_file = 1
 
    requires_config_file = True
 

	
 
    def notify_msg(self, msg, log=False):
 
        """Make a notification to user, additionally if logger is passed
 
        it logs this action using given logger
 

	
 
        :param msg: message that will be printed to user
 
        :param log: logging instance, to use to additionally log this message
 

	
 
        """
 
        if log and isinstance(log, logging):
 
            log(msg)
 

	
 
    def run(self, args):
 
        """
 
        Overrides Command.run
 

	
 
        Checks for a config file argument and loads it.
 
        """
 
        if len(args) < self.min_args:
 
            raise BadCommand(
 
                self.min_args_error % {'min_args': self.min_args,
 
                                       'actual_args': len(args)})
 

	
 
        # Decrement because we're going to lob off the first argument.
 
        # @@ This is hacky
 
        self.min_args -= 1
 
        self.bootstrap_config(args[0])
 
        self.update_parser()
 
        return super(BasePasterCommand, self).run(args[1:])
 

	
 
    def update_parser(self):
 
        """
 
        Abstract method.  Allows for the class's parser to be updated
 
        before the superclass's `run` method is called.  Necessary to
 
        allow options/arguments to be passed through to the underlying
 
        celery command.
 
        """
 
        raise NotImplementedError("Abstract Method.")
 

	
 
    def bootstrap_config(self, conf):
 
        """
 
        Loads the pylons configuration.
 
        """
 
        from pylons import config as pylonsconfig
 

	
 
        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)
 

	
 
    def _init_session(self):
 
        """
 
        Inits SqlAlchemy Session
 
        """
 
        logging.config.fileConfig(self.path_to_ini_file)
 
        from pylons import config
 
        from kallithea.model import init_model
 
        from kallithea.lib.utils2 import engine_from_config
 

	
 
        #get to remove repos !!
 
        add_cache(config)
 
        engine = engine_from_config(config, 'sqlalchemy.db1.')
 
        init_model(engine)
 

	
 

	
 
def check_git_version():
 
    """
 
    Checks what version of git is installed in system, and issues a warning
 
    if it's too old for Kallithea to properly work.
 
    """
 
    from kallithea import BACKENDS
 
    from kallithea.lib.vcs.backends.git.repository import GitRepository
 
    from kallithea.lib.vcs.conf import settings
 
    from distutils.version import StrictVersion
 

	
 
    if 'git' not in BACKENDS:
 
        return None
 

	
 
    stdout, stderr = GitRepository._run_git_command('--version', _bare=True,
 
                                                    _safe=True)
 

	
 
    ver = (stdout.split(' ')[-1] or '').strip() or '0.0.0'
 
    if len(ver.split('.')) > 3:
 
        #StrictVersion needs to be only 3 element type
 
        ver = '.'.join(ver.split('.')[:3])
 
    try:
 
        _ver = StrictVersion(ver)
 
    except Exception:
 
        _ver = StrictVersion('0.0.0')
 
        stderr = traceback.format_exc()
 

	
 
    req_ver = '1.7.4'
 
    to_old_git = False
 
    if  _ver < StrictVersion(req_ver):
 
        to_old_git = True
 

	
 
    if 'git' in BACKENDS:
 
        log.debug('GIT executable: "%s" version detected: %s'
 
        log.debug('Git executable: "%s" version detected: %s'
 
                  % (settings.GIT_EXECUTABLE_PATH, stdout))
 
        if stderr:
 
            log.warning('Unable to detect git version, org error was: %r' % stderr)
 
        elif to_old_git:
 
            log.warning('Kallithea detected git version %s, which is too old '
 
                        'for the system to function properly. Make sure '
 
                        'its version is at least %s' % (ver, req_ver))
 
    return _ver
 

	
 

	
 
@decorator.decorator
 
def jsonify(func, *args, **kwargs):
 
    """Action decorator that formats output for JSON
 

	
 
    Given a function that will return content, this decorator will turn
 
    the result into JSON, with a content-type of 'application/json' and
 
    output it.
 

	
 
    """
 
    from pylons.decorators.util import get_pylons
 
    from kallithea.lib.compat import json
 
    pylons = get_pylons(args)
 
    pylons.response.headers['Content-Type'] = 'application/json; charset=utf-8'
 
    data = func(*args, **kwargs)
 
    if isinstance(data, (list, tuple)):
 
        msg = "JSON responses with Array envelopes are susceptible to " \
 
              "cross-site data leak attacks, see " \
 
              "http://wiki.pylonshq.com/display/pylonsfaq/Warnings"
 
        warnings.warn(msg, Warning, 2)
 
        log.warning(msg)
 
    log.debug("Returning JSON wrapped action output")
 
    return json.dumps(data, encoding='utf-8')
 

	
 

	
 
def conditional_cache(region, prefix, condition, func):
 
    """
 

	
 
    Conditional caching function use like::
 
        def _c(arg):
 
            #heavy computation function
 
            return data
 

	
 
        # denpending from condition the compute is wrapped in cache or not
 
        compute = conditional_cache('short_term', 'cache_desc', codnition=True, func=func)
 
        return compute(arg)
 

	
 
    :param region: name of cache region
 
    :param prefix: cache region prefix
 
    :param condition: condition for cache to be triggered, and return data cached
 
    :param func: wrapped heavy function to compute
 

	
 
    """
 
    wrapped = func
 
    if condition:
 
        log.debug('conditional_cache: True, wrapping call of '
 
                  'func: %s into %s region cache' % (region, func))
 
        wrapped = _cache_decorate((prefix,), None, None, region)(func)
 

	
 
    return wrapped
kallithea/public/css/style.css
Show inline comments
 
@@ -3307,1856 +3307,1857 @@ table.code-browser .submodule-dir {
 
    padding-top: 0px;
 
    text-align: left;
 
}
 

	
 
.accept_icon {
 
    background: url("../images/icons/accept.png") no-repeat scroll 3px;
 
    padding-left: 20px;
 
    padding-top: 0px;
 
    text-align: left;
 
}
 

	
 
.edit_icon {
 
    background: url("../images/icons/application_form_edit.png") no-repeat scroll 3px;
 
    padding-left: 20px;
 
    padding-top: 0px;
 
    text-align: left;
 
}
 

	
 
.delete_icon {
 
    background: url("../images/icons/delete.png") no-repeat scroll 3px;
 
    padding-left: 20px;
 
    padding-top: 0px;
 
    text-align: left;
 
}
 

	
 
.refresh_icon {
 
    background: url("../images/icons/arrow_refresh.png") no-repeat scroll
 
        3px;
 
    padding-left: 20px;
 
    padding-top: 0px;
 
    text-align: left;
 
}
 

	
 
.pull_icon {
 
    background: url("../images/icons/connect.png") no-repeat scroll 3px;
 
    padding-left: 20px;
 
    padding-top: 0px;
 
    text-align: left;
 
}
 

	
 
.rss_icon {
 
    background: url("../images/icons/rss_16.png") no-repeat scroll 3px;
 
    padding-left: 20px;
 
    padding-top: 4px;
 
    text-align: left;
 
    font-size: 8px
 
}
 

	
 
.atom_icon {
 
    background: url("../images/icons/rss_16.png") no-repeat scroll 3px;
 
    padding-left: 20px;
 
    padding-top: 4px;
 
    text-align: left;
 
    font-size: 8px
 
}
 

	
 
.archive_icon {
 
    background: url("../images/icons/compress.png") no-repeat scroll 3px;
 
    padding-left: 20px;
 
    text-align: left;
 
    padding-top: 1px;
 
}
 

	
 
.start_following_icon {
 
    background: url("../images/icons/heart_add.png") no-repeat scroll 3px;
 
    padding-left: 20px;
 
    text-align: left;
 
    padding-top: 0px;
 
}
 

	
 
.stop_following_icon {
 
    background: url("../images/icons/heart_delete.png") no-repeat scroll 3px;
 
    padding-left: 20px;
 
    text-align: left;
 
    padding-top: 0px;
 
}
 

	
 
.action_button {
 
    border: 0;
 
    display: inline;
 
}
 

	
 
.action_button:hover {
 
    border: 0;
 
    text-decoration: underline;
 
    cursor: pointer;
 
}
 

	
 
#switch_repos {
 
    position: absolute;
 
    height: 25px;
 
    z-index: 1;
 
}
 

	
 
#switch_repos select {
 
    min-width: 150px;
 
    max-height: 250px;
 
    z-index: 1;
 
}
 

	
 
.breadcrumbs {
 
    border: medium none;
 
    color: #FFF;
 
    float: left;
 
    font-weight: 700;
 
    font-size: 14px;
 
    margin: 0;
 
    padding: 11px 0 11px 10px;
 
}
 

	
 
.breadcrumbs .hash {
 
    text-transform: none;
 
    color: #fff;
 
}
 

	
 
.breadcrumbs a {
 
    color: #FFF;
 
}
 

	
 
.flash_msg {
 
}
 

	
 
.flash_msg ul {
 
}
 

	
 
.error_red {
 
    color: red;
 
}
 

	
 
.flash_msg .alert-error {
 
    background-color: #c43c35;
 
    background-repeat: repeat-x;
 
    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-linear-gradient(top, #ee5f5b, #c43c35);
 
    background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
 
    background-image: linear-gradient(to bottom, #ee5f5b, #c43c35);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b',endColorstr='#c43c35', GradientType=0 );
 
    border-color: #c43c35 #c43c35 #882a25;
 
}
 

	
 
.flash_msg .alert-error a {
 
    text-decoration: underline;
 
}
 

	
 
.flash_msg .alert-warning {
 
    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: -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-linear-gradient(top, #fceec1, #eedc94);
 
    background-image: -o-linear-gradient(top, #fceec1, #eedc94);
 
    background-image: linear-gradient(to bottom, #fceec1, #eedc94);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0 );
 
    border-color: #eedc94 #eedc94 #e4c652;
 
}
 

	
 
.flash_msg .alert-warning a {
 
    text-decoration: underline;
 
}
 

	
 
.flash_msg .alert-success {
 
    background-color: #57a957;
 
    background-repeat: repeat-x !important;
 
    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-linear-gradient(top, #62c462, #57a957);
 
    background-image: -o-linear-gradient(top, #62c462, #57a957);
 
    background-image: linear-gradient(to bottom, #62c462, #57a957);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0 );
 
    border-color: #57a957 #57a957 #3d773d;
 
}
 

	
 
.flash_msg .alert-success a {
 
    text-decoration: underline;
 
    color: #FFF !important;
 
}
 

	
 
.flash_msg .alert-info {
 
    background-color: #339bb9;
 
    background-repeat: repeat-x;
 
    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-linear-gradient(top, #5bc0de, #339bb9);
 
    background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
 
    background-image: linear-gradient(to bottom, #5bc0de, #339bb9);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0 );
 
    border-color: #339bb9 #339bb9 #22697d;
 
}
 

	
 
.flash_msg .alert-info a {
 
    text-decoration: underline;
 
}
 

	
 
.flash_msg .alert-error,
 
.flash_msg .alert-warning,
 
.flash_msg .alert-success,
 
.flash_msg .alert-info {
 
    font-size: 12px;
 
    font-weight: 700;
 
    min-height: 14px;
 
    line-height: 14px;
 
    margin-bottom: 10px;
 
    margin-top: 0;
 
    display: block;
 
    overflow: auto;
 
    padding: 6px 10px 6px 10px;
 
    border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
 
    position: relative;
 
    color: #FFF;
 
    border-width: 1px;
 
    border-style: solid;
 
    -webkit-border-radius: 4px;
 
    border-radius: 4px;
 
    -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
 
    box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25);
 
}
 

	
 
#msg_close {
 
    background: transparent url("../images/cross_grey_small.png") no-repeat scroll 0 0;
 
    cursor: pointer;
 
    height: 16px;
 
    position: absolute;
 
    right: 5px;
 
    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;
 
}
 

	
 
table#permissions_manage {
 
    width: 0 !important;
 
}
 

	
 
table#permissions_manage span.private_repo_msg {
 
    font-size: 0.8em;
 
    opacity: 0.6;
 
}
 

	
 
table#permissions_manage td.private_repo_msg {
 
    font-size: 0.8em;
 
}
 

	
 
table#permissions_manage tr#add_perm_input td {
 
    vertical-align: middle;
 
}
 

	
 
div.gravatar {
 
    background-color: #FFF;
 
    float: left;
 
    margin-right: 0.7em;
 
    padding: 1px 1px 1px 1px;
 
    line-height: 0;
 
    -webkit-border-radius: 3px;
 
    -khtml-border-radius: 3px;
 
    border-radius: 3px;
 
}
 

	
 
div.gravatar img {
 
    -webkit-border-radius: 2px;
 
    -khtml-border-radius: 2px;
 
    border-radius: 2px;
 
}
 

	
 
#header, #content, #footer {
 
    min-width: 978px;
 
}
 

	
 
#content {
 
    clear: both;
 
    padding: 10px 10px 14px 10px;
 
}
 

	
 
#content.hover {
 
    padding: 55px 10px 14px 10px !important;
 
}
 

	
 
#content div.box div.title div.search {
 
    border-left: 1px solid #576622;
 
}
 

	
 
#content div.box div.title div.search div.input input {
 
    border: 1px solid #576622;
 
}
 

	
 
.btn {
 
    color: #515151;
 
    background-color: #DADADA;
 
    background-repeat: repeat-x;
 
    background-image: -khtml-gradient(linear, left top, left bottom, from(#F4F4F4),to(#DADADA) );
 
    background-image: -moz-linear-gradient(top, #F4F4F4, #DADADA);
 
    background-image: -ms-linear-gradient(top, #F4F4F4, #DADADA);
 
    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #F4F4F4),color-stop(100%, #DADADA) );
 
    background-image: -webkit-linear-gradient(top, #F4F4F4, #DADADA) );
 
    background-image: -o-linear-gradient(top, #F4F4F4, #DADADA) );
 
    background-image: linear-gradient(to bottom, #F4F4F4, #DADADA);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#F4F4F4', endColorstr='#DADADA', GradientType=0);
 

	
 
    border-top: 1px solid #DDD;
 
    border-left: 1px solid #c6c6c6;
 
    border-right: 1px solid #DDD;
 
    border-bottom: 1px solid #c6c6c6;
 
    color: #515151;
 
    outline: none;
 
    margin: 0px 3px 3px 0px;
 
    -webkit-border-radius: 4px 4px 4px 4px !important;
 
    -khtml-border-radius: 4px 4px 4px 4px !important;
 
    border-radius: 4px 4px 4px 4px !important;
 
    cursor: pointer !important;
 
    padding: 3px 3px 3px 3px;
 
    background-position: 0 -100px;
 
    display: inline-block;
 
}
 

	
 
ul.nav-stacked {
 
    margin: 20px;
 
    color: #393939;
 
    font-weight: 700;
 
}
 

	
 
ul.nav-stacked a {
 
    color: inherit;
 
}
 

	
 
/* make .btn inputs and buttons and divs look the same */
 
button.btn,
 
input.btn {
 
    font-family: inherit;
 
    font-size: inherit;
 
    line-height: inherit;
 
}
 

	
 
.btn::-moz-focus-inner {
 
    border: 0;
 
    padding: 0;
 
}
 

	
 
/* make .btn inputs and buttons and divs look the same */
 
button.btn,
 
input.btn {
 
    font-family: inherit;
 
    font-size: inherit;
 
    line-height: inherit;
 
}
 

	
 
.btn::-moz-focus-inner {
 
    border: 0;
 
    padding: 0;
 
}
 

	
 
.btn.badge {
 
    cursor: default !important;
 
}
 

	
 
.btn.disabled {
 
    color: #999;
 
}
 

	
 
.btn.btn-danger.disabled {
 
    color: #eee;
 
    background-color: #c77;
 
    border-color: #b66
 
}
 

	
 
.btn.btn-small {
 
    padding: 2px 6px;
 
}
 

	
 
.btn.btn-mini {
 
    padding: 0px 4px;
 
}
 

	
 
.btn.clone {
 
    padding: 5px 2px 6px 1px;
 
    margin: 0px 0px 3px -4px;
 
    -webkit-border-radius: 0px 4px 4px 0px !important;
 
    -khtml-border-radius: 0px 4px 4px 0px !important;
 
    border-radius: 0px 4px 4px 0px !important;
 
    width: 100px;
 
    text-align: center;
 
    display: inline-block;
 
    position: relative;
 
    top: -2px;
 
}
 
.btn:focus {
 
    outline: none;
 
}
 
.btn:hover {
 
    background-position: 0 -100px;
 
    text-decoration: none;
 
    color: #515151;
 
    box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25), 0 0 3px #FFFFFF !important;
 
}
 
.btn.badge:hover {
 
    box-shadow: none !important;
 
}
 
.btn.disabled:hover {
 
    background-position: 0;
 
    color: #999;
 
    text-decoration: none;
 
    box-shadow: none !important;
 
}
 

	
 
.btn.red {
 
    color: #fff;
 
    background-color: #c43c35;
 
    background-repeat: repeat-x;
 
    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-linear-gradient(top, #ee5f5b, #c43c35);
 
    background-image: -o-linear-gradient(top, #ee5f5b, #c43c35);
 
    background-image: linear-gradient(to bottom, #ee5f5b, #c43c35);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);
 
    border-color: #c43c35 #c43c35 #882a25;
 
    border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
 
}
 

	
 

	
 
.btn.blue {
 
    color: #fff;
 
    background-color: #339bb9;
 
    background-repeat: repeat-x;
 
    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-linear-gradient(top, #5bc0de, #339bb9);
 
    background-image: -o-linear-gradient(top, #5bc0de, #339bb9);
 
    background-image: linear-gradient(to bottom, #5bc0de, #339bb9);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);
 
    border-color: #339bb9 #339bb9 #22697d;
 
    border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
 
}
 

	
 
.btn.green {
 
    color: #fff;
 
    background-color: #57a957;
 
    background-repeat: repeat-x;
 
    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-linear-gradient(top, #62c462, #57a957);
 
    background-image: -o-linear-gradient(top, #62c462, #57a957);
 
    background-image: linear-gradient(to bottom, #62c462, #57a957);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);
 
    border-color: #57a957 #57a957 #3d773d;
 
    border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
 
}
 

	
 
.btn.yellow {
 
    color: #fff;
 
    background-color: #faa732;
 
    background-repeat: repeat-x;
 
    background-image: -khtml-gradient(linear, left top, left bottom, from(#fbb450), to(#f89406));
 
    background-image: -moz-linear-gradient(top, #fbb450, #f89406);
 
    background-image: -ms-linear-gradient(top, #fbb450, #f89406);
 
    background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #fbb450), color-stop(100%, #f89406));
 
    background-image: -webkit-linear-gradient(top, #fbb450, #f89406);
 
    background-image: -o-linear-gradient(top, #fbb450, #f89406);
 
    background-image: linear-gradient(to bottom, #fbb450, #f89406);
 
    filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fbb450', endColorstr='#f89406', GradientType=0);
 
    border-color: #f89406 #f89406 #ad6704;
 
    border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
 
}
 

	
 
label.disabled {
 
    color: #aaa;
 
}
 

	
 
.btn.blue.hidden {
 
    display: none;
 
}
 

	
 
.btn.active {
 
    font-weight: bold;
 
}
 

	
 
ins, div.options a:hover {
 
    text-decoration: none;
 
}
 

	
 
img,
 
#header #header-inner #quick li a:hover span.normal,
 
#content div.box div.form div.fields div.field div.textarea table td table td a,
 
#clone_url,
 
#clone_url_id
 
{
 
    border: none;
 
}
 

	
 
img.icon, .right .merge img {
 
    vertical-align: bottom;
 
}
 

	
 
#header ul#logged-user, #content div.box div.title ul.links,
 
#content div.box div.message div.dismiss,
 
#content div.box div.traffic div.legend ul {
 
    float: right;
 
    margin: 0;
 
    padding: 0;
 
}
 

	
 
#header #header-inner #home, #header #header-inner #logo,
 
#content div.box ul.left, #content div.box ol.left,
 
div#commit_history,
 
div#legend_data, div#legend_container, div#legend_choices {
 
    float: left;
 
}
 

	
 
#header #header-inner #quick li #quick_login,
 
#header #header-inner #quick li:hover ul ul,
 
#header #header-inner #quick li:hover ul ul ul,
 
#header #header-inner #quick li:hover ul ul ul ul,
 
#content #left #menu ul.closed, #content #left #menu li ul.collapsed, .yui-tt-shadow {
 
    display: none;
 
}
 

	
 
#header #header-inner #quick li:hover #quick_login,
 
#header #header-inner #quick li:hover ul, #header #header-inner #quick li li:hover ul, #header #header-inner #quick li li li:hover ul, #header #header-inner #quick li li li li:hover ul, #content #left #menu ul.opened, #content #left #menu li ul.expanded {
 
    display: block;
 
}
 

	
 
.repo-switcher .select2-choice {
 
    padding: 0px 8px 1px !important;
 
    display: block;
 
    height: 100%;
 
}
 

	
 
.repo-switcher .select2-container,
 
.repo-switcher .select2-choice,
 
.repo-switcher .select2-choice span {
 
    background: transparent !important;
 
    border: 0 !important;
 
    box-shadow: none !important;
 
    color: #FFFFFF !important;
 
}
 

	
 
.repo-switcher .select2-arrow {
 
    display: none !important;
 
}
 

	
 
.repo-switcher .select2-chosen:after {
 
    content: ' \25BE';
 
}
 

	
 
.repo-switcher-dropdown.select2-drop.select2-drop-active {
 
    xborder-color: black;
 
    -webkit-box-shadow: none;
 
    -moz-box-shadow: none;
 
    box-shadow: none;
 
    color: #fff;
 
    background-color: #576622;
 
}
 

	
 
.repo-switcher-dropdown.select2-drop.select2-drop-active .select2-results .select2-highlighted {
 
    background-color: #6388ad;
 
}
 

	
 
#content div.graph {
 
    padding: 0 10px 10px;
 
}
 

	
 
#content div.box div.title ul.links li a:hover,
 
#content div.box div.title ul.links li.ui-tabs-selected a {
 

	
 
    background: #6388ad; /* Old browsers */
 
    background: -moz-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* FF3.6+ */
 
    background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgba(255,255,255,0.1)), color-stop(100%,rgba(255,255,255,0))); /* Chrome,Safari4+ */
 
    background: -webkit-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* Chrome10+,Safari5.1+ */
 
    background: -o-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* Opera 11.10+ */
 
    background: -ms-linear-gradient(top, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* IE10+ */
 
    background: linear-gradient(to bottom, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0.1) 100%); /* W3C */
 
    /*filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#88bfe8', endColorstr='#70b0e0',GradientType=0 ); /* IE6-9 */
 
}
 

	
 
#content div.box ol.lower-roman, #content div.box ol.upper-roman, #content div.box ol.lower-alpha, #content div.box ol.upper-alpha, #content div.box ol.decimal {
 
    margin: 10px 24px 10px 44px;
 
}
 

	
 
#content div.box div.form, #content div.box div.table, #content div.box div.traffic {
 
    position: relative;
 
    clear: both;
 
    margin: 0;
 
    padding: 0 20px 10px;
 
}
 

	
 
#content div.box div.form div.fields, #login div.form, #login div.form div.fields, #register div.form, #register div.form div.fields {
 
    clear: both;
 
    overflow: hidden;
 
    margin: 0;
 
    padding: 0;
 
}
 

	
 
#content div.box div.form div.fields div.field div.label span, #login div.form div.fields div.field div.label span, #register div.form div.fields div.field div.label span {
 
    height: 1%;
 
    display: block;
 
    color: #363636;
 
    margin: 0;
 
    padding: 2px 0 0;
 
}
 

	
 
#content div.box div.form div.fields div.field div.input input.error, #login div.form div.fields div.field div.input input.error, #register div.form div.fields div.field div.input input.error {
 
    background: #FBE3E4;
 
    border-top: 1px solid #e1b2b3;
 
    border-left: 1px solid #e1b2b3;
 
    border-right: 1px solid #FBC2C4;
 
    border-bottom: 1px solid #FBC2C4;
 
}
 

	
 
#content div.box div.form div.fields div.field div.input input.success, #login div.form div.fields div.field div.input input.success, #register div.form div.fields div.field div.input input.success {
 
    background: #E6EFC2;
 
    border-top: 1px solid #cebb98;
 
    border-left: 1px solid #cebb98;
 
    border-right: 1px solid #c6d880;
 
    border-bottom: 1px solid #c6d880;
 
}
 

	
 
#content div.box-left div.form div.fields div.field div.textarea, #content div.box-right div.form div.fields div.field div.textarea, #content div.box div.form div.fields div.field div.select select, #content div.box table th.selected input, #content div.box table td.selected input {
 
    margin: 0;
 
}
 

	
 
#content div.box-left div.form div.fields div.field div.select, #content div.box-left div.form div.fields div.field div.checkboxes, #content div.box-left div.form div.fields div.field div.radios, #content div.box-right div.form div.fields div.field div.select, #content div.box-right div.form div.fields div.field div.checkboxes, #content div.box-right div.form div.fields div.field div.radios {
 
    margin: 0 0 0 0px !important;
 
    padding: 0;
 
}
 

	
 
#content div.box div.form div.fields div.field div.select, #content div.box div.form div.fields div.field div.checkboxes, #content div.box div.form div.fields div.field div.radios {
 
    margin: 0 0 0 200px;
 
    padding: 0;
 
}
 

	
 
#content div.box div.form div.fields div.field div.select a:hover, #content div.box div.form div.fields div.field div.select a.ui-selectmenu:hover, #content div.box div.action a:hover {
 
    color: #000;
 
    text-decoration: none;
 
}
 

	
 
#content div.box div.form div.fields div.field div.select a.ui-selectmenu-focus, #content div.box div.action a.ui-selectmenu-focus {
 
    border: 1px solid #666;
 
}
 

	
 
#content div.box div.form div.fields div.field div.checkboxes div.checkbox, #content div.box div.form div.fields div.field div.radios div.radio {
 
    clear: both;
 
    overflow: hidden;
 
    margin: 0;
 
    padding: 8px 0 2px;
 
}
 

	
 
#content div.box div.form div.fields div.field div.checkboxes div.checkbox input, #content div.box div.form div.fields div.field div.radios div.radio input {
 
    float: left;
 
    margin: 0;
 
}
 

	
 
#content div.box div.form div.fields div.field div.checkboxes div.checkbox label, #content div.box div.form div.fields div.field div.radios div.radio label {
 
    height: 1%;
 
    display: block;
 
    float: left;
 
    margin: 2px 0 0 4px;
 
}
 

	
 
div.form div.fields div.field div.button input,
 
#content div.box div.form div.fields div.buttons input
 
div.form div.fields div.buttons input,
 
#content div.box div.action div.button input {
 
    font-size: 11px;
 
    font-weight: 700;
 
    margin: 0;
 
}
 

	
 
input.ui-button {
 
    background: #e5e3e3 url("../images/button.png") repeat-x;
 
    border-top: 1px solid #DDD;
 
    border-left: 1px solid #c6c6c6;
 
    border-right: 1px solid #DDD;
 
    border-bottom: 1px solid #c6c6c6;
 
    color: #515151 !important;
 
    outline: none;
 
    margin: 0;
 
    padding: 6px 12px;
 
    -webkit-border-radius: 4px 4px 4px 4px;
 
    -khtml-border-radius: 4px 4px 4px 4px;
 
    border-radius: 4px 4px 4px 4px;
 
    box-shadow: 0 1px 0 #ececec;
 
    cursor: pointer;
 
}
 

	
 
input.ui-button:hover {
 
    background: #b4b4b4 url("../images/button_selected.png") repeat-x;
 
    border-top: 1px solid #ccc;
 
    border-left: 1px solid #bebebe;
 
    border-right: 1px solid #b1b1b1;
 
    border-bottom: 1px solid #afafaf;
 
}
 

	
 
div.form div.fields div.field div.highlight, #content div.box div.form div.fields div.buttons div.highlight {
 
    display: inline;
 
}
 

	
 
#content div.box div.form div.fields div.buttons, div.form div.fields div.buttons {
 
    margin: 10px 0 0 200px;
 
    padding: 0;
 
}
 

	
 
#content div.box-left div.form div.fields div.buttons, #content div.box-right div.form div.fields div.buttons, div.box-left div.form div.fields div.buttons, div.box-right div.form div.fields div.buttons {
 
    margin: 10px 0 0;
 
}
 

	
 
#content div.box table td.user, #content div.box table td.address {
 
    width: 10%;
 
    text-align: center;
 
}
 

	
 
#content div.box div.action div.button, #login div.form div.fields div.field div.input div.link, #register div.form div.fields div.field div.input div.link {
 
    text-align: right;
 
    margin: 6px 0 0;
 
    padding: 0;
 
}
 

	
 
#content div.box div.action div.button input.ui-state-hover, #login div.form div.fields div.buttons input.ui-state-hover, #register div.form div.fields div.buttons input.ui-state-hover {
 
    background: #b4b4b4 url("../images/button_selected.png") repeat-x;
 
    border-top: 1px solid #ccc;
 
    border-left: 1px solid #bebebe;
 
    border-right: 1px solid #b1b1b1;
 
    border-bottom: 1px solid #afafaf;
 
    color: #515151;
 
    margin: 0;
 
    padding: 6px 12px;
 
}
 

	
 
#content div.box div.pagination div.results, #content div.box div.pagination-wh div.results {
 
    text-align: left;
 
    float: left;
 
    margin: 0;
 
    padding: 0;
 
}
 

	
 
#content div.box div.pagination div.results span, #content div.box div.pagination-wh div.results span {
 
    height: 1%;
 
    display: block;
 
    float: left;
 
    background: #ebebeb url("../images/pager.png") repeat-x;
 
    border-top: 1px solid #dedede;
 
    border-left: 1px solid #cfcfcf;
 
    border-right: 1px solid #c4c4c4;
 
    border-bottom: 1px solid #c4c4c4;
 
    color: #4A4A4A;
 
    font-weight: 700;
 
    margin: 0;
 
    padding: 6px 8px;
 
}
 

	
 
#content div.box div.pagination ul.pager li.disabled, #content div.box div.pagination-wh a.disabled {
 
    color: #B4B4B4;
 
    padding: 6px;
 
}
 

	
 
#login, #register {
 
    width: 520px;
 
    margin: 10% auto 0;
 
    padding: 0;
 
}
 

	
 
#login div.color, #register div.color {
 
    clear: both;
 
    overflow: hidden;
 
    background: #FFF;
 
    margin: 10px auto 0;
 
    padding: 3px 3px 3px 0;
 
}
 

	
 
#login div.color a, #register div.color a {
 
    width: 20px;
 
    height: 20px;
 
    display: block;
 
    float: left;
 
    margin: 0 0 0 3px;
 
    padding: 0;
 
}
 

	
 
#login div.title h5, #register div.title h5 {
 
    color: #fff;
 
    margin: 10px;
 
    padding: 0;
 
}
 

	
 
#login div.form div.fields div.field, #register div.form div.fields div.field {
 
    clear: both;
 
    overflow: hidden;
 
    margin: 0;
 
    padding: 0 0 10px;
 
}
 

	
 
#login div.form div.fields div.field span.error-message, #register div.form div.fields div.field span.error-message {
 
    height: 1%;
 
    display: block;
 
    color: red;
 
    margin: 8px 0 0;
 
    padding: 0;
 
    max-width: 320px;
 
}
 

	
 
#login div.form div.fields div.field div.label label, #register div.form div.fields div.field div.label label {
 
    color: #000;
 
    font-weight: 700;
 
}
 

	
 
#login div.form div.fields div.field div.input, #register div.form div.fields div.field div.input {
 
    float: left;
 
    margin: 0;
 
    padding: 0;
 
}
 

	
 
#login div.form div.fields div.field div.input input.large {
 
    width: 250px;
 
}
 

	
 
#login div.form div.fields div.field div.checkbox, #register div.form div.fields div.field div.checkbox {
 
    margin: 0 0 0 184px;
 
    padding: 0;
 
}
 

	
 
#login div.form div.fields div.field div.checkbox label, #register div.form div.fields div.field div.checkbox label {
 
    color: #565656;
 
    font-weight: 700;
 
}
 

	
 
#login div.form div.fields div.buttons input, #register div.form div.fields div.buttons input {
 
    color: #000;
 
    font-size: 1em;
 
    font-weight: 700;
 
    margin: 0;
 
}
 

	
 
#changeset_content .container .wrapper, #graph_content .container .wrapper {
 
    width: 600px;
 
}
 

	
 
#changeset_content .container .date, .ac .match {
 
    font-weight: 700;
 
    padding-top: 5px;
 
    padding-bottom: 5px;
 
}
 

	
 
div#legend_container table td, div#legend_choices table td {
 
    border: none !important;
 
    height: 20px !important;
 
    padding: 0 !important;
 
}
 

	
 
.q_filter_box {
 
    -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
 
    -webkit-border-radius: 4px;
 
    border-radius: 4px;
 
    border: 0 none;
 
    margin-bottom: -4px;
 
    margin-top: -4px;
 
    padding-left: 3px;
 
}
 

	
 
#node_filter {
 
    border: 0px solid #545454;
 
    color: #AAAAAA;
 
    padding-left: 3px;
 
}
 

	
 

	
 
.group_members_wrap {
 
    min-height: 85px;
 
    padding-left: 20px;
 
}
 

	
 
.group_members .group_member {
 
    height: 30px;
 
    padding: 0px 0px 0px 0px;
 
}
 

	
 
.reviewer_status {
 
    float: left;
 
}
 

	
 
.reviewers_member {
 
    height: 15px;
 
    padding: 0px 0px 0px 10px;
 
}
 

	
 
.emails_wrap {
 
    padding: 0px 20px;
 
}
 

	
 
.emails_wrap .email_entry {
 
    height: 30px;
 
    padding: 0px 0px 0px 10px;
 
}
 
.emails_wrap .email_entry .email {
 
    float: left
 
}
 
.emails_wrap .email_entry .email_action {
 
    float: left
 
}
 

	
 
.ips_wrap {
 
    padding: 0px 20px;
 
}
 

	
 
.ips_wrap .ip_entry {
 
    height: 30px;
 
    padding: 0px 0px 0px 10px;
 
}
 
.ips_wrap .ip_entry .ip {
 
    float: left
 
}
 
.ips_wrap .ip_entry .ip_action {
 
    float: left
 
}
 

	
 

	
 
/*README STYLE*/
 

	
 
div.readme {
 
    padding: 0px;
 
}
 

	
 
div.readme h2 {
 
    font-weight: normal;
 
}
 

	
 
div.readme .readme_box {
 
    background-color: #fafafa;
 
}
 

	
 
div.readme .readme_box {
 
    clear: both;
 
    overflow: hidden;
 
    margin: 0;
 
    padding: 0 20px 10px;
 
}
 

	
 
div.readme .readme_box h1, div.readme .readme_box h2, div.readme .readme_box h3, div.readme .readme_box h4, div.readme .readme_box h5, div.readme .readme_box h6 {
 
    border-bottom: 0 !important;
 
    margin: 0 !important;
 
    padding: 0 !important;
 
    line-height: 1.5em !important;
 
}
 

	
 

	
 
div.readme .readme_box h1:first-child {
 
    padding-top: .25em !important;
 
}
 

	
 
div.readme .readme_box h2, div.readme .readme_box h3 {
 
    margin: 1em 0 !important;
 
}
 

	
 
div.readme .readme_box h2 {
 
    margin-top: 1.5em !important;
 
    border-top: 4px solid #e0e0e0 !important;
 
    padding-top: .5em !important;
 
}
 

	
 
div.readme .readme_box p {
 
    color: black !important;
 
    margin: 1em 0 !important;
 
    line-height: 1.5em !important;
 
}
 

	
 
div.readme .readme_box ul {
 
    list-style: disc !important;
 
    margin: 1em 0 1em 2em !important;
 
}
 

	
 
div.readme .readme_box ol {
 
    list-style: decimal;
 
    margin: 1em 0 1em 2em !important;
 
}
 

	
 
div.readme .readme_box code {
 
    font-size: 12px !important;
 
    background-color: ghostWhite !important;
 
    color: #444 !important;
 
    padding: 0 .2em !important;
 
    border: 1px solid #dedede !important;
 
}
 

	
 
div.readme .readme_box pre code {
 
    padding: 0 !important;
 
    font-size: 12px !important;
 
    background-color: #eee !important;
 
    border: none !important;
 
}
 

	
 
div.readme .readme_box pre {
 
    margin: 1em 0;
 
    font-size: 12px;
 
    background-color: #eee;
 
    border: 1px solid #ddd;
 
    padding: 5px;
 
    color: #444;
 
    overflow: auto;
 
    -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
 
    -webkit-border-radius: 3px;
 
    border-radius: 3px;
 
}
 

	
 
div.readme .readme_box table {
 
    display: table;
 
    border-collapse: separate;
 
    border-spacing: 2px;
 
    border-color: gray;
 
    width: auto !important;
 
}
 

	
 

	
 
/** RST STYLE **/
 

	
 

	
 
div.rst-block {
 
    padding: 0px;
 
}
 

	
 
div.rst-block h2 {
 
    font-weight: normal;
 
}
 

	
 
div.rst-block {
 
    background-color: #fafafa;
 
}
 

	
 
div.rst-block {
 
    clear: both;
 
    overflow: hidden;
 
    margin: 0;
 
    padding: 0 20px 10px;
 
}
 

	
 
div.rst-block  h1, div.rst-block  h2, div.rst-block  h3, div.rst-block  h4, div.rst-block  h5, div.rst-block  h6 {
 
    border-bottom: 0 !important;
 
    margin: 0 !important;
 
    padding: 0 !important;
 
    line-height: 1.5em !important;
 
}
 

	
 

	
 
div.rst-block  h1:first-child {
 
    padding-top: .25em !important;
 
}
 

	
 
div.rst-block  h2, div.rst-block  h3 {
 
    margin: 1em 0 !important;
 
}
 

	
 
div.rst-block  h2 {
 
    margin-top: 1.5em !important;
 
    border-top: 4px solid #e0e0e0 !important;
 
    padding-top: .5em !important;
 
}
 

	
 
div.rst-block  p {
 
    color: black !important;
 
    margin: 1em 0 !important;
 
    line-height: 1.5em !important;
 
}
 

	
 
div.rst-block  ul {
 
    list-style: disc !important;
 
    margin: 1em 0 1em 2em !important;
 
}
 

	
 
div.rst-block  ol {
 
    list-style: decimal;
 
    margin: 1em 0 1em 2em !important;
 
}
 

	
 
div.rst-block  code {
 
    font-size: 12px !important;
 
    background-color: ghostWhite !important;
 
    color: #444 !important;
 
    padding: 0 .2em !important;
 
    border: 1px solid #dedede !important;
 
}
 

	
 
div.rst-block  pre code {
 
    padding: 0 !important;
 
    font-size: 12px !important;
 
    background-color: #eee !important;
 
    border: none !important;
 
}
 

	
 
div.rst-block  pre {
 
    margin: 1em 0;
 
    font-size: 12px;
 
    background-color: #eee;
 
    border: 1px solid #ddd;
 
    padding: 5px;
 
    color: #444;
 
    overflow: auto;
 
    -webkit-box-shadow: rgba(0,0,0,0.07) 0 1px 2px inset;
 
    -webkit-border-radius: 3px;
 
    border-radius: 3px;
 
}
 

	
 

	
 
/** comment main **/
 
.comments {
 
    padding: 10px 20px;
 
    max-width: 978px;
 
}
 

	
 
.comments .comment {
 
    border: 1px solid #ddd;
 
    margin-top: 10px;
 
    -webkit-border-radius: 4px;
 
    border-radius: 4px;
 
}
 

	
 
.comments .comment .meta {
 
    background: #f8f8f8;
 
    padding: 4px;
 
    border-bottom: 1px solid #ddd;
 
    min-height: 18px;
 
    overflow: auto;
 
}
 

	
 
.comments .comment .meta img {
 
    vertical-align: middle;
 
}
 

	
 
.comments .comment .meta .user {
 
    font-weight: bold;
 
    float: left;
 
    padding: 4px 2px 2px 2px;
 
}
 

	
 
.comments .comment .meta .date {
 
    float: left;
 
    padding: 4px 4px 0px 4px;
 
}
 

	
 
.comments .comment .text {
 
    background-color: #FAFAFA;
 
}
 
.comment .text div.rst-block p {
 
    margin: 0.5em 0px !important;
 
}
 

	
 
.comments .comments-number,
 
.pr-comments-number {
 
    margin: 5px;
 
    padding: 0px 0px 10px 0px;
 
    font-weight: bold;
 
    color: #666;
 
    font-size: 16px;
 
}
 

	
 
/** comment form **/
 

	
 
.status-block {
 
    margin: 5px;
 
    clear: both
 
}
 

	
 

	
 
div.comment-form {
 
    margin-top: 20px;
 
}
 

	
 
.comment-form strong {
 
    display: block;
 
    margin-bottom: 15px;
 
}
 

	
 
.comment-form textarea {
 
    width: 100%;
 
    height: 100px;
 
    font-family: Consolas, Monaco, Inconsolata, Liberation Mono, monospace;
 
}
 

	
 
form.comment-form {
 
    margin-top: 10px;
 
    margin-left: 10px;
 
}
 

	
 
.comment-inline-form .comment-block-ta,
 
.comment-form .comment-block-ta {
 
    border: 1px solid #ccc;
 
    border-radius: 3px;
 
    box-sizing: border-box;
 
}
 

	
 
.comment-form-submit {
 
    margin-top: 5px;
 
    margin-left: 525px;
 
}
 

	
 
.file-comments {
 
    display: none;
 
}
 

	
 
.comment-form .comment {
 
    margin-left: 10px;
 
}
 

	
 
.comment-form .comment-help {
 
    padding: 5px 5px 5px 5px;
 
    color: #666;
 
}
 
.comment-form .comment-help .preview-btn,
 
.comment-form .comment-help .edit-btn {
 
    float: right;
 
    margin: -6px 0px 0px 0px;
 
}
 

	
 
.comment-form .preview-box.unloaded,
 
.comment-inline-form .preview-box.unloaded {
 
    height: 50px;
 
    text-align: center;
 
    padding: 20px;
 
    background-color: #fafafa;
 
}
 

	
 
.comment-form .comment-button {
 
    padding-top: 5px;
 
}
 

	
 
.add-another-button {
 
    margin-left: 10px;
 
    margin-top: 10px;
 
    margin-bottom: 10px;
 
}
 

	
 
.comment .buttons {
 
    float: right;
 
    margin: -1px 0px 0px 0px;
 
}
 

	
 

	
 
.show-inline-comments {
 
    position: relative;
 
    top: 1px
 
}
 

	
 
/** comment inline form **/
 
.comment-inline-form {
 
    margin: 4px;
 
    max-width: 978px;
 
}
 
.comment-inline-form .submitting-overlay {
 
    display: none;
 
    height: 0;
 
    text-align: center;
 
    font-size: 16px;
 
    opacity: 0.5;
 
}
 

	
 
.comment-inline-form .clearfix,
 
.comment-form .clearfix {
 
    background: #EEE;
 
    -webkit-border-radius: 4px;
 
    border-radius: 4px;
 
    padding: 5px;
 
    margin: 0px;
 
}
 

	
 
div.comment-inline-form {
 
    padding: 4px 0px 6px 0px;
 
}
 

	
 
.comment-inline-form strong {
 
    display: block;
 
    margin-bottom: 15px;
 
}
 

	
 
.comment-inline-form textarea {
 
    width: 100%;
 
    height: 100px;
 
    font-family: Consolas, Monaco, Inconsolata, Liberation Mono, monospace;
 
}
 

	
 
form.comment-inline-form {
 
    margin-top: 10px;
 
    margin-left: 10px;
 
}
 

	
 
.comment-inline-form-submit {
 
    margin-top: 5px;
 
    margin-left: 525px;
 
}
 

	
 
.file-comments {
 
    display: none;
 
}
 

	
 
.comment-inline-form .comment {
 
    margin-left: 10px;
 
}
 

	
 
.comment-inline-form .comment-help {
 
    padding: 5px 5px 5px 5px;
 
    color: #666;
 
}
 

	
 
.comment-inline-form .comment-help .preview-btn,
 
.comment-inline-form .comment-help .edit-btn {
 
    float: right;
 
    margin: -6px 0px 0px 0px;
 
}
 

	
 
.comment-inline-form .comment-button {
 
    padding-top: 5px;
 
}
 

	
 
/** comment inline **/
 
.inline-comments {
 
    padding: 10px 20px;
 
}
 

	
 
.inline-comments div.rst-block {
 
    clear: both;
 
    overflow: hidden;
 
    margin: 0;
 
    padding: 0 20px 0px;
 
}
 
.inline-comments .comment {
 
    max-width: 978px;
 
    border: 1px solid #ddd;
 
    -webkit-border-radius: 4px;
 
    border-radius: 4px;
 
    margin: 3px 3px 5px 5px;
 
    background-color: #FAFAFA;
 
}
 
.inline-comments .add-comment {
 
    padding: 2px 4px 8px 5px;
 
}
 

	
 
.inline-comments .comment-wrapp {
 
    padding: 1px;
 
}
 
.inline-comments .comment .meta {
 
    background: #f8f8f8;
 
    padding: 4px;
 
    border-bottom: 1px solid #ddd;
 
    min-height: 20px;
 
    overflow: auto;
 
}
 

	
 
.inline-comments .comment .meta img {
 
    vertical-align: middle;
 
}
 

	
 
.inline-comments .comment .meta .user {
 
    font-weight: bold;
 
    float: left;
 
    padding: 3px;
 
}
 

	
 
.inline-comments .comment .meta .date {
 
    float: left;
 
    padding: 3px;
 
}
 

	
 
.inline-comments .comment .text {
 
    background-color: #FAFAFA;
 
}
 

	
 
.inline-comments .comments-number {
 
    padding: 0px 0px 10px 0px;
 
    font-weight: bold;
 
    color: #666;
 
    font-size: 16px;
 
}
 
.inline-comments-button .add-comment {
 
    margin: 2px 0px 8px 5px !important
 
}
 

	
 
.notification-paginator {
 
    padding: 0px 0px 4px 16px;
 
}
 

	
 
#context-pages .pull-request span,
 
.menu_link_notifications {
 
    padding: 4px 4px !important;
 
    text-align: center;
 
    color: #888 !important;
 
    background-color: #DEDEDE !important;
 
    border-radius: 4px !important;
 
    -webkit-border-radius: 4px !important;
 
}
 

	
 
#context-pages .forks span,
 
.menu_link_notifications {
 
    padding: 4px 4px !important;
 
    text-align: center;
 
    color: #888 !important;
 
    background-color: #DEDEDE !important;
 
    border-radius: 4px !important;
 
    -webkit-border-radius: 4px !important;
 
}
 

	
 

	
 
.notification-header {
 
    padding-top: 6px;
 
}
 
.notification-header .desc {
 
    font-size: 16px;
 
    height: 24px;
 
    float: left
 
}
 
.notification-list .container.unread {
 
    background: none repeat scroll 0 0 rgba(255, 255, 180, 0.6);
 
}
 
.notification-header .gravatar {
 
    background: none repeat scroll 0 0 transparent;
 
    padding: 0px 0px 0px 8px;
 
}
 
.notification-list .container .notification-header .desc {
 
    font-weight: bold;
 
    font-size: 17px;
 
}
 
.notification-table {
 
    border: 1px solid #ccc;
 
    -webkit-border-radius: 6px 6px 6px 6px;
 
    border-radius: 6px 6px 6px 6px;
 
    clear: both;
 
    margin: 0px 20px 0px 20px;
 
}
 
.notification-header .delete-notifications {
 
    float: right;
 
    padding-top: 8px;
 
    cursor: pointer;
 
}
 
.notification-header .read-notifications {
 
    float: right;
 
    padding-top: 8px;
 
    cursor: pointer;
 
}
 
.notification-subject {
 
    clear: both;
 
    border-bottom: 1px solid #eee;
 
    padding: 5px 0px 5px 38px;
 
}
 

	
 
.notification-body {
 
    clear: both;
 
    margin: 34px 2px 2px 8px
 
}
 

	
 
/****
 
PULL REQUESTS
 
*****/
 
.pullrequests_section_head {
 
    padding: 10px 10px 10px 0px;
 
    font-size: 16px;
 
    font-weight: bold;
 
}
 

	
 
div.pr-details-title.closed,
 
#pullrequests_container li.closed div div
 
 {
 
    color: #555;
 
    background: #eee;
 
}
 

	
 
div.pr-title {
 
    font-size: 1.6em;
 
}
 
div.pr-details-title {
 
    font-size: 1.6em;
 
    padding: 5px 0px 5px 10px;
 
}
 

	
 
div.pr {
 
    margin: 0px 20px;
 
    padding: 4px 4px;
 
}
 
div.pr-desc {
 
    margin: 0px 20px;
 
}
 
div.pr-closed {
 
    background-color: #eee;
 
}
 

	
 
span.pr-closed-tag {
 
    margin-bottom: 1px;
 
    margin-right: 1px;
 
    padding: 1px 3px;
 
    font-size: 10px;
 
    padding: 1px 3px 1px 3px;
 
    font-size: 10px;
 
    color: #577632;
 
    white-space: nowrap;
 
    -webkit-border-radius: 4px;
 
    border-radius: 4px;
 
    border: 1px solid #d9e8f8;
 
    line-height: 1.5em;
 
}
 

	
 
.pr-box {
 
    max-width: 978px;
 
}
 

	
 
#s2id_org_ref,
 
#s2id_other_ref,
 
#s2id_org_repo,
 
#s2id_other_repo {
 
    min-width: 150px;
 
    margin: 5px;
 
}
 

	
 
#pr-summary .msg-div {
 
    margin: 5px 0;
 
}
 

	
 
/****
 
  PERMS
 
*****/
 
#perms .perms_section_head {
 
    padding: 10px 10px 10px 0px;
 
    font-size: 16px;
 
    font-weight: bold;
 
    text-transform: capitalize;
 
}
 

	
 
#perms .perm_tag {
 
    padding: 1px 3px 1px 3px;
 
    font-size: 10px;
 
    font-weight: bold;
 
    text-transform: uppercase;
 
    white-space: nowrap;
 
    -webkit-border-radius: 3px;
 
    border-radius: 3px;
 
}
 

	
 
#perms .perm_tag.admin {
 
    background-color: #B94A48;
 
    color: #ffffff;
 
}
 

	
 
#perms .perm_tag.write {
 
    background-color: #DB7525;
 
    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-collapse {
 
    text-align: center;
 
    margin-bottom: -15px;
 
}
 
.diff-collapse-button {
 
    cursor: pointer;
 
    color: #666;
 
    font-size: 16px;
 
}
 
.diff-container {
 

	
 
}
 

	
 
.diff-container.hidden {
 
    display: none;
 
    overflow: hidden;
 
}
 

	
 

	
 
div.diffblock {
 
    overflow: auto;
 
    padding: 0px;
 
    border: 1px solid #ccc;
 
    background: #f8f8f8;
 
    font-size: 100%;
 
    line-height: 100%;
 
    /* new */
 
    line-height: 125%;
 
    -webkit-border-radius: 6px 6px 0px 0px;
 
    border-radius: 6px 6px 0px 0px;
 
}
 
div.diffblock.margined {
 
    margin: 0px 20px 0px 20px;
 
    overflow: hidden;
 
}
 
div.diffblock .code-header {
 
    border-bottom: 1px solid #CCCCCC;
 
    background: #EEEEEE;
 
    padding: 10px 0 10px 0;
 
    min-height: 14px;
 
}
 

	
 
div.diffblock .code-header.banner {
 
    border-bottom: 1px solid #CCCCCC;
 
    background: #EEEEEE;
 
    height: 14px;
 
    margin: 0;
 
    padding: 3px 100px 11px 100px;
 
}
 

	
 
div.diffblock .code-header-title {
 
    padding: 0px 0px 10px 5px !important;
 
    margin: 0 !important;
 
}
 
div.diffblock .code-header .hash {
 
    float: left;
 
    padding: 2px 0 0 2px;
 
}
 
div.diffblock .code-header .date {
 
    float: left;
 
    text-transform: uppercase;
 
    padding: 2px 0px 0px 2px;
 
}
 
div.diffblock .code-header div {
 
    margin-left: 4px;
 
    font-weight: bold;
 
    font-size: 14px;
 
}
 

	
 
div.diffblock .parents {
 
    float: left;
 
    min-height: 26px;
 
    font-size: 10px;
 
    font-weight: 400;
 
    vertical-align: middle;
 
    padding: 0px 2px 2px 2px;
 
    background-color: #eeeeee;
 
    border-bottom: 1px solid #CCCCCC;
 
}
 

	
 
div.diffblock .children {
 
    float: right;
 
    min-height: 26px;
 
    font-size: 10px;
 
    font-weight: 400;
 
    vertical-align: middle;
 
    text-align: right;
 
    padding: 0px 2px 2px 2px;
 
    background-color: #eeeeee;
 
    border-bottom: 1px solid #CCCCCC;
 
}
 

	
 
div.diffblock .code-body {
 
    background: #FFFFFF;
 
    clear: both;
 
}
 
div.diffblock pre.raw {
 
    background: #FFFFFF;
 
    color: #000000;
 
}
 
table.code-difftable {
 
    border-collapse: collapse;
 
    width: 99%;
 
    border-radius: 0px !important;
 
}
 
table.code-difftable td {
 
    padding: 0 !important;
 
    background: none !important;
 
    border: 0 !important;
 
    vertical-align: baseline !important
 
}
 
table.code-difftable .context {
 
    background: none repeat scroll 0 0 #DDE7EF;
 
    color: #999;
 
}
 
table.code-difftable .add {
 
    background: none repeat scroll 0 0 #DDFFDD;
 
}
 
table.code-difftable .add ins {
 
    background: none repeat scroll 0 0 #AAFFAA;
 
    text-decoration: none;
 
}
 
table.code-difftable .del {
 
    background: none repeat scroll 0 0 #FFDDDD;
 
}
 
table.code-difftable .del del {
 
    background: none repeat scroll 0 0 #FFAAAA;
 
    text-decoration: none;
 
}
 

	
 
table.code-highlighttable div.code-highlight pre u:before,
 
table.code-difftable td.code pre u:before {
 
    content: "\21a6";
 
    display: inline-block;
 
    width: 0;
 
}
 
table.code-highlighttable div.code-highlight pre u,
 
table.code-difftable td.code pre u {
 
    color: rgba(0,0,0,0.15);
 
}
 
table.code-highlighttable div.code-highlight pre i,
 
table.code-difftable td.code pre i {
 
    border-style: solid;
 
    border-left-width: 1px;
 
    color: rgba(0,0,0,0.15);
 
}
 

	
 
/** LINE NUMBERS **/
 
table.code-difftable .lineno {
 
    padding-left: 2px;
 
    padding-right: 2px !important;
 
    text-align: right;
 
    width: 30px;
 
    -moz-user-select: none;
 
    -webkit-user-select: none;
 
    border-right: 1px solid #CCC !important;
 
    border-left: 0px solid #CCC !important;
 
    border-top: 0px solid #CCC !important;
 
    border-bottom: none !important;
 
    vertical-align: middle !important;
 
}
 
table.code-difftable .lineno.new {
 
}
 
table.code-difftable .lineno.old {
 
}
 
table.code-difftable .lineno a {
 
    color: #aaa !important;
 
    font: 11px Consolas, Monaco, Inconsolata, Liberation Mono, monospace !important;
 
    letter-spacing: -1px;
 
    text-align: right;
 
    padding-right: 2px;
 
    cursor: pointer;
 
    display: block;
 
    width: 30px;
 
}
 

	
 
table.code-difftable .lineno-inline {
 
    background: none repeat scroll 0 0 #FFF !important;
 
    padding-left: 2px;
 
    padding-right: 2px;
 
    text-align: right;
 
    width: 30px;
 
    -moz-user-select: none;
 
    -webkit-user-select: none;
 
}
 

	
 
/** CODE **/
 
table.code-difftable .code {
 
    display: block;
 
    width: 100%;
 
}
 
table.code-difftable .code td {
 
    margin: 0;
 
    padding: 0;
 
}
 
table.code-difftable .code pre {
 
    margin: 0;
 
    padding: 0;
 
    min-height: 17px;
 
    line-height: 17px;
 
    white-space: pre-wrap;
 
}
 

	
 
.add-bubble {
 
    display: none;
 
    float: left;
 
    width: 0px;
 
    height: 0px;
 
}
 

	
 
tr.line.add td.code:hover .add-bubble,
 
tr.line.del td.code:hover .add-bubble,
 
tr.line.unmod td.code:hover .add-bubble {
 
    display: inherit;
 
}
 

	
 
.add-bubble div {
 
    position: relative;
 
    left: -32px;
 
    width: 32px;
 
    top: -8px;
 
    height: 32px;
 
    background: url("../images/icons/comment_add.png") no-repeat 100% 50%;
 
    cursor: pointer;
 
}
 

	
 
div.comment:target>.comment-wrapp {
 
    border: solid 2px #ee0 !important;
 
}
 

	
 
.lineno:target a {
 
    border: solid 2px #ee0 !important;
 
    margin: -2px;
 
}
 

	
 
.btn-image-diff-show,
 
.btn-image-diff-swap {
 
    margin: 5px;
 
}
 

	
 
.img-diff {
 
    max-width: 45%;
 
    height: auto;
 
    margin: 5px;
 
    /* http://lea.verou.me/demos/css3-patterns.html */
 
    background-image:
 
        linear-gradient(45deg, #888 25%, transparent 25%, transparent),
 
        linear-gradient(-45deg, #888 25%, transparent 25%, transparent),
 
        linear-gradient(45deg, transparent 75%, #888 75%),
 
        linear-gradient(-45deg, transparent 75%, #888 75%);
 
    background-size: 10px 10px;
 
    background-color: #999;
 
}
 

	
 
.img-preview {
 
    max-width: 100%;
 
    height: auto;
 
    margin: 5px;
 
}
 

	
 
div.prev-next-comment div.prev-comment,
 
div.prev-next-comment div.next-comment {
 
    display: inline-block;
 
    min-width: 150px;
 
    margin: 3px 6px;
 
}
 

	
 
#help_kb {
 
    display: none;
 
}
 

	
 
.repo-switcher-dropdown .select2-result-label span.repo-icons {
 
    margin-left: -12px;
 
}
kallithea/public/js/base.js
Show inline comments
 
/**
 
Kallithea JS Files
 
**/
 

	
 
if (typeof console == "undefined" || typeof console.log == "undefined"){
 
    console = { log: function() {} }
 
}
 

	
 
/**
 
 * INJECT .format function into String
 
 * Usage: "My name is {0} {1}".format("Johny","Bravo")
 
 * Return "My name is Johny Bravo"
 
 * Inspired by https://gist.github.com/1049426
 
 */
 
String.prototype.format = function() {
 
    function format() {
 
        var str = this;
 
        var len = arguments.length+1;
 
        var safe = undefined;
 
        var arg = undefined;
 

	
 
        // For each {0} {1} {n...} replace with the argument in that position.  If
 
        // the argument is an object or an array it will be stringified to JSON.
 
        for (var i=0; i < len; arg = arguments[i++]) {
 
            safe = typeof arg === 'object' ? JSON.stringify(arg) : arg;
 
            str = str.replace(RegExp('\\{'+(i-1)+'\\}', 'g'), safe);
 
        }
 
        return str;
 
    }
 

	
 
    // Save a reference of what may already exist under the property native.
 
    // Allows for doing something like: if("".format.native) { /* use native */ }
 
    format.native = String.prototype.format;
 

	
 
    // Replace the prototype property
 
    return format;
 

	
 
}();
 

	
 
String.prototype.strip = function(char) {
 
    if(char === undefined){
 
        char = '\\s';
 
    }
 
    return this.replace(new RegExp('^'+char+'+|'+char+'+$','g'), '');
 
}
 

	
 
String.prototype.lstrip = function(char) {
 
    if(char === undefined){
 
        char = '\\s';
 
    }
 
    return this.replace(new RegExp('^'+char+'+'),'');
 
}
 

	
 
String.prototype.rstrip = function(char) {
 
    if(char === undefined){
 
        char = '\\s';
 
    }
 
    return this.replace(new RegExp(''+char+'+$'),'');
 
}
 

	
 
/* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf#Polyfill
 
   under MIT license / public domain, see
 
   https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses */
 
if(!Array.prototype.indexOf) {
 
    Array.prototype.indexOf = function (searchElement, fromIndex) {
 
        if ( this === undefined || this === null ) {
 
            throw new TypeError( '"this" is null or not defined' );
 
        }
 

	
 
        var length = this.length >>> 0; // Hack to convert object.length to a UInt32
 

	
 
        fromIndex = +fromIndex || 0;
 

	
 
        if (Math.abs(fromIndex) === Infinity) {
 
            fromIndex = 0;
 
        }
 

	
 
        if (fromIndex < 0) {
 
            fromIndex += length;
 
            if (fromIndex < 0) {
 
                fromIndex = 0;
 
            }
 
        }
 

	
 
        for (;fromIndex < length; fromIndex++) {
 
            if (this[fromIndex] === searchElement) {
 
                return fromIndex;
 
            }
 
        }
 

	
 
        return -1;
 
    };
 
}
 

	
 
/* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter#Compatibility
 
   under MIT license / public domain, see
 
   https://developer.mozilla.org/en-US/docs/MDN/About#Copyrights_and_licenses */
 
if (!Array.prototype.filter)
 
{
 
    Array.prototype.filter = function(fun /*, thisArg */)
 
    {
 
        "use strict";
 

	
 
        if (this === void 0 || this === null)
 
            throw new TypeError();
 

	
 
        var t = Object(this);
 
        var len = t.length >>> 0;
 
        if (typeof fun !== "function")
 
            throw new TypeError();
 

	
 
        var res = [];
 
        var thisArg = arguments.length >= 2 ? arguments[1] : void 0;
 
        for (var i = 0; i < len; i++)
 
        {
 
            if (i in t)
 
            {
 
                var val = t[i];
 

	
 
                // NOTE: Technically this should Object.defineProperty at
 
                //       the next index, as push can be affected by
 
                //       properties on Object.prototype and Array.prototype.
 
                //       But that method's new, and collisions should be
 
                //       rare, so use the more-compatible alternative.
 
                if (fun.call(thisArg, val, i, t))
 
                    res.push(val);
 
            }
 
        }
 

	
 
        return res;
 
    };
 
}
 

	
 
/**
 
 * A customized version of PyRoutes.JS from https://pypi.python.org/pypi/pyroutes.js/
 
 * which is copyright Stephane Klein and was made available under the BSD License.
 
 *
 
 * Usage pyroutes.url('mark_error_fixed',{"error_id":error_id}) // /mark_error_fixed/<error_id>
 
 */
 
var pyroutes = (function() {
 
    // access global map defined in special file pyroutes
 
    var matchlist = PROUTES_MAP;
 
    var sprintf = (function() {
 
        function get_type(variable) {
 
            return Object.prototype.toString.call(variable).slice(8, -1).toLowerCase();
 
        }
 
        function str_repeat(input, multiplier) {
 
            for (var output = []; multiplier > 0; output[--multiplier] = input) {/* do nothing */}
 
            return output.join('');
 
        }
 

	
 
        var str_format = function() {
 
            if (!str_format.cache.hasOwnProperty(arguments[0])) {
 
                str_format.cache[arguments[0]] = str_format.parse(arguments[0]);
 
            }
 
            return str_format.format.call(null, str_format.cache[arguments[0]], arguments);
 
        };
 

	
 
        str_format.format = function(parse_tree, argv) {
 
            var cursor = 1, tree_length = parse_tree.length, node_type = '', arg, output = [], i, k, match, pad, pad_character, pad_length;
 
            for (i = 0; i < tree_length; i++) {
 
                node_type = get_type(parse_tree[i]);
 
                if (node_type === 'string') {
 
                    output.push(parse_tree[i]);
 
                }
 
                else if (node_type === 'array') {
 
                    match = parse_tree[i]; // convenience purposes only
 
                    if (match[2]) { // keyword argument
 
                        arg = argv[cursor];
 
                        for (k = 0; k < match[2].length; k++) {
 
                            if (!arg.hasOwnProperty(match[2][k])) {
 
                                throw(sprintf('[sprintf] property "%s" does not exist', match[2][k]));
 
                            }
 
                            arg = arg[match[2][k]];
 
                        }
 
                    }
 
                    else if (match[1]) { // positional argument (explicit)
 
                        arg = argv[match[1]];
 
                    }
 
                    else { // positional argument (implicit)
 
                        arg = argv[cursor++];
 
                    }
 

	
 
                    if (/[^s]/.test(match[8]) && (get_type(arg) != 'number')) {
 
                        throw(sprintf('[sprintf] expecting number but found %s', get_type(arg)));
 
                    }
 
                    switch (match[8]) {
 
                        case 'b': arg = arg.toString(2); break;
 
                        case 'c': arg = String.fromCharCode(arg); break;
 
                        case 'd': arg = parseInt(arg, 10); break;
 
                        case 'e': arg = match[7] ? arg.toExponential(match[7]) : arg.toExponential(); break;
 
                        case 'f': arg = match[7] ? parseFloat(arg).toFixed(match[7]) : parseFloat(arg); break;
 
                        case 'o': arg = arg.toString(8); break;
 
                        case 's': arg = ((arg = String(arg)) && match[7] ? arg.substring(0, match[7]) : arg); break;
 
                        case 'u': arg = Math.abs(arg); break;
 
                        case 'x': arg = arg.toString(16); break;
 
                        case 'X': arg = arg.toString(16).toUpperCase(); break;
 
                    }
 
                    arg = (/[def]/.test(match[8]) && match[3] && arg >= 0 ? '+'+ arg : arg);
 
                    pad_character = match[4] ? match[4] == '0' ? '0' : match[4].charAt(1) : ' ';
 
                    pad_length = match[6] - String(arg).length;
 
                    pad = match[6] ? str_repeat(pad_character, pad_length) : '';
 
                    output.push(match[5] ? arg + pad : pad + arg);
 
                }
 
            }
 
            return output.join('');
 
        };
 

	
 
        str_format.cache = {};
 

	
 
        str_format.parse = function(fmt) {
 
            var _fmt = fmt, match = [], parse_tree = [], arg_names = 0;
 
            while (_fmt) {
 
                if ((match = /^[^\x25]+/.exec(_fmt)) !== null) {
 
                    parse_tree.push(match[0]);
 
                }
 
                else if ((match = /^\x25{2}/.exec(_fmt)) !== null) {
 
                    parse_tree.push('%');
 
                }
 
                else if ((match = /^\x25(?:([1-9]\d*)\$|\(([^\)]+)\))?(\+)?(0|'[^$])?(-)?(\d+)?(?:\.(\d+))?([b-fosuxX])/.exec(_fmt)) !== null) {
 
                    if (match[2]) {
 
                        arg_names |= 1;
 
                        var field_list = [], replacement_field = match[2], field_match = [];
 
                        if ((field_match = /^([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
 
                            field_list.push(field_match[1]);
 
                            while ((replacement_field = replacement_field.substring(field_match[0].length)) !== '') {
 
                                if ((field_match = /^\.([a-z_][a-z_\d]*)/i.exec(replacement_field)) !== null) {
 
                                    field_list.push(field_match[1]);
 
                                }
 
                                else if ((field_match = /^\[(\d+)\]/.exec(replacement_field)) !== null) {
 
                                    field_list.push(field_match[1]);
 
                                }
 
                                else {
 
                                    throw('[sprintf] huh?');
 
                                }
 
                            }
 
                        }
 
                        else {
 
                            throw('[sprintf] huh?');
 
                        }
 
                        match[2] = field_list;
 
                    }
 
                    else {
 
                        arg_names |= 2;
 
                    }
 
                    if (arg_names === 3) {
 
                        throw('[sprintf] mixing positional and named placeholders is not (yet) supported');
 
                    }
 
                    parse_tree.push(match);
 
                }
 
                else {
 
                    throw('[sprintf] huh?');
 
                }
 
                _fmt = _fmt.substring(match[0].length);
 
            }
 
            return parse_tree;
 
        };
 

	
 
        return str_format;
 
    })();
 

	
 
    var vsprintf = function(fmt, argv) {
 
        argv.unshift(fmt);
 
        return sprintf.apply(null, argv);
 
    };
 
    return {
 
        'url': function(route_name, params) {
 
            var result = route_name;
 
            if (typeof(params) != 'object'){
 
                params = {};
 
            }
 
            if (matchlist.hasOwnProperty(route_name)) {
 
                var route = matchlist[route_name];
 
                // param substitution
 
                for(var i=0; i < route[1].length; i++) {
 
                   if (!params.hasOwnProperty(route[1][i]))
 
                        throw new Error(route[1][i] + ' missing in "' + route_name + '" route generation');
 
                }
 
                result = sprintf(route[0], params);
 

	
 
                var ret = [];
 
                //extra params => GET
 
                for(param in params){
 
                    if (route[1].indexOf(param) == -1){
 
                        ret.push(encodeURIComponent(param) + "=" + encodeURIComponent(params[param]));
 
                    }
 
                }
 
                var _parts = ret.join("&");
 
                if(_parts){
 
                    result = result +'?'+ _parts
 
                }
 
            }
 

	
 
            return result;
 
        },
 
        'register': function(route_name, route_tmpl, req_params) {
 
            if (typeof(req_params) != 'object') {
 
                req_params = [];
 
            }
 
            //fix escape
 
            route_tmpl = unescape(route_tmpl);
 
            keys = [];
 
            for (var i=0; i < req_params.length; i++) {
 
                keys.push(req_params[i])
 
            }
 
            matchlist[route_name] = [
 
                route_tmpl,
 
                keys
 
            ]
 
        },
 
        '_routes': function(){
 
            return matchlist;
 
        }
 
    }
 
})();
 

	
 

	
 
/**
 
 * GLOBAL YUI Shortcuts
 
 */
 
var YUC = YAHOO.util.Connect;
 
var YUD = YAHOO.util.Dom;
 
var YUE = YAHOO.util.Event;
 
var YUQ = YAHOO.util.Selector.query;
 

	
 
/* Invoke all functions in callbacks */
 
var _run_callbacks = function(callbacks){
 
    if (callbacks !== undefined){
 
        var _l = callbacks.length;
 
        for (var i=0;i<_l;i++){
 
            var func = callbacks[i];
 
            if(typeof(func)=='function'){
 
                try{
 
                    func();
 
                }catch (err){};
 
            }
 
        }
 
    }
 
}
 

	
 
/**
 
 * turns objects into GET query string
 
 */
 
var _toQueryString = function(o) {
 
    if(typeof o !== 'object') {
 
        return false;
 
    }
 
    var _p, _qs = [];
 
    for(_p in o) {
 
        _qs.push(encodeURIComponent(_p) + '=' + encodeURIComponent(o[_p]));
 
    }
 
    return _qs.join('&');
 
};
 

	
 
/**
 
 * Load HTML into DOM using Ajax
 
 *
 
 * @param $target: load html async and place it (or an error message) here
 
 * @param success: success callback function
 
 * @param args: query parameters to pass to url
 
 */
 
function asynchtml(url, $target, success, args){
 
    if(args===undefined){
 
        args=null;
 
    }
 
    $target.html(_TM['Loading ...']).css('opacity','0.3');
 

	
 
    $.ajax({url: url, data: args, headers: {'X-PARTIAL-XHR': '1'}, cache: false, dataType: 'html'})
 
        .done(function(html) {
 
                $target.html(html);
 
                $target.css('opacity','1.0');
 
                //execute the given original callback
 
                if (success !== undefined && success) {
 
                    success();
 
                }
 
            })
 
        .fail(function(jqXHR, textStatus, errorThrown) {
 
                console.log('Ajax failure: ' + textStatus);
 
                $target.html('<span class="error_red">ERROR: {0}</span>'.format(textStatus));
 
                $target.css('opacity','1.0');
 
            })
 
        ;
 
};
 

	
 
var ajaxGET = function(url,success) {
 
    // Set special header for ajax == HTTP_X_PARTIAL_XHR
 
    YUC.initHeader('X-PARTIAL-XHR',true);
 

	
 
    var sUrl = url;
 
    var callback = {
 
        success: success,
 
        failure: function (o) {
 
            if (o.status != 0) {
 
                alert("Ajax GET error: " + o.statusText);
 
            };
 
        }
 
    };
 

	
 
    var request = YAHOO.util.Connect.asyncRequest('GET', sUrl, callback);
 
    return request;
 
};
 

	
 
var ajaxPOST = function(url,postData,success) {
 
    // Set special header for ajax == HTTP_X_PARTIAL_XHR
 
    YUC.initHeader('X-PARTIAL-XHR',true);
 

	
 
    var sUrl = url;
 
    var callback = {
 
        success: success,
 
        failure: function (o) {
 
            alert("Ajax POST error: " + o.statusText);
 
        }
 
    };
 
    var postData = _toQueryString(postData);
 
    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData);
 
    return request;
 
};
 

	
 

	
 
/**
 
 * activate .show_more links
 
 * the .show_more must have an id that is the the id of an element to hide prefixed with _
 
 * the parentnode will be displayed
 
 */
 
var show_more_event = function(){
 
    $('.show_more').click(function(e){
 
        var el = e.currentTarget;
 
        $('#' + el.id.substring(1)).hide();
 
        $(el.parentNode).show();
 
    });
 
};
 

	
 
/**
 
 * activate .lazy-cs mouseover for showing changeset tooltip
 
 */
 
var show_changeset_tooltip = function(){
 
    $('.lazy-cs').mouseover(function(e){
 
        var $target = $(e.currentTarget);
 
        var rid = $target.attr('raw_id');
 
        var repo_name = $target.attr('repo_name');
 
        if(rid && !$target.hasClass('tooltip')){
 
            _show_tooltip(e, _TM['loading ...']);
 
            var url = pyroutes.url('changeset_info', {"repo_name": repo_name, "revision": rid});
 
            ajaxGET(url, function(o){
 
                    var json = JSON.parse(o.responseText);
 
                    $target.addClass('tooltip')
 
                    _show_tooltip(e, json['message']);
 
                    _activate_tooltip($target);
 
                });
 
        }
 
    });
 
};
 

	
 
var _onSuccessFollow = function(target){
 
    var $target = $(target);
 
    var $f_cnt = $('#current_followers_count');
 
    if($target.hasClass('follow')){
 
        $target.attr('class', 'following');
 
        $target.attr('title', _TM['Stop following this repository']);
 
        if($f_cnt.html()){
 
            var cnt = Number($f_cnt.html())+1;
 
            $f_cnt.html(cnt);
 
        }
 
    }
 
    else{
 
        $target.attr('class', 'follow');
 
        $target.attr('title', _TM['Start following this repository']);
 
        if($f_cnt.html()){
 
            var cnt = Number($f_cnt.html())-1;
 
            $f_cnt.html(cnt);
 
        }
 
    }
 
}
 

	
 
var toggleFollowingRepo = function(target, follows_repo_id, token, user_id){
 
    args = 'follows_repo_id='+follows_repo_id;
 
    args+= '&amp;auth_token='+token;
 
    if(user_id != undefined){
 
        args+="&amp;user_id="+user_id;
 
    }
 
    $.post(TOGGLE_FOLLOW_URL, args, function(data){
 
            _onSuccessFollow(target);
 
        });
 
    return false;
 
};
 

	
 
var showRepoSize = function(target, repo_name, token){
 
    var args= 'auth_token='+token;
 

	
 
    if(!$("#" + target).hasClass('loaded')){
 
        $("#" + target).html(_TM['Loading ...']);
 
        var url = pyroutes.url('repo_size', {"repo_name":repo_name});
 
        $.post(url, args, function(data) {
 
            $("#" + target).html(data);
 
            $("#" + target).addClass('loaded');
 
        });
 
    }
 
    return false;
 
};
 

	
 
/**
 
 * tooltips
 
 */
 

	
 
var tooltip_activate = function(){
 
    $(document).ready(_init_tooltip);
 
};
 

	
 
var _activate_tooltip = function($tt){
 
    $tt.mouseover(_show_tooltip);
 
    $tt.mousemove(_move_tooltip);
 
    $tt.mouseout(_close_tooltip);
 
};
 

	
 
var _init_tooltip = function(){
 
    var $tipBox = $('#tip-box');
 
    if(!$tipBox.length){
 
        $tipBox = $('<div id="tip-box"></div>')
 
        $(document.body).append($tipBox);
 
    }
 

	
 
    $tipBox.hide();
 
    $tipBox.css('position', 'absolute');
 
    $tipBox.css('max-width', '600px');
 

	
 
    _activate_tooltip($('.tooltip'));
 
};
 

	
 
var _show_tooltip = function(e, tipText){
 
    e.stopImmediatePropagation();
 
    var el = e.currentTarget;
 
    if(tipText){
 
        // just use it
 
    } else if(el.tagName.toLowerCase() === 'img'){
 
        tipText = el.alt ? el.alt : '';
 
    } else {
 
        tipText = el.title ? el.title : '';
 
    }
 

	
 
    if(tipText !== ''){
 
        // save org title
 
        $(el).attr('tt_title', tipText);
 
        // reset title to not show org tooltips
 
        $(el).attr('title', '');
 

	
 
        var $tipBox = $('#tip-box');
 
        $tipBox.html(tipText);
 
        $tipBox.css('display', 'block');
 
    }
 
};
 

	
 
var _move_tooltip = function(e){
 
    e.stopImmediatePropagation();
 
    var $tipBox = $('#tip-box');
 
    $tipBox.css('top', (e.pageY + 15) + 'px');
 
    $tipBox.css('left', (e.pageX + 15) + 'px');
 
};
 

	
 
var _close_tooltip = function(e){
 
    e.stopImmediatePropagation();
 
    var $tipBox = $('#tip-box');
 
    $tipBox.hide();
 
    var el = e.currentTarget;
 
    $(el).attr('title', $(el).attr('tt_title'));
 
};
 

	
 
/**
 
 * Quick filter widget
 
 *
 
 * @param target: filter input target
 
 * @param nodes: list of nodes in html we want to filter.
 
 * @param display_element function that takes current node from nodes and
 
 *    does hide or show based on the node
 
 */
 
var q_filter = function(target, nodes, display_element){
 
    var nodes = nodes;
 
    var $q_filter_field = $('#' + target);
 
    var F = YAHOO.namespace(target);
 

	
 
    $q_filter_field.keyup(function(e){
 
        clearTimeout(F.filterTimeout);
 
        F.filterTimeout = setTimeout(F.updateFilter, 600);
 
    });
 

	
 
    F.filterTimeout = null;
 

	
 
    F.updateFilter  = function() {
 
        // Reset timeout
 
        F.filterTimeout = null;
 

	
 
        var obsolete = [];
 

	
 
        var req = $q_filter_field.val().toLowerCase();
 

	
 
        var l = nodes.length;
 
        var i;
 
        var showing = 0;
 

	
 
        for (i=0; i<l; i++ ){
 
            var n = nodes[i];
 
            var target_element = display_element(n)
 
            if(req && n.innerHTML.toLowerCase().indexOf(req) == -1){
 
                $(target_element).hide();
 
            }
 
            else{
 
                $(target_element).show();
 
                showing += 1;
 
            }
 
        }
 

	
 
        $('#repo_count').html(showing); /* FIXME: don't hardcode */
 
    }
 
};
 

	
 
/* return jQuery expression with a tr with body in 3rd column and class cls and id named after the body */
 
var _table_tr = function(cls, body){
 
    // like: <div class="comment" id="comment-8" line="o92"><div class="comment-wrapp">...
 
    // except new inlines which are different ...
 
    var comment_id = ($(body).attr('id') || 'comment-new').split('comment-')[1];
 
    var tr_id = 'comment-tr-{0}'.format(comment_id);
 
    return $(('<tr id="{0}" class="{1}">'+
 
                  '<td class="lineno-inline new-inline"></td>'+
 
                  '<td class="lineno-inline old-inline"></td>'+
 
                  '<td>{2}</td>'+
 
                 '</tr>').format(tr_id, cls, body));
 
};
 

	
 
/** return jQuery expression with new inline form based on template **/
 
var _createInlineForm = function(parent_tr, f_path, line) {
 
    var $tmpl = $('#comment-inline-form-template').html().format(f_path, line);
 
    var $form = _table_tr('comment-form-inline', $tmpl)
 

	
 
    // create event for hide button
 
    $form.find('.hide-inline-form').click(function(e) {
 
        var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
 
        if($(newtr).next().hasClass('inline-comments-button')){
 
            $(newtr).next().show();
 
        }
 
        $(newtr).remove();
 
        $(parent_tr).removeClass('form-open');
 
        $(parent_tr).removeClass('hl-comment');
 
    });
 

	
 
    return $form
 
};
 

	
 
/**
 
 * Inject inline comment for an given TR. This tr should always be a .line .
 
 * The form will be inject after any comments.
 
 */
 
var injectInlineForm = function(tr){
 
    $tr = $(tr);
 
    if(!$tr.hasClass('line')){
 
        return
 
    }
 
    var submit_url = AJAX_COMMENT_URL;
 
    var $td = $tr.find('.code');
 
    if($tr.hasClass('form-open') || $tr.hasClass('context') || $td.hasClass('no-comment')){
 
        return
 
    }
 
    $tr.addClass('form-open hl-comment');
 
    var $node = $tr.parent().parent().parent().find('.full_f_path');
 
    var f_path = $node.attr('path');
 
    var lineno = _getLineNo(tr);
 
    var $form = _createInlineForm(tr, f_path, lineno, submit_url);
 

	
 
    var $parent = $tr;
 
    while ($parent.next().hasClass('inline-comments')){
 
        var $parent = $parent.next();
 
    }
 
    $form.insertAfter($parent);
 
    var $overlay = $form.find('.submitting-overlay');
 
    var $inlineform = $form.find('.inline-form');
 

	
 
    $form.submit(function(e){
 
        e.preventDefault();
 

	
 
        if(lineno === undefined){
 
            alert('Error submitting, line ' + lineno + ' not found.');
 
            return
 
        }
 
        if(f_path === undefined){
 
            alert('Error submitting, file path ' + f_path + ' not found.');
 
            return
 
        }
 

	
 
        var text = $('#text_'+lineno).val();
 
        if(text == ""){
 
            return
 
        }
 

	
 
        $overlay.show();
 

	
 
        var success = function(o){
 
            $tr.removeClass('form-open');
 
            $form.remove();
 
            var json_data = JSON.parse(o.responseText);
 
            _renderInlineComment(json_data);
 
        };
 
        var postData = {
 
                'text': text,
 
                'f_path': f_path,
 
                'line': lineno
 
        };
 
        ajaxPOST(submit_url, postData, success);
 
    });
 

	
 
    $('#preview-btn_'+lineno).click(function(e){
 
        var text = $('#text_'+lineno).val();
 
        if(!text){
 
            return
 
        }
 
        $('#preview-box_'+lineno).addClass('unloaded');
 
        $('#preview-box_'+lineno).html(_TM['Loading ...']);
 
        $('#edit-container_'+lineno).hide();
 
        $('#edit-btn_'+lineno).show();
 
        $('#preview-container_'+lineno).show();
 
        $('#preview-btn_'+lineno).hide();
 

	
 
        var url = pyroutes.url('changeset_comment_preview', {'repo_name': REPO_NAME});
 
        var post_data = {'text': text};
 
        ajaxPOST(url, post_data, function(o){
 
            $('#preview-box_'+lineno).html(o.responseText);
 
            $('#preview-box_'+lineno).removeClass('unloaded');
 
        })
 
    })
 
    $('#edit-btn_'+lineno).click(function(e){
 
        $('#edit-container_'+lineno).show();
 
        $('#edit-btn_'+lineno).hide();
 
        $('#preview-container_'+lineno).hide();
 
        $('#preview-btn_'+lineno).show();
 
    })
 

	
 
    setTimeout(function(){
 
        // callbacks
 
        tooltip_activate();
 
        MentionsAutoComplete('text_'+lineno, 'mentions_container_'+lineno,
 
                             _USERS_AC_DATA, _GROUPS_AC_DATA);
 
        $('#text_'+lineno).focus();
 
    },10)
 
};
 

	
 
var deleteComment = function(comment_id){
 
    var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__',comment_id);
 
    var postData = {'_method':'delete'};
 
    var success = function(o){
 
        var $deleted = $('#comment-tr-'+comment_id);
 
        var $prev = $deleted.prev('tr');
 
        $deleted.remove();
 
        _placeAddButton($prev);
 
    }
 
    ajaxPOST(url,postData,success);
 
}
 

	
 
var _getLineNo = function(tr) {
 
    var line;
 
    var o = $(tr).children()[0].id.split('_');
 
    var n = $(tr).children()[1].id.split('_');
 

	
 
    if (n.length >= 2) {
 
        line = n[n.length-1];
 
    } else if (o.length >= 2) {
 
        line = o[o.length-1];
 
    }
 

	
 
    return line
 
};
 

	
 
var _placeAddButton = function($line_tr){
 
    var $tr = $line_tr;
 
    while ($tr.next().hasClass('inline-comments')){
 
        $tr.find('.add-comment').remove();
 
        $tr = $tr.next();
 
    }
 
    $tr.find('.add-comment').remove();
 
    var label = TRANSLATION_MAP['Add another comment'];
 
    var label = TRANSLATION_MAP['Add Another Comment'];
 
    var $html_el = $('<div class="add-comment"><span class="btn btn-mini">{0}</span></div>'.format(label));
 
    $html_el.click(function(e) {
 
        injectInlineForm($line_tr);
 
    });
 
    $tr.find('.comment').after($html_el);
 
};
 

	
 
/**
 
 * Places the inline comment into the changeset block in proper line position
 
 */
 
var _placeInline = function(target_id, lineno, html){
 
    var $td = $("#{0}_{1}".format(target_id, lineno));
 
    if (!$td.length){
 
        return false;
 
    }
 

	
 
    // check if there are comments already !
 
    var $line_tr = $td.parent(); // the tr
 
    var $after_tr = $line_tr;
 
    while ($after_tr.next().hasClass('inline-comments')){
 
        $after_tr = $after_tr.next();
 
    }
 
    // put in the comment at the bottom
 
    var $tr = _table_tr('inline-comments', html)
 
    $tr.find('div.comment').addClass('inline-comment');
 
    $after_tr.after($tr);
 

	
 
    // scan nodes, and attach add button to last one
 
    _placeAddButton($line_tr);
 
    return true;
 
}
 

	
 
/**
 
 * make a single inline comment and place it inside
 
 */
 
var _renderInlineComment = function(json_data){
 
    var html =  json_data['rendered_text'];
 
    var lineno = json_data['line_no'];
 
    var target_id = json_data['target_id'];
 
    return _placeInline(target_id, lineno, html);
 
}
 

	
 
/**
 
 * Iterates over all the inlines, and places them inside proper blocks of data
 
 */
 
var renderInlineComments = function(file_comments){
 
    for (f in file_comments){
 
        // holding all comments for a FILE
 
        var box = file_comments[f];
 

	
 
        var target_id = $(box).attr('target_id');
 
        // actual comments with line numbers
 
        var comments = box.children;
 
        for(var i=0; i<comments.length; i++){
 
            var data = {
 
                'rendered_text': comments[i].outerHTML,
 
                'line_no': $(comments[i]).attr('line'),
 
                'target_id': target_id
 
            }
 
            if (_renderInlineComment(data)) {
 
                $(comments[i]).hide();
 
            }else{
 
                var txt = document.createTextNode(
 
                        "Comment to " + YUD.getAttribute(comments[i].parentNode,'path') +
 
                        " line " + data.line_no +
 
                        " which is outside the diff context:");
 
                comments[i].insertBefore(txt, comments[i].firstChild);
 
            }
 
        }
 
        $(box).show();
 
    }
 
}
 

	
 
/**
 
 * Double link comments
 
 */
 
var linkInlineComments = function(firstlinks, comments){
 
    var $comments = $(comments);
 
    if ($comments.length > 0) {
 
        $(firstlinks).html('<a href="#{0}">First comment</a>'.format($comments.attr('id')));
 
    }
 
    if ($comments.length <= 1) {
 
        return;
 
    }
 

	
 
    $comments.each(function(i, e){
 
            var prev = '';
 
            if (i > 0){
 
                var prev_anchor = YUD.getAttribute(comments.item(i-1),'id');
 
                prev = '<a href="#{0}">Previous comment</a>'.format(prev_anchor);
 
            }
 
            var next = '';
 
            if (i+1 < comments.length){
 
                var next_anchor = YUD.getAttribute(comments.item(i+1),'id');
 
                next = '<a href="#{0}">Next comment</a>'.format(next_anchor);
 
            }
 
            var $div = $(('<div class="prev-next-comment">'+
 
                          '<div class="prev-comment">{0}</div>'+
 
                          '<div class="next-comment">{1}</div>').format(prev, next));
 
            $div.prependTo(this);
 
        });
 
}
 

	
 
/* activate files.html stuff */
 
var fileBrowserListeners = function(current_url, node_list_url, url_base){
 
    var current_url_branch = "?branch=__BRANCH__";
 

	
 
    $('#stay_at_branch').on('click',function(e){
 
        if(e.currentTarget.checked){
 
            var uri = current_url_branch;
 
            uri = uri.replace('__BRANCH__',e.currentTarget.value);
 
            window.location = uri;
 
        }
 
        else{
 
            window.location = current_url;
 
        }
 
    })
 

	
 
    var $node_filter = $('#node_filter');
 

	
 
    var filterTimeout = null;
 
    var nodes = null;
 

	
 
    var initFilter = function(){
 
        $('#node_filter_box_loading').show();
 
        $('#search_activate_id').hide();
 
        $('#add_node_id').hide();
 
        YUC.initHeader('X-PARTIAL-XHR',true);
 
        YUC.asyncRequest('GET', node_list_url, {
 
            success:function(o){
 
                nodes = JSON.parse(o.responseText).nodes;
 
                $('#node_filter_box_loading').hide();
 
                $('#node_filter_box').show();
 
                $node_filter.focus();
 
                if($node_filter.hasClass('init')){
 
                    $node_filter.val('');
 
                    $node_filter.removeClass('init');
 
                }
 
            },
 
            failure:function(o){
 
                console.log('failed to load');
 
            }
 
        },null);
 
    }
 

	
 
    var updateFilter = function(e) {
 
        return function(){
 
            // Reset timeout
 
            filterTimeout = null;
 
            var query = e.currentTarget.value.toLowerCase();
 
            var match = [];
 
            var matches = 0;
 
            var matches_max = 20;
 
            if (query != ""){
 
                for(var i=0;i<nodes.length;i++){
 
                    var pos = nodes[i].name.toLowerCase().indexOf(query)
 
                    if(query && pos != -1){
 
                        matches++
 
                        //show only certain amount to not kill browser
 
                        if (matches > matches_max){
 
                            break;
 
                        }
 

	
 
                        var n = nodes[i].name;
 
                        var t = nodes[i].type;
 
                        var n_hl = n.substring(0,pos)
 
                          +"<b>{0}</b>".format(n.substring(pos,pos+query.length))
 
                          +n.substring(pos+query.length)
 
                        var new_url = url_base.replace('__FPATH__',n);
 
                        match.push('<tr><td><a class="browser-{0}" href="{1}">{2}</a></td><td colspan="5"></td></tr>'.format(t,new_url,n_hl));
 
                    }
 
                    if(match.length >= matches_max){
 
                        match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['Search truncated']));
 
                        break;
 
                    }
 
                }
 
            }
 
            if(query != ""){
 
                $('#tbody').hide();
 
                $('#tbody_filtered').show();
 

	
 
                if (match.length==0){
 
                  match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['No matching files']));
 
                }
 

	
 
                $('#tbody_filtered').html(match.join(""));
 
            }
 
            else{
 
                $('#tbody').show();
 
                $('#tbody_filtered').hide();
 
            }
 
        }
 
    };
 

	
 
    $('#filter_activate').click(function(){
 
            initFilter();
 
        });
 
    $node_filter.click(function(){
 
            if($node_filter.hasClass('init')){
 
                $node_filter.val('');
 
                $node_filter.removeClass('init');
 
            }
 
        });
 
    $node_filter.keyup(function(e){
 
            clearTimeout(filterTimeout);
 
            filterTimeout = setTimeout(updateFilter(e),600);
 
        });
 
};
 

	
 

	
 
var initCodeMirror = function(textarea_id, resetUrl){
 
    var myCodeMirror = CodeMirror.fromTextArea($('#' + textarea_id)[0], {
 
            mode: "null",
 
            lineNumbers: true,
 
            indentUnit: 4,
 
            autofocus: true
 
        });
 
    $('#reset').click(function(e){
 
            window.location=resetUrl;
 
        });
 

	
 
    $('#file_enable').click(function(){
 
            $('#editor_container').show();
 
            $('#upload_file_container').hide();
 
            $('#filename_container').show();
 
            $('#set_mode_header').show();
 
        });
 

	
 
    $('#upload_file_enable').click(function(){
 
            $('#editor_container').hide();
 
            $('#upload_file_container').show();
 
            $('#filename_container').hide();
 
            $('#set_mode_header').hide();
 
        });
 

	
 
    return myCodeMirror
 
};
 

	
 
var setCodeMirrorMode = function(codeMirrorInstance, mode) {
 
    codeMirrorInstance.setOption("mode", mode);
 
    CodeMirror.autoLoadMode(codeMirrorInstance, mode);
 
}
 

	
 

	
 
var _getIdentNode = function(n){
 
    //iterate thrugh nodes until matching interesting node
 

	
 
    if (typeof n == 'undefined'){
 
        return -1
 
    }
 

	
 
    if(typeof n.id != "undefined" && n.id.match('L[0-9]+')){
 
        return n
 
    }
 
    else{
 
        return _getIdentNode(n.parentNode);
 
    }
 
};
 

	
 
/* generate links for multi line selects that can be shown by files.html page_highlights.
 
 * This is a mouseup handler for hlcode from CodeHtmlFormatter and pygmentize */
 
var getSelectionLink = function(e) {
 
    //get selection from start/to nodes
 
    if (typeof window.getSelection != "undefined") {
 
        s = window.getSelection();
 

	
 
        from = _getIdentNode(s.anchorNode);
 
        till = _getIdentNode(s.focusNode);
 

	
 
        f_int = parseInt(from.id.replace('L',''));
 
        t_int = parseInt(till.id.replace('L',''));
 

	
 
        var yoffset = 35;
 
        var ranges = [parseInt(from.id.replace('L','')), parseInt(till.id.replace('L',''))];
 
        if (ranges[0] > ranges[1]){
 
            //highlight from bottom
 
            yoffset = -yoffset;
 
            ranges = [ranges[1], ranges[0]];
 
        }
 
        var $hl_div = $('div#linktt');
 
        // if we select more than 2 lines
 
        if (ranges[0] != ranges[1]){
 
            if ($hl_div.length) {
 
                $hl_div.html('');
 
            } else {
 
                $hl_div = $('<div id="linktt" class="hl-tip-box">');
 
                $('body').prepend($hl_div);
 
            }
 

	
 
            $hl_div.append($('<a>').html(_TM['Selection link']).attr('href', location.href.substring(0, location.href.indexOf('#')) + '#L' + ranges[0] + '-'+ranges[1]));
 
            xy = $(till).offset();
 
            $hl_div.css('top', (xy.top + yoffset) + 'px').css('left', xy.left + 'px');
 
            $hl_div.show();
 
        }
 
        else{
 
            $hl_div.hide();
 
        }
 
    }
 
};
 

	
 
var deleteNotification = function(url, notification_id, callbacks){
 
    var callback = {
 
        success:function(o){
 
            $("#notification_"+notification_id).remove();
 
            _run_callbacks(callbacks);
 
        },
 
        failure:function(o){
 
            alert("deleteNotification failure");
 
        }
 
    };
 
    var postData = '_method=delete';
 
    var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
 
    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl,
 
                                                  callback, postData);
 
};
 

	
 
var readNotification = function(url, notification_id, callbacks){
 
    var callback = {
 
        success:function(o){
 
            var $obj = $("#notification_"+notification_id);
 
            $obj.removeClass('unread');
 
            $obj.find('.read-notification').remove();
 
            _run_callbacks(callbacks);
 
        },
 
        failure:function(o){
 
            alert("readNotification failure");
 
        }
 
    };
 
    var postData = '_method=put';
 
    var sUrl = url.replace('__NOTIFICATION_ID__',notification_id);
 
    var request = YAHOO.util.Connect.asyncRequest('POST', sUrl,
 
                                                  callback, postData);
 
};
 

	
 
/** MEMBERS AUTOCOMPLETE WIDGET **/
 

	
 
var _MembersAutoComplete = function (divid, cont, users_list, groups_list) {
 
    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 userGroups
 
    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(divid, cont, memberDS);
 
    membersAC.useShadow = false;
 
    membersAC.resultTypeList = false;
 
    membersAC.animVert = false;
 
    membersAC.animHoriz = false;
 
    membersAC.animSpeed = 0.1;
 

	
 
    // Instantiate AutoComplete for owner
 
    var ownerAC = new YAHOO.widget.AutoComplete("user", "owner_container", ownerDS);
 
    ownerAC.useShadow = false;
 
    ownerAC.resultTypeList = false;
 
    ownerAC.animVert = false;
 
    ownerAC.animHoriz = false;
 
    ownerAC.animSpeed = 0.1;
 

	
 
    // Helper highlight function for the formatter
 
    var highlightMatch = function (full, snippet, matchindex) {
 
            return full.substring(0, matchindex)
 
            + "<span class='match'>"
 
            + full.substr(matchindex, snippet.length)
 
            + "</span>" + 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 = '<div class="ac-container-wrap"><img class="perm-gravatar-ac" src="{0}"/>{1}</div>'
 
                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(_TM['Group']);
 
                var grsuffix = " (" + grmembers + "  )";
 
                var grsuffix = " ({0}  {1})".format(grmembers, _TM['members']);
 

	
 
                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.nname != undefined) {
 
                var fname = oResultData.fname || "";
 
                var lname = oResultData.lname || "";
 
                var nname = oResultData.nname;
 

	
 
                // Guard against null value
 
                var 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 nextId = divid.split('perm_new_member_name_')[1];
 
            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;
 
                $('#perm_new_member_type_'+nextId).val('user');
 
            } else {
 
                //groups
 
                myAC.getInputEl().value = oData.grname;
 
                $('#perm_new_member_type_'+nextId).val('users_group');
 
            }
 
        };
 

	
 
    membersAC.itemSelectEvent.subscribe(myHandler);
 
    if(ownerAC.itemSelectEvent){
 
        ownerAC.itemSelectEvent.subscribe(myHandler);
 
    }
 

	
 
    return {
 
        memberDS: memberDS,
 
        ownerDS: ownerDS,
 
        membersAC: membersAC,
 
        ownerAC: ownerAC
 
    };
 
}
 

	
 
var MentionsAutoComplete = function (divid, cont, users_list, groups_list) {
 
    var myUsers = users_list;
 
    var myGroups = groups_list;
 

	
 
    // Define a custom search function for the DataSource of users
 
    var matchUsers = function (sQuery) {
 
            var org_sQuery = sQuery;
 
            if(this.mentionQuery == null){
 
                return []
 
            }
 
            sQuery = this.mentionQuery;
 
            // 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
 
        };
 

	
 
    //match all
 
    var matchAll = function (sQuery) {
 
            u = matchUsers(sQuery);
 
            return u
 
        };
 

	
 
    // DataScheme for owner
 
    var ownerDS = new YAHOO.util.FunctionDataSource(matchUsers);
 

	
 
    ownerDS.responseSchema = {
 
        fields: ["id", "fname", "lname", "nname", "gravatar_lnk"]
 
    };
 

	
 
    // Instantiate AutoComplete for mentions
 
    var ownerAC = new YAHOO.widget.AutoComplete(divid, cont, ownerDS);
 
    ownerAC.useShadow = false;
 
    ownerAC.resultTypeList = false;
 
    ownerAC.suppressInputUpdate = true;
 
    ownerAC.animVert = false;
 
    ownerAC.animHoriz = false;
 
    ownerAC.animSpeed = 0.1;
 

	
 
    // Helper highlight function for the formatter
 
    var highlightMatch = function (full, snippet, matchindex) {
 
            return full.substring(0, matchindex)
 
            + "<span class='match'>"
 
            + full.substr(matchindex, snippet.length)
 
            + "</span>" + full.substring(matchindex + snippet.length);
 
        };
 

	
 
    // Custom formatter to highlight the matching letters
 
    ownerAC.formatResult = function (oResultData, sQuery, sResultMatch) {
 
            var org_sQuery = sQuery;
 
            if(this.dataSource.mentionQuery != null){
 
                sQuery = this.dataSource.mentionQuery;
 
            }
 

	
 
            var query = sQuery.toLowerCase();
 
            var _gravatar = function(res, em, group){
 
                if (group !== undefined){
 
                    em = '/images/icons/group.png'
 
                }
 
                tmpl = '<div class="ac-container-wrap"><img class="perm-gravatar-ac" src="{0}"/>{1}</div>'
 
                return tmpl.format(em,res)
 
            }
 
            if (oResultData.nname != undefined) {
 
                var fname = oResultData.fname || "";
 
                var lname = oResultData.lname || "";
 
                var nname = oResultData.nname;
 

	
 
                // Guard against null value
 
                var 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 '';
 
            }
 
        };
 

	
 
    if(ownerAC.itemSelectEvent){
 
        ownerAC.itemSelectEvent.subscribe(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
 
                //Replace the mention name with replaced
 
                var re = new RegExp();
 
                var org = myAC.getInputEl().value;
 
                var chunks = myAC.dataSource.chunks
 
                // replace middle chunk(the search term) with actuall  match
 
                chunks[1] = chunks[1].replace('@'+myAC.dataSource.mentionQuery,
 
                                              '@'+oData.nname+' ');
 
                myAC.getInputEl().value = chunks.join('')
 
                myAC.getInputEl().focus(); // Y U NO WORK !?
 
            } else {
 
                //groups
 
                myAC.getInputEl().value = oData.grname;
 
                $('#perm_new_member_type').val('users_group');
 
            }
 
        });
 
    }
 

	
 
    // in this keybuffer we will gather current value of search !
 
    // since we need to get this just when someone does `@` then we do the
 
    // search
 
    ownerAC.dataSource.chunks = [];
 
    ownerAC.dataSource.mentionQuery = null;
 

	
 
    ownerAC.get_mention = function(msg, max_pos) {
 
        var org = msg;
 
        // Must match utils2.py MENTIONS_REGEX.
 
        // Only matching on string up to cursor, so it must end with $
 
        var re = new RegExp('(?:^|[^a-zA-Z0-9])@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])$')
 
        var chunks  = [];
 

	
 
        // cut first chunk until current pos
 
        var to_max = msg.substr(0, max_pos);
 
        var at_pos = Math.max(0,to_max.lastIndexOf('@')-1);
 
        var msg2 = to_max.substr(at_pos);
 

	
 
        chunks.push(org.substr(0,at_pos))// prefix chunk
 
        chunks.push(msg2)                // search chunk
 
        chunks.push(org.substr(max_pos)) // postfix chunk
 

	
 
        // clean up msg2 for filtering and regex match
 
        var msg2 = msg2.lstrip(' ').lstrip('\n');
 

	
 
        if(re.test(msg2)){
 
            var unam = re.exec(msg2)[1];
 
            return [unam, chunks];
 
        }
 
        return [null, null];
 
    };
 

	
 
    $divid = $('#'+divid);
 
    $divid.keyup(function(e){
 
            var currentMessage = $divid.val();
 
            var currentCaretPosition = $divid[0].selectionStart;
 

	
 
            var unam = ownerAC.get_mention(currentMessage, currentCaretPosition);
 
            var curr_search = null;
 
            if(unam[0]){
 
                curr_search = unam[0];
 
            }
 

	
 
            ownerAC.dataSource.chunks = unam[1];
 
            ownerAC.dataSource.mentionQuery = curr_search;
 
        });
 
}
 

	
 
var addReviewMember = function(id,fname,lname,nname,gravatar_link){
 
    var displayname = "{0} {1} ({2})".format(fname, lname, nname);
 
    var element = (
 
        '     <li id="reviewer_{2}">\n'+
 
        '       <div class="reviewers_member">\n'+
 
        '           <div class="reviewer_status tooltip" title="not_reviewed">\n'+
 
        '             <img src="/images/icons/flag_status_not_reviewed.png"/>\n'+
 
        '           </div>\n'+
 
        '         <div class="reviewer_gravatar gravatar"><img alt="gravatar" src="{0}"/> </div>\n'+
 
        '         <div style="float:left;">{1}</div>\n'+
 
        '         <input type="hidden" value="{2}" name="review_members" />\n'+
 
        '         <div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">\n'+
 
        '             <i class="icon-remove-sign" style="color: #FF4444;"></i>\n'+
 
        '         </div> *\n'+
 
        '       </div>\n'+
 
        '     </li>\n'
 
        ).format(gravatar_link, displayname, id);
 
    // check if we don't have this ID already in
 
    var ids = [];
 
    $('#review_members').find('li').each(function() {
 
            ids.push(this.id);
 
        });
 
    if(ids.indexOf('reviewer_'+id) == -1){
 
        //only add if it's not there
 
        $('#review_members').append(element);
 
    }
 
}
 

	
 
var removeReviewMember = function(reviewer_id, repo_name, pull_request_id){
 
    var $li = $('#reviewer_{0}'.format(reviewer_id));
 
    $li.find('div div').css("text-decoration", "line-through");
 
    $li.find('input').remove();
 
    $li.find('.reviewer_member_remove').remove();
 
}
 

	
 
/* handle "Save Changes" of addReviewMember and removeReviewMember on PR */
 
var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
 
    if (reviewers_ids === undefined){
 
        var reviewers_ids = [];
 
        $('#review_members').find('input').each(function(){
 
                reviewers_ids.push(this.value);
 
            });
 
    }
 
    var url = pyroutes.url('pullrequest_update', {"repo_name":repo_name,
 
                                                  "pull_request_id": pull_request_id});
 
    var postData = {'_method':'put',
 
                    'reviewers_ids': reviewers_ids};
 
    var success = function(o){
 
        window.location.reload();
 
    }
 
    ajaxPOST(url,postData,success);
 
}
 

	
 
/* activate auto completion of users and groups ... but only used for users as PR reviewers */
 
var PullRequestAutoComplete = function (divid, cont, users_list, groups_list) {
 
    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 userGroups
 
    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);
 
            return u
 
        };
 

	
 
    // DataScheme for owner
 
    var ownerDS = new YAHOO.util.FunctionDataSource(matchUsers);
 

	
 
    ownerDS.responseSchema = {
 
        fields: ["id", "fname", "lname", "nname", "gravatar_lnk"]
 
    };
 

	
 
    // Instantiate AutoComplete for mentions
 
    var reviewerAC = new YAHOO.widget.AutoComplete(divid, cont, ownerDS);
 
    reviewerAC.useShadow = false;
 
    reviewerAC.resultTypeList = false;
 
    reviewerAC.suppressInputUpdate = true;
 
    reviewerAC.animVert = false;
 
    reviewerAC.animHoriz = false;
 
    reviewerAC.animSpeed = 0.1;
 

	
 
    // Helper highlight function for the formatter
 
    var highlightMatch = function (full, snippet, matchindex) {
 
            return full.substring(0, matchindex)
 
            + "<span class='match'>"
 
            + full.substr(matchindex, snippet.length)
 
            + "</span>" + full.substring(matchindex + snippet.length);
 
        };
 

	
 
    // Custom formatter to highlight the matching letters
 
    reviewerAC.formatResult = function (oResultData, sQuery, sResultMatch) {
 
            var org_sQuery = sQuery;
 
            if(this.dataSource.mentionQuery != null){
 
                sQuery = this.dataSource.mentionQuery;
 
            }
 

	
 
            var query = sQuery.toLowerCase();
 
            var _gravatar = function(res, em, group){
 
                if (group !== undefined){
 
                    em = '/images/icons/group.png'
 
                }
 
                tmpl = '<div class="ac-container-wrap"><img class="perm-gravatar-ac" src="{0}"/>{1}</div>'
 
                return tmpl.format(em,res)
 
            }
 
            if (oResultData.nname != undefined) {
 
                var fname = oResultData.fname || "";
 
                var lname = oResultData.lname || "";
 
                var nname = oResultData.nname;
 

	
 
                // Guard against null value
 
                var 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 '';
 
            }
 
        };
 

	
 
    //members cache to catch duplicates
 
    reviewerAC.dataSource.cache = [];
 
    // hack into select event
 
    if(reviewerAC.itemSelectEvent){
 
        reviewerAC.itemSelectEvent.subscribe(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) {
 
                addReviewMember(oData.id, oData.fname, oData.lname, oData.nname,
 
                                oData.gravatar_lnk);
 
                myAC.dataSource.cache.push(oData.id);
 
                $('#user').val('');
 
            }
 
        });
 
    }
 
}
 

	
 
/**
 
 * Activate .quick_repo_menu
 
 */
 
var quick_repo_menu = function(){
 
    $(".quick_repo_menu").mouseenter(function(e) {
 
            var $menu = $(e.currentTarget).children().first().children().first();
 
            if($menu.hasClass('hidden')){
 
                $menu.removeClass('hidden').addClass('active');
 
                $(e.currentTarget).removeClass('hidden').addClass('active');
 
            }
 
        })
 
    $(".quick_repo_menu").mouseleave(function(e) {
 
            var $menu = $(e.currentTarget).children().first().children().first();
 
            if($menu.hasClass('active')){
 
                $menu.removeClass('active').addClass('hidden');
 
                $(e.currentTarget).removeClass('active').addClass('hidden');
 
            }
 
        })
 
};
 

	
 

	
 
/**
 
 * TABLE SORTING
 
 */
 

	
 
var revisionSort = function(a, b, desc, field) {
 
    var a_ = parseInt(a.getData('last_rev_raw') || 0);
 
    var b_ = parseInt(b.getData('last_rev_raw') || 0);
 

	
 
    return YAHOO.util.Sort.compare(a_, b_, desc);
 
};
 

	
 
var ageSort = function(a, b, desc, field) {
 
    // data is like: <span class="tooltip" date="2014-06-04 18:18:55.325474" title="Wed, 04 Jun 2014 18:18:55">1 day and 23 hours ago</span>
 
    var a_ = $(a.getData(field)).attr('date');
 
    var b_ = $(b.getData(field)).attr('date');
 

	
 
    return YAHOO.util.Sort.compare(a_, b_, desc);
 
};
 

	
 
var lastLoginSort = function(a, b, desc, field) {
 
    var a_ = parseFloat(a.getData('last_login_raw') || 0);
 
    var b_ = parseFloat(b.getData('last_login_raw') || 0);
 

	
 
    return YAHOO.util.Sort.compare(a_, b_, desc);
 
};
 

	
 
var nameSort = function(a, b, desc, field) {
 
    var a_ = a.getData('raw_name') || 0;
 
    var b_ = b.getData('raw_name') || 0;
 

	
 
    return YAHOO.util.Sort.compare(a_, b_, desc);
 
};
 

	
 
var dateSort = function(a, b, desc, field) {
 
    var a_ = parseFloat(a.getData('raw_date') || 0);
 
    var b_ = parseFloat(b.getData('raw_date') || 0);
 

	
 
    return YAHOO.util.Sort.compare(a_, b_, desc);
 
};
 

	
 
var addPermAction = function(_html, users_list, groups_list){
 
    var $last_node = $('.last_new_member').last(); // empty tr between last and add
 
    var next_id = $('.new_members').length;
 
    $last_node.before($('<tr class="new_members">').append(_html.format(next_id)));
 
    _MembersAutoComplete("perm_new_member_name_"+next_id,
 
            "perm_container_"+next_id, users_list, groups_list);
 
}
 

	
 
function ajaxActionRevokePermission(url, obj_id, obj_type, field_id, extra_data) {
 
    var callback = {
 
        success: function (o) {
 
            $('#' + field_id).remove();
 
        },
 
        failure: function (o) {
 
            alert(_TM['Failed to revoke permission'] + ": " + o.status);
 
        }
 
    };
 
    query_params = {
 
        '_method': 'delete'
 
    }
 
    // put extra data into POST
 
    if (extra_data !== undefined && (typeof extra_data === 'object')){
 
        for(k in extra_data){
 
            query_params[k] = extra_data[k];
 
        }
 
    }
 

	
 
    if (obj_type=='user'){
 
        query_params['user_id'] = obj_id;
 
        query_params['obj_type'] = 'user';
 
    }
 
    else if (obj_type=='user_group'){
 
        query_params['user_group_id'] = obj_id;
 
        query_params['obj_type'] = 'user_group';
 
    }
 

	
 
    var request = YAHOO.util.Connect.asyncRequest('POST', url, callback,
 
            _toQueryString(query_params));
 
};
 

	
 
/* Multi selectors */
 

	
 
var MultiSelectWidget = function(selected_id, available_id, form_id){
 
    var $availableselect = $('#' + available_id);
 
    var $selectedselect = $('#' + selected_id);
 

	
 
    //fill available only with those not in selected
 
    var $selectedoptions = $selectedselect.children('option');
 
    $availableselect.children('option').filter(function(i, e){
 
            for(var j = 0, node; node = $selectedoptions[j]; j++){
 
                if(node.value == e.value){
 
                    return true;
 
                }
 
            }
 
            return false;
 
        }).remove();
 

	
 
    $('#add_element').click(function(e){
 
            $selectedselect.append($availableselect.children('option:selected'));
 
        });
 
    $('#remove_element').click(function(e){
 
            $availableselect.append($selectedselect.children('option:selected'));
 
        });
 
    $('#add_all_elements').click(function(e){
 
            $selectedselect.append($availableselect.children('option'));
 
        });
 
    $('#remove_all_elements').click(function(e){
 
            $availableselect.append($selectedselect.children('option'));
 
        });
 

	
 
    $('#'+form_id).submit(function(){
 
            $selectedselect.children('option').each(function(i, e){
 
                e.selected = 'selected';
 
            });
 
        });
 
}
 

	
 
// custom paginator
 
var YUI_paginator = function(links_per_page, containers){
 

	
 
    (function () {
 

	
 
        var Paginator = YAHOO.widget.Paginator,
 
            l         = YAHOO.lang,
 
            setId     = YAHOO.util.Dom.generateId;
 

	
 
        Paginator.ui.MyFirstPageLink = function (p) {
 
            this.paginator = p;
 

	
 
            p.subscribe('recordOffsetChange',this.update,this,true);
 
            p.subscribe('rowsPerPageChange',this.update,this,true);
 
            p.subscribe('totalRecordsChange',this.update,this,true);
 
            p.subscribe('destroy',this.destroy,this,true);
 

	
 
            // TODO: make this work
 
            p.subscribe('firstPageLinkLabelChange',this.update,this,true);
 
            p.subscribe('firstPageLinkClassChange',this.update,this,true);
 
        };
 

	
 
        Paginator.ui.MyFirstPageLink.init = function (p) {
 
            p.setAttributeConfig('firstPageLinkLabel', {
 
                value : 1,
 
                validator : l.isString
 
            });
 
            p.setAttributeConfig('firstPageLinkClass', {
 
                value : 'yui-pg-first',
 
                validator : l.isString
 
            });
 
            p.setAttributeConfig('firstPageLinkTitle', {
 
                value : 'First Page',
 
                validator : l.isString
 
            });
 
        };
 

	
 
        // Instance members and methods
 
        Paginator.ui.MyFirstPageLink.prototype = {
 
            current   : null,
 
            leftmost_page: null,
 
            rightmost_page: null,
 
            link      : null,
 
            span      : null,
 
            dotdot    : null,
 
            getPos    : function(cur_page, max_page, items){
 
                var edge = parseInt(items / 2) + 1;
 
                if (cur_page <= edge){
 
                    var radius = Math.max(parseInt(items / 2), items - cur_page);
 
                }
 
                else if ((max_page - cur_page) < edge) {
 
                    var radius = (items - 1) - (max_page - cur_page);
 
                }
 
                else{
 
                    var radius = parseInt(items / 2);
 
                }
 

	
 
                var left = Math.max(1, (cur_page - (radius)))
 
                var right = Math.min(max_page, cur_page + (radius))
 
                return [left, cur_page, right]
 
            },
 
            render : function (id_base) {
 
                var p      = this.paginator,
 
                    c      = p.get('firstPageLinkClass'),
 
                    label  = p.get('firstPageLinkLabel'),
 
                    title  = p.get('firstPageLinkTitle');
 

	
 
                this.link     = document.createElement('a');
 
                this.span     = document.createElement('span');
 
                $(this.span).hide();
 

	
 
                var _pos = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
 
                this.leftmost_page = _pos[0];
 
                this.rightmost_page = _pos[2];
 

	
 
                setId(this.link, id_base + '-first-link');
 
                this.link.href      = '#';
 
                this.link.className = c;
 
                this.link.innerHTML = label;
 
                this.link.title     = title;
 
                YUE.on(this.link,'click',this.onClick,this,true);
 

	
 
                setId(this.span, id_base + '-first-span');
 
                this.span.className = c;
 
                this.span.innerHTML = label;
 

	
 
                this.current = p.getCurrentPage() > 1 ? this.link : this.span;
 
                return this.current;
 
            },
 
            update : function (e) {
 
                var p      = this.paginator;
 
                var _pos   = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
 
                this.leftmost_page = _pos[0];
 
                this.rightmost_page = _pos[2];
 

	
 
                if (e && e.prevValue === e.newValue) {
 
                    return;
 
                }
 

	
 
                var par = this.current ? this.current.parentNode : null;
 
                if (this.leftmost_page > 1) {
 
                    if (par && this.current === this.span) {
 
                        par.replaceChild(this.link,this.current);
 
                        this.current = this.link;
 
                    }
 
                } else {
 
                    if (par && this.current === this.link) {
 
                        par.replaceChild(this.span,this.current);
 
                        this.current = this.span;
 
                    }
 
                }
 
            },
 
            destroy : function () {
 
                YUE.purgeElement(this.link);
 
                this.current.parentNode.removeChild(this.current);
 
                this.link = this.span = null;
 
            },
 
            onClick : function (e) {
 
                YUE.stopEvent(e);
 
                this.paginator.setPage(1);
 
            }
 
        };
 

	
 
        })();
 

	
 
    (function () {
 

	
 
        var Paginator = YAHOO.widget.Paginator,
 
            l         = YAHOO.lang,
 
            setId     = YAHOO.util.Dom.generateId;
 

	
 
        Paginator.ui.MyLastPageLink = function (p) {
 
            this.paginator = p;
 

	
 
            p.subscribe('recordOffsetChange',this.update,this,true);
 
            p.subscribe('rowsPerPageChange',this.update,this,true);
 
            p.subscribe('totalRecordsChange',this.update,this,true);
 
            p.subscribe('destroy',this.destroy,this,true);
 

	
 
            // TODO: make this work
 
            p.subscribe('lastPageLinkLabelChange',this.update,this,true);
 
            p.subscribe('lastPageLinkClassChange', this.update,this,true);
 
        };
 

	
 
        Paginator.ui.MyLastPageLink.init = function (p) {
 
            p.setAttributeConfig('lastPageLinkLabel', {
 
                value : -1,
 
                validator : l.isString
 
            });
 
            p.setAttributeConfig('lastPageLinkClass', {
 
                value : 'yui-pg-last',
 
                validator : l.isString
 
            });
 
            p.setAttributeConfig('lastPageLinkTitle', {
 
                value : 'Last Page',
 
                validator : l.isString
 
            });
 

	
 
        };
 

	
 
        Paginator.ui.MyLastPageLink.prototype = {
 

	
 
            current   : null,
 
            leftmost_page: null,
 
            rightmost_page: null,
 
            link      : null,
 
            span      : null,
 
            dotdot    : null,
 
            na        : null,
 
            getPos    : function(cur_page, max_page, items){
 
                var edge = parseInt(items / 2) + 1;
 
                if (cur_page <= edge){
 
                    var radius = Math.max(parseInt(items / 2), items - cur_page);
 
                }
 
                else if ((max_page - cur_page) < edge) {
 
                    var radius = (items - 1) - (max_page - cur_page);
 
                }
 
                else{
 
                    var radius = parseInt(items / 2);
 
                }
 

	
 
                var left = Math.max(1, (cur_page - (radius)))
 
                var right = Math.min(max_page, cur_page + (radius))
 
                return [left, cur_page, right]
 
            },
 
            render : function (id_base) {
 
                var p      = this.paginator,
 
                    c      = p.get('lastPageLinkClass'),
 
                    label  = p.get('lastPageLinkLabel'),
 
                    last   = p.getTotalPages(),
 
                    title  = p.get('lastPageLinkTitle');
 

	
 
                var _pos = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
 
                this.leftmost_page = _pos[0];
 
                this.rightmost_page = _pos[2];
 

	
 
                this.link = document.createElement('a');
 
                this.span = document.createElement('span');
 
                $(this.span).hide();
 

	
 
                this.na   = this.span.cloneNode(false);
 

	
 
                setId(this.link, id_base + '-last-link');
 
                this.link.href      = '#';
 
                this.link.className = c;
 
                this.link.innerHTML = label;
 
                this.link.title     = title;
 
                YUE.on(this.link,'click',this.onClick,this,true);
 

	
 
                setId(this.span, id_base + '-last-span');
 
                this.span.className = c;
 
                this.span.innerHTML = label;
 

	
 
                setId(this.na, id_base + '-last-na');
 

	
 
                if (this.rightmost_page < p.getTotalPages()){
 
                    this.current = this.link;
 
                }
 
                else{
 
                    this.current = this.span;
 
                }
 

	
 
                this.current.innerHTML = p.getTotalPages();
 
                return this.current;
 
            },
 

	
 
            update : function (e) {
 
                var p      = this.paginator;
 

	
 
                var _pos = this.getPos(p.getCurrentPage(), p.getTotalPages(), 5);
 
                this.leftmost_page = _pos[0];
 
                this.rightmost_page = _pos[2];
 

	
 
                if (e && e.prevValue === e.newValue) {
 
                    return;
 
                }
 

	
 
                var par   = this.current ? this.current.parentNode : null,
 
                    after = this.link;
 
                if (par) {
 

	
 
                    // only show the last page if the rightmost one is
 
                    // lower, so we don't have doubled entries at the end
 
                    if (!(this.rightmost_page < p.getTotalPages())){
 
                        after = this.span
 
                    }
 

	
 
                    if (this.current !== after) {
 
                        par.replaceChild(after,this.current);
 
                        this.current = after;
 
                    }
 
                }
 
                this.current.innerHTML = this.paginator.getTotalPages();
 

	
 
            },
 
            destroy : function () {
 
                YUE.purgeElement(this.link);
 
                this.current.parentNode.removeChild(this.current);
 
                this.link = this.span = null;
 
            },
 
            onClick : function (e) {
 
                YUE.stopEvent(e);
 
                this.paginator.setPage(this.paginator.getTotalPages());
 
            }
 
        };
 

	
 
        })();
 

	
 
    var pagi = new YAHOO.widget.Paginator({
 
        rowsPerPage: links_per_page,
 
        alwaysVisible: false,
 
        template : "{PreviousPageLink} {MyFirstPageLink} {PageLinks} {MyLastPageLink} {NextPageLink}",
 
        pageLinks: 5,
 
        containerClass: 'pagination-wh',
 
        currentPageClass: 'pager_curpage',
 
        pageLinkClass: 'pager_link',
 
        nextPageLinkLabel: '&gt;',
 
        previousPageLinkLabel: '&lt;',
 
        containers:containers
 
    })
 

	
 
    return pagi
 
}
 

	
 
var YUI_datatable = function(data, fields, columns, countnode, sortkey, rows){
 
    var myDataSource = new YAHOO.util.DataSource(data);
 
    myDataSource.responseType = YAHOO.util.DataSource.TYPE_JSON;
 
    myDataSource.responseSchema = {
 
        resultsList: "records",
 
        fields: fields
 
        };
 
    myDataSource.doBeforeCallback = function(req, raw, res, cb) {
 
        // This is the filter function
 
        var data     = res.results || [],
 
            filtered = [],
 
            i, l;
 

	
 
        if (req) {
 
            req = req.toLowerCase();
 
            for (i = 0; i<data.length; i++) {
 
                var pos = data[i].raw_name.toLowerCase().indexOf(req)
 
                if (pos != -1) {
 
                    filtered.push(data[i]);
 
                }
 
            }
 
            res.results = filtered;
 
        }
 
        $(countnode).html(res.results.length);
 
        return res;
 
    }
 

	
 
    var myDataTable = new YAHOO.widget.DataTable("datatable_list_wrap", columns, myDataSource, {
 
        sortedBy: {key:sortkey, dir:"asc"},
 
        paginator: YUI_paginator(rows !== undefined && rows ? rows : 25, ['user-paginator']),
 
        MSG_SORTASC: _TM['MSG_SORTASC'],
 
        MSG_SORTDESC: _TM['MSG_SORTDESC'],
 
        MSG_EMPTY: _TM['MSG_EMPTY'],
 
        MSG_ERROR: _TM['MSG_ERROR'],
 
        MSG_LOADING: _TM['MSG_LOADING']
 
        });
 
    myDataTable.subscribe('postRenderEvent',function(oArgs) {
 
        tooltip_activate();
 
        quick_repo_menu();
 
        });
 

	
 
    var filterTimeout = null;
 
    var $q_filter = $('#q_filter');
 

	
 
    updateFilter  = function () {
 
        // Reset timeout
 
        filterTimeout = null;
 

	
 
        // Reset sort
 
        var state = myDataTable.getState();
 
        state.sortedBy = {key:sortkey, dir:YAHOO.widget.DataTable.CLASS_ASC};
 

	
 
        // Get filtered data
 
        myDataSource.sendRequest($q_filter.val(), {
 
            success : myDataTable.onDataReturnInitializeTable,
 
            failure : myDataTable.onDataReturnInitializeTable,
 
            scope   : myDataTable,
 
            argument: state});
 
        };
 

	
 
    $q_filter.click(function(){
 
            if(!$q_filter.hasClass('loaded')){
 
                //TODO: load here full list later to do search within groups
 
                $q_filter.addClass('loaded');
 
            }
 
        });
 

	
 
    $q_filter.keyup(function (e) {
 
            clearTimeout(filterTimeout);
 
            filterTimeout = setTimeout(updateFilter, 600);
 
        });
 
}
 

	
 
// global hooks after DOM is loaded
 

	
 
$(document).ready(function(){
 
    $('.diff-collapse-button').click(function(e) {
 
        var $button = $(e.currentTarget);
 
        var $target = $('#' + $button.attr('target'));
 
        if($target.hasClass('hidden')){
 
            $target.removeClass('hidden');
 
            $button.html("&uarr; {0} &uarr;".format(_TM['Collapse diff']));
 
            $button.html("&uarr; {0} &uarr;".format(_TM['Collapse Diff']));
 
        }
 
        else if(!$target.hasClass('hidden')){
 
            $target.addClass('hidden');
 
            $button.html("&darr; {0} &darr;".format(_TM['Expand diff']));
 
            $button.html("&darr; {0} &darr;".format(_TM['Expand Diff']));
 
        }
 
    });
 
});
kallithea/templates/admin/admin.html
Show inline comments
 
## -*- coding: utf-8 -*-
 
<%inherit file="/base/base.html"/>
 

	
 
<%def name="title()">
 
    ${_('Admin journal')}
 
    ${_('Admin Journal')}
 
    %if c.site_name:
 
        &middot; ${c.site_name}
 
    %endif
 
</%def>
 

	
 
<%def name="breadcrumbs_links()">
 
    <form id="filter_form">
 
    <input class="q_filter_box ${'' if c.search_term else 'initial'}" id="j_filter" size="15" type="text" name="filter" value="${c.search_term or _('journal filter...')}"/>
 
    <span class="tooltip" title="${h.tooltip(h.journal_filter_help())}">?</span>
 
    <input type='submit' value="${_('filter')}" class="btn btn-mini" style="padding:0px 2px 0px 2px;margin:0px"/>
 
    ${_('Admin journal')} - ${ungettext('%s entry', '%s entries', c.users_log.item_count) % (c.users_log.item_count)}
 
    <input type='submit' value="${_('Filter')}" class="btn btn-mini" style="padding:0px 2px 0px 2px;margin:0px"/>
 
    ${_('Admin Journal')} - ${ungettext('%s Entry', '%s Entries', c.users_log.item_count) % (c.users_log.item_count)}
 
    </form>
 
    ${h.end_form()}
 
</%def>
 

	
 
<%def name="page_nav()">
 
    ${self.menu('admin')}
 
</%def>
 
<%def name="main()">
 
<div class="box">
 
    <!-- box / title -->
 
    <div class="title">
 
        ${self.breadcrumbs()}
 
    </div>
 
    <!-- end box / title -->
 
    <div class="table">
 
        <div id="user_log">
 
            ${c.log_data}
 
        </div>
 
    </div>
 
</div>
 

	
 
<script>
 
$(document).ready(function() {
 
  $('#j_filter').click(function(){
 
    var $jfilter = $('#j_filter');
 
    if($jfilter.hasClass('initial')){
 
        $jfilter.val('');
 
    }
 
  });
 
  var fix_j_filter_width = function(len){
 
      $('#j_filter').css('width', Math.max(80, len*6.50)+'px');
 
  };
 
  $('#j_filter').keyup(function () {
 
    fix_j_filter_width($('#j_filter').val().length);
 
  });
 
  $('#filter_form').submit(function (e) {
 
      e.preventDefault();
 
      var val = $('#j_filter').val();
 
      window.location = "${url.current(filter='__FILTER__')}".replace('__FILTER__',val);
 
  });
 
  fix_j_filter_width($('#j_filter').val().length);
 
});
 
</script>
 
</%def>
kallithea/templates/admin/defaults/defaults.html
Show inline comments
 
## -*- coding: utf-8 -*-
 
<%inherit file="/base/base.html"/>
 

	
 
<%def name="title()">
 
    ${_('Repositories defaults')}
 
    ${_('Repository Defaults')}
 
    %if c.site_name:
 
        &middot; ${c.site_name}
 
    %endif
 
</%def>
 

	
 
<%def name="breadcrumbs_links()">
 
    ${h.link_to(_('Admin'),h.url('admin_home'))}
 
    &raquo;
 
    ${_('Defaults')}
 
</%def>
 

	
 
<%def name="page_nav()">
 
    ${self.menu('admin')}
 
</%def>
 

	
 
<%def name="main()">
 
<div class="box">
 
    <!-- box / title -->
 
    <div class="title">
 
        ${self.breadcrumbs()}
 
    </div>
 

	
 
    <h3>${_('Repositories defaults')}</h3>
 
    <h3>${_('Repository Defaults')}</h3>
 

	
 
    ${h.form(url('default', id='defaults'),method='put')}
 
    <div class="form">
 
        <!-- fields -->
 

	
 
        <div class="fields">
 

	
 
            <div class="field">
 
                <div class="label">
 
                    <label for="default_repo_type">${_('Type')}:</label>
 
                </div>
 
                <div class="input">
 
                    ${h.select('default_repo_type','hg',c.backends,class_="medium")}
 
                </div>
 
            </div>
 

	
 
            <div class="field">
 
                <div class="label label-checkbox">
 
                    <label for="default_repo_private">${_('Private repository')}:</label>
 
                </div>
 
                <div class="checkboxes">
 
                    ${h.checkbox('default_repo_private',value="True")}
 
                    <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
 
                </div>
 
            </div>
 

	
 

	
 
            <div class="field">
 
                <div class="label label-checkbox">
 
                    <label for="default_repo_enable_statistics">${_('Enable statistics')}:</label>
 
                </div>
 
                <div class="checkboxes">
 
                    ${h.checkbox('default_repo_enable_statistics',value="True")}
 
                    <span class="help-block">${_('Enable statistics window on summary page.')}</span>
 
                </div>
 
            </div>
 

	
 
            <div class="field">
 
                <div class="label label-checkbox">
 
                    <label for="default_repo_enable_downloads">${_('Enable downloads')}:</label>
 
                </div>
 
                <div class="checkboxes">
 
                    ${h.checkbox('default_repo_enable_downloads',value="True")}
 
                    <span class="help-block">${_('Enable download menu on summary page.')}</span>
 
                </div>
 
            </div>
 

	
 
            <div class="field">
 
                <div class="label label-checkbox">
 
                    <label for="default_repo_enable_locking">${_('Enable locking')}:</label>
 
                </div>
 
                <div class="checkboxes">
 
                    ${h.checkbox('default_repo_enable_locking',value="True")}
 
                    <span class="help-block">${_('Enable lock-by-pulling on repository.')}</span>
 
                </div>
 
            </div>
 

	
 
            <div class="buttons">
 
            ${h.submit('save',_('Save'),class_="btn")}
 
            </div>
 
        </div>
 
    </div>
 
    ${h.end_form()}
 

	
 
    ##<h3>${_('Groups defaults')}</h3>
 

	
 
</div>
 
</%def>
kallithea/templates/admin/gists/index.html
Show inline comments
 
## -*- coding: utf-8 -*-
 
<%inherit file="/base/base.html"/>
 

	
 
<%def name="title()">
 
    %if c.show_private:
 
        ${_('Private Gists for user %s') % c.authuser.username}
 
        ${_('Private Gists for User %s') % c.authuser.username}
 
    %elif c.show_public:
 
        ${_('Public Gists for user %s') % c.authuser.username}
 
        ${_('Public Gists for User %s') % c.authuser.username}
 
    %else:
 
        ${_('Public Gists')}
 
    %endif
 
    %if c.site_name:
 
        &middot; ${c.site_name}
 
    %endif
 
</%def>
 

	
 
<%def name="breadcrumbs_links()">
 
    %if c.show_private:
 
        ${_('Private Gists for user %s') % c.authuser.username}
 
        ${_('Private Gists for User %s') % c.authuser.username}
 
    %elif c.show_public:
 
        ${_('Public Gists for user %s') % c.authuser.username}
 
        ${_('Public Gists for User %s') % c.authuser.username}
 
    %else:
 
        ${_('Public Gists')}
 
    %endif
 
    - ${c.gists_pager.item_count}
 
</%def>
 

	
 
<%def name="page_nav()">
 
    ${self.menu('gists')}
 
</%def>
 

	
 
<%def name="main()">
 
<div class="box">
 
    <!-- box / title -->
 
    <div class="title">
 
        ${self.breadcrumbs()}
 
        %if c.authuser.username != 'default':
 
        <ul class="links">
 
          <li>
 
             <a href="${h.url('new_gist')}" class="btn btn-small btn-success"><i class="icon-plus"></i> ${_(u'Create New Gist')}</a>
 
          </li>
 
        </ul>
 
        %endif
 
    </div>
 
    %if c.gists_pager.item_count>0:
 
        % for gist in c.gists_pager:
 
          <div class="gist-item" style="padding:10px 20px 10px 15px">
 

	
 
            <div class="gravatar">
 
               <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(gist.owner.full_contact),28)}"/>
 
            </div>
 
            <div title="${gist.owner.full_contact}" class="user" style="font-size: 16px">
 
                <b>${h.person(gist.owner.full_contact)}</b> /
 
                <b><a href="${h.url('gist',gist_id=gist.gist_access_id)}">gist: ${gist.gist_access_id}</a></b>
 
            </div>
 
            <div style="padding: 4px 0px 0px 0px">
 
                ${_('Created')} ${h.age(gist.created_on)} /
 
                <span style="color: #AAA">
 
                  %if gist.gist_expires == -1:
 
                   ${_('Expires')}: ${_('never')}
 
                  %else:
 
                   ${_('Expires')}: ${h.age(h.time_to_datetime(gist.gist_expires))}
 
                  %endif
 
                </span>
 
            </div>
 

	
 
            <div style="border:0px;padding:10px 0px 0px 40px;color:#AAA">${gist.gist_description}</div>
 
          </div>
 
        % endfor
 

	
 
        <div class="notification-paginator">
 
          <div class="pagination-wh pagination-left">
 
          ${c.gists_pager.pager('$link_previous ~2~ $link_next', **request.GET.mixed())}
 
          </div>
 
        </div>
 
    %else:
 
        <div class="table">${_('There are no gists yet')}</div>
 
    %endif
 
</div>
 
</%def>

Changeset was too big and was cut off... Show full diff anyway

0 comments (0 inline, 0 general)