Mercurial > kallithea
changeset 2735:9d8f63ff9219 beta
Merged in domruf/rhodecode (pull request #66)
author | Marcin Kuzminski <marcin@python-works.com> |
---|---|
date | Fri, 24 Aug 2012 14:58:26 +0200 |
parents | c25cc1c7c65f (diff) d2f552429ef3 (current diff) |
children | 3aad896d9a02 |
files | rhodecode/lib/helpers.py |
diffstat | 45 files changed, 1082 insertions(+), 496 deletions(-) [+] |
line wrap: on
line diff
--- a/docs/index.rst Fri Aug 24 10:37:17 2012 +0200 +++ b/docs/index.rst Fri Aug 24 14:58:26 2012 +0200 @@ -22,6 +22,7 @@ usage/general usage/git_support usage/performance + usage/locking usage/statistics usage/backup usage/debugging
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/docs/usage/locking.rst Fri Aug 24 14:58:26 2012 +0200 @@ -0,0 +1,41 @@ +.. _locking: + +=================================== +RhodeCode repository locking system +=================================== + + +| Repos with **locking function=disabled** is the default, that's how repos work + today. +| Repos with **locking function=enabled** behaves like follows: + +Repos have a state called `locked` that can be true or false. +The hg/git commands `hg/git clone`, `hg/git pull`, and `hg/git push` +influence this state: + +- The command `hg/git pull <repo>` will lock that repo (locked=true) + if the user has write/admin permissions on this repo + +- The command `hg/git clone <repo>` will lock that repo (locked=true) if the + user has write/admin permissions on this repo + + +RhodeCode will remember the user id who locked the repo +only this specific user can unlock the repo (locked=false) by calling + +- `hg/git push <repo>` + +every other command on that repo from this user and +every command from any other user will result in http return code 423 (locked) + + +additionally the http error includes the <user> that locked the repo +(e.g. “repository <repo> locked by user <user>”) + + +So the scenario of use for repos with `locking function` enabled is that +every initial clone and every pull gives users (with write permission) +the exclusive right to do a push. + + +Each repo can be manually unlocked by admin from the repo settings menu. \ No newline at end of file
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/rhodecode/config/pre_receive_tmpl.py Fri Aug 24 14:58:26 2012 +0200 @@ -0,0 +1,31 @@ +#!/usr/bin/env python +import os +import sys + +try: + import rhodecode + RC_HOOK_VER = '_TMPL_' + os.environ['RC_HOOK_VER'] = RC_HOOK_VER + from rhodecode.lib.hooks import handle_git_pre_receive +except ImportError: + rhodecode = None + + +def main(): + if rhodecode is None: + # exit with success if we cannot import rhodecode !! + # this allows simply push to this repo even without + # rhodecode + sys.exit(0) + + repo_path = os.path.abspath('.') + push_data = sys.stdin.readlines() + # os.environ is modified here by a subprocess call that + # runs git and later git executes this hook. + # Environ get's some additional info from rhodecode system + # like IP or username from basic-auth + handle_git_pre_receive(repo_path, push_data, os.environ) + sys.exit(0) + +if __name__ == '__main__': + main()
--- a/rhodecode/config/routing.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/config/routing.py Fri Aug 24 14:58:26 2012 +0200 @@ -138,7 +138,9 @@ m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*?}", action="repo_as_fork", conditions=dict(method=["PUT"], function=check_repo)) - + m.connect('repo_locking', "/repo_locking/{repo_name:.*?}", + action="repo_locking", conditions=dict(method=["PUT"], + function=check_repo)) with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='admin/repos_groups') as m: m.connect("repos_groups", "/repos_groups",
--- a/rhodecode/controllers/admin/repos.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/controllers/admin/repos.py Fri Aug 24 14:58:26 2012 +0200 @@ -381,6 +381,7 @@ RepoModel().delete_stats(repo_name) Session().commit() except Exception, e: + log.error(traceback.format_exc()) h.flash(_('An error occurred during deletion of repository stats'), category='error') return redirect(url('edit_repo', repo_name=repo_name)) @@ -397,11 +398,32 @@ ScmModel().mark_for_invalidation(repo_name) Session().commit() except Exception, e: + log.error(traceback.format_exc()) h.flash(_('An error occurred during cache invalidation'), category='error') return redirect(url('edit_repo', repo_name=repo_name)) @HasPermissionAllDecorator('hg.admin') + def repo_locking(self, repo_name): + """ + Unlock repository when it is locked ! + + :param repo_name: + """ + + try: + repo = Repository.get_by_repo_name(repo_name) + if request.POST.get('set_lock'): + Repository.lock(repo, c.rhodecode_user.user_id) + elif request.POST.get('set_unlock'): + Repository.unlock(repo) + except Exception, e: + log.error(traceback.format_exc()) + h.flash(_('An error occurred during unlocking'), + category='error') + return redirect(url('edit_repo', repo_name=repo_name)) + + @HasPermissionAllDecorator('hg.admin') def repo_public_journal(self, repo_name): """ Set's this repository to be visible in public journal,
--- a/rhodecode/controllers/files.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/controllers/files.py Fri Aug 24 14:58:26 2012 +0200 @@ -234,6 +234,15 @@ @LoginRequired() @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') def edit(self, repo_name, revision, f_path): + repo = Repository.get_by_repo_name(repo_name) + if repo.enable_locking and repo.locked[0]: + h.flash(_('This repository is has been locked by %s on %s') + % (h.person_by_id(repo.locked[0]), + h.fmt_date(h.time_to_datetime(repo.locked[1]))), + 'warning') + return redirect(h.url('files_home', + repo_name=repo_name, revision='tip')) + r_post = request.POST c.cs = self.__get_cs_or_redirect(revision, repo_name) @@ -284,6 +293,16 @@ @LoginRequired() @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') def add(self, repo_name, revision, f_path): + + repo = Repository.get_by_repo_name(repo_name) + if repo.enable_locking and repo.locked[0]: + h.flash(_('This repository is has been locked by %s on %s') + % (h.person_by_id(repo.locked[0]), + h.fmt_date(h.time_to_datetime(repo.locked[1]))), + 'warning') + return redirect(h.url('files_home', + repo_name=repo_name, revision='tip')) + r_post = request.POST c.cs = self.__get_cs_or_redirect(revision, repo_name, redirect_after=False)
--- a/rhodecode/controllers/pullrequests.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/controllers/pullrequests.py Fri Aug 24 14:58:26 2012 +0200 @@ -103,16 +103,18 @@ org_repo.user.username, c.repo_name)) ) - c.other_refs = c.org_refs + # add org repo to other so we can open pull request agains itself c.other_repos.extend(c.org_repos) + c.default_pull_request = org_repo.repo_name + c.default_revs = self._get_repo_refs(org_repo.scm_instance) #add orginal repo other_repos_info[org_repo.repo_name] = { 'gravatar': h.gravatar_url(org_repo.user.email, 24), - 'description': org_repo.description + 'description': org_repo.description, + 'revs': h.select('other_ref', '', c.default_revs, class_='refs') } - c.default_pull_request = org_repo.repo_name #gather forks and add to this list for fork in org_repo.forks: c.other_repos.append((fork.repo_name, '%s/%s' % ( @@ -120,7 +122,10 @@ ) other_repos_info[fork.repo_name] = { 'gravatar': h.gravatar_url(fork.user.email, 24), - 'description': fork.description + 'description': fork.description, + 'revs': h.select('other_ref', '', + self._get_repo_refs(fork.scm_instance), + class_='refs') } #add parents of this fork also if org_repo.parent: @@ -131,7 +136,10 @@ ) other_repos_info[org_repo.parent.repo_name] = { 'gravatar': h.gravatar_url(org_repo.parent.user.email, 24), - 'description': org_repo.parent.description + 'description': org_repo.parent.description, + 'revs': h.select('other_ref', '', + self._get_repo_refs(org_repo.parent.scm_instance), + class_='refs') } c.other_repos_info = json.dumps(other_repos_info) @@ -140,14 +148,12 @@ @NotAnonymous() def create(self, repo_name): - try: _form = PullRequestForm()().to_python(request.POST) except formencode.Invalid, errors: log.error(traceback.format_exc()) if errors.error_dict.get('revisions'): - msg = _('Cannot open a pull request with ' - 'empty list of changesets') + msg = 'Revisions: %s' % errors.error_dict['revisions'] elif errors.error_dict.get('pullrequest_title'): msg = _('Pull request requires a title with min. 3 chars') else: @@ -215,7 +221,7 @@ other_ref_name, other_ref_rev) = pull_request.other_ref.split(':') - # dispite opening revisions for bookmarks/branches/tags, we always + # despite opening revisions for bookmarks/branches/tags, we always # convert this to rev to prevent changes after book or branch change org_ref = ('rev', org_ref_rev) other_ref = ('rev', other_ref_rev)
--- a/rhodecode/lib/auth.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/auth.py Fri Aug 24 14:58:26 2012 +0200 @@ -807,7 +807,7 @@ return self.check_permissions() def check_permissions(self): - log.debug('checking mercurial protocol ' + log.debug('checking VCS protocol ' 'permissions %s for user:%s repository:%s', self.user_perms, self.username, self.repo_name) if self.required_perms.intersection(self.user_perms):
--- a/rhodecode/lib/base.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/base.py Fri Aug 24 14:58:26 2012 +0200 @@ -8,6 +8,7 @@ from paste.auth.basic import AuthBasicAuthenticator from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden +from webob.exc import HTTPClientError from paste.httpheaders import WWW_AUTHENTICATE from pylons import config, tmpl_context as c, request, session, url @@ -17,15 +18,17 @@ from rhodecode import __version__, BACKENDS -from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict +from rhodecode.lib.utils2 import str2bool, safe_unicode, AttributeDict,\ + safe_str from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\ HasPermissionAnyMiddleware, CookieStoreWrapper from rhodecode.lib.utils import get_repo_slug, invalidate_cache from rhodecode.model import meta -from rhodecode.model.db import Repository, RhodeCodeUi +from rhodecode.model.db import Repository, RhodeCodeUi, User from rhodecode.model.notification import NotificationModel from rhodecode.model.scm import ScmModel +from rhodecode.model.meta import Session log = logging.getLogger(__name__) @@ -159,6 +162,49 @@ return False return True + def _check_locking_state(self, environ, action, repo, user_id): + """ + Checks locking on this repository, if locking is enabled and lock is + present returns a tuple of make_lock, locked, locked_by. + make_lock can have 3 states None (do nothing) True, make lock + False release lock, This value is later propagated to hooks, which + do the locking. Think about this as signals passed to hooks what to do. + + """ + locked = False + make_lock = None + repo = Repository.get_by_repo_name(repo) + user = User.get(user_id) + + # this is kind of hacky, but due to how mercurial handles client-server + # server see all operation on changeset; bookmarks, phases and + # obsolescence marker in different transaction, we don't want to check + # locking on those + obsolete_call = environ['QUERY_STRING'] in ['cmd=listkeys',] + locked_by = repo.locked + if repo and repo.enable_locking and not obsolete_call: + if action == 'push': + #check if it's already locked !, if it is compare users + user_id, _date = repo.locked + if user.user_id == user_id: + log.debug('Got push from user, now unlocking' % (user)) + # unlock if we have push from user who locked + make_lock = False + else: + # we're not the same user who locked, ban with 423 ! + locked = True + if action == 'pull': + if repo.locked[0] and repo.locked[1]: + locked = True + else: + log.debug('Setting lock on repo %s by %s' % (repo, user)) + make_lock = True + + else: + log.debug('Repository %s do not have locking enabled' % (repo)) + + return make_lock, locked, locked_by + def __call__(self, environ, start_response): start = time.time() try:
--- a/rhodecode/lib/compat.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/compat.py Fri Aug 24 14:58:26 2012 +0200 @@ -25,7 +25,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -from rhodecode import __platform__, PLATFORM_WIN +from rhodecode import __platform__, PLATFORM_WIN, __py_version__ #============================================================================== # json @@ -404,3 +404,182 @@ from io import BytesIO except ImportError: from cStringIO import StringIO as BytesIO + + +#============================================================================== +# deque +#============================================================================== + +if __py_version__ >= (2, 6): + from collections import deque +else: + #need to implement our own deque with maxlen + class deque(object): + + def __init__(self, iterable=(), maxlen=-1): + if not hasattr(self, 'data'): + self.left = self.right = 0 + self.data = {} + self.maxlen = maxlen + self.extend(iterable) + + def append(self, x): + self.data[self.right] = x + self.right += 1 + if self.maxlen != -1 and len(self) > self.maxlen: + self.popleft() + + def appendleft(self, x): + self.left -= 1 + self.data[self.left] = x + if self.maxlen != -1 and len(self) > self.maxlen: + self.pop() + + def pop(self): + if self.left == self.right: + raise IndexError('cannot pop from empty deque') + self.right -= 1 + elem = self.data[self.right] + del self.data[self.right] + return elem + + def popleft(self): + if self.left == self.right: + raise IndexError('cannot pop from empty deque') + elem = self.data[self.left] + del self.data[self.left] + self.left += 1 + return elem + + def clear(self): + self.data.clear() + self.left = self.right = 0 + + def extend(self, iterable): + for elem in iterable: + self.append(elem) + + def extendleft(self, iterable): + for elem in iterable: + self.appendleft(elem) + + def rotate(self, n=1): + if self: + n %= len(self) + for i in xrange(n): + self.appendleft(self.pop()) + + def __getitem__(self, i): + if i < 0: + i += len(self) + try: + return self.data[i + self.left] + except KeyError: + raise IndexError + + def __setitem__(self, i, value): + if i < 0: + i += len(self) + try: + self.data[i + self.left] = value + except KeyError: + raise IndexError + + def __delitem__(self, i): + size = len(self) + if not (-size <= i < size): + raise IndexError + data = self.data + if i < 0: + i += size + for j in xrange(self.left + i, self.right - 1): + data[j] = data[j + 1] + self.pop() + + def __len__(self): + return self.right - self.left + + def __cmp__(self, other): + if type(self) != type(other): + return cmp(type(self), type(other)) + return cmp(list(self), list(other)) + + def __repr__(self, _track=[]): + if id(self) in _track: + return '...' + _track.append(id(self)) + r = 'deque(%r, maxlen=%s)' % (list(self), self.maxlen) + _track.remove(id(self)) + return r + + def __getstate__(self): + return (tuple(self),) + + def __setstate__(self, s): + self.__init__(s[0]) + + def __hash__(self): + raise TypeError + + def __copy__(self): + return self.__class__(self) + + def __deepcopy__(self, memo={}): + from copy import deepcopy + result = self.__class__() + memo[id(self)] = result + result.__init__(deepcopy(tuple(self), memo)) + return result + + +#============================================================================== +# threading.Event +#============================================================================== + +if __py_version__ >= (2, 6): + from threading import Event +else: + from threading import _Verbose, Condition, Lock + + def Event(*args, **kwargs): + return _Event(*args, **kwargs) + + class _Event(_Verbose): + + # After Tim Peters' event class (without is_posted()) + + def __init__(self, verbose=None): + _Verbose.__init__(self, verbose) + self.__cond = Condition(Lock()) + self.__flag = False + + def isSet(self): + return self.__flag + + is_set = isSet + + def set(self): + self.__cond.acquire() + try: + self.__flag = True + self.__cond.notify_all() + finally: + self.__cond.release() + + def clear(self): + self.__cond.acquire() + try: + self.__flag = False + finally: + self.__cond.release() + + def wait(self, timeout=None): + self.__cond.acquire() + try: + if not self.__flag: + self.__cond.wait(timeout) + finally: + self.__cond.release() + + +
--- a/rhodecode/lib/db_manage.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/db_manage.py Fri Aug 24 14:58:26 2012 +0200 @@ -307,37 +307,47 @@ hooks1.ui_key = hooks1_key hooks1.ui_value = 'hg update >&2' hooks1.ui_active = False + self.sa.add(hooks1) hooks2_key = RhodeCodeUi.HOOK_REPO_SIZE hooks2_ = self.sa.query(RhodeCodeUi)\ .filter(RhodeCodeUi.ui_key == hooks2_key).scalar() - hooks2 = RhodeCodeUi() if hooks2_ is None else hooks2_ hooks2.ui_section = 'hooks' hooks2.ui_key = hooks2_key hooks2.ui_value = 'python:rhodecode.lib.hooks.repo_size' + self.sa.add(hooks2) hooks3 = RhodeCodeUi() hooks3.ui_section = 'hooks' hooks3.ui_key = RhodeCodeUi.HOOK_PUSH hooks3.ui_value = 'python:rhodecode.lib.hooks.log_push_action' + self.sa.add(hooks3) hooks4 = RhodeCodeUi() hooks4.ui_section = 'hooks' - hooks4.ui_key = RhodeCodeUi.HOOK_PULL - hooks4.ui_value = 'python:rhodecode.lib.hooks.log_pull_action' + hooks4.ui_key = RhodeCodeUi.HOOK_PRE_PUSH + hooks4.ui_value = 'python:rhodecode.lib.hooks.pre_push' + self.sa.add(hooks4) - # For mercurial 1.7 set backward comapatibility with format - dotencode_disable = RhodeCodeUi() - dotencode_disable.ui_section = 'format' - dotencode_disable.ui_key = 'dotencode' - dotencode_disable.ui_value = 'false' + hooks5 = RhodeCodeUi() + hooks5.ui_section = 'hooks' + hooks5.ui_key = RhodeCodeUi.HOOK_PULL + hooks5.ui_value = 'python:rhodecode.lib.hooks.log_pull_action' + self.sa.add(hooks5) + + hooks6 = RhodeCodeUi() + hooks6.ui_section = 'hooks' + hooks6.ui_key = RhodeCodeUi.HOOK_PRE_PULL + hooks6.ui_value = 'python:rhodecode.lib.hooks.pre_pull' + self.sa.add(hooks6) # enable largefiles largefiles = RhodeCodeUi() largefiles.ui_section = 'extensions' largefiles.ui_key = 'largefiles' largefiles.ui_value = '' + self.sa.add(largefiles) # enable hgsubversion disabled by default hgsubversion = RhodeCodeUi() @@ -345,6 +355,7 @@ hgsubversion.ui_key = 'hgsubversion' hgsubversion.ui_value = '' hgsubversion.ui_active = False + self.sa.add(hgsubversion) # enable hggit disabled by default hggit = RhodeCodeUi() @@ -352,13 +363,6 @@ hggit.ui_key = 'hggit' hggit.ui_value = '' hggit.ui_active = False - - self.sa.add(hooks1) - self.sa.add(hooks2) - self.sa.add(hooks3) - self.sa.add(hooks4) - self.sa.add(largefiles) - self.sa.add(hgsubversion) self.sa.add(hggit) def create_ldap_options(self, skip_existing=False): @@ -461,6 +465,11 @@ paths.ui_key = '/' paths.ui_value = path + phases = RhodeCodeUi() + phases.ui_section = 'phases' + phases.ui_key = 'publish' + phases.ui_value = False + sett1 = RhodeCodeSetting('realm', 'RhodeCode authentication') sett2 = RhodeCodeSetting('title', 'RhodeCode') sett3 = RhodeCodeSetting('ga_code', '')
--- a/rhodecode/lib/exceptions.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/exceptions.py Fri Aug 24 14:58:26 2012 +0200 @@ -23,6 +23,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +from webob.exc import HTTPClientError + class LdapUsernameError(Exception): pass @@ -53,4 +55,17 @@ class StatusChangeOnClosedPullRequestError(Exception): - pass \ No newline at end of file + pass + + +class HTTPLockedRC(HTTPClientError): + """ + Special Exception For locked Repos in RhodeCode + """ + code = 423 + title = explanation = 'Repository Locked' + + def __init__(self, reponame, username, *args, **kwargs): + self.title = self.explanation = ('Repository `%s` locked by ' + 'user `%s`' % (reponame, username)) + super(HTTPLockedRC, self).__init__(*args, **kwargs)
--- a/rhodecode/lib/helpers.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/helpers.py Fri Aug 24 14:58:26 2012 +0200 @@ -41,7 +41,7 @@ from rhodecode.lib.annotate import annotate_highlight from rhodecode.lib.utils import repo_name_slug from rhodecode.lib.utils2 import str2bool, safe_unicode, safe_str, \ - get_changeset_safe + get_changeset_safe, datetime_to_time, time_to_datetime from rhodecode.lib.markup_renderer import MarkupRenderer from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError from rhodecode.lib.vcs.backends.base import BaseChangeset @@ -396,8 +396,14 @@ def email_or_none(author): + # extract email from the commit string _email = email(author) if _email != '': + # check it against RhodeCode database, and use the MAIN email for this + # user + user = User.get_by_email(_email, case_insensitive=True, cache=True) + if user is not None: + return user.email return _email # See if it contains a username we can get an email from @@ -410,9 +416,9 @@ return None -def person(author): +def person(author, show_attr="username_and_name"): # attr to return from fetched user - person_getter = lambda usr: usr.username + person_getter = lambda usr: getattr(usr, show_attr) # Valid email in the attribute passed, see if they're in the system _email = email(author) @@ -433,6 +439,19 @@ return _author +def person_by_id(id_, show_attr="username_and_name"): + # attr to return from fetched user + person_getter = lambda usr: getattr(usr, show_attr) + + #maybe it's an ID ? + if str(id_).isdigit() or isinstance(id_, int): + id_ = int(id_) + user = User.get(id_) + if user is not None: + return person_getter(user) + return id_ + + def desc_stylize(value): """ converts tags from value into html equivalent
--- a/rhodecode/lib/hooks.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/hooks.py Fri Aug 24 14:58:26 2012 +0200 @@ -33,6 +33,10 @@ from rhodecode.lib import helpers as h from rhodecode.lib.utils import action_logger from rhodecode.lib.vcs.backends.base import EmptyChangeset +from rhodecode.lib.compat import json +from rhodecode.model.db import Repository, User +from rhodecode.lib.utils2 import safe_str +from rhodecode.lib.exceptions import HTTPLockedRC def _get_scm_size(alias, root_path): @@ -83,6 +87,59 @@ sys.stdout.write(msg) +def pre_push(ui, repo, **kwargs): + # pre push function, currently used to ban pushing when + # repository is locked + try: + rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}")) + except: + rc_extras = {} + extras = dict(repo.ui.configitems('rhodecode_extras')) + + if 'username' in extras: + username = extras['username'] + repository = extras['repository'] + scm = extras['scm'] + locked_by = extras['locked_by'] + elif 'username' in rc_extras: + username = rc_extras['username'] + repository = rc_extras['repository'] + scm = rc_extras['scm'] + locked_by = rc_extras['locked_by'] + else: + raise Exception('Missing data in repo.ui and os.environ') + + usr = User.get_by_username(username) + + if locked_by[0] and usr.user_id != int(locked_by[0]): + raise HTTPLockedRC(username, repository) + + +def pre_pull(ui, repo, **kwargs): + # pre push function, currently used to ban pushing when + # repository is locked + try: + rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}")) + except: + rc_extras = {} + extras = dict(repo.ui.configitems('rhodecode_extras')) + if 'username' in extras: + username = extras['username'] + repository = extras['repository'] + scm = extras['scm'] + locked_by = extras['locked_by'] + elif 'username' in rc_extras: + username = rc_extras['username'] + repository = rc_extras['repository'] + scm = rc_extras['scm'] + locked_by = rc_extras['locked_by'] + else: + raise Exception('Missing data in repo.ui and os.environ') + + if locked_by[0]: + raise HTTPLockedRC(username, repository) + + def log_pull_action(ui, repo, **kwargs): """ Logs user last pull action @@ -90,13 +147,26 @@ :param ui: :param repo: """ + try: + rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}")) + except: + rc_extras = {} extras = dict(repo.ui.configitems('rhodecode_extras')) - username = extras['username'] - repository = extras['repository'] - scm = extras['scm'] + if 'username' in extras: + username = extras['username'] + repository = extras['repository'] + scm = extras['scm'] + make_lock = extras['make_lock'] + elif 'username' in rc_extras: + username = rc_extras['username'] + repository = rc_extras['repository'] + scm = rc_extras['scm'] + make_lock = rc_extras['make_lock'] + else: + raise Exception('Missing data in repo.ui and os.environ') + user = User.get_by_username(username) action = 'pull' - - action_logger(username, action, repository, extras['ip'], commit=True) + action_logger(user, action, repository, extras['ip'], commit=True) # extension hook call from rhodecode import EXTENSIONS callback = getattr(EXTENSIONS, 'PULL_HOOK', None) @@ -105,6 +175,12 @@ kw = {} kw.update(extras) callback(**kw) + + if make_lock is True: + Repository.lock(Repository.get_by_repo_name(repository), user.user_id) + #msg = 'Made lock on repo `%s`' % repository + #sys.stdout.write(msg) + return 0 @@ -116,11 +192,26 @@ :param repo: repo object containing the `ui` object """ + try: + rc_extras = json.loads(os.environ.get('RC_SCM_DATA', "{}")) + except: + rc_extras = {} + extras = dict(repo.ui.configitems('rhodecode_extras')) - username = extras['username'] - repository = extras['repository'] - action = extras['action'] + ':%s' - scm = extras['scm'] + if 'username' in extras: + username = extras['username'] + repository = extras['repository'] + scm = extras['scm'] + make_lock = extras['make_lock'] + elif 'username' in rc_extras: + username = rc_extras['username'] + repository = rc_extras['repository'] + scm = rc_extras['scm'] + make_lock = rc_extras['make_lock'] + else: + raise Exception('Missing data in repo.ui and os.environ') + + action = 'push' + ':%s' if scm == 'hg': node = kwargs['node'] @@ -154,6 +245,12 @@ kw = {'pushed_revs': revs} kw.update(extras) callback(**kw) + + if make_lock is False: + Repository.unlock(Repository.get_by_repo_name(repository)) + msg = 'Released lock on repo `%s`\n' % repository + sys.stdout.write(msg) + return 0 @@ -194,8 +291,13 @@ return 0 +handle_git_pre_receive = (lambda repo_path, revs, env: + handle_git_receive(repo_path, revs, env, hook_type='pre')) +handle_git_post_receive = (lambda repo_path, revs, env: + handle_git_receive(repo_path, revs, env, hook_type='post')) -def handle_git_post_receive(repo_path, revs, env): + +def handle_git_receive(repo_path, revs, env, hook_type='post'): """ A really hacky method that is runned by git post-receive hook and logs an push action together with pushed revisions. It's executed by subprocess @@ -215,7 +317,6 @@ from rhodecode.model import init_model from rhodecode.model.db import RhodeCodeUi from rhodecode.lib.utils import make_ui - from rhodecode.model.db import Repository path, ini_name = os.path.split(env['RHODECODE_CONFIG_FILE']) conf = appconfig('config:%s' % ini_name, relative_to=path) @@ -230,20 +331,18 @@ repo_path = repo_path[:-4] repo = Repository.get_by_full_path(repo_path) _hooks = dict(baseui.configitems('hooks')) or {} - # if push hook is enabled via web interface - if repo and _hooks.get(RhodeCodeUi.HOOK_PUSH): - extras = { - 'username': env['RHODECODE_USER'], - 'repository': repo.repo_name, - 'scm': 'git', - 'action': 'push', - 'ip': env['RHODECODE_CONFIG_IP'], - } - for k, v in extras.items(): - baseui.setconfig('rhodecode_extras', k, v) - repo = repo.scm_instance - repo.ui = baseui + extras = json.loads(env['RHODECODE_EXTRAS']) + for k, v in extras.items(): + baseui.setconfig('rhodecode_extras', k, v) + repo = repo.scm_instance + repo.ui = baseui + + if hook_type == 'pre': + pre_push(baseui, repo) + + # if push hook is enabled via web interface + elif hook_type == 'post' and _hooks.get(RhodeCodeUi.HOOK_PUSH): rev_data = [] for l in revs:
--- a/rhodecode/lib/indexers/__init__.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/indexers/__init__.py Fri Aug 24 14:58:26 2012 +0200 @@ -89,6 +89,7 @@ CHGSET_IDX_NAME = 'CHGSET_INDEX' + class MakeIndex(BasePasterCommand): max_args = 1 @@ -218,6 +219,11 @@ 'content_short_hl': self.highlight(content_short), 'f_path': f_path }) + elif self.search_type == 'path': + full_repo_path = jn(self.repo_location, res['repository']) + f_path = res['path'].split(full_repo_path)[-1] + f_path = f_path.lstrip(os.sep) + res.update({'f_path': f_path}) elif self.search_type == 'message': res.update({'message_hl': self.highlight(res['message'])})
--- a/rhodecode/lib/middleware/pygrack.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/middleware/pygrack.py Fri Aug 24 14:58:26 2012 +0200 @@ -41,7 +41,7 @@ git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs']) commands = ['git-upload-pack', 'git-receive-pack'] - def __init__(self, repo_name, content_path, username): + def __init__(self, repo_name, content_path, extras): files = set([f.lower() for f in os.listdir(content_path)]) if not (self.git_folder_signature.intersection(files) == self.git_folder_signature): @@ -50,7 +50,7 @@ self.valid_accepts = ['application/x-%s-result' % c for c in self.commands] self.repo_name = repo_name - self.username = username + self.extras = extras def _get_fixedpath(self, path): """ @@ -67,7 +67,7 @@ HTTP /info/refs request. """ - git_command = request.GET['service'] + git_command = request.GET.get('service') if git_command not in self.commands: log.debug('command %s not allowed' % git_command) return exc.HTTPMethodNotAllowed() @@ -119,9 +119,8 @@ try: gitenv = os.environ from rhodecode import CONFIG - from rhodecode.lib.base import _get_ip_addr - gitenv['RHODECODE_USER'] = self.username - gitenv['RHODECODE_CONFIG_IP'] = _get_ip_addr(environ) + from rhodecode.lib.compat import json + gitenv['RHODECODE_EXTRAS'] = json.dumps(self.extras) # forget all configs gitenv['GIT_CONFIG_NOGLOBAL'] = '1' # we need current .ini file used to later initialize rhodecode @@ -174,7 +173,7 @@ class GitDirectory(object): - def __init__(self, repo_root, repo_name, username): + def __init__(self, repo_root, repo_name, extras): repo_location = os.path.join(repo_root, repo_name) if not os.path.isdir(repo_location): raise OSError(repo_location) @@ -182,12 +181,12 @@ self.content_path = repo_location self.repo_name = repo_name self.repo_location = repo_location - self.username = username + self.extras = extras def __call__(self, environ, start_response): content_path = self.content_path try: - app = GitRepository(self.repo_name, content_path, self.username) + app = GitRepository(self.repo_name, content_path, self.extras) except (AssertionError, OSError): if os.path.isdir(os.path.join(content_path, '.git')): app = GitRepository(self.repo_name, @@ -198,5 +197,5 @@ return app(environ, start_response) -def make_wsgi_app(repo_name, repo_root, username): - return GitDirectory(repo_root, repo_name, username) +def make_wsgi_app(repo_name, repo_root, extras): + return GitDirectory(repo_root, repo_name, extras)
--- a/rhodecode/lib/middleware/simplegit.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/middleware/simplegit.py Fri Aug 24 14:58:26 2012 +0200 @@ -31,6 +31,8 @@ from dulwich import server as dulserver from dulwich.web import LimitedInputFilter, GunzipFilter +from rhodecode.lib.exceptions import HTTPLockedRC +from rhodecode.lib.hooks import pre_pull class SimpleGitUploadPackHandler(dulserver.UploadPackHandler): @@ -81,6 +83,7 @@ from rhodecode.lib.base import BaseVCSController from rhodecode.lib.auth import get_container_username from rhodecode.lib.utils import is_valid_repo, make_ui +from rhodecode.lib.compat import json from rhodecode.model.db import User, RhodeCodeUi log = logging.getLogger(__name__) @@ -101,11 +104,11 @@ class SimpleGit(BaseVCSController): def _handle_request(self, environ, start_response): - if not is_git(environ): return self.application(environ, start_response) if not self._check_ssl(environ, start_response): return HTTPNotAcceptable('SSL REQUIRED !')(environ, start_response) + ipaddr = self._get_ip_addr(environ) username = None self._git_first_op = False @@ -122,7 +125,7 @@ return HTTPInternalServerError()(environ, start_response) # quick check if that dir exists... - if is_valid_repo(repo_name, self.basepath) is False: + if is_valid_repo(repo_name, self.basepath, 'git') is False: return HTTPNotFound()(environ, start_response) #====================================================================== @@ -183,12 +186,16 @@ if perm is not True: return HTTPForbidden()(environ, start_response) + # extras are injected into UI object and later available + # in hooks executed by rhodecode extras = { 'ip': ipaddr, 'username': username, 'action': action, 'repository': repo_name, 'scm': 'git', + 'make_lock': None, + 'locked_by': [None, None] } #=================================================================== @@ -197,6 +204,21 @@ repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) log.debug('Repository path is %s' % repo_path) + # CHECK LOCKING only if it's not ANONYMOUS USER + if username != User.DEFAULT_USER: + log.debug('Checking locking on repository') + (make_lock, + locked, + locked_by) = self._check_locking_state( + environ=environ, action=action, + repo=repo_name, user_id=user.user_id + ) + # store the make_lock for later evaluation in hooks + extras.update({'make_lock': make_lock, + 'locked_by': locked_by}) + # set the environ variables for this request + os.environ['RC_SCM_DATA'] = json.dumps(extras) + log.debug('HOOKS extras is %s' % extras) baseui = make_ui('db') self.__inject_extras(repo_path, baseui, extras) @@ -207,13 +229,16 @@ self._handle_githooks(repo_name, action, baseui, environ) log.info('%s action on GIT repo "%s"' % (action, repo_name)) - app = self.__make_app(repo_name, repo_path, username) + app = self.__make_app(repo_name, repo_path, extras) return app(environ, start_response) + except HTTPLockedRC, e: + log.debug('Repositry LOCKED ret code 423!') + return e(environ, start_response) except Exception: log.error(traceback.format_exc()) return HTTPInternalServerError()(environ, start_response) - def __make_app(self, repo_name, repo_path, username): + def __make_app(self, repo_name, repo_path, extras): """ Make an wsgi application using dulserver @@ -225,7 +250,7 @@ app = make_wsgi_app( repo_root=safe_str(self.basepath), repo_name=repo_name, - username=username, + extras=extras, ) app = GunzipFilter(LimitedInputFilter(app)) return app @@ -277,6 +302,7 @@ """ from rhodecode.lib.hooks import log_pull_action service = environ['QUERY_STRING'].split('=') + if len(service) < 2: return @@ -286,6 +312,9 @@ _repo._repo.ui = baseui _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(RhodeCodeUi.HOOK_PULL): log_pull_action(ui=baseui, repo=_repo._repo)
--- a/rhodecode/lib/middleware/simplehg.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/middleware/simplehg.py Fri Aug 24 14:58:26 2012 +0200 @@ -40,7 +40,9 @@ from rhodecode.lib.base import BaseVCSController from rhodecode.lib.auth import get_container_username from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections +from rhodecode.lib.compat import json from rhodecode.model.db import User +from rhodecode.lib.exceptions import HTTPLockedRC log = logging.getLogger(__name__) @@ -87,7 +89,7 @@ return HTTPInternalServerError()(environ, start_response) # quick check if that dir exists... - if is_valid_repo(repo_name, self.basepath) is False: + if is_valid_repo(repo_name, self.basepath, 'hg') is False: return HTTPNotFound()(environ, start_response) #====================================================================== @@ -156,14 +158,31 @@ 'action': action, 'repository': repo_name, 'scm': 'hg', + 'make_lock': None, + 'locked_by': [None, None] } - #====================================================================== # MERCURIAL REQUEST HANDLING #====================================================================== repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) log.debug('Repository path is %s' % repo_path) + # CHECK LOCKING only if it's not ANONYMOUS USER + if username != User.DEFAULT_USER: + log.debug('Checking locking on repository') + (make_lock, + locked, + locked_by) = self._check_locking_state( + environ=environ, action=action, + repo=repo_name, user_id=user.user_id + ) + # store the make_lock for later evaluation in hooks + extras.update({'make_lock': make_lock, + 'locked_by': locked_by}) + + # set the environ variables for this request + os.environ['RC_SCM_DATA'] = json.dumps(extras) + log.debug('HOOKS extras is %s' % extras) baseui = make_ui('db') self.__inject_extras(repo_path, baseui, extras) @@ -177,6 +196,9 @@ except RepoError, e: if str(e).find('not found') != -1: return HTTPNotFound()(environ, start_response) + except HTTPLockedRC, e: + log.debug('Repositry LOCKED ret code 423!') + return e(environ, start_response) except Exception: log.error(traceback.format_exc()) return HTTPInternalServerError()(environ, start_response)
--- a/rhodecode/lib/subprocessio.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/subprocessio.py Fri Aug 24 14:58:26 2012 +0200 @@ -25,7 +25,7 @@ import os import subprocess import threading -from collections import deque +from rhodecode.lib.compat import deque, Event class StreamFeeder(threading.Thread): @@ -89,16 +89,16 @@ self.chunk_count_max = int(buffer_size / chunk_size) + 1 self.chunk_size = chunk_size - self.data_added = threading.Event() + self.data_added = Event() self.data_added.clear() - self.keep_reading = threading.Event() + self.keep_reading = Event() self.keep_reading.set() - self.EOF = threading.Event() + self.EOF = Event() self.EOF.clear() - self.go = threading.Event() + self.go = Event() self.go.set() def stop(self):
--- a/rhodecode/lib/utils.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/utils.py Fri Aug 24 14:58:26 2012 +0200 @@ -203,19 +203,24 @@ return _get_repos(path) -def is_valid_repo(repo_name, base_path): +def is_valid_repo(repo_name, base_path, scm=None): """ - Returns True if given path is a valid repository False otherwise + 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: - get_scm(full_path) + scm_ = get_scm(full_path) + if scm: + return scm_[0] == scm return True except VCSError: return False @@ -275,7 +280,7 @@ 'ui', 'web', ] -def make_ui(read_from='file', path=None, checkpaths=True): +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 @@ -320,8 +325,8 @@ # force set push_ssl requirement to False, rhodecode # handles that baseui.setconfig(ui_.ui_section, ui_.ui_key, False) - - meta.Session.remove() + if clear_session: + meta.Session.remove() return baseui
--- a/rhodecode/lib/utils2.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/lib/utils2.py Fri Aug 24 14:58:26 2012 +0200 @@ -25,7 +25,7 @@ import re import time -from datetime import datetime +import datetime from pylons.i18n.translation import _, ungettext from rhodecode.lib.vcs.utils.lazy import LazyProperty @@ -300,7 +300,7 @@ deltas = {} # Get date parts deltas - now = datetime.now() + now = datetime.datetime.now() for part in order: deltas[part] = getattr(now, part) - getattr(prevdate, part) @@ -435,6 +435,15 @@ return time.mktime(dt.timetuple()) +def time_to_datetime(tm): + if tm: + if isinstance(tm, basestring): + try: + tm = float(tm) + except ValueError: + return + return datetime.datetime.fromtimestamp(tm) + MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})'
--- a/rhodecode/model/db.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/model/db.py Fri Aug 24 14:58:26 2012 +0200 @@ -28,6 +28,7 @@ import datetime import traceback import hashlib +import time from collections import defaultdict from sqlalchemy import * @@ -232,7 +233,9 @@ HOOK_UPDATE = 'changegroup.update' HOOK_REPO_SIZE = 'changegroup.repo_size' HOOK_PUSH = 'changegroup.push_logger' - HOOK_PULL = 'preoutgoing.pull_logger' + HOOK_PRE_PUSH = 'prechangegroup.pre_push' + HOOK_PULL = 'outgoing.pull_logger' + HOOK_PRE_PULL = 'preoutgoing.pre_pull' ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) ui_section = Column("ui_section", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) @@ -247,17 +250,17 @@ @classmethod def get_builtin_hooks(cls): q = cls.query() - q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, - cls.HOOK_REPO_SIZE, - cls.HOOK_PUSH, cls.HOOK_PULL])) + q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE, + cls.HOOK_PUSH, cls.HOOK_PRE_PUSH, + cls.HOOK_PULL, cls.HOOK_PRE_PULL])) return q.all() @classmethod def get_custom_hooks(cls): q = cls.query() - q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, - cls.HOOK_REPO_SIZE, - cls.HOOK_PUSH, cls.HOOK_PULL])) + q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE, + cls.HOOK_PUSH, cls.HOOK_PRE_PUSH, + cls.HOOK_PULL, cls.HOOK_PRE_PULL])) q = q.filter(cls.ui_section == 'hooks') return q.all() @@ -280,9 +283,13 @@ __tablename__ = 'users' __table_args__ = ( UniqueConstraint('username'), UniqueConstraint('email'), + Index('u_username_idx', 'username'), + Index('u_email_idx', 'email'), {'extend_existing': True, 'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8'} ) + DEFAULT_USER = 'default' + user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True) username = Column("username", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) password = Column("password", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) @@ -323,26 +330,35 @@ self._email = val.lower() if val else None @property + def firstname(self): + # alias for future + return self.name + + @property def emails(self): other = UserEmailMap.query().filter(UserEmailMap.user==self).all() return [self.email] + [x.email for x in other] @property + def username_and_name(self): + return '%s (%s %s)' % (self.username, self.firstname, self.lastname) + + @property def full_name(self): - return '%s %s' % (self.name, self.lastname) + return '%s %s' % (self.firstname, self.lastname) @property def full_name_or_username(self): - return ('%s %s' % (self.name, self.lastname) - if (self.name and self.lastname) else self.username) + return ('%s %s' % (self.firstname, self.lastname) + if (self.firstname and self.lastname) else self.username) @property def full_contact(self): - return '%s %s <%s>' % (self.name, self.lastname, self.email) + return '%s %s <%s>' % (self.firstname, self.lastname, self.email) @property def short_contact(self): - return '%s %s' % (self.name, self.lastname) + return '%s %s' % (self.firstname, self.lastname) @property def is_admin(self): @@ -572,6 +588,7 @@ __tablename__ = 'repositories' __table_args__ = ( UniqueConstraint('repo_name'), + Index('r_repo_name_idx', 'repo_name'), {'extend_existing': True, 'mysql_engine': 'InnoDB', 'mysql_charset': 'utf8'}, ) @@ -587,6 +604,8 @@ description = Column("description", String(10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None) created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now) landing_rev = Column("landing_revision", String(255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default=None) + enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False) + _locked = Column("locked", String(255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None) fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None) group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None) @@ -617,6 +636,21 @@ return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id, self.repo_name) + @hybrid_property + def locked(self): + # always should return [user_id, timelocked] + if self._locked: + _lock_info = self._locked.split(':') + return int(_lock_info[0]), _lock_info[1] + return [None, None] + + @locked.setter + def locked(self, val): + if val and isinstance(val, (list, tuple)): + self._locked = ':'.join(map(str, val)) + else: + self._locked = None + @classmethod def url_sep(cls): return URL_SEP @@ -744,7 +778,7 @@ if ui_.ui_key == 'push_ssl': # force set push_ssl requirement to False, rhodecode # handles that - baseui.setconfig(ui_.ui_section, ui_.ui_key, False) + baseui.setconfig(ui_.ui_section, ui_.ui_key, False) return baseui @@ -793,6 +827,18 @@ return data + @classmethod + def lock(cls, repo, user_id): + repo.locked = [user_id, time.time()] + Session().add(repo) + Session().commit() + + @classmethod + def unlock(cls, repo): + repo.locked = None + Session().add(repo) + Session().commit() + #========================================================================== # SCM PROPERTIES #========================================================================== @@ -1467,6 +1513,7 @@ revision = Column('revision', String(40), nullable=True) pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True) line_no = Column('line_no', Unicode(10), nullable=True) + hl_lines = Column('hl_lines', Unicode(512), nullable=True) f_path = Column('f_path', Unicode(1000), nullable=True) user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False) text = Column('text', Unicode(25000), nullable=False)
--- a/rhodecode/model/forms.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/model/forms.py Fri Aug 24 14:58:26 2012 +0200 @@ -182,6 +182,7 @@ private = v.StringBoolean(if_missing=False) enable_statistics = v.StringBoolean(if_missing=False) enable_downloads = v.StringBoolean(if_missing=False) + enable_locking = v.StringBoolean(if_missing=False) landing_rev = v.OneOf(landing_revs, hideList=True) if edit: @@ -265,7 +266,7 @@ hooks_changegroup_update = v.StringBoolean(if_missing=False) hooks_changegroup_repo_size = v.StringBoolean(if_missing=False) hooks_changegroup_push_logger = v.StringBoolean(if_missing=False) - hooks_preoutgoing_pull_logger = v.StringBoolean(if_missing=False) + hooks_outgoing_pull_logger = v.StringBoolean(if_missing=False) extensions_largefiles = v.StringBoolean(if_missing=False) extensions_hgsubversion = v.StringBoolean(if_missing=False) @@ -333,8 +334,8 @@ org_ref = v.UnicodeString(strip=True, required=True) other_repo = v.UnicodeString(strip=True, required=True) other_ref = v.UnicodeString(strip=True, required=True) - revisions = v.Set(required=True) - review_members = v.Set(required=True) + revisions = All(v.NotReviewedRevisions()(), v.UniqueList(not_empty=True)) + review_members = v.UniqueList(not_empty=True) pullrequest_title = v.UnicodeString(strip=True, required=True, min=3) pullrequest_desc = v.UnicodeString(strip=True, required=False)
--- a/rhodecode/model/scm.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/model/scm.py Fri Aug 24 14:58:26 2012 +0200 @@ -571,34 +571,41 @@ if not os.path.isdir(loc): os.makedirs(loc) - tmpl = pkg_resources.resource_string( + tmpl_post = pkg_resources.resource_string( 'rhodecode', jn('config', 'post_receive_tmpl.py') ) + tmpl_pre = pkg_resources.resource_string( + 'rhodecode', jn('config', 'pre_receive_tmpl.py') + ) - _hook_file = jn(loc, 'post-receive') - _rhodecode_hook = False - log.debug('Installing git hook in repo %s' % repo) - if os.path.exists(_hook_file): - # let's take a look at this hook, maybe it's rhodecode ? - log.debug('hook exists, checking if it is from rhodecode') - _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER') - with open(_hook_file, 'rb') as f: - data = f.read() - matches = re.compile(r'(?:%s)\s*=\s*(.*)' - % 'RC_HOOK_VER').search(data) - if matches: - try: - ver = matches.groups()[0] - log.debug('got %s it is rhodecode' % (ver)) - _rhodecode_hook = True - except: - log.error(traceback.format_exc()) + for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]: + _hook_file = jn(loc, '%s-receive' % h_type) + _rhodecode_hook = False + log.debug('Installing git hook in repo %s' % repo) + if os.path.exists(_hook_file): + # let's take a look at this hook, maybe it's rhodecode ? + log.debug('hook exists, checking if it is from rhodecode') + _HOOK_VER_PAT = re.compile(r'^RC_HOOK_VER') + with open(_hook_file, 'rb') as f: + data = f.read() + matches = re.compile(r'(?:%s)\s*=\s*(.*)' + % 'RC_HOOK_VER').search(data) + if matches: + try: + ver = matches.groups()[0] + log.debug('got %s it is rhodecode' % (ver)) + _rhodecode_hook = True + except: + log.error(traceback.format_exc()) + else: + # there is no hook in this dir, so we want to create one + _rhodecode_hook = True - if _rhodecode_hook or force_create: - log.debug('writing hook file !') - with open(_hook_file, 'wb') as f: - tmpl = tmpl.replace('_TMPL_', rhodecode.__version__) - f.write(tmpl) - os.chmod(_hook_file, 0755) - else: - log.debug('skipping writing hook file') + if _rhodecode_hook or force_create: + log.debug('writing %s hook file !' % h_type) + with open(_hook_file, 'wb') as f: + tmpl = tmpl.replace('_TMPL_', rhodecode.__version__) + f.write(tmpl) + os.chmod(_hook_file, 0755) + else: + log.debug('skipping writing hook file')
--- a/rhodecode/model/validators.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/model/validators.py Fri Aug 24 14:58:26 2012 +0200 @@ -10,17 +10,46 @@ from formencode.validators import ( UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, + NotEmpty ) from rhodecode.lib.utils import repo_name_slug -from rhodecode.model.db import RepoGroup, Repository, UsersGroup, User +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 +UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \ + NotEmpty 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): + return list(value) + elif value is None: + return [] + else: + return [value] + + def empty_value(self, value): + return [] + + class StateObj(object): """ this is needed to translate the messages using _() in validators @@ -377,10 +406,10 @@ ## initially check if it's at least the proper URL ## or does it pass basic auth MercurialRepository._check_url(url) - httppeer(make_ui('db'), url)._capabilities() + httppeer(ui, url)._capabilities() elif url.startswith('svn+http'): from hgsubversion.svnrepo import svnremoterepo - svnremoterepo(make_ui('db'), url).capabilities + svnremoterepo(ui, url).capabilities elif url.startswith('git+http'): raise NotImplementedError() @@ -410,7 +439,7 @@ pass else: try: - url_handler(repo_type, url, make_ui('db')) + url_handler(repo_type, url, make_ui('db', clear_session=False)) except Exception: log.exception('Url validation failed') msg = M(self, 'clone_uri') @@ -599,3 +628,33 @@ ) return _validator + + +def NotReviewedRevisions(): + class _validator(formencode.validators.FancyValidator): + messages = { + 'rev_already_reviewed': + _(u'Revisions %(revs)s are already part of pull request ' + 'or have set status') + } + + def validate_python(self, value, state): + # 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) + raise formencode.Invalid(msg, value, state, + error_dict=dict(revisions=revs) + ) + + return _validator
--- a/rhodecode/public/css/style.css Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/public/css/style.css Fri Aug 24 14:58:26 2012 +0200 @@ -219,6 +219,11 @@ margin-bottom: 0; margin-top: 5px; } + +.empty_data{ + color:#B9B9B9; +} + a.permalink{ visibility: hidden; }
--- a/rhodecode/public/js/rhodecode.js Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/public/js/rhodecode.js Fri Aug 24 14:58:26 2012 +0200 @@ -398,7 +398,7 @@ // create event for hide button form = new YAHOO.util.Element(form); - var form_hide_button = new YAHOO.util.Element(form.getElementsByClassName('hide-inline-form')[0]); + 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')){ @@ -422,12 +422,12 @@ return } var submit_url = AJAX_COMMENT_URL; - var _td = tr.getElementsByClassName('code')[0]; + var _td = YUD.getElementsByClassName('code',null,tr)[0]; if(YUD.hasClass(tr,'form-open') || YUD.hasClass(tr,'context') || YUD.hasClass(_td,'no-comment')){ return } YUD.addClass(tr,'form-open'); - var node = tr.parentNode.parentNode.parentNode.getElementsByClassName('full_f_path')[0]; + 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); @@ -447,8 +447,8 @@ var f = YUD.get(form); - var overlay = f.getElementsByClassName('overlay')[0]; - var _form = f.getElementsByClassName('inline-form')[0]; + 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); @@ -568,8 +568,8 @@ // next element are comments ! if(YUD.hasClass(n,'inline-comments')){ last_node = n; - //also remove the comment button from previos - var comment_add_buttons = last_node.getElementsByClassName('add-comment'); + //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); @@ -582,7 +582,7 @@ var add = createInlineAddButton(target_tr); // get the comment div - var comment_block = last_node.getElementsByClassName('comment')[0]; + var comment_block = YUD.getElementsByClassName('comment',null,last_node)[0]; // attach add button YUD.insertAfter(add,comment_block); } @@ -889,7 +889,7 @@ success:function(o){ var obj = YUD.get(String("notification_"+notification_id)); YUD.removeClass(obj, 'unread'); - var r_button = obj.children[0].getElementsByClassName('read-notification')[0] + var r_button = YUD.getElementsByClassName('read-notification',null,obj.children[0])[0]; if(r_button.parentNode !== undefined){ r_button.parentNode.removeChild(r_button); @@ -1737,4 +1737,4 @@ } }); } -} \ No newline at end of file +}
--- a/rhodecode/templates/admin/notifications/notifications_data.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/admin/notifications/notifications_data.html Fri Aug 24 14:58:26 2012 +0200 @@ -10,7 +10,7 @@ <div id="notification_${notification.notification.notification_id}" class="container ${unread(notification.read)}"> <div class="notification-header"> <div class="gravatar"> - <img alt="gravatar" src="${h.gravatar_url(h.email(notification.notification.created_by_user.email),24)}"/> + <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(notification.notification.created_by_user.email),24)}"/> </div> <div class="desc ${unread(notification.read)}"> <a href="${url('notification', notification_id=notification.notification.notification_id)}">${notification.notification.description}</a>
--- a/rhodecode/templates/admin/notifications/show_notification.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/admin/notifications/show_notification.html Fri Aug 24 14:58:26 2012 +0200 @@ -30,7 +30,7 @@ <div id="notification_${c.notification.notification_id}"> <div class="notification-header"> <div class="gravatar"> - <img alt="gravatar" src="${h.gravatar_url(h.email(c.notification.created_by_user.email),24)}"/> + <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.notification.created_by_user.email),24)}"/> </div> <div class="desc"> ${c.notification.description}
--- a/rhodecode/templates/admin/repos/repo_edit.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/admin/repos/repo_edit.html Fri Aug 24 14:58:26 2012 +0200 @@ -108,6 +108,15 @@ </div> </div> <div class="field"> + <div class="label label-checkbox"> + <label for="enable_locking">${_('Enable locking')}:</label> + </div> + <div class="checkboxes"> + ${h.checkbox('enable_locking',value="True")} + <span class="help-block">${_('Enable lock-by-pulling on repository.')}</span> + </div> + </div> + <div class="field"> <div class="label"> <label for="user">${_('Owner')}:</label> </div> @@ -196,26 +205,31 @@ </div> <div class="field" style="border:none;color:#888"> <ul> - <li>${_('''All actions made on this repository will be accessible to everyone in public journal''')} + <li>${_('All actions made on this repository will be accessible to everyone in public journal')} </li> </ul> </div> </div> ${h.end_form()} - <h3>${_('Delete')}</h3> - ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='delete')} + <h3>${_('Locking')}</h3> + ${h.form(url('repo_locking', repo_name=c.repo_info.repo_name),method='put')} <div class="form"> <div class="fields"> - ${h.submit('remove_%s' % c.repo_info.repo_name,_('Remove this repository'),class_="ui-btn red",onclick="return confirm('"+_('Confirm to delete this repository')+"');")} + %if c.repo_info.locked[0]: + ${h.submit('set_unlock' ,_('Unlock locked repo'),class_="ui-btn",onclick="return confirm('"+_('Confirm to unlock repository')+"');")} + ${'Locked by %s on %s' % (h.person_by_id(c.repo_info.locked[0]),h.fmt_date(h.time_to_datetime(c.repo_info.locked[1])))} + %else: + ${h.submit('set_lock',_('lock repo'),class_="ui-btn",onclick="return confirm('"+_('Confirm to lock repository')+"');")} + ${_('Repository is not locked')} + %endif </div> <div class="field" style="border:none;color:#888"> <ul> - <li>${_('''This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems. - If you need fully delete it from filesystem please do it manually''')} + <li>${_('Force locking on repository. Works only when anonymous access is disabled')} </li> </ul> - </div> + </div> </div> ${h.end_form()} @@ -231,10 +245,24 @@ <li>${_('''Manually set this repository as a fork of another from the list''')}</li> </ul> </div> - </div> + </div> ${h.end_form()} - + + <h3>${_('Delete')}</h3> + ${h.form(url('repo', repo_name=c.repo_info.repo_name),method='delete')} + <div class="form"> + <div class="fields"> + ${h.submit('remove_%s' % c.repo_info.repo_name,_('Remove this repository'),class_="ui-btn red",onclick="return confirm('"+_('Confirm to delete this repository')+"');")} + </div> + <div class="field" style="border:none;color:#888"> + <ul> + <li>${_('''This repository will be renamed in a special way in order to be unaccesible for RhodeCode and VCS systems. + If you need fully delete it from filesystem please do it manually''')} + </li> + </ul> + </div> + </div> + ${h.end_form()} </div> - </%def>
--- a/rhodecode/templates/admin/settings/settings.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/admin/settings/settings.html Fri Aug 24 14:58:26 2012 +0200 @@ -211,8 +211,8 @@ <label for="hooks_changegroup_push_logger">${_('Log user push commands')}</label> </div> <div class="checkbox"> - ${h.checkbox('hooks_preoutgoing_pull_logger','True')} - <label for="hooks_preoutgoing_pull_logger">${_('Log user pull commands')}</label> + ${h.checkbox('hooks_outgoing_pull_logger','True')} + <label for="hooks_outgoing_pull_logger">${_('Log user pull commands')}</label> </div> </div> <div class="input" style="margin-top:10px">
--- a/rhodecode/templates/admin/users/user_edit.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/admin/users/user_edit.html Fri Aug 24 14:58:26 2012 +0200 @@ -183,7 +183,7 @@ %for section in sorted(c.perm_user.permissions.keys()): <div class="perms_section_head">${section.replace("_"," ").capitalize()}</div> %if not c.perm_user.permissions[section]: - <span style="color:#B9B9B9">${_('Nothing here yet')}</span> + <span class="empty_data">${_('Nothing here yet')}</span> %else: <div id='tbl_list_wrap_${section}' class="yui-skin-sam"> <table id="tbl_list_${section}">
--- a/rhodecode/templates/admin/users/user_edit_my_account.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/admin/users/user_edit_my_account.html Fri Aug 24 14:58:26 2012 +0200 @@ -96,8 +96,11 @@ </div> <div id="my" class="table" style="display:none"> </div> - <div id="pullrequests" class="table" style="display:none"> + <div id="pullrequests" class="table" style="display:none"></div> </div> + + + <script type="text/javascript"> var filter_activate = function(){ var nodes = YUQ('#my tr td a.repo_name');
--- a/rhodecode/templates/admin/users/user_edit_my_account_pullrequests.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/admin/users/user_edit_my_account_pullrequests.html Fri Aug 24 14:58:26 2012 +0200 @@ -1,22 +1,30 @@ <div class="pullrequests_section_head">${_('Opened by me')}</div> <ul> - %for pull_request in c.my_pull_requests: - <li> - <a href="${h.url('pullrequest_show',repo_name=pull_request.other_repo.repo_name,pull_request_id=pull_request.pull_request_id)}"> - ${_('Pull request #%s opened on %s') % (pull_request.pull_request_id, h.fmt_date(pull_request.created_on))} - </a> - </li> - %endfor + %if c.my_pull_requests: + %for pull_request in c.my_pull_requests: + <li> + <a href="${h.url('pullrequest_show',repo_name=pull_request.other_repo.repo_name,pull_request_id=pull_request.pull_request_id)}"> + ${_('Pull request #%s opened on %s') % (pull_request.pull_request_id, h.fmt_date(pull_request.created_on))} + </a> + </li> + %endfor + %else: + <li><span class="empty_data">${_('Nothing here yet')}</span></li> + %endif </ul> <div class="pullrequests_section_head">${_('I participate in')}</div> <ul> - %for pull_request in c.participate_in_pull_requests: - <li> - <a href="${h.url('pullrequest_show',repo_name=pull_request.other_repo.repo_name,pull_request_id=pull_request.pull_request_id)}"> - ${_('Pull request #%s opened by %s on %s') % (pull_request.pull_request_id, pull_request.author.full_name, h.fmt_date(pull_request.created_on))} - </a> - </li> - %endfor + %if c.my_pull_requests: + %for pull_request in c.participate_in_pull_requests: + <li> + <a href="${h.url('pullrequest_show',repo_name=pull_request.other_repo.repo_name,pull_request_id=pull_request.pull_request_id)}"> + ${_('Pull request #%s opened by %s on %s') % (pull_request.pull_request_id, pull_request.author.full_name, h.fmt_date(pull_request.created_on))} + </a> + </li> + %endfor + %else: + <li><span class="empty_data">${_('Nothing here yet')}</span></li> + %endif </ul>
--- a/rhodecode/templates/changelog/changelog.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/changelog/changelog.html Fri Aug 24 14:58:26 2012 +0200 @@ -59,7 +59,7 @@ </div> <div class="author"> <div class="gravatar"> - <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),16)}"/> + <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),16)}"/> </div> <div title="${cs.author}" class="user">${h.shorter(h.person(cs.author),22)}</div> </div>
--- a/rhodecode/templates/changeset/changeset.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/changeset/changeset.html Fri Aug 24 14:58:26 2012 +0200 @@ -53,7 +53,7 @@ <div class="left"> <div class="author"> <div class="gravatar"> - <img alt="gravatar" src="${h.gravatar_url(h.email(c.changeset.author),20)}"/> + <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.changeset.author),20)}"/> </div> <span>${h.person(c.changeset.author)}</span><br/> <span><a href="mailto:${h.email_or_none(c.changeset.author)}">${h.email_or_none(c.changeset.author)}</a></span><br/> @@ -149,6 +149,9 @@ YUE.on(YUQ('.show-inline-comments'),'change',function(e){ var show = 'none'; var target = e.currentTarget; + if(target == null){ + target = this; + } if(target.checked){ var show = '' } @@ -165,6 +168,9 @@ YUE.on(YUQ('.line'),'click',function(e){ var tr = e.currentTarget; + if(tr == null){ + tr = this; + } injectInlineForm(tr); });
--- a/rhodecode/templates/changeset/changeset_range.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/changeset/changeset_range.html Fri Aug 24 14:58:26 2012 +0200 @@ -37,7 +37,7 @@ <table class="compare_view_commits noborder"> %for cnt,cs in enumerate(c.cs_ranges): <tr> - <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),14)}"/></div></td> + <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),14)}"/></div></td> <td>${h.link_to('r%s:%s' % (cs.revision,h.short_id(cs.raw_id)),h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}</td> <td><div class="author">${h.person(cs.author)}</div></td> <td><span class="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td> @@ -71,7 +71,7 @@ <h3 style="padding-top:8px;"> <a class="tooltip" title="${h.tooltip(cs.message)}" href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}">${'r%s:%s' % (cs.revision,h.short_id(cs.raw_id))}</a> <div class="gravatar"> - <img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),20)}"/> + <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),20)}"/> </div> </h3> ${diff_block.diff_block(c.changes[cs.raw_id])}
--- a/rhodecode/templates/compare/compare_cs.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/compare/compare_cs.html Fri Aug 24 14:58:26 2012 +0200 @@ -6,7 +6,7 @@ %else: %for cnt, cs in enumerate(c.cs_ranges): <tr> - <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email(cs.author),14)}"/></div></td> + <td><div class="gravatar"><img alt="gravatar" src="${h.gravatar_url(h.email_or_none(cs.author),14)}"/></div></td> <td> %if cs.raw_id in c.statuses: <div title="${c.statuses[cs.raw_id][1]}" class="changeset-status-ico"><img src="${h.url('/images/icons/flag_status_%s.png' % c.statuses[cs.raw_id][0])}" /></div>
--- a/rhodecode/templates/files/files_source.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/files/files_source.html Fri Aug 24 14:58:26 2012 +0200 @@ -48,7 +48,7 @@ </div> <div class="author"> <div class="gravatar"> - <img alt="gravatar" src="${h.gravatar_url(h.email(c.file.changeset.author),16)}"/> + <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(c.file.changeset.author),16)}"/> </div> <div title="${c.file.changeset.author}" class="user">${h.person(c.file.changeset.author)}</div> </div>
--- a/rhodecode/templates/pullrequests/pullrequest.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/pullrequests/pullrequest.html Fri Aug 24 14:58:26 2012 +0200 @@ -53,7 +53,7 @@ <img id="other_repo_gravatar" alt="gravatar" src=""/> </div> <span style="font-size: 20px"> - ${h.select('other_repo',c.default_pull_request ,c.other_repos,class_='refs')}:${h.select('other_ref','',c.other_refs,class_='refs')} + ${h.select('other_repo',c.default_pull_request ,c.other_repos,class_='refs')}:${h.select('other_ref','',c.default_revs,class_='refs')} </span> <div id="other_repo_desc" style="padding:5px 3px 3px 42px;"></div> </div> @@ -174,6 +174,7 @@ YUD.setStyle(YUD.get('pull_request_overview_url').parentElement,'display',''); YUD.get('other_repo_gravatar').src = other_repos_info[repo_name]['gravatar']; YUD.get('other_repo_desc').innerHTML = other_repos_info[repo_name]['description']; + YUD.get('other_ref').innerHTML = other_repos_info[repo_name]['revs']; }) } YUE.on('refresh','click',function(e){
--- a/rhodecode/templates/search/search_commit.html Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/templates/search/search_commit.html Fri Aug 24 14:58:26 2012 +0200 @@ -12,7 +12,7 @@ <div class="left"> <div class="author"> <div class="gravatar"> - <img alt="gravatar" src="${h.gravatar_url(h.email(sr['author']),20)}"/> + <img alt="gravatar" src="${h.gravatar_url(h.email_or_none(sr['author']),20)}"/> </div> <span>${h.person(sr['author'])}</span><br/> <span><a href="mailto:${h.email_or_none(sr['author'])}">${h.email_or_none(sr['author'])}</a></span><br/>
--- a/rhodecode/tests/__init__.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/tests/__init__.py Fri Aug 24 14:58:26 2012 +0200 @@ -41,12 +41,13 @@ __all__ = [ 'parameterized', 'environ', 'url', 'get_new_dir', 'TestController', 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO', - 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_REGULAR_LOGIN', - 'TEST_USER_REGULAR_PASS', 'TEST_USER_REGULAR_EMAIL', - 'TEST_USER_REGULAR2_LOGIN', 'TEST_USER_REGULAR2_PASS', - 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO', 'TEST_HG_REPO_CLONE', - 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO', 'TEST_GIT_REPO_CLONE', - 'TEST_GIT_REPO_PULL', 'HG_REMOTE_REPO', 'GIT_REMOTE_REPO', 'SCM_TESTS', + 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS', + 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS', + 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN', + 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL', 'TEST_HG_REPO', + 'TEST_HG_REPO_CLONE', 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO', + 'TEST_GIT_REPO_CLONE', 'TEST_GIT_REPO_PULL', 'HG_REMOTE_REPO', + 'GIT_REMOTE_REPO', 'SCM_TESTS', ] # Invoke websetup with the current config file
--- a/rhodecode/tests/functional/test_search.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/tests/functional/test_search.py Fri Aug 24 14:58:26 2012 +0200 @@ -90,3 +90,10 @@ 'type': 'commit'}) response.mustcontain('1 results') + + def test_search_file_name(self): + self.log_user() + response = self.app.get(url(controller='search', action='index'), + {'q': 'README.rst', 'type': 'path'}) + + response.mustcontain('2 results') \ No newline at end of file
--- a/rhodecode/tests/scripts/test_scm_operations.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/tests/scripts/test_scm_operations.py Fri Aug 24 14:58:26 2012 +0200 @@ -1,9 +1,12 @@ # -*- coding: utf-8 -*- """ - rhodecode.tests.test_hg_operations - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + rhodecode.tests.test_scm_operations + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - Test suite for making push/pull operations + Test suite for making push/pull operations. + Run using:: + + RC_WHOOSH_TEST_DISABLE=1 nosetests rhodecode/tests/scripts/test_scm_operations.py :created_on: Dec 30, 2010 :author: marcink @@ -24,47 +27,19 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import time -import sys -import shutil -import logging - +import tempfile from os.path import join as jn from os.path import dirname as dn from tempfile import _RandomNameSequence from subprocess import Popen, PIPE -from paste.deploy import appconfig -from pylons import config -from sqlalchemy import engine_from_config - -from rhodecode.lib.utils import add_cache -from rhodecode.model import init_model -from rhodecode.model import meta +from rhodecode.tests import * from rhodecode.model.db import User, Repository, UserLog -from rhodecode.lib.auth import get_crypt_password - -from rhodecode.tests import TESTS_TMP_PATH, NEW_HG_REPO, HG_REPO -from rhodecode.config.environment import load_environment - -rel_path = dn(dn(dn(os.path.abspath(__file__)))) +from rhodecode.model.meta import Session -conf = appconfig('config:%s' % sys.argv[1], relative_to=rel_path) -load_environment(conf.global_conf, conf.local_conf) - -add_cache(conf) - -USER = 'test_admin' -PASS = 'test12' -HOST = '127.0.0.1:5000' -DEBUG = False -print 'DEBUG:', DEBUG -log = logging.getLogger(__name__) - -engine = engine_from_config(conf, 'sqlalchemy.db1.') -init_model(engine) -sa = meta.Session() +DEBUG = True +HOST = '127.0.0.1:5000' # test host class Command(object): @@ -73,13 +48,13 @@ self.cwd = cwd def execute(self, cmd, *args): - """Runs command on the system with given ``args``. + """ + Runs command on the system with given ``args``. """ command = cmd + ' ' + ' '.join(args) - log.debug('Executing %s' % command) if DEBUG: - print command + print '*** CMD %s ***' % command p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, cwd=self.cwd) stdout, stderr = p.communicate() if DEBUG: @@ -87,324 +62,181 @@ return stdout, stderr -def test_wrapp(func): - - def __wrapp(*args, **kwargs): - print '>>>%s' % func.__name__ - try: - res = func(*args, **kwargs) - except Exception, e: - print ('###############\n-' - '--%s failed %s--\n' - '###############\n' % (func.__name__, e)) - sys.exit() - print '++OK++' - return res - return __wrapp +def _get_tmp_dir(): + return tempfile.mkdtemp(prefix='rc_integration_test') -def create_test_user(force=True): - print '\tcreating test user' - - user = User.get_by_username(USER) - - if force and user is not None: - print '\tremoving current user' - for repo in Repository.query().filter(Repository.user == user).all(): - sa.delete(repo) - sa.delete(user) - sa.commit() - - if user is None or force: - print '\tcreating new one' - new_usr = User() - new_usr.username = USER - new_usr.password = get_crypt_password(PASS) - new_usr.email = 'mail@mail.com' - new_usr.name = 'test' - new_usr.lastname = 'lasttestname' - new_usr.active = True - new_usr.admin = True - sa.add(new_usr) - sa.commit() - - print '\tdone' - - -def create_test_repo(force=True): - from rhodecode.model.repo import RepoModel - - user = User.get_by_username(USER) - if user is None: - raise Exception('user not found') - - repo = sa.query(Repository).filter(Repository.repo_name == HG_REPO).scalar() - - if repo is None: - print '\trepo not found creating' - - form_data = {'repo_name':HG_REPO, - 'repo_type':'hg', - 'private':False, - 'clone_uri':'' } - rm = RepoModel(sa) - rm.base_path = '/home/hg' - rm.create(form_data, user) +def _construct_url(repo, dest=None, **kwargs): + if dest is None: + #make temp clone + dest = _get_tmp_dir() + params = { + 'user': TEST_USER_ADMIN_LOGIN, + 'passwd': TEST_USER_ADMIN_PASS, + 'host': HOST, + 'cloned_repo': repo, + 'dest': dest + } + params.update(**kwargs) + if params['user'] and params['passwd']: + _url = 'http://%(user)s:%(passwd)s@%(host)s/%(cloned_repo)s %(dest)s' % params + else: + _url = 'http://(host)s/%(cloned_repo)s %(dest)s' % params + return _url def set_anonymous_access(enable=True): - user = User.get_by_username('default') + user = User.get_by_username(User.DEFAULT_USER) user.active = enable - sa.add(user) - sa.commit() + Session().add(user) + Session().commit() print '\tanonymous access is now:', enable - if enable != User.get_by_username('default').active: + if enable != User.get_by_username(User.DEFAULT_USER).active: raise Exception('Cannot set anonymous access') -def get_anonymous_access(): - user = User.get_by_username('default') - return user.active +def setup_module(): + #DISABLE ANONYMOUS ACCESS + set_anonymous_access(False) + + +def test_clone_hg_repo_by_admin(): + clone_url = _construct_url(HG_REPO) + stdout, stderr = Command('/tmp').execute('hg clone', clone_url) + + assert 'requesting all changes' in stdout + assert 'adding changesets' in stdout + assert 'adding manifests' in stdout + assert 'adding file changes' in stdout + + assert stderr == '' -#============================================================================== -# TESTS -#============================================================================== -@test_wrapp -def test_clone_with_credentials(no_errors=False): - cwd = path = jn(TESTS_TMP_PATH, HG_REPO) - - try: - shutil.rmtree(path, ignore_errors=True) - os.makedirs(path) - #print 'made dirs %s' % jn(path) - except OSError: - raise +def test_clone_git_repo_by_admin(): + clone_url = _construct_url(GIT_REPO) + stdout, stderr = Command('/tmp').execute('git clone', clone_url) - print '\tchecking if anonymous access is enabled' - anonymous_access = get_anonymous_access() - if anonymous_access: - print '\tenabled, disabling it ' - set_anonymous_access(enable=False) - - clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s %(dest)s' % \ - {'user':USER, - 'pass':PASS, - 'host':HOST, - 'cloned_repo':HG_REPO, - 'dest':path} - - stdout, stderr = Command(cwd).execute('hg clone', clone_url) - - if no_errors is False: - assert """adding file changes""" in stdout, 'no messages about cloning' - assert """abort""" not in stderr , 'got error from clone' + assert 'Cloning into' in stdout + assert stderr == '' -@test_wrapp -def test_clone_anonymous(): - cwd = path = jn(TESTS_TMP_PATH, HG_REPO) +def test_clone_wrong_credentials_hg(): + clone_url = _construct_url(HG_REPO, passwd='bad!') + stdout, stderr = Command('/tmp').execute('hg clone', clone_url) + assert 'abort: authorization failed' in stderr - try: - shutil.rmtree(path, ignore_errors=True) - os.makedirs(path) - #print 'made dirs %s' % jn(path) - except OSError: - raise + +def test_clone_wrong_credentials_git(): + clone_url = _construct_url(GIT_REPO, passwd='bad!') + stdout, stderr = Command('/tmp').execute('git clone', clone_url) + assert 'fatal: Authentication failed' in stderr - print '\tchecking if anonymous access is enabled' - anonymous_access = get_anonymous_access() - if not anonymous_access: - print '\tnot enabled, enabling it ' - set_anonymous_access(enable=True) +def test_clone_git_dir_as_hg(): + clone_url = _construct_url(GIT_REPO) + stdout, stderr = Command('/tmp').execute('hg clone', clone_url) + assert 'HTTP Error 404: Not Found' in stderr + - clone_url = 'http://%(host)s/%(cloned_repo)s %(dest)s' % \ - {'user':USER, - 'pass':PASS, - 'host':HOST, - 'cloned_repo':HG_REPO, - 'dest':path} +def test_clone_hg_repo_as_git(): + clone_url = _construct_url(HG_REPO) + stdout, stderr = Command('/tmp').execute('git clone', clone_url) + assert 'not found: did you run git update-server-info on the server' in stderr - stdout, stderr = Command(cwd).execute('hg clone', clone_url) - assert """adding file changes""" in stdout, 'no messages about cloning' - assert """abort""" not in stderr , 'got error from clone' - - #disable if it was enabled - if not anonymous_access: - print '\tdisabling anonymous access' - set_anonymous_access(enable=False) +def test_clone_non_existing_path_hg(): + clone_url = _construct_url('trololo') + stdout, stderr = Command('/tmp').execute('hg clone', clone_url) + assert 'HTTP Error 404: Not Found' in stderr -@test_wrapp -def test_clone_wrong_credentials(): - cwd = path = jn(TESTS_TMP_PATH, HG_REPO) - - try: - shutil.rmtree(path, ignore_errors=True) - os.makedirs(path) - #print 'made dirs %s' % jn(path) - except OSError: - raise - - print '\tchecking if anonymous access is enabled' - anonymous_access = get_anonymous_access() - if anonymous_access: - print '\tenabled, disabling it ' - set_anonymous_access(enable=False) - - clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s %(dest)s' % \ - {'user':USER + 'error', - 'pass':PASS, - 'host':HOST, - 'cloned_repo':HG_REPO, - 'dest':path} - - stdout, stderr = Command(cwd).execute('hg clone', clone_url) - - if not """abort: authorization failed""" in stderr: - raise Exception('Failure') +def test_clone_non_existing_path_git(): + clone_url = _construct_url('trololo') + stdout, stderr = Command('/tmp').execute('git clone', clone_url) + assert 'not found: did you run git update-server-info on the server' in stderr -@test_wrapp -def test_pull(): - pass - - -@test_wrapp -def test_push_modify_file(f_name='setup.py'): - cwd = path = jn(TESTS_TMP_PATH, HG_REPO) - modified_file = jn(TESTS_TMP_PATH, HG_REPO, f_name) - for i in xrange(5): - cmd = """echo 'added_line%s' >> %s""" % (i, modified_file) - Command(cwd).execute(cmd) +def test_push_new_file_hg(): + DEST = _get_tmp_dir() + clone_url = _construct_url(HG_REPO, dest=DEST) + stdout, stderr = Command('/tmp').execute('hg clone', clone_url) - cmd = """hg ci -m 'changed file %s' %s """ % (i, modified_file) - Command(cwd).execute(cmd) - - Command(cwd).execute('hg push %s' % jn(TESTS_TMP_PATH, HG_REPO)) - - -@test_wrapp -def test_push_new_file(commits=15, with_clone=True): - - if with_clone: - test_clone_with_credentials(no_errors=True) - - cwd = path = jn(TESTS_TMP_PATH, HG_REPO) + # commit some stuff into this repo + cwd = path = jn(DEST) added_file = jn(path, '%ssetupążźć.py' % _RandomNameSequence().next()) - Command(cwd).execute('touch %s' % added_file) - Command(cwd).execute('hg add %s' % added_file) - for i in xrange(commits): + for i in xrange(3): cmd = """echo 'added_line%s' >> %s""" % (i, added_file) Command(cwd).execute(cmd) - cmd = """hg ci -m 'commited new %s' -u '%s' %s """ % (i, - 'Marcin Kuźminski <marcin@python-blog.com>', - added_file) + cmd = """hg ci -m 'commited new %s' -u '%s' %s """ % ( + i, + 'Marcin Kuźminski <marcin@python-blog.com>', + added_file + ) Command(cwd).execute(cmd) + # PUSH it back + clone_url = _construct_url(HG_REPO, dest='') + stdout, stderr = Command(cwd).execute('hg push --verbose', clone_url) - push_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s' % \ - {'user':USER, - 'pass':PASS, - 'host':HOST, - 'cloned_repo':HG_REPO, - 'dest':jn(TESTS_TMP_PATH, HG_REPO)} - - Command(cwd).execute('hg push --verbose --debug %s' % push_url) + assert 'pushing to' in stdout + assert 'Repository size' in stdout + assert 'Last revision is now' in stdout -@test_wrapp -def test_push_wrong_credentials(): - cwd = path = jn(TESTS_TMP_PATH, HG_REPO) - clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s' % \ - {'user':USER + 'xxx', - 'pass':PASS, - 'host':HOST, - 'cloned_repo':HG_REPO, - 'dest':jn(TESTS_TMP_PATH, HG_REPO)} - - modified_file = jn(TESTS_TMP_PATH, HG_REPO, 'setup.py') - for i in xrange(5): - cmd = """echo 'added_line%s' >> %s""" % (i, modified_file) - Command(cwd).execute(cmd) - - cmd = """hg ci -m 'commited %s' %s """ % (i, modified_file) - Command(cwd).execute(cmd) +def test_push_new_file_git(): + DEST = _get_tmp_dir() + clone_url = _construct_url(GIT_REPO, dest=DEST) + stdout, stderr = Command('/tmp').execute('git clone', clone_url) - Command(cwd).execute('hg push %s' % clone_url) - - -@test_wrapp -def test_push_wrong_path(): - cwd = path = jn(TESTS_TMP_PATH, HG_REPO) - added_file = jn(path, 'somefile.py') + # commit some stuff into this repo + cwd = path = jn(DEST) + added_file = jn(path, '%ssetupążźć.py' % _RandomNameSequence().next()) + Command(cwd).execute('touch %s' % added_file) + Command(cwd).execute('git add %s' % added_file) - try: - shutil.rmtree(path, ignore_errors=True) - os.makedirs(path) - print '\tmade dirs %s' % jn(path) - except OSError: - raise - - Command(cwd).execute("""echo '' > %s""" % added_file) - Command(cwd).execute("""hg init %s""" % path) - Command(cwd).execute("""hg add %s""" % added_file) - - for i in xrange(2): + for i in xrange(3): cmd = """echo 'added_line%s' >> %s""" % (i, added_file) Command(cwd).execute(cmd) - cmd = """hg ci -m 'commited new %s' %s """ % (i, added_file) + cmd = """git ci -m 'commited new %s' --author '%s' %s """ % ( + i, + 'Marcin Kuźminski <marcin@python-blog.com>', + added_file + ) Command(cwd).execute(cmd) + # PUSH it back + clone_url = _construct_url(GIT_REPO, dest='') + stdout, stderr = Command(cwd).execute('git push --verbose', clone_url) - clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s' % \ - {'user':USER, - 'pass':PASS, - 'host':HOST, - 'cloned_repo':HG_REPO + '_error', - 'dest':jn(TESTS_TMP_PATH, HG_REPO)} - - stdout, stderr = Command(cwd).execute('hg push %s' % clone_url) - if not """abort: HTTP Error 403: Forbidden""" in stderr: - raise Exception('Failure') + #WTF git stderr ?! + assert 'master -> master' in stderr -@test_wrapp -def get_logs(): - return UserLog.query().all() +def test_push_modify_existing_file_hg(): + assert 0 -@test_wrapp -def test_logs(initial): - logs = UserLog.query().all() - operations = 4 - if len(initial) + operations != len(logs): - raise Exception("missing number of logs initial:%s vs current:%s" % \ - (len(initial), len(logs))) +def test_push_modify_existing_file_git(): + assert 0 + + +def test_push_wrong_credentials_hg(): + assert 0 -if __name__ == '__main__': - create_test_user(force=False) - create_test_repo() +def test_push_wrong_credentials_git(): + assert 0 + + +def test_push_back_to_wrong_url_hg(): + assert 0 - initial_logs = get_logs() - print 'initial activity logs: %s' % len(initial_logs) - s = time.time() - #test_push_modify_file() - test_clone_with_credentials() - test_clone_wrong_credentials() - test_push_new_file(commits=2, with_clone=True) - - test_clone_anonymous() - test_push_wrong_path() +def test_push_back_to_wrong_url_git(): + assert 0 - test_push_wrong_credentials() - test_logs(initial_logs) - print 'finished ok in %.3f' % (time.time() - s) +#TODO: write all locking tests
--- a/rhodecode/tests/test_validators.py Fri Aug 24 10:37:17 2012 +0200 +++ b/rhodecode/tests/test_validators.py Fri Aug 24 14:58:26 2012 +0200 @@ -10,6 +10,9 @@ from rhodecode.model.meta import Session from rhodecode.model.repos_group import ReposGroupModel from rhodecode.config.routing import ADMIN_PREFIX +from rhodecode.model.db import ChangesetStatus +from rhodecode.model.changeset_status import ChangesetStatusModel +from rhodecode.model.comment import ChangesetCommentsModel class TestReposGroups(unittest.TestCase): @@ -222,3 +225,22 @@ def test_AttrLoginValidator(self): validator = v.AttrLoginValidator() self.assertRaises(formencode.Invalid, validator.to_python, 123) + + def test_NotReviewedRevisions(self): + validator = v.NotReviewedRevisions() + rev = '0' * 40 + # add status for a rev, that should throw an error because it is already + # reviewed + new_status = ChangesetStatus() + new_status.author = ChangesetStatusModel()._get_user(TEST_USER_ADMIN_LOGIN) + new_status.repo = ChangesetStatusModel()._get_repo(HG_REPO) + new_status.status = ChangesetStatus.STATUS_APPROVED + new_status.comment = None + new_status.revision = rev + Session().add(new_status) + Session().commit() + try: + self.assertRaises(formencode.Invalid, validator.to_python, [rev]) + finally: + Session().delete(new_status) + Session().commit()