diff --git a/docs/changelog.rst b/docs/changelog.rst old mode 100644 new mode 100755 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -4,6 +4,39 @@ Changelog ========= +1.3.5 (**2012-05-10**) +---------------------- + +news +++++ + +- use ext_json for json module +- unified annotation view with file source view +- notification improvements, better inbox + css +- #419 don't strip passwords for login forms, make rhodecode + more compatible with LDAP servers +- Added HTTP_X_FORWARDED_FOR as another method of extracting + IP for pull/push logs. - moved all to base controller +- #415: Adding comment to changeset causes reload. + Comments are now added via ajax and doesn't reload the page +- #374 LDAP config is discarded when LDAP can't be activated +- limited push/pull operations are now logged for git in the journal +- bumped mercurial to 2.2.X series +- added support for displaying submodules in file-browser +- #421 added bookmarks in changelog view + +fixes ++++++ + +- fixed dev-version marker for stable when served from source codes +- fixed missing permission checks on show forks page +- #418 cast to unicode fixes in notification objects +- #426 fixed mention extracting regex +- fixed remote-pulling for git remotes remopositories +- fixed #434: Error when accessing files or changesets of a git repository + with submodules +- fixed issue with empty APIKEYS for users after registration ref. #438 +- fixed issue with getting README files from git repositories 1.3.4 (**2012-03-28**) ---------------------- diff --git a/docs/conf.py b/docs/conf.py --- a/docs/conf.py +++ b/docs/conf.py @@ -11,7 +11,9 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os +import sys +import os +import datetime # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the @@ -25,7 +27,9 @@ sys.path.insert(0, os.path.abspath('..') # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.todo', 'sphinx.ext.viewcode'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', + 'sphinx.ext.intersphinx', 'sphinx.ext.todo', + 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -41,7 +45,7 @@ master_doc = 'index' # General information about the project. project = u'RhodeCode' -copyright = u'2010, Marcin Kuzminski' +copyright = u'%s, Marcin Kuzminski' % (datetime.datetime.now().year) # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -26,7 +26,7 @@ import sys import platform -VERSION = (1, 3, 4) +VERSION = (1, 3, 5) try: from rhodecode.lib import get_current_revision @@ -46,19 +46,22 @@ __py_version__ = sys.version_info PLATFORM_WIN = ('Windows') PLATFORM_OTHERS = ('Linux', 'Darwin', 'FreeBSD', 'OpenBSD', 'SunOS') +is_windows = __platform__ in PLATFORM_WIN +is_unix = __platform__ in PLATFORM_OTHERS + requirements = [ "Pylons==1.0.0", "Beaker==1.6.3", "WebHelpers==1.3", "formencode==1.2.4", "SQLAlchemy==0.7.6", - "Mako==0.6.2", + "Mako==0.7.0", "pygments>=1.4", - "whoosh>=2.3.0,<2.4", + "whoosh>=2.4.0,<2.5", "celery>=2.2.5,<2.3", "babel", "python-dateutil>=1.5.0,<2.0.0", - "dulwich>=0.8.4,<0.9.0", + "dulwich>=0.8.5,<0.9.0", "webob==1.0.8", "markdown==2.1.1", "docutils==0.8.1", @@ -68,11 +71,11 @@ if __py_version__ < (2, 6): requirements.append("simplejson") requirements.append("pysqlite") -if __platform__ in PLATFORM_WIN: - requirements.append("mercurial>=2.1,<2.2") +if is_windows: + requirements.append("mercurial>=2.2.1,<2.3") else: requirements.append("py-bcrypt") - requirements.append("mercurial>=2.1,<2.2") + requirements.append("mercurial>=2.2.1,<2.3") def get_version(): diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -454,8 +454,8 @@ def make_map(config): rmap.connect('files_annotate_home', '/{repo_name:.*}/annotate/{revision}/{f_path:.*}', - controller='files', action='annotate', revision='tip', - f_path='', conditions=dict(function=check_repo)) + controller='files', action='index', revision='tip', + f_path='', annotate=True, conditions=dict(function=check_repo)) rmap.connect('files_edit_home', '/{repo_name:.*}/edit/{revision}/{f_path:.*}', diff --git a/rhodecode/controllers/admin/ldap_settings.py b/rhodecode/controllers/admin/ldap_settings.py --- a/rhodecode/controllers/admin/ldap_settings.py +++ b/rhodecode/controllers/admin/ldap_settings.py @@ -100,25 +100,37 @@ class LdapSettingsController(BaseControl _form = LdapSettingsForm([x[0] for x in self.tls_reqcert_choices], [x[0] for x in self.search_scope_choices], [x[0] for x in self.tls_kind_choices])() + # check the ldap lib + ldap_active = False + try: + import ldap + ldap_active = True + except ImportError: + pass try: form_result = _form.to_python(dict(request.POST)) + try: for k, v in form_result.items(): if k.startswith('ldap_'): + if k == 'ldap_active': + v = ldap_active setting = RhodeCodeSetting.get_by_name(k) setting.app_settings_value = v self.sa.add(setting) self.sa.commit() h.flash(_('Ldap settings updated successfully'), - category='success') + category='success') + if not ldap_active: + #if ldap is missing send an info to user + h.flash(_('Unable to activate ldap. The "python-ldap" library ' + 'is missing.'), category='warning') + except (DatabaseError,): raise - except LdapImportError: - h.flash(_('Unable to activate ldap. The "python-ldap" library ' - 'is missing.'), category='warning') except formencode.Invalid, errors: e = errors.error_dict or {} diff --git a/rhodecode/controllers/admin/notifications.py b/rhodecode/controllers/admin/notifications.py --- a/rhodecode/controllers/admin/notifications.py +++ b/rhodecode/controllers/admin/notifications.py @@ -30,6 +30,8 @@ from pylons import request from pylons import tmpl_context as c, url from pylons.controllers.util import redirect +from webhelpers.paginate import Page + from rhodecode.lib.base import BaseController, render from rhodecode.model.db import Notification @@ -58,8 +60,9 @@ class NotificationsController(BaseContro """GET /_admin/notifications: All items in the collection""" # url('notifications') c.user = self.rhodecode_user - c.notifications = NotificationModel()\ - .get_for_user(self.rhodecode_user.user_id) + notif = NotificationModel().get_for_user(self.rhodecode_user.user_id) + p = int(request.params.get('page', 1)) + c.notifications = Page(notif, page=p, items_per_page=10) return render('admin/notifications/notifications.html') def mark_all_read(self): @@ -69,7 +72,8 @@ class NotificationsController(BaseContro nm.mark_all_read_for_user(self.rhodecode_user.user_id) Session.commit() c.user = self.rhodecode_user - c.notifications = nm.get_for_user(self.rhodecode_user.user_id) + notif = nm.get_for_user(self.rhodecode_user.user_id) + c.notifications = Page(notif, page=1, items_per_page=10) return render('admin/notifications/notifications_data.html') def create(self): diff --git a/rhodecode/controllers/admin/settings.py b/rhodecode/controllers/admin/settings.py --- a/rhodecode/controllers/admin/settings.py +++ b/rhodecode/controllers/admin/settings.py @@ -26,6 +26,8 @@ import logging import traceback import formencode +import pkg_resources +import platform from sqlalchemy import func from formencode import htmlfill @@ -64,6 +66,11 @@ class SettingsController(BaseController) def __before__(self): c.admin_user = session.get('admin_user') c.admin_username = session.get('admin_username') + c.modules = sorted([(p.project_name, p.version) + for p in pkg_resources.working_set], + key=lambda k: k[0].lower()) + c.py_version = platform.python_version() + c.platform = platform.platform() super(SettingsController, self).__before__() @HasPermissionAllDecorator('hg.admin') @@ -73,6 +80,7 @@ class SettingsController(BaseController) defaults = RhodeCodeSetting.get_app_settings() defaults.update(self.get_hg_ui_settings()) + return htmlfill.render( render('admin/settings/settings.html'), defaults=defaults, diff --git a/rhodecode/controllers/changelog.py b/rhodecode/controllers/changelog.py --- a/rhodecode/controllers/changelog.py +++ b/rhodecode/controllers/changelog.py @@ -125,7 +125,8 @@ class ChangelogController(BaseRepoContro data.append(['', vtx, edges]) elif repo.alias == 'hg': - c.dag = graphmod.colored(graphmod.dagwalker(repo._repo, revs)) + dag = graphmod.dagwalker(repo._repo, revs) + c.dag = graphmod.colored(dag, repo._repo) for (id, type, ctx, vtx, edges) in c.dag: if type != graphmod.CHANGESET: continue diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py --- a/rhodecode/controllers/changeset.py +++ b/rhodecode/controllers/changeset.py @@ -359,16 +359,31 @@ class ChangesetController(BaseRepoContro return render('changeset/raw_changeset.html') + @jsonify def comment(self, repo_name, revision): - ChangesetCommentsModel().create(text=request.POST.get('text'), - repo_id=c.rhodecode_db_repo.repo_id, - user_id=c.rhodecode_user.user_id, - revision=revision, - f_path=request.POST.get('f_path'), - line_no=request.POST.get('line')) + comm = ChangesetCommentsModel().create( + text=request.POST.get('text'), + repo_id=c.rhodecode_db_repo.repo_id, + user_id=c.rhodecode_user.user_id, + revision=revision, + f_path=request.POST.get('f_path'), + line_no=request.POST.get('line') + ) Session.commit() - return redirect(h.url('changeset_home', repo_name=repo_name, - revision=revision)) + if not request.environ.get('HTTP_X_PARTIAL_XHR'): + return redirect(h.url('changeset_home', repo_name=repo_name, + revision=revision)) + + data = { + 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))), + } + if comm: + c.co = comm + data.update(comm.get_dict()) + data.update({'rendered_text': + render('changeset/changeset_comment_block.html')}) + + return data @jsonify def delete_comment(self, repo_name, comment_id): diff --git a/rhodecode/controllers/files.py b/rhodecode/controllers/files.py --- a/rhodecode/controllers/files.py +++ b/rhodecode/controllers/files.py @@ -48,6 +48,7 @@ from rhodecode.lib.vcs.nodes import File from rhodecode.model.repo import RepoModel from rhodecode.model.scm import ScmModel +from rhodecode.model.db import Repository from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\ _context_url, get_line_ctx, get_ignore_ws @@ -112,7 +113,7 @@ class FilesController(BaseRepoController @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', 'repository.admin') - def index(self, repo_name, revision, f_path): + def index(self, repo_name, revision, f_path, annotate=False): # redirect to given revision from form if given post_revision = request.POST.get('at_rev', None) if post_revision: @@ -123,7 +124,7 @@ class FilesController(BaseRepoController c.changeset = self.__get_cs_or_redirect(revision, repo_name) c.branch = request.GET.get('branch', None) c.f_path = f_path - + c.annotate = annotate cur_rev = c.changeset.revision # prev link @@ -168,7 +169,7 @@ class FilesController(BaseRepoController file_node = self.__get_filenode_or_redirect(repo_name, cs, f_path) response.content_disposition = 'attachment; filename=%s' % \ - safe_str(f_path.split(os.sep)[-1]) + safe_str(f_path.split(Repository.url_sep())[-1]) response.content_type = file_node.mimetype return file_node.content @@ -219,16 +220,6 @@ class FilesController(BaseRepoController response.content_type = mimetype return file_node.content - @HasRepoPermissionAnyDecorator('repository.read', 'repository.write', - 'repository.admin') - def annotate(self, repo_name, revision, f_path): - c.cs = self.__get_cs_or_redirect(revision, repo_name) - c.file = self.__get_filenode_or_redirect(repo_name, c.cs, f_path) - - c.file_history = self._get_node_history(c.cs, f_path) - c.f_path = f_path - return render('files/files_annotate.html') - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') def edit(self, repo_name, revision, f_path): r_post = request.POST @@ -316,10 +307,10 @@ class FilesController(BaseRepoController try: self.scm_model.create_node(repo=c.rhodecode_repo, - repo_name=repo_name, cs=c.cs, - user=self.rhodecode_user, - author=author, message=message, - content=content, f_path=node_path) + repo_name=repo_name, cs=c.cs, + user=self.rhodecode_user, + author=author, message=message, + content=content, f_path=node_path) h.flash(_('Successfully committed to %s' % node_path), category='success') except NodeAlreadyExistsError, e: diff --git a/rhodecode/controllers/forks.py b/rhodecode/controllers/forks.py --- a/rhodecode/controllers/forks.py +++ b/rhodecode/controllers/forks.py @@ -35,7 +35,7 @@ import rhodecode.lib.helpers as h from rhodecode.lib.helpers import Page from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \ - NotAnonymous + NotAnonymous, HasRepoPermissionAny from rhodecode.lib.base import BaseRepoController, render from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User from rhodecode.model.repo import RepoModel @@ -103,7 +103,13 @@ class ForksController(BaseRepoController def forks(self, repo_name): p = int(request.params.get('page', 1)) repo_id = c.rhodecode_db_repo.repo_id - d = Repository.get_repo_forks(repo_id) + d = [] + for r in Repository.get_repo_forks(repo_id): + if not HasRepoPermissionAny( + 'repository.read', 'repository.write', 'repository.admin' + )(r.repo_name, 'get forks check'): + continue + d.append(r) c.forks_pager = Page(d, page=p, items_per_page=20) c.forks_data = render('/forks/forks_data.html') diff --git a/rhodecode/controllers/summary.py b/rhodecode/controllers/summary.py --- a/rhodecode/controllers/summary.py +++ b/rhodecode/controllers/summary.py @@ -179,10 +179,12 @@ class SummaryController(BaseRepoControll if c.enable_downloads: c.download_options = self._get_download_links(c.rhodecode_repo) - c.readme_data, c.readme_file = self.__get_readme_data(c.rhodecode_db_repo) + c.readme_data, c.readme_file = self.__get_readme_data( + c.rhodecode_db_repo.repo_name, c.rhodecode_repo + ) return render('summary/summary.html') - def __get_readme_data(self, repo): + def __get_readme_data(self, repo_name, repo): @cache_region('long_term') def _get_readme_from_cache(key): @@ -190,7 +192,7 @@ class SummaryController(BaseRepoControll readme_file = None log.debug('Fetching readme file') try: - cs = repo.get_changeset('tip') + cs = repo.get_changeset() # fetches TIP renderer = MarkupRenderer() for f in README_FILES: try: @@ -202,6 +204,7 @@ class SummaryController(BaseRepoControll except NodeDoesNotExistError: continue except ChangesetError: + log.error(traceback.format_exc()) pass except EmptyRepositoryError: pass @@ -210,7 +213,7 @@ class SummaryController(BaseRepoControll return readme_data, readme_file - key = repo.repo_name + '_README' + key = repo_name + '_README' inv = CacheInvalidation.invalidate(key) if inv is not None: region_invalidate(_get_readme_from_cache, None, key) diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -116,6 +116,17 @@ class BaseVCSController(object): return True + def _get_ip_addr(self, environ): + proxy_key = 'HTTP_X_REAL_IP' + proxy_key2 = 'HTTP_X_FORWARDED_FOR' + def_key = 'REMOTE_ADDR' + + return environ.get(proxy_key2, + environ.get(proxy_key, + environ.get(def_key, '0.0.0.0') + ) + ) + def __call__(self, environ, start_response): start = time.time() try: diff --git a/rhodecode/lib/celerylib/tasks.py b/rhodecode/lib/celerylib/tasks.py --- a/rhodecode/lib/celerylib/tasks.py +++ b/rhodecode/lib/celerylib/tasks.py @@ -47,6 +47,7 @@ from rhodecode.lib.helpers import person from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer from rhodecode.lib.utils import add_cache, action_logger from rhodecode.lib.compat import json, OrderedDict +from rhodecode.lib.hooks import log_create_repository from rhodecode.model.db import Statistics, Repository, User @@ -372,7 +373,8 @@ def create_repo_fork(form_data, cur_user base_path = Repository.base_path() - RepoModel(DBS).create(form_data, cur_user, just_db=True, fork=True) + fork_repo = RepoModel(DBS).create(form_data, cur_user, + just_db=True, fork=True) alias = form_data['repo_type'] org_repo_name = form_data['org_path'] @@ -387,6 +389,8 @@ def create_repo_fork(form_data, cur_user backend(safe_str(destination_fork_path), create=True, src_url=safe_str(source_repo_path), update_after_clone=update_after_clone) + log_create_repository(fork_repo.get_dict(), created_by=cur_user.username) + action_logger(cur_user, 'user_forked_repo:%s' % fork_name, org_repo_name, '', DBS) @@ -395,6 +399,7 @@ def create_repo_fork(form_data, cur_user # finally commit at latest possible stage DBS.commit() + def __get_codes_stats(repo_name): from rhodecode.config.conf import LANGUAGES_EXTENSIONS_MAP repo = Repository.get_by_repo_name(repo_name).scm_instance diff --git a/rhodecode/lib/compat.py b/rhodecode/lib/compat.py --- a/rhodecode/lib/compat.py +++ b/rhodecode/lib/compat.py @@ -25,92 +25,12 @@ # along with this program. If not, see . import os -import datetime -import functools -import decimal from rhodecode import __platform__, PLATFORM_WIN #============================================================================== # json #============================================================================== - - -def _is_aware(value): - """ - Determines if a given datetime.time is aware. - - The logic is described in Python's docs: - http://docs.python.org/library/datetime.html#datetime.tzinfo - """ - return (value.tzinfo is not None - and value.tzinfo.utcoffset(value) is not None) - - -def _obj_dump(obj): - """ - Custom function for dumping objects to JSON, if obj has __json__ attribute - or method defined it will be used for serialization - - :param obj: - """ - - if isinstance(obj, complex): - return [obj.real, obj.imag] - # See "Date Time String Format" in the ECMA-262 specification. - # some code borrowed from django 1.4 - elif isinstance(obj, datetime.datetime): - r = obj.isoformat() - if obj.microsecond: - r = r[:23] + r[26:] - if r.endswith('+00:00'): - r = r[:-6] + 'Z' - return r - elif isinstance(obj, datetime.date): - return obj.isoformat() - elif isinstance(obj, decimal.Decimal): - return str(obj) - elif isinstance(obj, datetime.time): - if _is_aware(obj): - raise ValueError("JSON can't represent timezone-aware times.") - r = obj.isoformat() - if obj.microsecond: - r = r[:12] - return r - elif isinstance(obj, set): - return list(obj) - elif isinstance(obj, OrderedDict): - return obj.as_dict() - elif hasattr(obj, '__json__'): - if callable(obj.__json__): - return obj.__json__() - else: - return obj.__json__ - else: - raise NotImplementedError - -try: - import json - - # extended JSON encoder for json - class ExtendedEncoder(json.JSONEncoder): - def default(self, obj): - try: - return _obj_dump(obj) - except NotImplementedError: - pass - return json.JSONEncoder.default(self, obj) - # monkey-patch JSON encoder to use extended version - json.dumps = functools.partial(json.dumps, cls=ExtendedEncoder) -except ImportError: - import simplejson as json - - def extended_encode(obj): - try: - return _obj_dump(obj) - except NotImplementedError: - pass - raise TypeError("%r is not JSON serializable" % (obj,)) - json.dumps = functools.partial(json.dumps, default=extended_encode) +from rhodecode.lib.ext_json import json #============================================================================== diff --git a/rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py b/rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py --- a/rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py +++ b/rhodecode/lib/dbmigrate/versions/003_version_1_2_0.py @@ -71,7 +71,6 @@ def upgrade(migrate_engine): is_ldap = Column("is_ldap", Boolean(), nullable=False, unique=None, default=False) is_ldap.drop(User().__table__) - #========================================================================== # Upgrade of `repositories` table #========================================================================== @@ -100,7 +99,6 @@ def upgrade(migrate_engine): group_id.create(Repository().__table__) - #========================================================================== # Upgrade of `user_followings` table #========================================================================== diff --git a/rhodecode/lib/diffs.py b/rhodecode/lib/diffs.py --- a/rhodecode/lib/diffs.py +++ b/rhodecode/lib/diffs.py @@ -33,8 +33,8 @@ from itertools import tee, imap from pylons.i18n.translation import _ from rhodecode.lib.vcs.exceptions import VCSError -from rhodecode.lib.vcs.nodes import FileNode - +from rhodecode.lib.vcs.nodes import FileNode, SubModuleNode +from rhodecode.lib.helpers import escape from rhodecode.lib.utils import EmptyChangeset @@ -79,9 +79,13 @@ def wrapped_diff(filenode_old, filenode_ 'diff menu to display this diff')) stats = (0, 0) size = 0 - if not diff: - diff = wrap_to_table(_('No changes detected')) + submodules = filter(lambda o: isinstance(o, SubModuleNode), + [filenode_new, filenode_old]) + if submodules: + diff = wrap_to_table(escape('Submodule %r' % submodules[0])) + else: + diff = wrap_to_table(_('No changes detected')) cs1 = filenode_old.changeset.raw_id cs2 = filenode_new.changeset.raw_id @@ -97,6 +101,10 @@ def get_gitdiff(filenode_old, filenode_n """ # make sure we pass in default context context = context or 3 + submodules = filter(lambda o: isinstance(o, SubModuleNode), + [filenode_new, filenode_old]) + if submodules: + return '' for filenode in (filenode_old, filenode_new): if not isinstance(filenode, FileNode): @@ -109,7 +117,6 @@ def get_gitdiff(filenode_old, filenode_n vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path, ignore_whitespace, context) - return vcs_gitdiff diff --git a/rhodecode/lib/ext_json.py b/rhodecode/lib/ext_json.py new file mode 100644 --- /dev/null +++ b/rhodecode/lib/ext_json.py @@ -0,0 +1,104 @@ +import datetime +import functools +import decimal + +__all__ = ['json', 'simplejson', 'stdjson'] + + +def _is_aware(value): + """ + Determines if a given datetime.time is aware. + + The logic is described in Python's docs: + http://docs.python.org/library/datetime.html#datetime.tzinfo + """ + return (value.tzinfo is not None + and value.tzinfo.utcoffset(value) is not None) + + +def _obj_dump(obj): + """ + Custom function for dumping objects to JSON, if obj has __json__ attribute + or method defined it will be used for serialization + + :param obj: + """ + + if isinstance(obj, complex): + return [obj.real, obj.imag] + # See "Date Time String Format" in the ECMA-262 specification. + # some code borrowed from django 1.4 + elif isinstance(obj, datetime.datetime): + r = obj.isoformat() + if obj.microsecond: + r = r[:23] + r[26:] + if r.endswith('+00:00'): + r = r[:-6] + 'Z' + return r + elif isinstance(obj, datetime.date): + return obj.isoformat() + elif isinstance(obj, decimal.Decimal): + return str(obj) + elif isinstance(obj, datetime.time): + if _is_aware(obj): + raise ValueError("JSON can't represent timezone-aware times.") + r = obj.isoformat() + if obj.microsecond: + r = r[:12] + return r + elif isinstance(obj, set): + return list(obj) + elif hasattr(obj, '__json__'): + if callable(obj.__json__): + return obj.__json__() + else: + return obj.__json__ + else: + raise NotImplementedError + + +# Import simplejson +try: + # import simplejson initially + import simplejson as _sj + + def extended_encode(obj): + try: + return _obj_dump(obj) + except NotImplementedError: + pass + raise TypeError("%r is not JSON serializable" % (obj,)) + # we handle decimals our own it makes unified behavior of json vs + # simplejson + _sj.dumps = functools.partial(_sj.dumps, default=extended_encode, + use_decimal=False) + _sj.dump = functools.partial(_sj.dump, default=extended_encode, + use_decimal=False) + simplejson = _sj + +except ImportError: + # no simplejson set it to None + _sj = None + + +# simplejson not found try out regular json module +import json as _json + + +# extended JSON encoder for json +class ExtendedEncoder(_json.JSONEncoder): + def default(self, obj): + try: + return _obj_dump(obj) + except NotImplementedError: + pass + return _json.JSONEncoder.default(self, obj) +# monkey-patch JSON encoder to use extended version +_json.dumps = functools.partial(_json.dumps, cls=ExtendedEncoder) +_json.dump = functools.partial(_json.dump, cls=ExtendedEncoder) +stdlib = _json + +# set all available json modules +simplejson = _sj +stdjson = _json +json = _sj if _sj else _json diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -87,7 +87,7 @@ def get_token(): if not token_key in session: try: token = hashlib.sha1(str(random.getrandbits(128))).hexdigest() - except AttributeError: # Python < 2.4 + except AttributeError: # Python < 2.4 token = hashlib.sha1(str(random.randrange(2 ** 128))).hexdigest() session[token_key] = token if hasattr(session, 'save'): @@ -454,11 +454,14 @@ def action_parser(user_log, feed=False): revision=rev.raw_id), title=tooltip(message(rev)), class_='tooltip') ) - # get only max revs_top_limit of changeset for performance/ui reasons - revs = [ - x for x in repo.get_changesets(revs_ids[0], - revs_ids[:revs_top_limit][-1]) - ] + + revs = [] + if len(filter(lambda v: v != '', revs_ids)) > 0: + # get only max revs_top_limit of changeset for performance/ui reasons + revs = [ + x for x in repo.get_changesets(revs_ids[0], + revs_ids[:revs_top_limit][-1]) + ] cs_links = [] cs_links.append(" " + ', '.join( diff --git a/rhodecode/lib/hooks.py b/rhodecode/lib/hooks.py --- a/rhodecode/lib/hooks.py +++ b/rhodecode/lib/hooks.py @@ -33,6 +33,33 @@ from rhodecode.lib.utils import action_l from inspect import isfunction +def _get_scm_size(alias, root_path): + + if not alias.startswith('.'): + alias += '.' + + size_scm, size_root = 0, 0 + for path, dirs, files in os.walk(root_path): + if path.find(alias) != -1: + for f in files: + try: + size_scm += os.path.getsize(os.path.join(path, f)) + except OSError: + pass + else: + for f in files: + try: + size_root += os.path.getsize(os.path.join(path, f)) + except OSError: + pass + + size_scm_f = h.format_byte_size(size_scm) + size_root_f = h.format_byte_size(size_root) + size_total_f = h.format_byte_size(size_root + size_scm) + + return size_scm_f, size_root_f, size_total_f + + def repo_size(ui, repo, hooktype=None, **kwargs): """ Presents size of repository after push @@ -42,24 +69,7 @@ def repo_size(ui, repo, hooktype=None, * :param hooktype: """ - size_hg, size_root = 0, 0 - for path, dirs, files in os.walk(repo.root): - if path.find('.hg') != -1: - for f in files: - try: - size_hg += os.path.getsize(os.path.join(path, f)) - except OSError: - pass - else: - for f in files: - try: - size_root += os.path.getsize(os.path.join(path, f)) - except OSError: - pass - - size_hg_f = h.format_byte_size(size_hg) - size_root_f = h.format_byte_size(size_root) - size_total_f = h.format_byte_size(size_root + size_hg) + size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', repo.root) last_cs = repo[len(repo) - 1] @@ -82,6 +92,7 @@ def log_pull_action(ui, repo, **kwargs): extras = dict(repo.ui.configitems('rhodecode_extras')) username = extras['username'] repository = extras['repository'] + scm = extras['scm'] action = 'pull' action_logger(username, action, repository, extras['ip'], commit=True) @@ -100,28 +111,33 @@ def log_push_action(ui, repo, **kwargs): Maps user last push action to new changeset id, from mercurial :param ui: - :param repo: + :param repo: repo object containing the `ui` object """ extras = dict(repo.ui.configitems('rhodecode_extras')) username = extras['username'] repository = extras['repository'] action = extras['action'] + ':%s' - node = kwargs['node'] + scm = extras['scm'] - def get_revs(repo, rev_opt): - if rev_opt: - revs = revrange(repo, rev_opt) + if scm == 'hg': + node = kwargs['node'] + + def get_revs(repo, rev_opt): + if rev_opt: + revs = revrange(repo, rev_opt) - if len(revs) == 0: - return (nullrev, nullrev) - return (max(revs), min(revs)) - else: - return (len(repo) - 1, 0) + if len(revs) == 0: + return (nullrev, nullrev) + return (max(revs), min(revs)) + else: + return (len(repo) - 1, 0) - stop, start = get_revs(repo, [node + ':']) + stop, start = get_revs(repo, [node + ':']) - revs = (str(repo[r]) for r in xrange(start, stop + 1)) + revs = (str(repo[r]) for r in xrange(start, stop + 1)) + elif scm == 'git': + revs = [] action = action % ','.join(revs) diff --git a/rhodecode/lib/markup_renderer.py b/rhodecode/lib/markup_renderer.py --- a/rhodecode/lib/markup_renderer.py +++ b/rhodecode/lib/markup_renderer.py @@ -27,7 +27,7 @@ import re import logging -from rhodecode.lib.utils2 import safe_unicode +from rhodecode.lib.utils2 import safe_unicode, MENTIONS_REGEX log = logging.getLogger(__name__) @@ -128,10 +128,10 @@ class MarkupRenderer(object): @classmethod def rst_with_mentions(cls, source): - mention_pat = re.compile(r'(?:^@|\s@)(\w+)') + mention_pat = re.compile(MENTIONS_REGEX) def wrapp(match_obj): uname = match_obj.groups()[0] - return ' **@%(uname)s** ' % {'uname':uname} + return ' **@%(uname)s** ' % {'uname': uname} mention_hl = mention_pat.sub(wrapp, source).strip() return cls.rst(mention_hl) diff --git a/rhodecode/lib/middleware/simplegit.py b/rhodecode/lib/middleware/simplegit.py --- a/rhodecode/lib/middleware/simplegit.py +++ b/rhodecode/lib/middleware/simplegit.py @@ -44,13 +44,14 @@ class SimpleGitUploadPackHandler(dulserv graph_walker.determine_wants, graph_walker, self.progress, get_tagged=self.get_tagged) - # Do they want any objects? - if objects_iter is None or len(objects_iter) == 0: + # Did the process short-circuit (e.g. in a stateless RPC call)? Note + # that the client still expects a 0-object pack in most cases. + if objects_iter is None: return self.progress("counting objects: %d, done.\n" % len(objects_iter)) dulserver.write_pack_objects(dulserver.ProtocolFile(None, write), - objects_iter, len(objects_iter)) + objects_iter) messages = [] messages.append('thank you for using rhodecode') @@ -59,6 +60,7 @@ class SimpleGitUploadPackHandler(dulserv # we are done self.proto.write("0000") + dulserver.DEFAULT_HANDLERS = { 'git-upload-pack': SimpleGitUploadPackHandler, 'git-receive-pack': dulserver.ReceivePackHandler, @@ -72,7 +74,7 @@ from paste.httpheaders import REMOTE_USE from rhodecode.lib.utils2 import safe_str from rhodecode.lib.base import BaseVCSController from rhodecode.lib.auth import get_container_username -from rhodecode.lib.utils import is_valid_repo +from rhodecode.lib.utils import is_valid_repo, make_ui from rhodecode.model.db import User from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError @@ -99,10 +101,9 @@ class SimpleGit(BaseVCSController): if not is_git(environ): return self.application(environ, start_response) - proxy_key = 'HTTP_X_REAL_IP' - def_key = 'REMOTE_ADDR' - ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0')) + ipaddr = self._get_ip_addr(environ) username = None + self._git_first_op = False # skip passing error to error controller environ['pylons.status_code_redirect'] = True @@ -178,6 +179,13 @@ class SimpleGit(BaseVCSController): perm = self._check_permission(action, user, repo_name) if perm is not True: return HTTPForbidden()(environ, start_response) + extras = { + 'ip': ipaddr, + 'username': username, + 'action': action, + 'repository': repo_name, + 'scm': 'git', + } #=================================================================== # GIT REQUEST HANDLING @@ -185,10 +193,16 @@ class SimpleGit(BaseVCSController): repo_path = os.path.join(safe_str(self.basepath), safe_str(repo_name)) log.debug('Repository path is %s' % repo_path) + baseui = make_ui('db') + self.__inject_extras(repo_path, baseui, extras) + + try: - #invalidate cache on push + # invalidate cache on push if action == 'push': self._invalidate_cache(repo_name) + 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) return app(environ, start_response) @@ -249,3 +263,38 @@ class SimpleGit(BaseVCSController): # operation is pull/push op = getattr(self, '_git_stored_op', 'pull') return op + + def _handle_githooks(self, repo_name, action, baseui, environ): + from rhodecode.lib.hooks import log_pull_action, log_push_action + service = environ['QUERY_STRING'].split('=') + if len(service) < 2: + return + + from rhodecode.model.db import Repository + _repo = Repository.get_by_repo_name(repo_name) + _repo = _repo.scm_instance + _repo._repo.ui = baseui + + push_hook = 'pretxnchangegroup.push_logger' + pull_hook = 'preoutgoing.pull_logger' + _hooks = dict(baseui.configitems('hooks')) or {} + if action == 'push' and _hooks.get(push_hook): + log_push_action(ui=baseui, repo=_repo._repo) + elif action == 'pull' and _hooks.get(pull_hook): + log_pull_action(ui=baseui, repo=_repo._repo) + + def __inject_extras(self, repo_path, baseui, extras={}): + """ + Injects some extra params into baseui instance + + :param baseui: baseui instance + :param extras: dict with extra params to put into baseui + """ + + # make our hgweb quiet so it doesn't print output + baseui.setconfig('ui', 'quiet', 'true') + + #inject some additional parameters that will be available in ui + #for hooks + for k, v in extras.items(): + baseui.setconfig('rhodecode_extras', k, v) diff --git a/rhodecode/lib/middleware/simplehg.py b/rhodecode/lib/middleware/simplehg.py --- a/rhodecode/lib/middleware/simplehg.py +++ b/rhodecode/lib/middleware/simplehg.py @@ -69,9 +69,7 @@ class SimpleHg(BaseVCSController): if not is_mercurial(environ): return self.application(environ, start_response) - proxy_key = 'HTTP_X_REAL_IP' - def_key = 'REMOTE_ADDR' - ipaddr = environ.get(proxy_key, environ.get(def_key, '0.0.0.0')) + ipaddr = self._get_ip_addr(environ) # skip passing error to error controller environ['pylons.status_code_redirect'] = True @@ -155,7 +153,8 @@ class SimpleHg(BaseVCSController): 'ip': ipaddr, 'username': username, 'action': action, - 'repository': repo_name + 'repository': repo_name, + 'scm': 'hg', } #====================================================================== diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -150,7 +150,7 @@ def action_logger(user, action, repo, ip user_log = UserLog() user_log.user_id = user_obj.user_id - user_log.action = action + user_log.action = safe_unicode(action) user_log.repository_id = repo_obj.repo_id user_log.repository_name = repo_name diff --git a/rhodecode/lib/utils2.py b/rhodecode/lib/utils2.py --- a/rhodecode/lib/utils2.py +++ b/rhodecode/lib/utils2.py @@ -392,14 +392,17 @@ def get_changeset_safe(repo, rev): return cs +MENTIONS_REGEX = r'(?:^@|\s@)([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)(?:\s{1})' + + def extract_mentioned_users(s): """ Returns unique usernames from given string s that have @mention :param s: string to get mentions """ - usrs = {} - for username in re.findall(r'(?:^@|\s@)(\w+)', s): - usrs[username] = username + usrs = set() + for username in re.findall(MENTIONS_REGEX, s): + usrs.add(username) - return sorted(usrs.keys()) + return sorted(list(usrs), key=lambda k: k.lower()) diff --git a/rhodecode/lib/vcs/backends/base.py b/rhodecode/lib/vcs/backends/base.py --- a/rhodecode/lib/vcs/backends/base.py +++ b/rhodecode/lib/vcs/backends/base.py @@ -909,3 +909,48 @@ class BaseInMemoryChangeset(object): :raises ``CommitError``: if any error occurs while committing """ raise NotImplementedError + + +class EmptyChangeset(BaseChangeset): + """ + An dummy empty changeset. It's possible to pass hash when creating + an EmptyChangeset + """ + + def __init__(self, cs='0' * 40, repo=None, requested_revision=None, + alias=None): + self._empty_cs = cs + self.revision = -1 + self.message = '' + self.author = '' + self.date = '' + self.repository = repo + self.requested_revision = requested_revision + self.alias = alias + + @LazyProperty + def raw_id(self): + """ + Returns raw string identifying this changeset, useful for web + representation. + """ + + return self._empty_cs + + @LazyProperty + def branch(self): + from rhodecode.lib.vcs.backends import get_backend + return get_backend(self.alias).DEFAULT_BRANCH_NAME + + @LazyProperty + def short_id(self): + return self.raw_id[:12] + + def get_file_changeset(self, path): + return self + + def get_file_content(self, path): + return u'' + + def get_file_size(self, path): + return 0 diff --git a/rhodecode/lib/vcs/backends/git/changeset.py b/rhodecode/lib/vcs/backends/git/changeset.py --- a/rhodecode/lib/vcs/backends/git/changeset.py +++ b/rhodecode/lib/vcs/backends/git/changeset.py @@ -10,7 +10,8 @@ from rhodecode.lib.vcs.exceptions import from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError from rhodecode.lib.vcs.exceptions import ImproperArchiveTypeError from rhodecode.lib.vcs.backends.base import BaseChangeset -from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, RemovedFileNode +from rhodecode.lib.vcs.nodes import FileNode, DirNode, NodeKind, RootNode, \ + RemovedFileNode, SubModuleNode from rhodecode.lib.vcs.utils import safe_unicode from rhodecode.lib.vcs.utils import date_fromtimestamp from rhodecode.lib.vcs.utils.lazy import LazyProperty @@ -66,26 +67,12 @@ class GitChangeset(BaseChangeset): @LazyProperty def branch(self): - # TODO: Cache as we walk (id <-> branch name mapping) - refs = self.repository._repo.get_refs() - heads = {} - for key, val in refs.items(): - for ref_key in ['refs/heads/', 'refs/remotes/origin/']: - if key.startswith(ref_key): - n = key[len(ref_key):] - if n not in ['HEAD']: - heads[n] = val + + heads = self.repository._heads(reverse=False) - for name, id in heads.iteritems(): - walker = self.repository._repo.object_store.get_graph_walker([id]) - while True: - id_ = walker.next() - if not id_: - break - if id_ == self.id: - return safe_unicode(name) - raise ChangesetError("This should not happen... Have you manually " - "change id of the changeset?") + ref = heads.get(self.raw_id) + if ref: + return safe_unicode(ref) def _fix_path(self, path): """ @@ -144,7 +131,6 @@ class GitChangeset(BaseChangeset): name = item self._paths[name] = id self._stat_modes[name] = stat - if not path in self._paths: raise NodeDoesNotExistError("There is no file nor directory " "at the given path %r at revision %r" @@ -344,7 +330,13 @@ class GitChangeset(BaseChangeset): tree = self.repository._repo[id] dirnodes = [] filenodes = [] + als = self.repository.alias for name, stat, id in tree.iteritems(): + if objects.S_ISGITLINK(stat): + dirnodes.append(SubModuleNode(name, url=None, changeset=id, + alias=als)) + continue + obj = self.repository._repo.get_object(id) if path != '': obj_path = '/'.join((path, name)) @@ -372,24 +364,31 @@ class GitChangeset(BaseChangeset): path = self._fix_path(path) if not path in self.nodes: try: - id = self._get_id_for_path(path) + id_ = self._get_id_for_path(path) except ChangesetError: raise NodeDoesNotExistError("Cannot find one of parents' " "directories for a given path: %s" % path) - obj = self.repository._repo.get_object(id) - if isinstance(obj, objects.Tree): - if path == '': - node = RootNode(changeset=self) + + als = self.repository.alias + _GL = lambda m: m and objects.S_ISGITLINK(m) + if _GL(self._stat_modes.get(path)): + node = SubModuleNode(path, url=None, changeset=id_, alias=als) + else: + obj = self.repository._repo.get_object(id_) + + if isinstance(obj, objects.Tree): + if path == '': + node = RootNode(changeset=self) + else: + node = DirNode(path, changeset=self) + node._tree = obj + elif isinstance(obj, objects.Blob): + node = FileNode(path, changeset=self) + node._blob = obj else: - node = DirNode(path, changeset=self) - node._tree = obj - elif isinstance(obj, objects.Blob): - node = FileNode(path, changeset=self) - node._blob = obj - else: - raise NodeDoesNotExistError("There is no file nor directory " - "at the given path %r at revision %r" - % (path, self.short_id)) + raise NodeDoesNotExistError("There is no file nor directory " + "at the given path %r at revision %r" + % (path, self.short_id)) # cache node self.nodes[path] = node return self.nodes[path] @@ -406,7 +405,7 @@ class GitChangeset(BaseChangeset): def _diff_name_status(self): output = [] for parent in self.parents: - cmd = 'diff --name-status %s %s' % (parent.raw_id, self.raw_id) + cmd = 'diff --name-status %s %s --encoding=utf8' % (parent.raw_id, self.raw_id) so, se = self.repository.run_git_command(cmd) output.append(so.strip()) return '\n'.join(output) @@ -422,13 +421,15 @@ class GitChangeset(BaseChangeset): for line in self._diff_name_status.splitlines(): if not line: continue + if line.startswith(char): - splitted = line.split(char,1) + splitted = line.split(char, 1) if not len(splitted) == 2: raise VCSError("Couldn't parse diff result:\n%s\n\n and " "particularly that line: %s" % (self._diff_name_status, line)) - paths.add(splitted[1].strip()) + _path = splitted[1].strip() + paths.add(_path) return sorted(paths) @LazyProperty diff --git a/rhodecode/lib/vcs/backends/git/inmemory.py b/rhodecode/lib/vcs/backends/git/inmemory.py --- a/rhodecode/lib/vcs/backends/git/inmemory.py +++ b/rhodecode/lib/vcs/backends/git/inmemory.py @@ -5,12 +5,13 @@ from dulwich import objects from dulwich.repo import Repo from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset from rhodecode.lib.vcs.exceptions import RepositoryError +from rhodecode.lib.vcs.utils import safe_str class GitInMemoryChangeset(BaseInMemoryChangeset): def commit(self, message, author, parents=None, branch=None, date=None, - **kwargs): + **kwargs): """ Performs in-memory commit (doesn't check workdir in any way) and returns newly created ``Changeset``. Updates repository's @@ -120,9 +121,9 @@ class GitInMemoryChangeset(BaseInMemoryC commit = objects.Commit() commit.tree = commit_tree.id commit.parents = [p._commit.id for p in self.parents if p] - commit.author = commit.committer = author + commit.author = commit.committer = safe_str(author) commit.encoding = ENCODING - commit.message = message + ' ' + commit.message = safe_str(message) + ' ' # Compute date if date is None: diff --git a/rhodecode/lib/vcs/backends/git/repository.py b/rhodecode/lib/vcs/backends/git/repository.py --- a/rhodecode/lib/vcs/backends/git/repository.py +++ b/rhodecode/lib/vcs/backends/git/repository.py @@ -47,6 +47,15 @@ class GitRepository(BaseRepository): self.path = abspath(repo_path) self._repo = self._get_repo(create, src_url, update_after_clone, bare) + #temporary set that to now at later we will move it to constructor + baseui = None + if baseui is None: + from mercurial.ui import ui + baseui = ui() + # patch the instance of GitRepo with an "FAKE" ui object to add + # compatibility layer with Mercurial + setattr(self._repo, 'ui', baseui) + try: self.head = self._repo.head() except KeyError: @@ -78,11 +87,16 @@ class GitRepository(BaseRepository): :param cmd: git command to be executed """ - #cmd = '(cd %s && git %s)' % (self.path, cmd) + + _copts = ['-c', 'core.quotepath=false', ] + _str_cmd = False if isinstance(cmd, basestring): - cmd = 'git %s' % cmd - else: - cmd = ['git'] + cmd + cmd = [cmd] + _str_cmd = True + + cmd = ['GIT_CONFIG_NOGLOBAL=1', 'git'] + _copts + cmd + if _str_cmd: + cmd = ' '.join(cmd) try: opts = dict( shell=isinstance(cmd, basestring), @@ -245,6 +259,19 @@ class GitRepository(BaseRepository): if ref.startswith('refs/heads/') and not ref.endswith('/HEAD')] return OrderedDict(sorted(_branches, key=sortkey, reverse=False)) + def _heads(self, reverse=False): + refs = self._repo.get_refs() + heads = {} + + for key, val in refs.items(): + for ref_key in ['refs/heads/', 'refs/remotes/origin/']: + if key.startswith(ref_key): + n = key[len(ref_key):] + if n not in ['HEAD']: + heads[n] = val + + return heads if reverse else dict((y,x) for x,y in heads.iteritems()) + def _get_tags(self): if not self.revisions: return {} @@ -384,7 +411,7 @@ class GitRepository(BaseRepository): yield self.get_changeset(rev) def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False, - context=3): + context=3): """ Returns (git like) *diff*, as plain text. Shows changes introduced by ``rev2`` since ``rev1``. @@ -453,6 +480,18 @@ class GitRepository(BaseRepository): # If error occurs run_git_command raises RepositoryError already self.run_git_command(cmd) + def pull(self, url): + """ + Tries to pull changes from external location. + """ + url = self._get_url(url) + cmd = ['pull'] + cmd.append("--ff-only") + cmd.append(url) + cmd = ' '.join(cmd) + # If error occurs run_git_command raises RepositoryError already + self.run_git_command(cmd) + @LazyProperty def workdir(self): """ diff --git a/rhodecode/lib/vcs/backends/hg/changeset.py b/rhodecode/lib/vcs/backends/hg/changeset.py --- a/rhodecode/lib/vcs/backends/hg/changeset.py +++ b/rhodecode/lib/vcs/backends/hg/changeset.py @@ -5,8 +5,9 @@ from rhodecode.lib.vcs.backends.base imp from rhodecode.lib.vcs.conf import settings from rhodecode.lib.vcs.exceptions import ChangesetDoesNotExistError, \ ChangesetError, ImproperArchiveTypeError, NodeDoesNotExistError, VCSError -from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, ChangedFileNodesGenerator, \ - DirNode, FileNode, NodeKind, RemovedFileNodesGenerator, RootNode +from rhodecode.lib.vcs.nodes import AddedFileNodesGenerator, \ + ChangedFileNodesGenerator, DirNode, FileNode, NodeKind, \ + RemovedFileNodesGenerator, RootNode, SubModuleNode from rhodecode.lib.vcs.utils import safe_str, safe_unicode, date_fromtimestamp from rhodecode.lib.vcs.utils.lazy import LazyProperty @@ -36,6 +37,10 @@ class MercurialChangeset(BaseChangeset): return safe_unicode(self._ctx.branch()) @LazyProperty + def bookmarks(self): + return map(safe_unicode, self._ctx.bookmarks()) + + @LazyProperty def message(self): return safe_unicode(self._ctx.description()) @@ -159,6 +164,13 @@ class MercurialChangeset(BaseChangeset): " %r" % (self.revision, path)) return self._ctx.filectx(path) + def _extract_submodules(self): + """ + returns a dictionary with submodule information from substate file + of hg repository + """ + return self._ctx.substate + def get_file_mode(self, path): """ Returns stat mode of the file at the given ``path``. @@ -271,17 +283,27 @@ class MercurialChangeset(BaseChangeset): raise ChangesetError("Directory does not exist for revision %r at " " %r" % (self.revision, path)) path = self._fix_path(path) + filenodes = [FileNode(f, changeset=self) for f in self._file_paths if os.path.dirname(f) == path] dirs = path == '' and '' or [d for d in self._dir_paths if d and posixpath.dirname(d) == path] dirnodes = [DirNode(d, changeset=self) for d in dirs if os.path.dirname(d) == path] + + als = self.repository.alias + for k, vals in self._extract_submodules().iteritems(): + #vals = url,rev,type + loc = vals[0] + cs = vals[1] + dirnodes.append(SubModuleNode(k, url=loc, changeset=cs, + alias=als)) nodes = dirnodes + filenodes # cache nodes for node in nodes: self.nodes[node.path] = node nodes.sort() + return nodes def get_node(self, path): diff --git a/rhodecode/lib/vcs/backends/hg/inmemory.py b/rhodecode/lib/vcs/backends/hg/inmemory.py --- a/rhodecode/lib/vcs/backends/hg/inmemory.py +++ b/rhodecode/lib/vcs/backends/hg/inmemory.py @@ -4,7 +4,7 @@ import errno from rhodecode.lib.vcs.backends.base import BaseInMemoryChangeset from rhodecode.lib.vcs.exceptions import RepositoryError -from ...utils.hgcompat import memfilectx, memctx, hex +from ...utils.hgcompat import memfilectx, memctx, hex, tolocal class MercurialInMemoryChangeset(BaseInMemoryChangeset): @@ -30,9 +30,9 @@ class MercurialInMemoryChangeset(BaseInM self.check_integrity(parents) from .repository import MercurialRepository - if not isinstance(message, str) or not isinstance(author, str): + if not isinstance(message, unicode) or not isinstance(author, unicode): raise RepositoryError('Given message and author needs to be ' - 'an instance') + 'an instance') if branch is None: branch = MercurialRepository.DEFAULT_BRANCH_NAME @@ -70,7 +70,7 @@ class MercurialInMemoryChangeset(BaseInM copied=False) raise RepositoryError("Given path haven't been marked as added," - "changed or removed (%s)" % path) + "changed or removed (%s)" % path) parents = [None, None] for i, parent in enumerate(self.parents): @@ -89,9 +89,11 @@ class MercurialInMemoryChangeset(BaseInM date=date, extra=kwargs) + loc = lambda u: tolocal(u.encode('utf-8')) + # injecting given _repo params - commit_ctx._text = message - commit_ctx._user = author + commit_ctx._text = loc(message) + commit_ctx._user = loc(author) commit_ctx._date = date # TODO: Catch exceptions! diff --git a/rhodecode/lib/vcs/nodes.py b/rhodecode/lib/vcs/nodes.py --- a/rhodecode/lib/vcs/nodes.py +++ b/rhodecode/lib/vcs/nodes.py @@ -8,19 +8,22 @@ :created_on: Apr 8, 2010 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak. """ +import os import stat import posixpath import mimetypes +from pygments import lexers + from rhodecode.lib.vcs.utils.lazy import LazyProperty -from rhodecode.lib.vcs.utils import safe_unicode +from rhodecode.lib.vcs.utils import safe_unicode, safe_str from rhodecode.lib.vcs.exceptions import NodeError from rhodecode.lib.vcs.exceptions import RemovedFileNodeError - -from pygments import lexers +from rhodecode.lib.vcs.backends.base import EmptyChangeset class NodeKind: + SUBMODULE = -1 DIR = 1 FILE = 2 @@ -120,6 +123,10 @@ class Node(object): return None @LazyProperty + def unicode_path(self): + return safe_unicode(self.path) + + @LazyProperty def name(self): """ Returns name of the node so if its path @@ -205,6 +212,13 @@ class Node(object): """ return self.kind == NodeKind.DIR and self.path == '' + def is_submodule(self): + """ + Returns ``True`` if node's kind is ``NodeKind.SUBMODULE``, ``False`` + otherwise. + """ + return self.kind == NodeKind.SUBMODULE + @LazyProperty def added(self): return self.state is NodeState.ADDED @@ -557,3 +571,41 @@ class RootNode(DirNode): def __repr__(self): return '<%s>' % self.__class__.__name__ + + +class SubModuleNode(Node): + """ + represents a SubModule of Git or SubRepo of Mercurial + """ + is_binary = False + size = 0 + + def __init__(self, name, url=None, changeset=None, alias=None): + self.path = name + self.kind = NodeKind.SUBMODULE + self.alias = alias + # we have to use emptyChangeset here since this can point to svn/git/hg + # submodules we cannot get from repository + self.changeset = EmptyChangeset(str(changeset), alias=alias) + self.url = url or self._extract_submodule_url() + + def __repr__(self): + return '<%s %r @ %s>' % (self.__class__.__name__, self.path, + self.changeset.short_id) + + def _extract_submodule_url(self): + if self.alias == 'git': + #TODO: find a way to parse gits submodule file and extract the + # linking URL + return self.path + if self.alias == 'hg': + return self.path + + @LazyProperty + def name(self): + """ + Returns name of the node so if its path + then only last part is returned. + """ + org = safe_unicode(self.path.rstrip('/').split('/')[-1]) + return u'%s @ %s' % (org, self.changeset.short_id) diff --git a/rhodecode/lib/vcs/utils/hgcompat.py b/rhodecode/lib/vcs/utils/hgcompat.py --- a/rhodecode/lib/vcs/utils/hgcompat.py +++ b/rhodecode/lib/vcs/utils/hgcompat.py @@ -1,6 +1,7 @@ -"""Mercurial libs compatibility +""" +Mercurial libs compatibility +""" -""" from mercurial import archival, merge as hg_merge, patch, ui from mercurial.commands import clone, nullid, pull from mercurial.context import memctx, memfilectx @@ -10,3 +11,4 @@ from mercurial.localrepo import localrep from mercurial.match import match from mercurial.mdiff import diffopts from mercurial.node import hex +from mercurial.encoding import tolocal diff --git a/rhodecode/model/comment.py b/rhodecode/model/comment.py --- a/rhodecode/model/comment.py +++ b/rhodecode/model/comment.py @@ -29,7 +29,7 @@ import traceback from pylons.i18n.translation import _ from sqlalchemy.util.compat import defaultdict -from rhodecode.lib.utils2 import extract_mentioned_users +from rhodecode.lib.utils2 import extract_mentioned_users, safe_unicode from rhodecode.lib import helpers as h from rhodecode.model import BaseModel from rhodecode.model.db import ChangesetComment, User, Repository, Notification @@ -67,7 +67,7 @@ class ChangesetCommentsModel(BaseModel): if text: repo = Repository.get(repo_id) cs = repo.scm_instance.get_changeset(revision) - desc = cs.message + desc = "%s - %s" % (cs.short_id, h.shorter(cs.message, 256)) author_email = cs.author_email comment = ChangesetComment() comment.repo = repo @@ -83,14 +83,17 @@ class ChangesetCommentsModel(BaseModel): line = '' if line_no: line = _('on line %s') % line_no - subj = h.link_to('Re commit: %(commit_desc)s %(line)s' % \ - {'commit_desc': desc, 'line': line}, - h.url('changeset_home', repo_name=repo.repo_name, - revision=revision, - anchor='comment-%s' % comment.comment_id, - qualified=True, - ) - ) + subj = safe_unicode( + h.link_to('Re commit: %(commit_desc)s %(line)s' % \ + {'commit_desc': desc, 'line': line}, + h.url('changeset_home', repo_name=repo.repo_name, + revision=revision, + anchor='comment-%s' % comment.comment_id, + qualified=True, + ) + ) + ) + body = text # get the current participants of this changeset @@ -139,7 +142,9 @@ class ChangesetCommentsModel(BaseModel): .filter(ChangesetComment.repo_id == repo_id)\ .filter(ChangesetComment.revision == revision)\ .filter(ChangesetComment.line_no != None)\ - .filter(ChangesetComment.f_path != None).all() + .filter(ChangesetComment.f_path != None)\ + .order_by(ChangesetComment.comment_id.asc())\ + .all() paths = defaultdict(lambda: defaultdict(list)) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -645,7 +645,7 @@ class Repository(Base, BaseModel): # SCM PROPERTIES #========================================================================== - def get_changeset(self, rev): + def get_changeset(self, rev=None): return get_changeset_safe(self.scm_instance, rev) @property @@ -1233,7 +1233,8 @@ class Notification(Base, BaseModel): @property def recipients(self): return [x.user for x in UserNotification.query()\ - .filter(UserNotification.notification == self).all()] + .filter(UserNotification.notification == self)\ + .order_by(UserNotification.user).all()] @classmethod def create(cls, created_by, subject, body, recipients, type_=None): diff --git a/rhodecode/model/forms.py b/rhodecode/model/forms.py --- a/rhodecode/model/forms.py +++ b/rhodecode/model/forms.py @@ -551,7 +551,7 @@ class LoginForm(formencode.Schema): ) password = UnicodeString( - strip=True, + strip=False, min=3, not_empty=True, messages={ @@ -571,13 +571,13 @@ def UserForm(edit=False, old_data={}): username = All(UnicodeString(strip=True, min=1, not_empty=True), ValidUsername(edit, old_data)) if edit: - new_password = All(UnicodeString(strip=True, min=6, not_empty=False)) - password_confirmation = All(UnicodeString(strip=True, min=6, + new_password = All(UnicodeString(strip=False, min=6, not_empty=False)) + password_confirmation = All(UnicodeString(strip=False, min=6, not_empty=False)) admin = StringBoolean(if_missing=False) else: - password = All(UnicodeString(strip=True, min=6, not_empty=True)) - password_confirmation = All(UnicodeString(strip=True, min=6, + password = All(UnicodeString(strip=False, min=6, not_empty=True)) + password_confirmation = All(UnicodeString(strip=False, min=6, not_empty=False)) active = StringBoolean(if_missing=False) @@ -632,8 +632,8 @@ def RegisterForm(edit=False, old_data={} filter_extra_fields = True username = All(ValidUsername(edit, old_data), UnicodeString(strip=True, min=1, not_empty=True)) - password = All(UnicodeString(strip=True, min=6, not_empty=True)) - password_confirmation = All(UnicodeString(strip=True, min=6, not_empty=True)) + password = All(UnicodeString(strip=False, min=6, not_empty=True)) + password_confirmation = All(UnicodeString(strip=False, min=6, not_empty=True)) active = StringBoolean(if_missing=False) name = UnicodeString(strip=True, min=1, not_empty=False) lastname = UnicodeString(strip=True, min=1, not_empty=False) @@ -754,7 +754,7 @@ def LdapSettingsForm(tls_reqcert_choices class _LdapSettingsForm(formencode.Schema): allow_extra_fields = True filter_extra_fields = True - pre_validators = [LdapLibValidator] + #pre_validators = [LdapLibValidator] ldap_active = StringBoolean(if_missing=False) ldap_host = UnicodeString(strip=True,) ldap_port = Number(strip=True,) diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -286,12 +286,12 @@ class RepoModel(BaseModel): self.__create_repo(repo_name, form_data['repo_type'], form_data['repo_group'], form_data['clone_uri']) + log_create_repository(new_repo.get_dict(), + created_by=cur_user.username) # now automatically start following this repository as owner ScmModel(self.sa).toggle_following_repo(new_repo.repo_id, cur_user.user_id) - log_create_repository(new_repo.get_dict(), - created_by=cur_user.username) return new_repo except: log.error(traceback.format_exc()) diff --git a/rhodecode/model/scm.py b/rhodecode/model/scm.py --- a/rhodecode/model/scm.py +++ b/rhodecode/model/scm.py @@ -35,7 +35,7 @@ from rhodecode.lib.vcs.nodes import File from rhodecode import BACKENDS from rhodecode.lib import helpers as h -from rhodecode.lib.utils2 import safe_str +from rhodecode.lib.utils2 import safe_str, safe_unicode from rhodecode.lib.auth import HasRepoPermissionAny, HasReposGroupPermissionAny from rhodecode.lib.utils import get_repos as get_filesystem_repos, make_ui, \ action_logger, EmptyChangeset, REMOVED_REPO_PAT @@ -343,12 +343,15 @@ class ScmModel(BaseModel): repo = dbrepo.scm_instance try: - extras = {'ip': '', - 'username': username, - 'action': 'push_remote', - 'repository': repo_name} + extras = { + 'ip': '', + 'username': username, + 'action': 'push_remote', + 'repository': repo_name, + 'scm': repo.alias, + } - #inject ui extra param to log this action via push logger + # inject ui extra param to log this action via push logger for k, v in extras.items(): repo._repo.ui.setconfig('rhodecode_extras', k, v) @@ -362,21 +365,25 @@ class ScmModel(BaseModel): content, f_path): if repo.alias == 'hg': - from rhodecode.lib.vcs.backends.hg import MercurialInMemoryChangeset as IMC + from rhodecode.lib.vcs.backends.hg import \ + MercurialInMemoryChangeset as IMC elif repo.alias == 'git': - from rhodecode.lib.vcs.backends.git import GitInMemoryChangeset as IMC + from rhodecode.lib.vcs.backends.git import \ + GitInMemoryChangeset as IMC # decoding here will force that we have proper encoded values # in any other case this will throw exceptions and deny commit content = safe_str(content) - message = safe_str(message) path = safe_str(f_path) - author = safe_str(author) + # message and author needs to be unicode + # proper backend should then translate that into required type + message = safe_unicode(message) + author = safe_unicode(author) m = IMC(repo) m.change(FileNode(path, content)) tip = m.commit(message=message, - author=author, - parents=[cs], branch=cs.branch) + author=author, + parents=[cs], branch=cs.branch) new_cs = tip.short_id action = 'push_local:%s' % new_cs @@ -403,21 +410,21 @@ class ScmModel(BaseModel): type(content) )) - message = safe_str(message) + message = safe_unicode(message) + author = safe_unicode(author) path = safe_str(f_path) - author = safe_str(author) m = IMC(repo) if isinstance(cs, EmptyChangeset): - # Emptychangeset means we we're editing empty repository + # EmptyChangeset means we we're editing empty repository parents = None else: parents = [cs] m.add(FileNode(path, content=content)) tip = m.commit(message=message, - author=author, - parents=parents, branch=cs.branch) + author=author, + parents=parents, branch=cs.branch) new_cs = tip.short_id action = 'push_local:%s' % new_cs diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -225,10 +225,8 @@ class UserModel(BaseModel): from rhodecode.model.notification import NotificationModel try: - new_user = User() - for k, v in form_data.items(): - if k != 'admin': - setattr(new_user, k, v) + form_data['admin'] = False + new_user = self.create(form_data) self.sa.add(new_user) self.sa.flush() @@ -398,145 +396,148 @@ class UserModel(BaseModel): rg_k = perm.UserRepoGroupToPerm.group.group_name p = 'group.admin' user.permissions[GK][rg_k] = p + return user - else: - #================================================================== - # set default permissions first for repositories and groups - #================================================================== - uid = user.user_id + #================================================================== + # set default permissions first for repositories and groups + #================================================================== + uid = user.user_id - # default global permissions - default_global_perms = self.sa.query(UserToPerm)\ - .filter(UserToPerm.user_id == default_user_id) + # default global permissions + default_global_perms = self.sa.query(UserToPerm)\ + .filter(UserToPerm.user_id == default_user_id) - for perm in default_global_perms: - user.permissions[GLOBAL].add(perm.permission.permission_name) + for perm in default_global_perms: + user.permissions[GLOBAL].add(perm.permission.permission_name) - # defaults for repositories, taken from default user - for perm in default_repo_perms: - r_k = perm.UserRepoToPerm.repository.repo_name - if perm.Repository.private and not (perm.Repository.user_id == uid): - # disable defaults for private repos, - p = 'repository.none' - elif perm.Repository.user_id == uid: - # set admin if owner - p = 'repository.admin' - else: - p = perm.Permission.permission_name + # defaults for repositories, taken from default user + for perm in default_repo_perms: + r_k = perm.UserRepoToPerm.repository.repo_name + if perm.Repository.private and not (perm.Repository.user_id == uid): + # disable defaults for private repos, + p = 'repository.none' + elif perm.Repository.user_id == uid: + # set admin if owner + p = 'repository.admin' + else: + p = perm.Permission.permission_name + + user.permissions[RK][r_k] = p - user.permissions[RK][r_k] = p + # defaults for repositories groups taken from default user permission + # on given group + for perm in default_repo_groups_perms: + rg_k = perm.UserRepoGroupToPerm.group.group_name + p = perm.Permission.permission_name + user.permissions[GK][rg_k] = p + + #================================================================== + # overwrite defaults with user permissions if any found + #================================================================== + + # user global permissions + user_perms = self.sa.query(UserToPerm)\ + .options(joinedload(UserToPerm.permission))\ + .filter(UserToPerm.user_id == uid).all() + + for perm in user_perms: + user.permissions[GLOBAL].add(perm.permission.permission_name) - # defaults for repositories groups taken from default user permission - # on given group - for perm in default_repo_groups_perms: - rg_k = perm.UserRepoGroupToPerm.group.group_name - p = perm.Permission.permission_name - user.permissions[GK][rg_k] = p + # user explicit permissions for repositories + user_repo_perms = \ + self.sa.query(UserRepoToPerm, Permission, Repository)\ + .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\ + .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\ + .filter(UserRepoToPerm.user_id == uid)\ + .all() - #================================================================== - # overwrite defaults with user permissions if any found - #================================================================== + for perm in user_repo_perms: + # set admin if owner + r_k = perm.UserRepoToPerm.repository.repo_name + if perm.Repository.user_id == uid: + p = 'repository.admin' + else: + p = perm.Permission.permission_name + user.permissions[RK][r_k] = p - # user global permissions - user_perms = self.sa.query(UserToPerm)\ - .options(joinedload(UserToPerm.permission))\ - .filter(UserToPerm.user_id == uid).all() + # USER GROUP + #================================================================== + # check if user is part of user groups for this repository and + # fill in (or replace with higher) permissions + #================================================================== - for perm in user_perms: - user.permissions[GLOBAL].add(perm.permission.permission_name) + # users group global + user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\ + .options(joinedload(UsersGroupToPerm.permission))\ + .join((UsersGroupMember, UsersGroupToPerm.users_group_id == + UsersGroupMember.users_group_id))\ + .filter(UsersGroupMember.user_id == uid).all() + + for perm in user_perms_from_users_groups: + user.permissions[GLOBAL].add(perm.permission.permission_name) - # user explicit permissions for repositories - user_repo_perms = \ - self.sa.query(UserRepoToPerm, Permission, Repository)\ - .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\ - .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\ - .filter(UserRepoToPerm.user_id == uid)\ - .all() + # users group for repositories permissions + user_repo_perms_from_users_groups = \ + self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\ + .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\ + .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\ + .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\ + .filter(UsersGroupMember.user_id == uid)\ + .all() - for perm in user_repo_perms: - # set admin if owner - r_k = perm.UserRepoToPerm.repository.repo_name - if perm.Repository.user_id == uid: - p = 'repository.admin' - else: - p = perm.Permission.permission_name + for perm in user_repo_perms_from_users_groups: + r_k = perm.UsersGroupRepoToPerm.repository.repo_name + p = perm.Permission.permission_name + cur_perm = user.permissions[RK][r_k] + # overwrite permission only if it's greater than permission + # given from other sources + if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: user.permissions[RK][r_k] = p - #================================================================== - # check if user is part of user groups for this repository and - # fill in (or replace with higher) permissions - #================================================================== - - # users group global - user_perms_from_users_groups = self.sa.query(UsersGroupToPerm)\ - .options(joinedload(UsersGroupToPerm.permission))\ - .join((UsersGroupMember, UsersGroupToPerm.users_group_id == - UsersGroupMember.users_group_id))\ - .filter(UsersGroupMember.user_id == uid).all() - - for perm in user_perms_from_users_groups: - user.permissions[GLOBAL].add(perm.permission.permission_name) + # REPO GROUP + #================================================================== + # get access for this user for repos group and override defaults + #================================================================== - # users group for repositories permissions - user_repo_perms_from_users_groups = \ - self.sa.query(UsersGroupRepoToPerm, Permission, Repository,)\ - .join((Repository, UsersGroupRepoToPerm.repository_id == Repository.repo_id))\ - .join((Permission, UsersGroupRepoToPerm.permission_id == Permission.permission_id))\ - .join((UsersGroupMember, UsersGroupRepoToPerm.users_group_id == UsersGroupMember.users_group_id))\ - .filter(UsersGroupMember.user_id == uid)\ - .all() + # user explicit permissions for repository + user_repo_groups_perms = \ + self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\ + .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\ + .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\ + .filter(UserRepoGroupToPerm.user_id == uid)\ + .all() - for perm in user_repo_perms_from_users_groups: - r_k = perm.UsersGroupRepoToPerm.repository.repo_name - p = perm.Permission.permission_name - cur_perm = user.permissions[RK][r_k] - # overwrite permission only if it's greater than permission - # given from other sources - if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: - user.permissions[RK][r_k] = p - - #================================================================== - # get access for this user for repos group and override defaults - #================================================================== + for perm in user_repo_groups_perms: + rg_k = perm.UserRepoGroupToPerm.group.group_name + p = perm.Permission.permission_name + cur_perm = user.permissions[GK][rg_k] + if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: + user.permissions[GK][rg_k] = p - # user explicit permissions for repository - user_repo_groups_perms = \ - self.sa.query(UserRepoGroupToPerm, Permission, RepoGroup)\ - .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\ - .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\ - .filter(UserRepoGroupToPerm.user_id == uid)\ - .all() - - for perm in user_repo_groups_perms: - rg_k = perm.UserRepoGroupToPerm.group.group_name - p = perm.Permission.permission_name - cur_perm = user.permissions[GK][rg_k] - if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: - user.permissions[GK][rg_k] = p + # REPO GROUP + USER GROUP + #================================================================== + # check if user is part of user groups for this repo group and + # fill in (or replace with higher) permissions + #================================================================== - #================================================================== - # check if user is part of user groups for this repo group and - # fill in (or replace with higher) permissions - #================================================================== + # users group for repositories permissions + user_repo_group_perms_from_users_groups = \ + self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\ + .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\ + .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\ + .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\ + .filter(UsersGroupMember.user_id == uid)\ + .all() - # users group for repositories permissions - user_repo_group_perms_from_users_groups = \ - self.sa.query(UsersGroupRepoGroupToPerm, Permission, RepoGroup)\ - .join((RepoGroup, UsersGroupRepoGroupToPerm.group_id == RepoGroup.group_id))\ - .join((Permission, UsersGroupRepoGroupToPerm.permission_id == Permission.permission_id))\ - .join((UsersGroupMember, UsersGroupRepoGroupToPerm.users_group_id == UsersGroupMember.users_group_id))\ - .filter(UsersGroupMember.user_id == uid)\ - .all() - - for perm in user_repo_group_perms_from_users_groups: - g_k = perm.UsersGroupRepoGroupToPerm.group.group_name - print perm, g_k - p = perm.Permission.permission_name - cur_perm = user.permissions[GK][g_k] - # overwrite permission only if it's greater than permission - # given from other sources - if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: - user.permissions[GK][g_k] = p + for perm in user_repo_group_perms_from_users_groups: + g_k = perm.UsersGroupRepoGroupToPerm.group.group_name + print perm, g_k + p = perm.Permission.permission_name + cur_perm = user.permissions[GK][g_k] + # overwrite permission only if it's greater than permission + # given from other sources + if PERM_WEIGHTS[p] > PERM_WEIGHTS[cur_perm]: + user.permissions[GK][g_k] = p return user diff --git a/rhodecode/public/css/style.css b/rhodecode/public/css/style.css --- a/rhodecode/public/css/style.css +++ b/rhodecode/public/css/style.css @@ -2520,6 +2520,10 @@ h3.files_location { .right .logtags{ padding: 2px 2px 2px 2px; } +.right .logtags .branchtag,.right .logtags .tagtag,.right .logtags .booktag{ + margin: 0px 2px; +} + .right .logtags .branchtag,.logtags .branchtag { padding: 1px 3px 1px 3px; background-color: #bfbfbf; @@ -2558,10 +2562,10 @@ h3.files_location { text-decoration: none; color: #ffffff; } -.right .logbooks .bookbook,.logbooks .bookbook { - padding: 1px 3px 2px; +.right .logbooks .bookbook,.logbooks .bookbook,.right .logtags .bookbook,.logtags .bookbook { + padding: 1px 3px 1px 3px; background-color: #46A546; - font-size: 9.75px; + font-size: 10px; font-weight: bold; color: #ffffff; text-transform: uppercase; @@ -2570,10 +2574,10 @@ h3.files_location { -moz-border-radius: 3px; border-radius: 3px; } -.right .logbooks .bookbook,.logbooks .bookbook a{ +.right .logbooks .bookbook,.logbooks .bookbook a,.right .logtags .bookbook,.logtags .bookbook a{ color: #ffffff; } -.right .logbooks .bookbook,.logbooks .bookbook a:hover{ +.right .logbooks .bookbook,.logbooks .bookbook a:hover,.right .logtags .bookbook,.logtags .bookbook a:hover{ text-decoration: none; color: #ffffff; } @@ -2718,6 +2722,14 @@ table.code-browser .browser-dir { text-align: left; } +table.code-browser .submodule-dir { + background: url("../images/icons/disconnect.png") no-repeat scroll 3px; + height: 16px; + padding-left: 20px; + text-align: left; +} + + .box .search { clear: both; overflow: hidden; @@ -3966,6 +3978,7 @@ form.comment-form { .comment .buttons { float: right; + padding:2px 2px 0px 0px; } @@ -3975,6 +3988,23 @@ form.comment-form { } /** comment inline form **/ +.comment-inline-form .overlay{ + display: none; +} +.comment-inline-form .overlay.submitting{ + display:block; + background: none repeat scroll 0 0 white; + font-size: 16px; + opacity: 0.5; + position: absolute; + text-align: center; + vertical-align: top; + +} +.comment-inline-form .overlay.submitting .overlay-text{ + width:100%; + margin-top:5%; +} .comment-inline-form .clearfix{ background: #EEE; @@ -3987,6 +4017,7 @@ form.comment-form { div.comment-inline-form { margin-top: 5px; padding:2px 6px 8px 6px; + } .comment-inline-form strong { @@ -4047,6 +4078,10 @@ form.comment-inline-form { margin: 3px 3px 5px 5px; background-color: #FAFAFA; } +.inline-comments .add-comment { + padding: 2px 4px 8px 5px; +} + .inline-comments .comment-wrapp{ padding:1px; } @@ -4078,8 +4113,15 @@ form.comment-inline-form { font-size: 16px; } .inline-comments-button .add-comment{ - margin:10px 5px !important; -} + margin:2px 0px 8px 5px !important +} + + +.notification-paginator{ + padding: 0px 0px 4px 16px; + float: left; +} + .notifications{ border-radius: 4px 4px 4px 4px; -webkit-border-radius: 4px; @@ -4113,16 +4155,24 @@ form.comment-inline-form { float: left } .notification-list .container.unread{ - + background: none repeat scroll 0 0 rgba(255, 255, 180, 0.6); } .notification-header .gravatar{ - + background: none repeat scroll 0 0 transparent; + padding: 0px 0px 0px 8px; } .notification-header .desc.unread{ font-weight: bold; font-size: 17px; } - +.notification-table{ + border: 1px solid #ccc; + -webkit-border-radius: 6px 6px 6px 6px; + -moz-border-radius: 6px 6px 6px 6px; + border-radius: 6px 6px 6px 6px; + clear: both; + margin: 0px 20px 0px 20px; +} .notification-header .delete-notifications{ float: right; padding-top: 8px; @@ -4134,6 +4184,11 @@ form.comment-inline-form { padding:5px 0px 5px 38px; } +.notification-body{ + clear:both; + margin: 34px 2px 2px 8px +} + /**** PERMS *****/ diff --git a/rhodecode/public/js/rhodecode.js b/rhodecode/public/js/rhodecode.js --- a/rhodecode/public/js/rhodecode.js +++ b/rhodecode/public/js/rhodecode.js @@ -195,6 +195,34 @@ function ypjax(url,container,s_call,f_ca }; +var ajaxPOST = function(url,postData,success) { + // Set special header for ajax == HTTP_X_PARTIAL_XHR + YUC.initHeader('X-PARTIAL-XHR',true); + + 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 */ @@ -300,33 +328,25 @@ var q_filter = function(target,nodes,dis } }; -var ajaxPOST = function(url,postData,success) { - var sUrl = url; - var callback = { - success: success, - failure: function (o) { - alert("error"); - }, - }; - var postData = postData; - var request = YAHOO.util.Connect.asyncRequest('POST', sUrl, callback, postData); +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 = ''+ + ''+ + '{0}'.format(body); + return tr; }; - /** comments **/ var removeInlineForm = function(form) { form.parentNode.removeChild(form); }; -var tableTr = function(cls,body){ - var form = document.createElement('tr'); - YUD.addClass(form, cls); - form.innerHTML = ''+ - ''+ - '{0}'.format(body); - return form; -}; - var createInlineForm = function(parent_tr, f_path, line) { var tmpl = YUD.get('comment-inline-form-template').innerHTML; tmpl = tmpl.format(f_path, line); @@ -337,12 +357,27 @@ var createInlineForm = function(parent_t var form_hide_button = new YAHOO.util.Element(form.getElementsByClassName('hide-inline-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')){ + return + } + var submit_url = AJAX_COMMENT_URL; if(YUD.hasClass(tr,'form-open') || YUD.hasClass(tr,'context') || YUD.hasClass(tr,'no-comment')){ return } @@ -350,20 +385,96 @@ var injectInlineForm = function(tr){ var node = tr.parentNode.parentNode.parentNode.getElementsByClassName('full_f_path')[0]; var f_path = YUD.getAttribute(node,'path'); var lineno = getLineNo(tr); - var form = createInlineForm(tr, f_path, lineno); - var target_tr = tr; - if(YUD.hasClass(YUD.getNextSibling(tr),'inline-comments')){ - target_tr = YUD.getNextSibling(tr); - } - YUD.insertAfter(form,target_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; + } + else{ + break; + } + } + YUD.insertAfter(form,parent); + YUD.get('text_'+lineno).focus(); + var f = YUD.get(form); + + var overlay = f.getElementsByClassName('overlay')[0]; + var _form = f.getElementsByClassName('inline-form')[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 !'); + return + } + if(f_path === undefined){ + alert('missing file path !'); + return + } + + if(text == ""){ + return + } + + 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); + }); + tooltip_activate(); }; -var createInlineAddButton = function(tr,label){ - var html = '
{0}
'.format(label); - - var add = new YAHOO.util.Element(tableTr('inline-comments-button',html)); +var deleteComment = function(comment_id){ + var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__',comment_id); + var postData = {'_method':'delete'}; + var success = function(o){ + var n = YUD.get('comment-tr-'+comment_id); + var root = n.previousElementSibling.previousElementSibling; + n.parentNode.removeChild(n); + + // scann nodes, and attach add button to last one + placeAddButton(root); + } + ajaxPOST(url,postData,success); +} + + +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 = '{0}'.format(label); + + var add = new YAHOO.util.Element(html_el); add.on('click', function(e) { injectInlineForm(tr); }); @@ -384,6 +495,103 @@ var getLineNo = function(tr) { return line }; +var placeAddButton = function(target_tr){ + if(!target_tr){ + return + } + var last_node = target_tr; + //scann + while (1){ + var n = last_node.nextElementSibling; + // 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'); + for(var i=0;i %if c.notifications: -
+
${_('Mark all read')}
%endif @@ -39,15 +39,18 @@ YUE.on(YUQ('.delete-notification'),'clic var notification_id = e.currentTarget.id; deleteNotification(url_del,notification_id) }) - YUE.on('mark_all_read','click',function(e){ - var url = "${h.url('notifications_mark_all_read')}"; - ypjax(url,'notification_data',function(){ - YUD.get('notification_counter').innerHTML=0; - YUE.on(YUQ('.delete-notification'),'click',function(e){ - var notification_id = e.currentTarget.id; - deleteNotification(url_del,notification_id) - }) - }); - }) +YUE.on('mark_all_read','click',function(e){ + var url = "${h.url('notifications_mark_all_read')}"; + ypjax(url,'notification_data',function(){ + var notification_counter = YUD.get('notification_counter'); + if(notification_counter){ + notification_counter.innerHTML=0; + } + YUE.on(YUQ('.delete-notification'),'click',function(e){ + var notification_id = e.currentTarget.id; + deleteNotification(url_del,notification_id) + }) + }); +}) diff --git a/rhodecode/templates/admin/notifications/notifications_data.html b/rhodecode/templates/admin/notifications/notifications_data.html --- a/rhodecode/templates/admin/notifications/notifications_data.html +++ b/rhodecode/templates/admin/notifications/notifications_data.html @@ -3,26 +3,37 @@ <% unread = lambda n:{False:'unread'}.get(n) %> -
-
- %for notification in c.notifications: -
- -
${h.literal(notification.notification.subject)}
-
- %endfor +
+
+ ${c.notifications.pager('$link_previous ~2~ $link_next')}
+ +
+%for notification in c.notifications: +
+ +
${h.literal(notification.notification.subject)}
+
+%endfor +
+ +
+
+ ${c.notifications.pager('$link_previous ~2~ $link_next')} +
+
+ %else:
${_('No notifications here yet')}
%endif diff --git a/rhodecode/templates/admin/notifications/show_notification.html b/rhodecode/templates/admin/notifications/show_notification.html --- a/rhodecode/templates/admin/notifications/show_notification.html +++ b/rhodecode/templates/admin/notifications/show_notification.html @@ -39,7 +39,7 @@
-
${h.rst_w_mentions(c.notification.body)}
+
${h.rst_w_mentions(c.notification.body)}
diff --git a/rhodecode/templates/admin/settings/settings.html b/rhodecode/templates/admin/settings/settings.html --- a/rhodecode/templates/admin/settings/settings.html +++ b/rhodecode/templates/admin/settings/settings.html @@ -210,5 +210,37 @@ ${h.end_form()} +

${_('System Info and Packages')}

+
+
+
↓ ${_('show')} ↓
+
+ +
+ + + diff --git a/rhodecode/templates/base/root.html b/rhodecode/templates/base/root.html --- a/rhodecode/templates/base/root.html +++ b/rhodecode/templates/base/root.html @@ -47,9 +47,13 @@ diff --git a/rhodecode/templates/changeset/changeset_comment_block.html b/rhodecode/templates/changeset/changeset_comment_block.html new file mode 100644 --- /dev/null +++ b/rhodecode/templates/changeset/changeset_comment_block.html @@ -0,0 +1,2 @@ +<%namespace name="comment" file="/changeset/changeset_file_comment.html"/> +${comment.comment_block(c.co)} diff --git a/rhodecode/templates/changeset/changeset_file_comment.html b/rhodecode/templates/changeset/changeset_file_comment.html --- a/rhodecode/templates/changeset/changeset_file_comment.html +++ b/rhodecode/templates/changeset/changeset_file_comment.html @@ -4,7 +4,7 @@ ## ${comment.comment_block(co)} ## <%def name="comment_block(co)"> -
+
@@ -32,7 +32,8 @@