.. _changelog:
=========
Changelog
1.4.0 (**2012-XX-XX**)
----------------------
:status: in-progress
:branch: beta
news
++++
- new codereview system
- email map, allowing users to have multiple email addresses mapped into
their accounts
- improved git-hook system. Now all actions for git are logged into journal
including pushed revisions, user and IP address
- changed setup-app into setup-rhodecode and added default options to it.
- new git repos are created as bare now by default
- #464 added links to groups in permission box
- #465 mentions autocomplete inside comments boxes
- #469 added --update-only option to whoosh to re-index only given list
of repos in index
- rhodecode-api CLI client
- new git http protocol replaced buggy dulwich implementation.
Now based on pygrack & gitweb
- Improved RSS/ATOM feeds. Discoverable by browsers using proper headers, and
reformated based on user suggestions. Additional rss/atom feeds for user
journal
- various i18n improvements
- #478 permissions overview for admin in user edit view
- File view now displays small gravatars off all authors of given file
- Implemented landing revisions. Each repository will get landing_rev attribute
that defines 'default' revision/branch for generating readme files
- Implemented #509, RhodeCode enforces SSL for push/pulling if requested.
- Import remote svn repositories to mercurial using hgsubversion
- Fixed #508 RhodeCode now has a option to explicitly set forking permissions
- RhodeCode can use alternative server for generating avatar icons
- implemented repositories locking. Pull locks, push unlocks. Also can be done
via API calls
- #538 form for permissions can handle multiple users at once
fixes
+++++
- improved translations
- fixes issue #455 Creating an archive generates an exception on Windows
- fixes #448 Download ZIP archive keeps file in /tmp open and results
in out of disk space
- fixes issue #454 Search results under Windows include proceeding
backslash
- fixed issue #450. Rhodecode no longer will crash when bad revision is
present in journal data.
- fix for issue #417, git execution was broken on windows for certain
commands.
- fixed #413. Don't disable .git directory for bare repos on deleting
- fixed issue #459. Changed the way of obtaining logger in reindex task.
- fixed #453 added ID field in whoosh SCHEMA that solves the issue of
reindexing modified files
- fixed #481 rhodecode emails are sent without Date header
- fixed #458 wrong count when no repos are present
- fixed issue #492 missing `\ No newline at end of file` test at the end of
new chunk in html diff
- full text search now works also for commit messages
1.3.6 (**2012-05-17**)
- chinese traditional translation
- changed setup-app into setup-rhodecode and added arguments for auto-setup
mode that doesn't need user interaction
- fixed no scm found warning
- fixed __future__ import error on rcextensions
- made simplejson required lib for speedup on JSON encoding
- fixes #449 bad regex could get more than revisions from parsing history
- don't clear DB session when CELERY_EAGER is turned ON
1.3.5 (**2012-05-10**)
- use ext_json for json module
- unified annotation view with file source view
- notification improvements, better inbox + css
- #419 don't strip passwords for login forms, make rhodecode
more compatible with LDAP servers
- Added HTTP_X_FORWARDED_FOR as another method of extracting
IP for pull/push logs. - moved all to base controller
- #415: Adding comment to changeset causes reload.
Comments are now added via ajax and doesn't reload the page
- #374 LDAP config is discarded when LDAP can't be activated
- limited push/pull operations are now logged for git in the journal
- bumped mercurial to 2.2.X series
- added support for displaying submodules in file-browser
- #421 added bookmarks in changelog view
- fixed dev-version marker for stable when served from source codes
- fixed missing permission checks on show forks page
- #418 cast to unicode fixes in notification objects
- #426 fixed mention extracting regex
- fixed remote-pulling for git remotes remopositories
- fixed #434: Error when accessing files or changesets of a git repository
with submodules
- fixed issue with empty APIKEYS for users after registration ref. #438
- fixed issue with getting README files from git repositories
1.3.4 (**2012-03-28**)
- Whoosh logging is now controlled by the .ini files logging setup
- added clone-url into edit form on /settings page
- added help text into repo add/edit forms
- created rcextensions module with additional mappings (ref #322) and
post push/pull/create repo hooks callbacks
- implemented #377 Users view for his own permissions on account page
- #399 added inheritance of permissions for users group on repos groups
- #401 repository group is automatically pre-selected when adding repos
inside a repository group
- added alternative HTTP 403 response when client failed to authenticate. Helps
solving issues with Mercurial and LDAP
- #402 removed group prefix from repository name when listing repositories
inside a group
- added gravatars into permission view and permissions autocomplete
- #347 when running multiple RhodeCode instances, properly invalidates cache
for all registered servers
- fixed #390 cache invalidation problems on repos inside group
- fixed #385 clone by ID url was loosing proxy prefix in URL
- fixed some unicode problems with waitress
- fixed issue with escaping < and > in changeset commits
- fixed error occurring during recursive group creation in API
create_repo function
- fixed #393 py2.5 fixes for routes url generator
- fixed #397 Private repository groups shows up before login
- fixed #396 fixed problems with revoking users in nested groups
- fixed mysql unicode issues + specified InnoDB as default engine with
utf8 charset
- #406 trim long branch/tag names in changelog to not break UI
1.3.3 (**2012-03-02**)
- fixed some python2.5 compatibility issues
- fixed issues with removed repos was accidentally added as groups, after
full rescan of paths
- fixes #376 Cannot edit user (using container auth)
- fixes #378 Invalid image urls on changeset screen with proxy-prefix
configuration
- fixed initial sorting of repos inside repo group
- fixes issue when user tried to resubmit same permission into user/user_groups
- bumped beaker version that fixes #375 leap error bug
- fixed raw_changeset for git. It was generated with hg patch headers
- fixed vcs issue with last_changeset for filenodes
- fixed missing commit after hook delete
- fixed #372 issues with git operation detection that caused a security issue
for git repos
1.3.2 (**2012-02-28**)
- fixed git protocol issues with repos-groups
- fixed git remote repos validator that prevented from cloning remote git repos
- fixes #370 ending slashes fixes for repo and groups
- fixes #368 improved git-protocol detection to handle other clients
- fixes #366 When Setting Repository Group To Blank Repo Group Wont Be
Moved To Root
- fixes #371 fixed issues with beaker/sqlalchemy and non-ascii cache keys
- fixed #373 missing cascade drop on user_group_to_perm table
1.3.1 (**2012-02-27**)
- redirection loop occurs when remember-me wasn't checked during login
- fixes issues with git blob history generation
- don't fetch branch for git in file history dropdown. Causes unneeded slowness
1.3.0 (**2012-02-26**)
- code review, inspired by github code-comments
- #215 rst and markdown README files support
- #252 Container-based and proxy pass-through authentication support
- #44 branch browser. Filtering of changelog by branches
- mercurial bookmarks support
- new hover top menu, optimized to add maximum size for important views
- configurable clone url template with possibility to specify protocol like
ssh:// or http:// and also manually alter other parts of clone_url.
- enabled largefiles extension by default
- optimized summary file pages and saved a lot of unused space in them
- #239 option to manually mark repository as fork
- #320 mapping of commit authors to RhodeCode users
- #304 hashes are displayed using monospace font
- diff configuration, toggle white lines and context lines
- #307 configurable diffs, whitespace toggle, increasing context lines
- sorting on branches, tags and bookmarks using YUI datatable
- improved file filter on files page
- implements #330 api method for listing nodes ar particular revision
- #73 added linking issues in commit messages to chosen issue tracker url
based on user defined regular expression
- added linking of changesets in commit messages
- new compact changelog with expandable commit messages
- firstname and lastname are optional in user creation
- #348 added post-create repository hook
- #212 global encoding settings is now configurable from .ini files
- #227 added repository groups permissions
- markdown gets codehilite extensions
- new API methods, delete_repositories, grante/revoke permissions for groups
and repos
- rewrote dbsession management for atomic operations, and better error handling
- fixed sorting of repo tables
- #326 escape of special html entities in diffs
- normalized user_name => username in api attributes
- fixes #298 ldap created users with mixed case emails created conflicts
on saving a form
- fixes issue when owner of a repo couldn't revoke permissions for users
and groups
- fixes #271 rare JSON serialization problem with statistics
- fixes #337 missing validation check for conflicting names of a group with a
repositories group
- #340 fixed session problem for mysql and celery tasks
- fixed #331 RhodeCode mangles repository names if the a repository group
contains the "full path" to the repositories
- #355 RhodeCode doesn't store encrypted LDAP passwords
1.2.5 (**2012-01-28**)
- #340 Celery complains about MySQL server gone away, added session cleanup
for celery tasks
- #341 "scanning for repositories in None" log message during Rescan was missing
a parameter
- fixed creating archives with subrepos. Some hooks were triggered during that
operation leading to crash.
- fixed missing email in account page.
- Reverted Mercurial to 2.0.1 for windows due to bug in Mercurial that makes
forking on windows impossible
1.2.4 (**2012-01-19**)
- RhodeCode is bundled with mercurial series 2.0.X by default, with
full support to largefiles extension. Enabled by default in new installations
- #329 Ability to Add/Remove Groups to/from a Repository via AP
- added requires.txt file with requirements
- fixes db session issues with celery when emailing admins
- #331 RhodeCode mangles repository names if the a repository group
- #298 Conflicting e-mail addresses for LDAP and RhodeCode users
- DB session cleanup after hg protocol operations, fixes issues with
`mysql has gone away` errors
- #333 doc fixes for get_repo api function
- #271 rare JSON serialization problem with statistics enabled
- #337 Fixes issues with validation of repository name conflicting with
a group name. A proper message is now displayed.
- #292 made ldap_dn in user edit readonly, to get rid of confusion that field
doesn't work
- #316 fixes issues with web description in hgrc files
1.2.3 (**2011-11-02**)
- added option to manage repos group for non admin users
- added following API methods for get_users, create_user, get_users_groups,
get_users_group, create_users_group, add_user_to_users_groups, get_repos,
get_repo, create_repo, add_user_to_repo
- implements #237 added password confirmation for my account
and admin edit user.
- implements #291 email notification for global events are now sent to all
administrator users, and global config email.
- added option for passing auth method for smtp mailer
- #276 issue with adding a single user with id>10 to usergroups
- #277 fixes windows LDAP settings in which missing values breaks the ldap auth
- #288 fixes managing of repos in a group for non admin user
1.2.2 (**2011-10-17**)
- #226 repo groups are available by path instead of numerical id
- #259 Groups with the same name but with different parent group
- #260 Put repo in group, then move group to another group -> repo becomes unavailable
- #258 RhodeCode 1.2 assumes egg folder is writable (lockfiles problems)
- #265 ldap save fails sometimes on converting attributes to booleans,
added getter and setter into model that will prevent from this on db model level
- fixed problems with timestamps issues #251 and #213
- fixes #266 RhodeCode allows to create repo with the same name and in
the same parent as group
- fixes #245 Rescan of the repositories on Windows
- fixes #248 cannot edit repos inside a group on windows
- fixes #219 forking problems on windows
1.2.1 (**2011-10-08**)
- fixed problems with basic auth and push problems
- gui fixes
- fixed logger
1.2.0 (**2011-10-07**)
- implemented #47 repository groups
- implemented #89 Can setup google analytics code from settings menu
- implemented #91 added nicer looking archive urls with more download options
like tags, branches
- implemented #44 into file browsing, and added follow branch option
- implemented #84 downloads can be enabled/disabled for each repository
- anonymous repository can be cloned without having to pass default:default
into clone url
- fixed #90 whoosh indexer can index chooses repositories passed in command
line
- extended journal with day aggregates and paging
- implemented #107 source code lines highlight ranges
- implemented #93 customizable changelog on combined revision ranges -
equivalent of githubs compare view
- implemented #108 extended and more powerful LDAP configuration
- implemented #56 users groups
- major code rewrites optimized codes for speed and memory usage
- raw and diff downloads are now in git format
- setup command checks for write access to given path
- fixed many issues with international characters and unicode. It uses utf8
decode with replace to provide less errors even with non utf8 encoded strings
- #125 added API KEY access to feeds
- #109 Repository can be created from external Mercurial link (aka. remote
repository, and manually updated (via pull) from admin panel
- beta git support - push/pull server + basic view for git repos
- added followers page and forks page
- server side file creation (with binary file upload interface)
and edition with commits powered by codemirror
- #111 file browser file finder, quick lookup files on whole file tree
- added quick login sliding menu into main page
- changelog uses lazy loading of affected files details, in some scenarios
this can improve speed of changelog page dramatically especially for
larger repositories.
- implements #214 added support for downloading subrepos in download menu.
- Added basic API for direct operations on rhodecode via JSON
- Implemented advanced hook management
- fixed file browser bug, when switching into given form revision the url was
not changing
- fixed propagation to error controller on simplehg and simplegit middlewares
- fixed error when trying to make a download on empty repository
- fixed problem with '[' chars in commit messages in journal
- fixed #99 Unicode errors, on file node paths with non utf-8 characters
- journal fork fixes
- removed issue with space inside renamed repository after deletion
- fixed strange issue on formencode imports
- fixed #126 Deleting repository on Windows, rename used incompatible chars.
- #150 fixes for errors on repositories mapped in db but corrupted in
filesystem
- fixed problem with ascendant characters in realm #181
- fixed problem with sqlite file based database connection pool
- whoosh indexer and code stats share the same dynamic extensions map
- fixes #188 - relationship delete of repo_to_perm entry on user removal
- fixes issue #189 Trending source files shows "show more" when no more exist
- fixes issue #197 Relative paths for pidlocks
- fixes issue #198 password will require only 3 chars now for login form
- fixes issue #199 wrong redirection for non admin users after creating a repository
- fixes issues #202, bad db constraint made impossible to attach same group
more than one time. Affects only mysql/postgres
- fixes #218 os.kill patch for windows was missing sig param
- improved rendering of dag (they are not trimmed anymore when number of
heads exceeds 5)
1.1.8 (**2011-04-12**)
- improved windows support
- fixed #140 freeze of python dateutil library, since new version is python2.x
incompatible
- setup-app will check for write permission in given path
- cleaned up license info issue #149
- fixes for issues #137,#116 and problems with unicode and accented characters.
- fixes crashes on gravatar, when passed in email as unicode
- fixed tooltip flickering problems
- fixed came_from redirection on windows
- fixed logging modules, and sql formatters
- windows fixes for os.kill issue #133
- fixes path splitting for windows issues #148
- fixed issue #143 wrong import on migration to 1.1.X
- fixed problems with displaying binary files, thanks to Thomas Waldmann
- removed name from archive files since it's breaking ui for long repo names
- fixed issue with archive headers sent to browser, thanks to Thomas Waldmann
- fixed compatibility for 1024px displays, and larger dpi settings, thanks to
Thomas Waldmann
- fixed issue #166 summary pager was skipping 10 revisions on second page
1.1.7 (**2011-03-23**)
- fixed (again) #136 installation support for FreeBSD
1.1.6 (**2011-03-21**)
- fixed #136 installation support for FreeBSD
- RhodeCode will check for python version during installation
1.1.5 (**2011-03-17**)
- basic windows support, by exchanging pybcrypt into sha256 for windows only
highly inspired by idea of mantis406
- fixed sorting by author in main page
- fixed crashes with diffs on binary files
- fixed #131 problem with boolean values for LDAP
- fixed #122 mysql problems thanks to striker69
- fixed problem with errors on calling raw/raw_files/annotate functions
with unknown revisions
- fixed returned rawfiles attachment names with international character
- cleaned out docs, big thanks to Jason Harris
1.1.4 (**2011-02-19**)
- fixed formencode import problem on settings page, that caused server crash
when that page was accessed as first after server start
- journal fixes
- fixed option to access repository just by entering http://server/<repo_name>
1.1.3 (**2011-02-16**)
- implemented #102 allowing the '.' character in username
- added option to access repository just by entering http://server/<repo_name>
- celery task ignores result for better performance
- fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
apollo13 and Johan Walles
- small fixes in journal
- fixed problems with getting setting for celery from .ini files
- registration, password reset and login boxes share the same title as main
application now
- fixed #113: to high permissions to fork repository
- db transaction fixes when filesystem repository creation failed
- fixed #106 relation issues on databases different than sqlite
- fixed static files paths links to use of url() method
1.1.2 (**2011-01-12**)
- fixes #98 protection against float division of percentage stats
- fixed graph bug
- forced webhelpers version since it was making troubles during installation
1.1.1 (**2011-01-06**)
- added force https option into ini files for easier https usage (no need to
set server headers with this options)
- small css updates
- fixed #96 redirect loop on files view on repositories without changesets
- fixed #97 unicode string passed into server header in special cases (mod_wsgi)
and server crashed with errors
- fixed large tooltips problems on main page
- fixed #92 whoosh indexer is more error proof
1.1.0 (**2010-12-18**)
- rewrite of internals for vcs >=0.1.10
- uses mercurial 1.7 with dotencode disabled for maintaining compatibility
with older clients
- anonymous access, authentication via ldap
- performance upgrade for cached repos list - each repository has its own
cache that's invalidated when needed.
- performance upgrades on repositories with large amount of commits (20K+)
- main page quick filter for filtering repositories
- user dashboards with ability to follow chosen repositories actions
- sends email to admin on new user registration
- added cache/statistics reset options into repository settings
- more detailed action logger (based on hooks) with pushed changesets lists
and options to disable those hooks from admin panel
- introduced new enhanced changelog for merges that shows more accurate results
- new improved and faster code stats (based on pygments lexers mapping tables,
showing up to 10 trending sources for each repository. Additionally stats
can be disabled in repository settings.
- gui optimizations, fixed application width to 1024px
- added cut off (for large files/changesets) limit into config files
- whoosh, celeryd, upgrade moved to paster command
- other than sqlite database backends can be used
- fixes #61 forked repo was showing only after cache expired
- fixes #76 no confirmation on user deletes
- fixes #66 Name field misspelled
- fixes #72 block user removal when he owns repositories
- fixes #69 added password confirmation fields
- fixes #87 RhodeCode crashes occasionally on updating repository owner
- fixes #82 broken annotations on files with more than 1 blank line at the end
- a lot of fixes and tweaks for file browser
- fixed detached session issues
- fixed when user had no repos he would see all repos listed in my account
- fixed ui() instance bug when global hgrc settings was loaded for server
instance and all hgrc options were merged with our db ui() object
- numerous small bugfixes
(special thanks for TkSoh for detailed feedback)
1.0.2 (**2010-11-12**)
- tested under python2.7
- bumped sqlalchemy and celery versions
- fixed #59 missing graph.js
- fixed repo_size crash when repository had broken symlinks
- fixed python2.5 crashes.
1.0.1 (**2010-11-10**)
- small css updated
- fixed #53 python2.5 incompatible enumerate calls
- fixed #52 disable mercurial extension for web
- fixed #51 deleting repositories don't delete it's dependent objects
1.0.0 (**2010-11-02**)
- security bugfix simplehg wasn't checking for permissions on commands
other than pull or push.
- fixed doubled messages after push or pull in admin journal
- templating and css corrections, fixed repo switcher on chrome, updated titles
- admin menu accessible from options menu on repository view
- permissions cached queries
1.0.0rc4 (**2010-10-12**)
--------------------------
- fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
- removed cache_manager settings from sqlalchemy meta
- added sqlalchemy cache settings to ini files
- validated password length and added second try of failure on paster setup-app
- fixed setup database destroy prompt even when there was no db
1.0.0rc3 (**2010-10-11**)
-------------------------
- fixed i18n during installation.
1.0.0rc2 (**2010-10-11**)
- Disabled dirsize in file browser, it's causing nasty bug when dir renames
occure. After vcs is fixed it'll be put back again.
- templating/css rewrites, optimized css.
\ No newline at end of file
# -*- coding: utf-8 -*-
"""
rhodecode.lib.utils
~~~~~~~~~~~~~~~~~~~
Some simple helper functions
:created_on: Jan 5, 2011
:author: marcink
:copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
:license: GPLv3, see COPYING for more details.
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import re
import time
import datetime
from pylons.i18n.translation import _, ungettext
from rhodecode.lib.vcs.utils.lazy import LazyProperty
def __get_lem():
Get language extension map based on what's inside pygments lexers
from pygments import lexers
from string import lower
from collections import defaultdict
d = defaultdict(lambda: [])
def __clean(s):
s = s.lstrip('*')
s = s.lstrip('.')
if s.find('[') != -1:
exts = []
start, stop = s.find('['), s.find(']')
for suffix in s[start + 1:stop]:
exts.append(s[:s.find('[')] + suffix)
return map(lower, exts)
else:
return map(lower, [s])
for lx, t in sorted(lexers.LEXERS.items()):
m = map(__clean, t[-2])
if m:
m = reduce(lambda x, y: x + y, m)
for ext in m:
desc = lx.replace('Lexer', '')
d[ext].append(desc)
return dict(d)
def str2bool(_str):
returs True/False value from given string, it tries to translate the
string into boolean
:param _str: string value to translate into boolean
:rtype: boolean
:returns: boolean from given string
if _str is None:
return False
if _str in (True, False):
return _str
_str = str(_str).strip().lower()
return _str in ('t', 'true', 'y', 'yes', 'on', '1')
def convert_line_endings(line, mode):
Converts a given line "line end" accordingly to given mode
Available modes are::
0 - Unix
1 - Mac
2 - DOS
:param line: given line to convert
:param mode: mode to convert to
:rtype: str
:return: converted line according to mode
from string import replace
if mode == 0:
line = replace(line, '\r\n', '\n')
line = replace(line, '\r', '\n')
elif mode == 1:
line = replace(line, '\r\n', '\r')
line = replace(line, '\n', '\r')
elif mode == 2:
line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
return line
def detect_mode(line, default):
Detects line break for given line, if line break couldn't be found
given default value is returned
:param line: str line
:param default: default
:rtype: int
:return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
if line.endswith('\r\n'):
return 2
elif line.endswith('\n'):
return 0
elif line.endswith('\r'):
return 1
return default
def generate_api_key(username, salt=None):
Generates unique API key for given username, if salt is not given
it'll be generated from some random string
:param username: username as string
:param salt: salt to hash generate KEY
:returns: sha1 hash from username+salt
from tempfile import _RandomNameSequence
import hashlib
if salt is None:
salt = _RandomNameSequence().next()
return hashlib.sha1(username + salt).hexdigest()
def safe_unicode(str_, from_encoding=None):
safe unicode function. Does few trick to turn str_ into unicode
In case of UnicodeDecode error we try to return it with encoding detected
by chardet library if it fails fallback to unicode with errors replaced
:param str_: string to decode
:rtype: unicode
:returns: unicode object
if isinstance(str_, unicode):
return str_
if not from_encoding:
import rhodecode
DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
from_encoding = DEFAULT_ENCODING
try:
return unicode(str_)
except UnicodeDecodeError:
pass
return unicode(str_, from_encoding)
import chardet
encoding = chardet.detect(str_)['encoding']
if encoding is None:
raise Exception()
return str_.decode(encoding)
except (ImportError, UnicodeDecodeError, Exception):
return unicode(str_, from_encoding, 'replace')
def safe_str(unicode_, to_encoding=None):
safe str function. Does few trick to turn unicode_ into string
In case of UnicodeEncodeError we try to return it with encoding detected
by chardet library if it fails fallback to string with errors replaced
:param unicode_: unicode to encode
:returns: str object
# if it's not basestr cast to str
if not isinstance(unicode_, basestring):
return str(unicode_)
if isinstance(unicode_, str):
return unicode_
if not to_encoding:
to_encoding = DEFAULT_ENCODING
return unicode_.encode(to_encoding)
except UnicodeEncodeError:
encoding = chardet.detect(unicode_)['encoding']
raise UnicodeEncodeError()
return unicode_.encode(encoding)
except (ImportError, UnicodeEncodeError):
return unicode_.encode(to_encoding, 'replace')
return safe_str
def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
Custom engine_from_config functions that makes sure we use NullPool for
file based sqlite databases. This prevents errors on sqlite. This only
applies to sqlalchemy versions < 0.7.0
import sqlalchemy
from sqlalchemy import engine_from_config as efc
import logging
if int(sqlalchemy.__version__.split('.')[1]) < 7:
# This solution should work for sqlalchemy < 0.7.0, and should use
# proxy=TimerProxy() for execution time profiling
from sqlalchemy.pool import NullPool
url = configuration[prefix + 'url']
if url.startswith('sqlite'):
kwargs.update({'poolclass': NullPool})
return efc(configuration, prefix, **kwargs)
from sqlalchemy import event
from sqlalchemy.engine import Engine
log = logging.getLogger('sqlalchemy.engine')
BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = xrange(30, 38)
engine = efc(configuration, prefix, **kwargs)
def color_sql(sql):
COLOR_SEQ = "\033[1;%dm"
COLOR_SQL = YELLOW
normal = '\x1b[0m'
return ''.join([COLOR_SEQ % COLOR_SQL, sql, normal])
if configuration['debug']:
#attach events only for debug configuration
def before_cursor_execute(conn, cursor, statement,
parameters, context, executemany):
context._query_start_time = time.time()
log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
def after_cursor_execute(conn, cursor, statement,
total = time.time() - context._query_start_time
log.info(color_sql("<<<<< TOTAL TIME: %f <<<<<" % total))
event.listen(engine, "before_cursor_execute",
before_cursor_execute)
event.listen(engine, "after_cursor_execute",
after_cursor_execute)
return engine
def age(prevdate):
turns a datetime into an age string.
:param prevdate: datetime object
:returns: unicode words describing age
order = ['year', 'month', 'day', 'hour', 'minute', 'second']
deltas = {}
# Get date parts deltas
now = datetime.datetime.now()
for part in order:
deltas[part] = getattr(now, part) - getattr(prevdate, part)
# Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
# not 1 hour, -59 minutes and -59 seconds)
for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
part = order[num]
carry_part = order[num - 1]
if deltas[part] < 0:
deltas[part] += length
deltas[carry_part] -= 1
# Same thing for days except that the increment depends on the (variable)
# number of days in the month
month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
if deltas['day'] < 0:
if prevdate.month == 2 and (prevdate.year % 4 == 0 and
(prevdate.year % 100 != 0 or prevdate.year % 400 == 0)):
deltas['day'] += 29
deltas['day'] += month_lengths[prevdate.month - 1]
deltas['month'] -= 1
if deltas['month'] < 0:
deltas['month'] += 12
deltas['year'] -= 1
# Format the result
fmt_funcs = {
'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
}
for i, part in enumerate(order):
value = deltas[part]
if value == 0:
continue
if i < 5:
sub_part = order[i + 1]
sub_value = deltas[sub_part]
sub_value = 0
if sub_value == 0:
return _(u'%s ago') % fmt_funcs[part](value)
return _(u'%s and %s ago') % (fmt_funcs[part](value),
fmt_funcs[sub_part](sub_value))
return _(u'just now')
def uri_filter(uri):
Removes user:password from given url string
:param uri:
:returns: filtered list of strings
if not uri:
return ''
proto = ''
for pat in ('https://', 'http://'):
if uri.startswith(pat):
uri = uri[len(pat):]
proto = pat
break
# remove passwords and username
uri = uri[uri.find('@') + 1:]
# get the port
cred_pos = uri.find(':')
if cred_pos == -1:
host, port = uri, None
host, port = uri[:cred_pos], uri[cred_pos + 1:]
return filter(None, [proto, host, port])
def credentials_filter(uri):
Returns a url with removed credentials
uri = uri_filter(uri)
#check if we have port
if len(uri) > 2 and uri[2]:
uri[2] = ':' + uri[2]
return ''.join(uri)
def get_changeset_safe(repo, rev):
Safe version of get_changeset if this changeset doesn't exists for a
repo it returns a Dummy one instead
:param repo:
:param rev:
from rhodecode.lib.vcs.backends.base import BaseRepository
from rhodecode.lib.vcs.exceptions import RepositoryError
from rhodecode.lib.vcs.backends.base import EmptyChangeset
if not isinstance(repo, BaseRepository):
raise Exception('You must pass an Repository '
'object as first argument got %s', type(repo))
cs = repo.get_changeset(rev)
except RepositoryError:
cs = EmptyChangeset(requested_revision=rev)
return cs
def datetime_to_time(dt):
if dt:
return time.mktime(dt.timetuple())
def time_to_datetime(tm):
if tm:
if isinstance(tm, basestring):
tm = float(tm)
except ValueError:
return
return datetime.datetime.fromtimestamp(tm)
MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
def extract_mentioned_users(s):
Returns unique usernames from given string s that have @mention
:param s: string to get mentions
usrs = set()
for username in re.findall(MENTIONS_REGEX, s):
usrs.add(username)
return sorted(list(usrs), key=lambda k: k.lower())
class AttributeDict(dict):
def __getattr__(self, attr):
return self.get(attr, None)
__setattr__ = dict.__setitem__
__delattr__ = dict.__delitem__
Set of generic validators
import os
import formencode
from pylons.i18n.translation import _
from webhelpers.pylonslib.secure_form import authentication_token
from formencode.validators import (
UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set,
NotEmpty
)
from rhodecode.lib.compat import OrderedSet
from rhodecode.lib.utils import repo_name_slug
from rhodecode.model.db import RepoGroup, Repository, UsersGroup, User,\
ChangesetStatus
from rhodecode.lib.exceptions import LdapImportError
from rhodecode.config.routing import ADMIN_PREFIX
# silence warnings and pylint
UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
log = logging.getLogger(__name__)
class UniqueList(formencode.FancyValidator):
Unique List !
messages = dict(
empty=_('Value cannot be an empty list'),
missing_value=_('Value cannot be an empty list'),
def _to_python(self, value, state):
if isinstance(value, list):
return value
elif isinstance(value, set):
return list(value)
elif isinstance(value, tuple):
elif value is None:
return []
return [value]
def empty_value(self, value):
class StateObj(object):
this is needed to translate the messages using _() in validators
_ = staticmethod(_)
def M(self, key, state=None, **kwargs):
returns string from self.message based on given key,
passed kw params are used to substitute %(named)s params inside
translated strings
:param msg:
:param state:
if state is None:
state = StateObj()
state._ = staticmethod(_)
#inject validator into state object
return self.message(key, state, **kwargs)
def ValidUsername(edit=False, old_data={}):
class _validator(formencode.validators.FancyValidator):
messages = {
'username_exists': _(u'Username "%(username)s" already exists'),
'system_invalid_username':
_(u'Username "%(username)s" is forbidden'),
'invalid_username':
_(u'Username may only contain alphanumeric characters '
'underscores, periods or dashes and must begin with '
'alphanumeric character')
def validate_python(self, value, state):
if value in ['default', 'new_user']:
msg = M(self, 'system_invalid_username', state, username=value)
raise formencode.Invalid(msg, value, state)
#check if user is unique
old_un = None
if edit:
old_un = User.get(old_data.get('user_id')).username
if old_un != value or not edit:
if User.get_by_username(value, case_insensitive=True):
msg = M(self, 'username_exists', state, username=value)
if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
msg = M(self, 'invalid_username', state)
return _validator
def ValidRepoUser():
'invalid_username': _(u'Username %(username)s is not valid')
User.query().filter(User.active == True)\
.filter(User.username == value).one()
except Exception:
msg = M(self, 'invalid_username', state, username=value)
raise formencode.Invalid(msg, value, state,
error_dict=dict(username=msg)
def ValidUsersGroup(edit=False, old_data={}):
'invalid_group': _(u'Invalid users group name'),
'group_exist': _(u'Users group "%(usersgroup)s" already exists'),
'invalid_usersgroup_name':
_(u'users group name may only contain alphanumeric '
'characters underscores, periods or dashes and must begin '
'with alphanumeric character')
if value in ['default']:
msg = M(self, 'invalid_group', state)
error_dict=dict(users_group_name=msg)
#check if group is unique
old_ugname = None
old_id = old_data.get('users_group_id')
old_ugname = UsersGroup.get(old_id).users_group_name
if old_ugname != value or not edit:
is_existing_group = UsersGroup.get_by_group_name(value,
case_insensitive=True)
if is_existing_group:
msg = M(self, 'group_exist', state, usersgroup=value)
msg = M(self, 'invalid_usersgroup_name', state)
def ValidReposGroup(edit=False, old_data={}):
'group_parent_id': _(u'Cannot assign this group as parent'),
'group_exists': _(u'Group "%(group_name)s" already exists'),
'repo_exists':
_(u'Repository with name "%(group_name)s" already exists')
# TODO WRITE VALIDATIONS
group_name = value.get('group_name')
group_parent_id = value.get('group_parent_id')
# slugify repo group just in case :)
slug = repo_name_slug(group_name)
# check for parent of self
parent_of_self = lambda: (
old_data['group_id'] == int(group_parent_id)
if group_parent_id else False
if edit and parent_of_self():
msg = M(self, 'group_parent_id', state)
error_dict=dict(group_parent_id=msg)
old_gname = None
old_gname = RepoGroup.get(old_data.get('group_id')).group_name
if old_gname != group_name or not edit:
# check group
gr = RepoGroup.query()\
.filter(RepoGroup.group_name == slug)\
.filter(RepoGroup.group_parent_id == group_parent_id)\
.scalar()
if gr:
msg = M(self, 'group_exists', state, group_name=slug)
error_dict=dict(group_name=msg)
# check for same repo
repo = Repository.query()\
.filter(Repository.repo_name == slug)\
if repo:
msg = M(self, 'repo_exists', state, group_name=slug)
def ValidPassword():
'invalid_password':
_(u'Invalid characters (non-ascii) in password')
(value or '').decode('ascii')
except UnicodeError:
msg = M(self, 'invalid_password', state)
raise formencode.Invalid(msg, value, state,)
def ValidPasswordsMatch():
'password_mismatch': _(u'Passwords do not match'),
pass_val = value.get('password') or value.get('new_password')
if pass_val != value['password_confirmation']:
msg = M(self, 'password_mismatch', state)
error_dict=dict(password_confirmation=msg)
def ValidAuth():
'invalid_password': _(u'invalid password'),
'invalid_username': _(u'invalid user name'),
'disabled_account': _(u'Your account is disabled')
from rhodecode.lib.auth import authenticate
password = value['password']
username = value['username']
if not authenticate(username, password):
user = User.get_by_username(username)
if user and user.active is False:
log.warning('user %s is disabled' % username)
msg = M(self, 'disabled_account', state)
log.warning('user %s failed to authenticate' % username)
msg2 = M(self, 'invalid_password', state)
error_dict=dict(username=msg, password=msg2)
def ValidAuthToken():
'invalid_token': _(u'Token mismatch')
if value != authentication_token():
msg = M(self, 'invalid_token', state)
def ValidRepoName(edit=False, old_data={}):
'invalid_repo_name':
_(u'Repository name %(repo)s is disallowed'),
'repository_exists':
_(u'Repository named %(repo)s already exists'),
'repository_in_group_exists': _(u'Repository "%(repo)s" already '
'exists in group "%(group)s"'),
'same_group_exists': _(u'Repositories group with name "%(repo)s" '
'already exists')
repo_name = repo_name_slug(value.get('repo_name', ''))
repo_group = value.get('repo_group')
if repo_group:
gr = RepoGroup.get(repo_group)
group_path = gr.full_path
group_name = gr.group_name
# value needs to be aware of group name in order to check
# db key This is an actual just the name to store in the
# database
repo_name_full = group_path + RepoGroup.url_sep() + repo_name
group_name = group_path = ''
repo_name_full = repo_name
value['repo_name'] = repo_name
value['repo_name_full'] = repo_name_full
value['group_path'] = group_path
value['group_name'] = group_name
repo_name = value.get('repo_name')
repo_name_full = value.get('repo_name_full')
group_path = value.get('group_path')
if repo_name in [ADMIN_PREFIX, '']:
msg = M(self, 'invalid_repo_name', state, repo=repo_name)
error_dict=dict(repo_name=msg)
rename = old_data.get('repo_name') != repo_name_full
create = not edit
if rename or create:
if group_path != '':
if Repository.get_by_repo_name(repo_name_full):
msg = M(self, 'repository_in_group_exists', state,
repo=repo_name, group=group_name)
elif RepoGroup.get_by_group_name(repo_name_full):
msg = M(self, 'same_group_exists', state,
repo=repo_name)
elif Repository.get_by_repo_name(repo_name_full):
msg = M(self, 'repository_exists', state,
def ValidForkName(*args, **kwargs):
return ValidRepoName(*args, **kwargs)
def SlugifyName():
return repo_name_slug(value)
def ValidCloneUri():
from rhodecode.lib.utils import make_ui
def url_handler(repo_type, url, ui=None):
if repo_type == 'hg':
from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository
from mercurial.httppeer import httppeer
if url.startswith('http'):
## initially check if it's at least the proper URL
## or does it pass basic auth
MercurialRepository._check_url(url)
httppeer(ui, url)._capabilities()
elif url.startswith('svn+http'):
from hgsubversion.svnrepo import svnremoterepo
svnremoterepo(ui, url).capabilities
elif url.startswith('git+http'):
raise NotImplementedError()
elif repo_type == 'git':
from rhodecode.lib.vcs.backends.git.repository import GitRepository
GitRepository._check_url(url)
elif url.startswith('hg+http'):
'clone_uri': _(u'invalid clone url'),
'invalid_clone_uri': _(u'Invalid clone url, provide a '
'valid clone http(s)/svn+http(s) url')
repo_type = value.get('repo_type')
url = value.get('clone_uri')
if not url:
url_handler(repo_type, url, make_ui('db', clear_session=False))
log.exception('Url validation failed')
msg = M(self, 'clone_uri')
error_dict=dict(clone_uri=msg)
def ValidForkType(old_data={}):
'invalid_fork_type': _(u'Fork have to be the same type as parent')
if old_data['repo_type'] != value:
msg = M(self, 'invalid_fork_type', state)
error_dict=dict(repo_type=msg)
def ValidPerms(type_='repo'):
if type_ == 'group':
EMPTY_PERM = 'group.none'
elif type_ == 'repo':
EMPTY_PERM = 'repository.none'
'perm_new_member_name':
_(u'This username or users group name is not valid')
def to_python(self, value, state):
perms_update = []
perms_new = []
perms_update = OrderedSet()
perms_new = OrderedSet()
# build a list of permission to update and new permission to create
for k, v in value.items():
# means new added member to permissions
#CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
new_perms_group = defaultdict(dict)
for k, v in value.copy().iteritems():
if k.startswith('perm_new_member'):
new_perm = value.get('perm_new_member', False)
new_member = value.get('perm_new_member_name', False)
new_type = value.get('perm_new_member_type')
del value[k]
_type, part = k.split('perm_new_member_')
args = part.split('_')
if len(args) == 1:
new_perms_group[args[0]]['perm'] = v
elif len(args) == 2:
_key, pos = args
new_perms_group[pos][_key] = v
if new_member and new_perm:
if (new_member, new_perm, new_type) not in perms_new:
perms_new.append((new_member, new_perm, new_type))
elif k.startswith('u_perm_') or k.startswith('g_perm_'):
# fill new permissions in order of how they were added
for k in sorted(map(int, new_perms_group.keys())):
perm_dict = new_perms_group[str(k)]
new_member = perm_dict['name']
new_perm = perm_dict['perm']
new_type = perm_dict['type']
if new_member and new_perm and new_type:
perms_new.add((new_member, new_perm, new_type))
for k, v in value.iteritems():
if k.startswith('u_perm_') or k.startswith('g_perm_'):
member = k[7:]
t = {'u': 'user',
'g': 'users_group'
}[k[0]]
if member == 'default':
if value.get('private'):
# set none for default when updating to
# private repo
v = EMPTY_PERM
perms_update.append((member, v, t))
perms_update.add((member, v, t))
value['perms_updates'] = perms_update
value['perms_new'] = perms_new
value['perms_updates'] = list(perms_update)
value['perms_new'] = list(perms_new)
# update permissions
for k, v, t in perms_new:
if t is 'user':
self.user_db = User.query()\
.filter(User.active == True)\
.filter(User.username == k).one()
if t is 'users_group':
self.user_db = UsersGroup.query()\
.filter(UsersGroup.users_group_active == True)\
.filter(UsersGroup.users_group_name == k).one()
log.exception('Updated permission failed')
msg = M(self, 'perm_new_member_type', state)
error_dict=dict(perm_new_member_name=msg)
def ValidSettings():
# settings form can't edit user
if 'user' in value:
del value['user']
def ValidPath():
'invalid_path': _(u'This is not a valid path')
if not os.path.isdir(value):
msg = M(self, 'invalid_path', state)
error_dict=dict(paths_root_path=msg)
def UniqSystemEmail(old_data={}):
'email_taken': _(u'This e-mail address is already taken')
return value.lower()
if (old_data.get('email') or '').lower() != value:
user = User.get_by_email(value, case_insensitive=True)
if user:
msg = M(self, 'email_taken', state)
error_dict=dict(email=msg)
def ValidSystemEmail():
'non_existing_email': _(u'e-mail "%(email)s" does not exist.')
if user is None:
msg = M(self, 'non_existing_email', state, email=value)
def LdapLibValidator():
import ldap
ldap # pyflakes silence !
except ImportError:
raise LdapImportError()
def AttrLoginValidator():
'invalid_cn':
_(u'The LDAP Login attribute of the CN must be specified - '
'this is the name of the attribute that is equivalent '
'to "username"')
if not value or not isinstance(value, (str, unicode)):
msg = M(self, 'invalid_cn', state)
error_dict=dict(ldap_attr_login=msg)
def NotReviewedRevisions():
'rev_already_reviewed':
_(u'Revisions %(revs)s are already part of pull request '
'or have set status')
# check revisions if they are not reviewed, or a part of another
# pull request
statuses = ChangesetStatus.query()\
.filter(ChangesetStatus.revision.in_(value)).all()
errors = []
for cs in statuses:
if cs.pull_request_id:
errors.append(['pull_req', cs.revision[:12]])
elif cs.status:
errors.append(['status', cs.revision[:12]])
if errors:
revs = ','.join([x[1] for x in errors])
msg = M(self, 'rev_already_reviewed', state, revs=revs)
error_dict=dict(revisions=revs)
/**
RhodeCode JS Files
**/
if (typeof console == "undefined" || typeof console.log == "undefined"){
console = { log: function() {} }
var str_repeat = function(i, m) {
for (var o = []; m > 0; o[--m] = i);
return o.join('');
};
* 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) {
return this.replace(new RegExp('^'+char+'+'),'');
String.prototype.rstrip = function(char) {
return this.replace(new RegExp(''+char+'+$'),'');
if(!Array.prototype.indexOf) {
Array.prototype.indexOf = function(needle) {
for(var i = 0; i < this.length; i++) {
if(this[i] === needle) {
return i;
return -1;
// IE(CRAP) doesn't support previousElementSibling
var prevElementSibling = function( el ) {
if( el.previousElementSibling ) {
return el.previousElementSibling;
} else {
while( el = el.previousSibling ) {
if( el.nodeType === 1 ) return el;
* SmartColorGenerator
*
*usage::
* var CG = new ColorGenerator();
* var col = CG.getColor(key); //returns array of RGB
* 'rgb({0})'.format(col.join(',')
* @returns {ColorGenerator}
var ColorGenerator = function(){
this.GOLDEN_RATIO = 0.618033988749895;
this.CURRENT_RATIO = 0.22717784590367374 // this can be random
this.HSV_1 = 0.75;//saturation
this.HSV_2 = 0.95;
this.color;
this.cacheColorMap = {};
ColorGenerator.prototype = {
getColor:function(key){
if(this.cacheColorMap[key] !== undefined){
return this.cacheColorMap[key];
else{
this.cacheColorMap[key] = this.generateColor();
},
_hsvToRgb:function(h,s,v){
if (s == 0.0)
return [v, v, v];
i = parseInt(h * 6.0)
f = (h * 6.0) - i
p = v * (1.0 - s)
q = v * (1.0 - s * f)
t = v * (1.0 - s * (1.0 - f))
i = i % 6
if (i == 0)
return [v, t, p]
if (i == 1)
return [q, v, p]
if (i == 2)
return [p, v, t]
if (i == 3)
return [p, q, v]
if (i == 4)
return [t, p, v]
if (i == 5)
return [v, p, q]
generateColor:function(){
this.CURRENT_RATIO = this.CURRENT_RATIO+this.GOLDEN_RATIO;
this.CURRENT_RATIO = this.CURRENT_RATIO %= 1;
HSV_tuple = [this.CURRENT_RATIO, this.HSV_1, this.HSV_2]
RGB_tuple = this._hsvToRgb(HSV_tuple[0],HSV_tuple[1],HSV_tuple[2]);
function toRgb(v){
return ""+parseInt(v*256)
return [toRgb(RGB_tuple[0]),toRgb(RGB_tuple[1]),toRgb(RGB_tuple[2])];
* GLOBAL YUI Shortcuts
var YUC = YAHOO.util.Connect;
var YUD = YAHOO.util.Dom;
var YUE = YAHOO.util.Event;
var YUQ = YAHOO.util.Selector.query;
// defines if push state is enabled for this browser ?
var push_state_enabled = Boolean(
window.history && window.history.pushState && window.history.replaceState
&& !( /* disable for versions of iOS before version 4.3 (8F190) */
(/ Mobile\/([1-7][a-z]|(8([abcde]|f(1[0-8]))))/i).test(navigator.userAgent)
/* disable for the mercury iOS browser, or at least older versions of the webkit engine */
|| (/AppleWebKit\/5([0-2]|3[0-2])/i).test(navigator.userAgent)
);
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){};
* Partial Ajax Implementation
* @param url: defines url to make partial request
* @param container: defines id of container to input partial result
* @param s_call: success callback function that takes o as arg
* o.tId
* o.status
* o.statusText
* o.getResponseHeader[ ]
* o.getAllResponseHeaders
* o.responseText
* o.responseXML
* o.argument
* @param f_call: failure callback
* @param args arguments
function ypjax(url,container,s_call,f_call,args){
var method='GET';
if(args===undefined){
args=null;
// Set special header for partial ajax == HTTP_X_PARTIAL_XHR
YUC.initHeader('X-PARTIAL-XHR',true);
// wrapper of passed callback
var s_wrapper = (function(o){
return function(o){
YUD.get(container).innerHTML=o.responseText;
YUD.setStyle(container,'opacity','1.0');
//execute the given original callback
if (s_call !== undefined){
s_call(o);
})()
YUD.setStyle(container,'opacity','0.3');
YUC.asyncRequest(method,url,{
success:s_wrapper,
failure:function(o){
console.log(o);
YUD.get(container).innerHTML='<span class="error_red">ERROR: {0}</span>'.format(o.status);
cache:false
},args);
var ajaxPOST = function(url,postData,success) {
// Set special header for ajax == HTTP_X_PARTIAL_XHR
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('&');
var sUrl = url;
var callback = {
success: success,
failure: function (o) {
alert("error");
var postData = toQueryString(postData);
var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData);
return request;
* tooltip activate
var tooltip_activate = function(){
function toolTipsId(){
var ids = [];
var tts = YUQ('.tooltip');
for (var i = 0; i < tts.length; i++) {
// if element doesn't not have and id
// autogenerate one for tooltip
if (!tts[i].id){
tts[i].id='tt'+((i*100)+tts.length);
ids.push(tts[i].id);
return ids
var myToolTips = new YAHOO.widget.Tooltip("tooltip", {
context: [[toolTipsId()],"tl","bl",null,[0,5]],
monitorresize:false,
xyoffset :[0,0],
autodismissdelay:300000,
hidedelay:5,
showdelay:20,
});
* show more
var show_more_event = function(){
YUE.on(YUD.getElementsByClassName('show_more'),'click',function(e){
var el = e.target;
YUD.setStyle(YUD.get(el.id.substring(1)),'display','');
YUD.setStyle(el.parentNode,'display','none');
* 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 = YUD.get(target);
var F = YAHOO.namespace(target);
YUE.on(q_filter_field,'click',function(){
q_filter_field.value = '';
YUE.on(q_filter_field,'keyup',function(e){
clearTimeout(F.filterTimeout);
F.filterTimeout = setTimeout(F.updateFilter,600);
F.filterTimeout = null;
var show_node = function(node){
YUD.setStyle(node,'display','')
var hide_node = function(node){
YUD.setStyle(node,'display','none');
F.updateFilter = function() {
// Reset timeout
var obsolete = [];
var req = q_filter_field.value.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){
hide_node(target_element);
show_node(target_element);
showing+=1;
// if repo_count is set update the number
var cnt = YUD.get('repo_count');
if(cnt){
YUD.get('repo_count').innerHTML = showing;
var tableTr = function(cls,body){
var tr = document.createElement('tr');
YUD.addClass(tr, cls);
var cont = new YAHOO.util.Element(body);
var comment_id = fromHTML(body).children[0].id.split('comment-')[1];
tr.id = 'comment-tr-{0}'.format(comment_id);
tr.innerHTML = '<td class="lineno-inline new-inline"></td>'+
'<td class="lineno-inline old-inline"></td>'+
'<td>{0}</td>'.format(body);
return tr;
/** comments **/
var removeInlineForm = function(form) {
form.parentNode.removeChild(form);
var createInlineForm = function(parent_tr, f_path, line) {
var tmpl = YUD.get('comment-inline-form-template').innerHTML;
tmpl = tmpl.format(f_path, line);
var form = tableTr('comment-form-inline',tmpl)
// create event for hide button
form = new YAHOO.util.Element(form);
var form_hide_button = new YAHOO.util.Element(YUD.getElementsByClassName('hide-inline-form',null,form)[0]);
form_hide_button.on('click', function(e) {
var newtr = e.currentTarget.parentNode.parentNode.parentNode.parentNode.parentNode;
if(YUD.hasClass(newtr.nextElementSibling,'inline-comments-button')){
YUD.setStyle(newtr.nextElementSibling,'display','');
removeInlineForm(newtr);
YUD.removeClass(parent_tr, 'form-open');
return form
* Inject inline comment for on given TR this tr should be always an .line
* tr containing the line. Code will detect comment, and always put the comment
* block at the very bottom
var injectInlineForm = function(tr){
if(!YUD.hasClass(tr, 'line')){
var submit_url = AJAX_COMMENT_URL;
var _td = YUD.getElementsByClassName('code',null,tr)[0];
if(YUD.hasClass(tr,'form-open') || YUD.hasClass(tr,'context') || YUD.hasClass(_td,'no-comment')){
YUD.addClass(tr,'form-open');
var node = YUD.getElementsByClassName('full_f_path',null,tr.parentNode.parentNode.parentNode)[0];
var f_path = YUD.getAttribute(node,'path');
var lineno = getLineNo(tr);
var form = createInlineForm(tr, f_path, lineno, submit_url);
var parent = tr;
while (1){
var n = parent.nextElementSibling;
// next element are comments !
if(YUD.hasClass(n,'inline-comments')){
parent = n;
break;
YUD.insertAfter(form,parent);
var f = YUD.get(form);
var overlay = YUD.getElementsByClassName('overlay',null,f)[0];
var _form = YUD.getElementsByClassName('inline-form',null,f)[0];
form.on('submit',function(e){
YUE.preventDefault(e);
//ajax submit
var text = YUD.get('text_'+lineno).value;
var postData = {
'text':text,
'f_path':f_path,
'line':lineno
if(lineno === undefined){
alert('missing line !');
if(f_path === undefined){
alert('missing file path !');
if(text == ""){
var success = function(o){
YUD.removeClass(tr, 'form-open');
removeInlineForm(f);
var json_data = JSON.parse(o.responseText);
renderInlineComment(json_data);
if (YUD.hasClass(overlay,'overlay')){
var w = _form.offsetWidth;
var h = _form.offsetHeight;
YUD.setStyle(overlay,'width',w+'px');
YUD.setStyle(overlay,'height',h+'px');
YUD.addClass(overlay, 'submitting');
ajaxPOST(submit_url, postData, success);
setTimeout(function(){
// callbacks
tooltip_activate();
MentionsAutoComplete('text_'+lineno, 'mentions_container_'+lineno,
_USERS_AC_DATA, _GROUPS_AC_DATA);
var _e = YUD.get('text_'+lineno);
if(_e){
_e.focus();
},10)
var deleteComment = function(comment_id){
var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__',comment_id);
var postData = {'_method':'delete'};
var n = YUD.get('comment-tr-'+comment_id);
var root = prevElementSibling(prevElementSibling(n));
n.parentNode.removeChild(n);
// scann nodes, and attach add button to last one
placeAddButton(root);
ajaxPOST(url,postData,success);
var updateReviewers = function(reviewers_ids){
var url = AJAX_UPDATE_PULLREQUEST;
var postData = {'_method':'put',
'reviewers_ids': reviewers_ids};
window.location.reload();
var createInlineAddButton = function(tr){
var label = TRANSLATION_MAP['add another comment'];
var html_el = document.createElement('div');
YUD.addClass(html_el, 'add-comment');
html_el.innerHTML = '<span class="ui-btn">{0}</span>'.format(label);
var add = new YAHOO.util.Element(html_el);
add.on('click', function(e) {
injectInlineForm(tr);
return add;
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];
var placeAddButton = function(target_tr){
if(!target_tr){
var last_node = target_tr;
//scann
var n = last_node.nextElementSibling;
last_node = n;
//also remove the comment button from previous
var comment_add_buttons = YUD.getElementsByClassName('add-comment',null,last_node);
for(var i=0;i<comment_add_buttons.length;i++){
var b = comment_add_buttons[i];
b.parentNode.removeChild(b);
var add = createInlineAddButton(target_tr);
// get the comment div
var comment_block = YUD.getElementsByClassName('comment',null,last_node)[0];
// attach add button
YUD.insertAfter(add,comment_block);
* Places the inline comment into the changeset block in proper line position
var placeInline = function(target_container,lineno,html){
var lineid = "{0}_{1}".format(target_container,lineno);
var target_line = YUD.get(lineid);
var comment = new YAHOO.util.Element(tableTr('inline-comments',html))
// check if there are comments already !
var parent = target_line.parentNode;
var root_parent = parent;
// put in the comment at the bottom
YUD.insertAfter(comment,parent);
placeAddButton(root_parent);
return target_line;
* 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'];
placeInline(target_id, lineno, html);
}catch(e){
console.log(e);
* 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 = YUD.getAttribute(box,'target_id');
// actually 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': YUD.getAttribute(comments[i],'line'),
'target_id': target_id
renderInlineComment(data);
var removeReviewer = function(reviewer_id){
var el = YUD.get('reviewer_{0}'.format(reviewer_id));
if (el.parentNode !== undefined){
el.parentNode.removeChild(el);
var fileBrowserListeners = function(current_url, node_list_url, url_base){
var current_url_branch = +"?branch=__BRANCH__";
var url = url_base;
var node_url = node_list_url;
YUE.on('stay_at_branch','click',function(e){
if(e.target.checked){
var uri = current_url_branch;
uri = uri.replace('__BRANCH__',e.target.value);
window.location = uri;
window.location = current_url;
})
var n_filter = YUD.get('node_filter');
var F = YAHOO.namespace('node_filter');
var nodes = null;
F.initFilter = function(){
YUD.setStyle('node_filter_box_loading','display','');
YUD.setStyle('search_activate_id','display','none');
YUD.setStyle('add_node_id','display','none');
YUC.asyncRequest('GET',url,{
success:function(o){
nodes = JSON.parse(o.responseText).nodes;
YUD.setStyle('node_filter_box_loading','display','none');
YUD.setStyle('node_filter_box','display','');
n_filter.focus();
if(YUD.hasClass(n_filter,'init')){
n_filter.value = '';
YUD.removeClass(n_filter,'init');
console.log('failed to load');
},null);
F.updateFilter = function(e) {
return function(){
var query = e.target.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){
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)
node_url = node_url.replace('__FPATH__',n);
match.push('<tr><td><a class="browser-{0}" href="{1}">{2}</a></td><td colspan="5"></td></tr>'.format(t,node_url,n_hl));
if(match.length >= matches_max){
match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['search truncated']));
if(query != ""){
YUD.setStyle('tbody','display','none');
YUD.setStyle('tbody_filtered','display','');
if (match.length==0){
match.push('<tr><td>{0}</td><td colspan="5"></td></tr>'.format(_TM['no matching files']));
YUD.get('tbody_filtered').innerHTML = match.join("");
YUD.setStyle('tbody','display','');
YUD.setStyle('tbody_filtered','display','none');
YUE.on(YUD.get('filter_activate'),'click',function(){
F.initFilter();
YUE.on(n_filter,'click',function(){
YUE.on(n_filter,'keyup',function(e){
F.filterTimeout = setTimeout(F.updateFilter(e),600);
var initCodeMirror = function(textAreadId,resetUrl){
var myCodeMirror = CodeMirror.fromTextArea(YUD.get(textAreadId),{
mode: "null",
lineNumbers:true
YUE.on('reset','click',function(e){
window.location=resetUrl
YUE.on('file_enable','click',function(){
YUD.setStyle('editor_container','display','');
YUD.setStyle('upload_file_container','display','none');
YUD.setStyle('filename_container','display','');
YUE.on('upload_file_enable','click',function(){
YUD.setStyle('editor_container','display','none');
YUD.setStyle('upload_file_container','display','');
YUD.setStyle('filename_container','display','none');
var getIdentNode = function(n){
//iterate thru nodes untill matched interesting node !
if (typeof n == 'undefined'){
return -1
if(typeof n.id != "undefined" && n.id.match('L[0-9]+')){
return n
return getIdentNode(n.parentNode);
var getSelectionLink = function(selection_link_label) {
//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',''));
if (f_int > t_int){
//highlight from bottom
offset = -35;
ranges = [t_int,f_int];
//highligth from top
offset = 35;
ranges = [f_int,t_int];
if (ranges[0] != ranges[1]){
if(YUD.get('linktt') == null){
hl_div = document.createElement('div');
hl_div.id = 'linktt';
anchor = '#L'+ranges[0]+'-'+ranges[1];
hl_div.innerHTML = '';
l = document.createElement('a');
l.href = location.href.substring(0,location.href.indexOf('#'))+anchor;
l.innerHTML = selection_link_label;
hl_div.appendChild(l);
YUD.get('body').appendChild(hl_div);
xy = YUD.getXY(till.id);
YUD.addClass('linktt','yui-tt');
YUD.setStyle('linktt','top',xy[1]+offset+'px');
YUD.setStyle('linktt','left',xy[0]+'px');
YUD.setStyle('linktt','visibility','visible');
YUD.setStyle('linktt','visibility','hidden');
var deleteNotification = function(url, notification_id,callbacks){
var obj = YUD.get(String("notification_"+notification_id));
if(obj.parentNode !== undefined){
obj.parentNode.removeChild(obj);
_run_callbacks(callbacks);
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){
YUD.removeClass(obj, 'unread');
var r_button = YUD.getElementsByClassName('read-notification',null,obj.children[0])[0];
if(r_button.parentNode !== undefined){
r_button.parentNode.removeChild(r_button);
var postData = '_method=put';
/** MEMBERS AUTOCOMPLETE WIDGET **/
var MembersAutoComplete = function (users_list, groups_list) {
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 usersGroups
var matchGroups = function (sQuery) {
var l = myGroups.length;
matched_group = myGroups[i];
if (matched_group.grname.toLowerCase().indexOf(query) > -1) {
matches[matches.length] = matched_group;
//match all
var matchAll = function (sQuery) {
u = matchUsers(sQuery);
g = matchGroups(sQuery);
return u.concat(g);
// DataScheme for members
var memberDS = new YAHOO.util.FunctionDataSource(matchAll);
memberDS.responseSchema = {
fields: ["id", "fname", "lname", "nname", "grname", "grmembers", "gravatar_lnk"]
// DataScheme for owner
var ownerDS = new YAHOO.util.FunctionDataSource(matchUsers);
ownerDS.responseSchema = {
fields: ["id", "fname", "lname", "nname", "gravatar_lnk"]
// Instantiate AutoComplete for perms
var membersAC = new YAHOO.widget.AutoComplete("perm_new_member_name", "perm_container", memberDS);
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 _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);
displayfname = fname;
if (lnameMatchIndex > -1) {
displaylname = highlightMatch(lname, query, lnameMatchIndex);
displaylname = lname;
if (nnameMatchIndex > -1) {
displaynname = "(" + highlightMatch(nname, query, nnameMatchIndex) + ")";
displaynname = nname ? "(" + nname + ")" : "";
return _gravatar(displayfname + " " + displaylname + " " + displaynname, oResultData.gravatar_lnk);
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;
YUD.get('perm_new_member_type').value = 'user';
YUD.get('perm_new_member_type_'+nextId).value = 'user';
//groups
myAC.getInputEl().value = oData.grname;
YUD.get('perm_new_member_type').value = 'users_group';
YUD.get('perm_new_member_type_'+nextId).value = '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 org_sQuery = sQuery;
if(this.mentionQuery == null){
sQuery = this.mentionQuery;
return matches
return u
// Instantiate AutoComplete for mentions
var ownerAC = new YAHOO.widget.AutoComplete(divid, cont, ownerDS);
ownerAC.suppressInputUpdate = true;
ownerAC.formatResult = function (oResultData, sQuery, sResultMatch) {
if(this.dataSource.mentionQuery != null){
sQuery = this.dataSource.mentionQuery;
if (oResultData.nname != undefined) {
ownerAC.itemSelectEvent.subscribe(function (sType, aArgs) {
//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('')
YUD.get(myAC.getInputEl()).focus(); // Y U NO WORK !?
// 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;
var re = new RegExp('(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)$')
var chunks = [];
// cut first chunk until curret 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];
if (ownerAC.textboxKeyUpEvent){
ownerAC.textboxKeyUpEvent.subscribe(function(type, args){
var ac_obj = args[0];
var currentMessage = args[1];
var currentCaretPosition = args[0]._elTextbox.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 PullRequestAutoComplete = function (divid, cont, users_list, groups_list) {
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;
reviewerAC.formatResult = function (oResultData, sQuery, sResultMatch) {
//members cache to catch duplicates
reviewerAC.dataSource.cache = [];
// hack into select event
if(reviewerAC.itemSelectEvent){
reviewerAC.itemSelectEvent.subscribe(function (sType, aArgs) {
var members = YUD.get('review_members');
if (myAC.dataSource.cache.indexOf(oData.id) != -1){
var tmpl = '<li id="reviewer_{2}">'+
'<div class="reviewers_member">'+
'<div class="gravatar"><img alt="gravatar" src="{0}"/> </div>'+
'<div style="float:left">{1}</div>'+
'<input type="hidden" value="{2}" name="review_members" />'+
'<span class="delete_icon action_button" onclick="removeReviewer({2})"></span>'+
'</div>'+
'</li>'
var displayname = "{0} {1} ({2})".format(oData.fname,oData.lname,oData.nname);
var element = tmpl.format(oData.gravatar_lnk,displayname,oData.id);
members.innerHTML += element;
myAC.dataSource.cache.push(oData.id);
YUD.get('user').value = ''
reviewerAC: reviewerAC,
* QUICK REPO MENU
var quick_repo_menu = function(){
YUE.on(YUQ('.quick_repo_menu'),'mouseenter',function(e){
var menu = e.currentTarget.firstElementChild.firstElementChild;
if(YUD.hasClass(menu,'hidden')){
YUD.replaceClass(e.currentTarget,'hidden', 'active');
YUD.replaceClass(menu, 'hidden', 'active');
YUE.on(YUQ('.quick_repo_menu'),'mouseleave',function(e){
if(YUD.hasClass(menu,'active')){
YUD.replaceClass(e.currentTarget, 'active', 'hidden');
YUD.replaceClass(menu, 'active', 'hidden');
* TABLE SORTING
// returns a node from given html;
var fromHTML = function(html){
var _html = document.createElement('element');
_html.innerHTML = html;
return _html;
var get_rev = function(node){
var n = node.firstElementChild.firstElementChild;
if (n===null){
out = n.firstElementChild.innerHTML.split(':')[0].replace('r','');
return parseInt(out);
var get_name = function(node){
var name = node.firstElementChild.children[2].innerHTML;
return name
var get_group_name = function(node){
var name = node.firstElementChild.children[1].innerHTML;
var get_date = function(node){
var date_ = YUD.getAttribute(node.firstElementChild,'date');
return date_
var get_age = function(node){
return node
var get_link = function(node){
return node.firstElementChild.text;
var revisionSort = function(a, b, desc, field) {
var a_ = fromHTML(a.getData(field));
var b_ = fromHTML(b.getData(field));
// extract revisions from string nodes
a_ = get_rev(a_)
b_ = get_rev(b_)
var comp = YAHOO.util.Sort.compare;
var compState = comp(a_, b_, desc);
return compState;
var ageSort = function(a, b, desc, field) {
// extract name from table
a_ = get_date(a_)
b_ = get_date(b_)
var lastLoginSort = function(a, b, desc, field) {
var a_ = a.getData('last_login_raw') || 0;
var b_ = b.getData('last_login_raw') || 0;
var nameSort = function(a, b, desc, field) {
a_ = get_name(a_)
b_ = get_name(b_)
var permNameSort = function(a, b, desc, field) {
a_ = a_.children[0].innerHTML;
b_ = b_.children[0].innerHTML;
var groupNameSort = function(a, b, desc, field) {
a_ = get_group_name(a_)
b_ = get_group_name(b_)
var dateSort = function(a, b, desc, field) {
var linkSort = function(a, b, desc, field) {
var b_ = fromHTML(a.getData(field));
// extract url text from string nodes
a_ = get_link(a_)
b_ = get_link(b_)
var addPermAction = function(_html, users_list, groups_list){
var elmts = YUD.getElementsByClassName('last_new_member');
var last_node = elmts[elmts.length-1];
if (last_node){
var next_id = (YUD.getElementsByClassName('new_members')).length;
_html = _html.format(next_id);
last_node.innerHTML = _html;
YUD.setStyle(last_node, 'display', '');
YUD.removeClass(last_node, 'last_new_member');
MembersAutoComplete("perm_new_member_name_"+next_id,
"perm_container_"+next_id, users_list, groups_list);
//create new last NODE
var el = document.createElement('tr');
el.id = 'add_perm_input';
YUD.addClass(el,'last_new_member');
YUD.addClass(el,'new_members');
YUD.insertAfter(el, last_node);
/* Multi selectors */
var MultiSelectWidget = function(selected_id, available_id, form_id){
//definition of containers ID's
var selected_container = selected_id;
var available_container = available_id;
//temp container for selected storage.
var cache = new Array();
var av_cache = new Array();
var c = YUD.get(selected_container);
var ac = YUD.get(available_container);
//get only selected options for further fullfilment
for(var i = 0;node =c.options[i];i++){
if(node.selected){
//push selected to my temp storage left overs :)
cache.push(node);
//get all available options to cache
for(var i = 0;node =ac.options[i];i++){
av_cache.push(node);
//fill available only with those not in choosen
ac.options.length=0;
tmp_cache = new Array();
for(var i = 0;node = av_cache[i];i++){
var add = true;
for(var i2 = 0;node_2 = cache[i2];i2++){
if(node.value == node_2.value){
add=false;
if(add){
tmp_cache.push(new Option(node.text, node.value, false, false));
for(var i = 0;node = tmp_cache[i];i++){
ac.options[i] = node;
function prompts_action_callback(e){
var choosen = YUD.get(selected_container);
var available = YUD.get(available_container);
//get checked and unchecked options from field
function get_checked(from_field){
//temp container for storage.
var sel_cache = new Array();
var oth_cache = new Array();
for(var i = 0;node = from_field.options[i];i++){
//push selected fields :)
sel_cache.push(node);
oth_cache.push(node)
return [sel_cache,oth_cache]
//fill the field with given options
function fill_with(field,options){
//clear firtst
field.options.length=0;
for(var i = 0;node = options[i];i++){
field.options[i]=new Option(node.text, node.value,
false, false);
//adds to current field
function add_to(field,options){
field.appendChild(new Option(node.text, node.value,
false, false));
// add action
if (this.id=='add_element'){
var c = get_checked(available);
add_to(choosen,c[0]);
fill_with(available,c[1]);
// remove action
if (this.id=='remove_element'){
var c = get_checked(choosen);
add_to(available,c[0]);
fill_with(choosen,c[1]);
// add all elements
if(this.id=='add_all_elements'){
for(var i=0; node = available.options[i];i++){
choosen.appendChild(new Option(node.text,
node.value, false, false));
available.options.length = 0;
//remove all elements
if(this.id=='remove_all_elements'){
for(var i=0; node = choosen.options[i];i++){
available.appendChild(new Option(node.text,
choosen.options.length = 0;
YUE.addListener(['add_element','remove_element',
'add_all_elements','remove_all_elements'],'click',
prompts_action_callback)
if (form_id !== undefined) {
YUE.addListener(form_id,'submit',function(){
for (var i = 0; i < choosen.options.length; i++) {
choosen.options[i].selected = 'selected';
<table id="permissions_manage" class="noborder">
<tr>
<td>${_('none')}</td>
<td>${_('read')}</td>
<td>${_('write')}</td>
<td>${_('admin')}</td>
<td>${_('member')}</td>
<td></td>
</tr>
## USERS
%for r2p in c.repo_info.repo_to_perm:
%if r2p.user.username =='default' and c.repo_info.private:
<td colspan="4">
<span class="private_repo_msg">
${_('private repository')}
</span>
</td>
<td class="private_repo_msg"><img style="vertical-align:bottom" src="${h.url('/images/icons/user.png')}"/>${_('default')}</td>
%else:
<tr id="id${id(r2p.user.username)}">
<td>${h.radio('u_perm_%s' % r2p.user.username,'repository.none')}</td>
<td>${h.radio('u_perm_%s' % r2p.user.username,'repository.read')}</td>
<td>${h.radio('u_perm_%s' % r2p.user.username,'repository.write')}</td>
<td>${h.radio('u_perm_%s' % r2p.user.username,'repository.admin')}</td>
<td style="white-space: nowrap;">
<img class="perm-gravatar" src="${h.gravatar_url(r2p.user.email,14)}"/>${r2p.user.username if r2p.user.username != 'default' else _('default')}
<td>
%if r2p.user.username !='default':
<span class="delete_icon action_button" onclick="ajaxActionUser(${r2p.user.user_id},'${'id%s'%id(r2p.user.username)}')">
${_('revoke')}
%endif
%endfor
## USERS GROUPS
%for g2p in c.repo_info.users_group_to_perm:
<tr id="id${id(g2p.users_group.users_group_name)}">
<td>${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'repository.none')}</td>
<td>${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'repository.read')}</td>
<td>${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'repository.write')}</td>
<td>${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'repository.admin')}</td>
<img class="perm-gravatar" src="${h.url('/images/icons/group.png')}"/>
%if h.HasPermissionAny('hg.admin')():
<a href="${h.url('edit_users_group',id=g2p.users_group.users_group_id)}">${g2p.users_group.users_group_name}</a>
${g2p.users_group.users_group_name}
<span class="delete_icon action_button" onclick="ajaxActionUsersGroup(${g2p.users_group.users_group_id},'${'id%s'%id(g2p.users_group.users_group_name)}')">
<tr id="add_perm_input">
<td>${h.radio('perm_new_member','repository.none')}</td>
<td>${h.radio('perm_new_member','repository.read')}</td>
<td>${h.radio('perm_new_member','repository.write')}</td>
<td>${h.radio('perm_new_member','repository.admin')}</td>
<td class='ac'>
<div class="perm_ac" id="perm_ac">
${h.text('perm_new_member_name',class_='yui-ac-input')}
${h.hidden('perm_new_member_type')}
<div id="perm_container"></div>
</div>
<%
_tmpl = h.literal("""' \
<td><input type="radio" value="repository.none" name="perm_new_member_{0}" id="perm_new_member_{0}"></td> \
<td><input type="radio" value="repository.read" name="perm_new_member_{0}" id="perm_new_member_{0}"></td> \
<td><input type="radio" value="repository.write" name="perm_new_member_{0}" id="perm_new_member_{0}"></td> \
<td><input type="radio" value="repository.admin" name="perm_new_member_{0}" id="perm_new_member_{0}"></td> \
<td class="ac"> \
<div class="perm_ac" id="perm_ac_{0}"> \
<input class="yui-ac-input" id="perm_new_member_name_{0}" name="perm_new_member_name_{0}" value="" type="text"> \
<input id="perm_new_member_type_{0}" name="perm_new_member_type_{0}" value="" type="hidden"> \
<div id="perm_container_{0}"></div> \
</div> \
</td> \
<td></td>'""")
%>
## ADD HERE DYNAMICALLY NEW INPUTS FROM THE '_tmpl'
<tr class="new_members last_new_member" id="add_perm_input"></tr>
<td colspan="6">
<span id="add_perm" class="add_icon" style="cursor: pointer;">
${_('Add another member')}
</table>
<script type="text/javascript">
function ajaxActionUser(user_id, field_id) {
var sUrl = "${h.url('delete_repo_user',repo_name=c.repo_name)}";
success: function (o) {
var tr = YUD.get(String(field_id));
tr.parentNode.removeChild(tr);
alert("${_('Failed to remove user')}");
var postData = '_method=delete&user_id=' + user_id;
function ajaxActionUsersGroup(users_group_id,field_id){
var sUrl = "${h.url('delete_repo_users_group',repo_name=c.repo_name)}";
alert("${_('Failed to remove users group')}");
var postData = '_method=delete&users_group_id='+users_group_id;
YUE.onDOMReady(function () {
if (!YUD.hasClass('perm_new_member_name', 'error')) {
YUD.setStyle('add_perm_input', 'display', 'none');
YAHOO.util.Event.addListener('add_perm', 'click', function () {
YUD.setStyle('add_perm_input', 'display', '');
YUD.setStyle('add_perm', 'opacity', '0.6');
YUD.setStyle('add_perm', 'cursor', 'default');
addPermAction(${_tmpl}, ${c.users_array|n}, ${c.users_groups_array|n});
MembersAutoComplete(${c.users_array|n}, ${c.users_groups_array|n});
</script>
%for r2p in c.repos_group.repo_group_to_perm:
<td>${h.radio('u_perm_%s' % r2p.user.username,'group.none')}</td>
<td>${h.radio('u_perm_%s' % r2p.user.username,'group.read')}</td>
<td>${h.radio('u_perm_%s' % r2p.user.username,'group.write')}</td>
<td>${h.radio('u_perm_%s' % r2p.user.username,'group.admin')}</td>
%for g2p in c.repos_group.users_group_to_perm:
<td>${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'group.none')}</td>
<td>${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'group.read')}</td>
<td>${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'group.write')}</td>
<td>${h.radio('g_perm_%s' % g2p.users_group.users_group_name,'group.admin')}</td>
<img class="perm-gravatar" src="${h.url('/images/icons/group.png')}"/>${g2p.users_group.users_group_name}
<td>${h.radio('perm_new_member','group.none')}</td>
<td>${h.radio('perm_new_member','group.read')}</td>
<td>${h.radio('perm_new_member','group.write')}</td>
<td>${h.radio('perm_new_member','group.admin')}</td>
<td><input type="radio" value="group.none" name="perm_new_member_{0}" id="perm_new_member_{0}"></td> \
<td><input type="radio" value="group.read" name="perm_new_member_{0}" id="perm_new_member_{0}"></td> \
<td><input type="radio" value="group.write" name="perm_new_member_{0}" id="perm_new_member_{0}"></td> \
<td><input type="radio" value="group.admin" name="perm_new_member_{0}" id="perm_new_member_{0}"></td> \
var sUrl = "${h.url('delete_repos_group_user_perm',group_name=c.repos_group.group_name)}";
var sUrl = "${h.url('delete_repos_group_users_group_perm',group_name=c.repos_group.group_name)}";
Status change: