##// END OF EJS Templates
merge 1.3 into stable
marcink -
r2031:82a88013 merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

1 NO CONTENT: new file 100644
@@ -0,0 +1,34 b''
1 # celeryd - run the celeryd daemon as an upstart job for rhodecode
2 # Change variables/paths as necessary and place file /etc/init/celeryd.conf
3 # start/stop/restart as normal upstart job (ie: $ start celeryd)
4
5 description "Celery for RhodeCode Mercurial Server"
6 author "Matt Zuba <matt.zuba@goodwillaz.org"
7
8 start on starting rhodecode
9 stop on stopped rhodecode
10
11 respawn
12
13 umask 0022
14
15 env PIDFILE=/tmp/celeryd.pid
16 env APPINI=/var/hg/rhodecode/production.ini
17 env HOME=/var/hg
18 env USER=hg
19 # To use group (if different from user), you must edit sudoers file and change
20 # root's entry from (ALL) to (ALL:ALL)
21 # env GROUP=hg
22
23 script
24 COMMAND="/var/hg/.virtualenvs/rhodecode/bin/paster celeryd $APPINI --pidfile=$PIDFILE"
25 if [ -z "$GROUP" ]; then
26 exec sudo -u $USER $COMMAND
27 else
28 exec sudo -u $USER -g $GROUP $COMMAND
29 fi
30 end script
31
32 post-stop script
33 rm -f $PIDFILE
34 end script
@@ -0,0 +1,26 b''
1 # rhodecode - run the rhodecode daemon as an upstart job
2 # Change variables/paths as necessary and place file /etc/init/rhodecode.conf
3 # start/stop/restart as normal upstart job (ie: $ start rhodecode)
4
5 description "RhodeCode Mercurial Server"
6 author "Matt Zuba <matt.zuba@goodwillaz.org"
7
8 start on (local-filesystems and runlevel [2345])
9 stop on runlevel [!2345]
10
11 respawn
12
13 umask 0022
14
15 env PIDFILE=/var/hg/rhodecode/rhodecode.pid
16 env LOGFILE=/var/hg/rhodecode/log/rhodecode.log
17 env APPINI=/var/hg/rhodecode/production.ini
18 env HOME=/var/hg
19 env USER=hg
20 env GROUP=hg
21
22 exec /var/hg/.virtualenvs/rhodecode/bin/paster serve --user=$USER --group=$GROUP --pid-file=$PIDFILE --log-file=$LOGFILE $APPINI
23
24 post-stop script
25 rm -f $PIDFILE
26 end script
@@ -0,0 +1,140 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.controllers.admin.notifications
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 notifications controller for RhodeCode
7
8 :created_on: Nov 23, 2010
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 import logging
27 import traceback
28
29 from pylons import request
30 from pylons import tmpl_context as c, url
31 from pylons.controllers.util import redirect
32
33 from rhodecode.lib.base import BaseController, render
34 from rhodecode.model.db import Notification
35
36 from rhodecode.model.notification import NotificationModel
37 from rhodecode.lib.auth import LoginRequired, NotAnonymous
38 from rhodecode.lib import helpers as h
39 from rhodecode.model.meta import Session
40
41
42 log = logging.getLogger(__name__)
43
44
45 class NotificationsController(BaseController):
46 """REST Controller styled on the Atom Publishing Protocol"""
47 # To properly map this controller, ensure your config/routing.py
48 # file has a resource setup:
49 # map.resource('notification', 'notifications', controller='_admin/notifications',
50 # path_prefix='/_admin', name_prefix='_admin_')
51
52 @LoginRequired()
53 @NotAnonymous()
54 def __before__(self):
55 super(NotificationsController, self).__before__()
56
57 def index(self, format='html'):
58 """GET /_admin/notifications: All items in the collection"""
59 # url('notifications')
60 c.user = self.rhodecode_user
61 c.notifications = NotificationModel()\
62 .get_for_user(self.rhodecode_user.user_id)
63 return render('admin/notifications/notifications.html')
64
65 def mark_all_read(self):
66 if request.environ.get('HTTP_X_PARTIAL_XHR'):
67 nm = NotificationModel()
68 # mark all read
69 nm.mark_all_read_for_user(self.rhodecode_user.user_id)
70 Session.commit()
71 c.user = self.rhodecode_user
72 c.notifications = nm.get_for_user(self.rhodecode_user.user_id)
73 return render('admin/notifications/notifications_data.html')
74
75 def create(self):
76 """POST /_admin/notifications: Create a new item"""
77 # url('notifications')
78
79 def new(self, format='html'):
80 """GET /_admin/notifications/new: Form to create a new item"""
81 # url('new_notification')
82
83 def update(self, notification_id):
84 """PUT /_admin/notifications/id: Update an existing item"""
85 # Forms posted to this method should contain a hidden field:
86 # <input type="hidden" name="_method" value="PUT" />
87 # Or using helpers:
88 # h.form(url('notification', notification_id=ID),
89 # method='put')
90 # url('notification', notification_id=ID)
91
92 def delete(self, notification_id):
93 """DELETE /_admin/notifications/id: Delete an existing item"""
94 # Forms posted to this method should contain a hidden field:
95 # <input type="hidden" name="_method" value="DELETE" />
96 # Or using helpers:
97 # h.form(url('notification', notification_id=ID),
98 # method='delete')
99 # url('notification', notification_id=ID)
100
101 try:
102 no = Notification.get(notification_id)
103 owner = lambda: (no.notifications_to_users.user.user_id
104 == c.rhodecode_user.user_id)
105 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
106 NotificationModel().delete(c.rhodecode_user.user_id, no)
107 Session.commit()
108 return 'ok'
109 except Exception:
110 Session.rollback()
111 log.error(traceback.format_exc())
112 return 'fail'
113
114 def show(self, notification_id, format='html'):
115 """GET /_admin/notifications/id: Show a specific item"""
116 # url('notification', notification_id=ID)
117 c.user = self.rhodecode_user
118 no = Notification.get(notification_id)
119
120 owner = lambda: (no.notifications_to_users.user.user_id
121 == c.user.user_id)
122 if no and (h.HasPermissionAny('hg.admin', 'repository.admin')() or owner):
123 unotification = NotificationModel()\
124 .get_user_notification(c.user.user_id, no)
125
126 # if this association to user is not valid, we don't want to show
127 # this message
128 if unotification:
129 if unotification.read is False:
130 unotification.mark_as_read()
131 Session.commit()
132 c.notification = no
133
134 return render('admin/notifications/show_notification.html')
135
136 return redirect(url('notifications'))
137
138 def edit(self, notification_id, format='html'):
139 """GET /_admin/notifications/id/edit: Form to edit an existing item"""
140 # url('edit_notification', notification_id=ID)
@@ -0,0 +1,57 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.controllers.bookmarks
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 Bookmarks controller for rhodecode
7
8 :created_on: Dec 1, 2011
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 import logging
26
27 from pylons import tmpl_context as c
28
29 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
30 from rhodecode.lib.base import BaseRepoController, render
31 from rhodecode.lib.compat import OrderedDict
32 from webob.exc import HTTPNotFound
33
34 log = logging.getLogger(__name__)
35
36
37 class BookmarksController(BaseRepoController):
38
39 @LoginRequired()
40 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
41 'repository.admin')
42 def __before__(self):
43 super(BookmarksController, self).__before__()
44
45 def index(self):
46 if c.rhodecode_repo.alias != 'hg':
47 raise HTTPNotFound()
48
49 c.repo_bookmarks = OrderedDict()
50
51 bookmarks = [(name, c.rhodecode_repo.get_changeset(hash_)) for \
52 name, hash_ in c.rhodecode_repo._repo._bookmarks.items()]
53 ordered_tags = sorted(bookmarks, key=lambda x: x[1].date, reverse=True)
54 for name, cs_book in ordered_tags:
55 c.repo_bookmarks[name] = cs_book
56
57 return render('bookmarks/bookmarks.html')
@@ -0,0 +1,190 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.annotate
4 ~~~~~~~~~~~~~~~~~~~~~~
5
6 Anontation library for usage in rhodecode, previously part of vcs
7
8 :created_on: Dec 4, 2011
9 :author: marcink
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13
14 from rhodecode.lib.vcs.exceptions import VCSError
15 from rhodecode.lib.vcs.nodes import FileNode
16 from pygments.formatters import HtmlFormatter
17 from pygments import highlight
18
19 import StringIO
20
21
22 def annotate_highlight(filenode, annotate_from_changeset_func=None,
23 order=None, headers=None, **options):
24 """
25 Returns html portion containing annotated table with 3 columns: line
26 numbers, changeset information and pygmentized line of code.
27
28 :param filenode: FileNode object
29 :param annotate_from_changeset_func: function taking changeset and
30 returning single annotate cell; needs break line at the end
31 :param order: ordered sequence of ``ls`` (line numbers column),
32 ``annotate`` (annotate column), ``code`` (code column); Default is
33 ``['ls', 'annotate', 'code']``
34 :param headers: dictionary with headers (keys are whats in ``order``
35 parameter)
36 """
37 options['linenos'] = True
38 formatter = AnnotateHtmlFormatter(filenode=filenode, order=order,
39 headers=headers,
40 annotate_from_changeset_func=annotate_from_changeset_func, **options)
41 lexer = filenode.lexer
42 highlighted = highlight(filenode.content, lexer, formatter)
43 return highlighted
44
45
46 class AnnotateHtmlFormatter(HtmlFormatter):
47
48 def __init__(self, filenode, annotate_from_changeset_func=None,
49 order=None, **options):
50 """
51 If ``annotate_from_changeset_func`` is passed it should be a function
52 which returns string from the given changeset. For example, we may pass
53 following function as ``annotate_from_changeset_func``::
54
55 def changeset_to_anchor(changeset):
56 return '<a href="/changesets/%s/">%s</a>\n' %\
57 (changeset.id, changeset.id)
58
59 :param annotate_from_changeset_func: see above
60 :param order: (default: ``['ls', 'annotate', 'code']``); order of
61 columns;
62 :param options: standard pygment's HtmlFormatter options, there is
63 extra option tough, ``headers``. For instance we can pass::
64
65 formatter = AnnotateHtmlFormatter(filenode, headers={
66 'ls': '#',
67 'annotate': 'Annotate',
68 'code': 'Code',
69 })
70
71 """
72 super(AnnotateHtmlFormatter, self).__init__(**options)
73 self.annotate_from_changeset_func = annotate_from_changeset_func
74 self.order = order or ('ls', 'annotate', 'code')
75 headers = options.pop('headers', None)
76 if headers and not ('ls' in headers and 'annotate' in headers and
77 'code' in headers):
78 raise ValueError("If headers option dict is specified it must "
79 "all 'ls', 'annotate' and 'code' keys")
80 self.headers = headers
81 if isinstance(filenode, FileNode):
82 self.filenode = filenode
83 else:
84 raise VCSError("This formatter expect FileNode parameter, not %r"
85 % type(filenode))
86
87 def annotate_from_changeset(self, changeset):
88 """
89 Returns full html line for single changeset per annotated line.
90 """
91 if self.annotate_from_changeset_func:
92 return self.annotate_from_changeset_func(changeset)
93 else:
94 return ''.join((changeset.id, '\n'))
95
96 def _wrap_tablelinenos(self, inner):
97 dummyoutfile = StringIO.StringIO()
98 lncount = 0
99 for t, line in inner:
100 if t:
101 lncount += 1
102 dummyoutfile.write(line)
103
104 fl = self.linenostart
105 mw = len(str(lncount + fl - 1))
106 sp = self.linenospecial
107 st = self.linenostep
108 la = self.lineanchors
109 aln = self.anchorlinenos
110 if sp:
111 lines = []
112
113 for i in range(fl, fl + lncount):
114 if i % st == 0:
115 if i % sp == 0:
116 if aln:
117 lines.append('<a href="#%s-%d" class="special">'
118 '%*d</a>' %
119 (la, i, mw, i))
120 else:
121 lines.append('<span class="special">'
122 '%*d</span>' % (mw, i))
123 else:
124 if aln:
125 lines.append('<a href="#%s-%d">'
126 '%*d</a>' % (la, i, mw, i))
127 else:
128 lines.append('%*d' % (mw, i))
129 else:
130 lines.append('')
131 ls = '\n'.join(lines)
132 else:
133 lines = []
134 for i in range(fl, fl + lncount):
135 if i % st == 0:
136 if aln:
137 lines.append('<a href="#%s-%d">%*d</a>' \
138 % (la, i, mw, i))
139 else:
140 lines.append('%*d' % (mw, i))
141 else:
142 lines.append('')
143 ls = '\n'.join(lines)
144
145 annotate_changesets = [tup[1] for tup in self.filenode.annotate]
146 # If pygments cropped last lines break we need do that too
147 ln_cs = len(annotate_changesets)
148 ln_ = len(ls.splitlines())
149 if ln_cs > ln_:
150 annotate_changesets = annotate_changesets[:ln_ - ln_cs]
151 annotate = ''.join((self.annotate_from_changeset(changeset)
152 for changeset in annotate_changesets))
153 # in case you wonder about the seemingly redundant <div> here:
154 # since the content in the other cell also is wrapped in a div,
155 # some browsers in some configurations seem to mess up the formatting.
156 '''
157 yield 0, ('<table class="%stable">' % self.cssclass +
158 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
159 ls + '</pre></div></td>' +
160 '<td class="code">')
161 yield 0, dummyoutfile.getvalue()
162 yield 0, '</td></tr></table>'
163
164 '''
165 headers_row = []
166 if self.headers:
167 headers_row = ['<tr class="annotate-header">']
168 for key in self.order:
169 td = ''.join(('<td>', self.headers[key], '</td>'))
170 headers_row.append(td)
171 headers_row.append('</tr>')
172
173 body_row_start = ['<tr>']
174 for key in self.order:
175 if key == 'ls':
176 body_row_start.append(
177 '<td class="linenos"><div class="linenodiv"><pre>' +
178 ls + '</pre></div></td>')
179 elif key == 'annotate':
180 body_row_start.append(
181 '<td class="annotate"><div class="annotatediv"><pre>' +
182 annotate + '</pre></div></td>')
183 elif key == 'code':
184 body_row_start.append('<td class="code">')
185 yield 0, ('<table class="%stable">' % self.cssclass +
186 ''.join(headers_row) +
187 ''.join(body_row_start)
188 )
189 yield 0, dummyoutfile.getvalue()
190 yield 0, '</td></tr></table>'
@@ -0,0 +1,14 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.dbmigrate.schema
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6
7 Schemas for migrations
8
9
10 :created_on: Nov 1, 2011
11 :author: marcink
12 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
13 :license: <name>, see LICENSE_FILE for more details.
14 """
@@ -0,0 +1,94 b''
1 from sqlalchemy import *
2 from sqlalchemy.exc import DatabaseError
3 from sqlalchemy.orm import relation, backref, class_mapper
4 from sqlalchemy.orm.session import Session
5 from rhodecode.model.meta import Base
6
7 class BaseModel(object):
8 """Base Model for all classess
9
10 """
11
12 @classmethod
13 def _get_keys(cls):
14 """return column names for this model """
15 return class_mapper(cls).c.keys()
16
17 def get_dict(self):
18 """return dict with keys and values corresponding
19 to this model data """
20
21 d = {}
22 for k in self._get_keys():
23 d[k] = getattr(self, k)
24 return d
25
26 def get_appstruct(self):
27 """return list with keys and values tupples corresponding
28 to this model data """
29
30 l = []
31 for k in self._get_keys():
32 l.append((k, getattr(self, k),))
33 return l
34
35 def populate_obj(self, populate_dict):
36 """populate model with data from given populate_dict"""
37
38 for k in self._get_keys():
39 if k in populate_dict:
40 setattr(self, k, populate_dict[k])
41
42 @classmethod
43 def query(cls):
44 return Session.query(cls)
45
46 @classmethod
47 def get(cls, id_):
48 if id_:
49 return cls.query().get(id_)
50
51 @classmethod
52 def getAll(cls):
53 return cls.query().all()
54
55 @classmethod
56 def delete(cls, id_):
57 obj = cls.query().get(id_)
58 Session.delete(obj)
59 Session.commit()
60
61
62 class UserFollowing(Base, BaseModel):
63 __tablename__ = 'user_followings'
64 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
65 UniqueConstraint('user_id', 'follows_user_id')
66 , {'useexisting':True})
67
68 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
69 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
70 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None)
71 follows_user_id = Column("follows_user_id", Integer(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None)
72
73 user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id')
74
75 follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
76 follows_repository = relation('Repository')
77
78
79 class CacheInvalidation(Base, BaseModel):
80 __tablename__ = 'cache_invalidation'
81 __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True})
82 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
83 cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
84 cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
85 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
86
87
88 def __init__(self, cache_key, cache_args=''):
89 self.cache_key = cache_key
90 self.cache_args = cache_args
91 self.cache_active = False
92
93 def __repr__(self):
94 return "<CacheInvalidation('%s:%s')>" % (self.cache_id, self.cache_key)
This diff has been collapsed as it changes many lines, (1098 lines changed) Show them Hide them
@@ -0,0 +1,1098 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.model.db
4 ~~~~~~~~~~~~~~~~~~
5
6 Database Models for RhodeCode
7
8 :created_on: Apr 08, 2010
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 import os
27 import logging
28 import datetime
29 import traceback
30 from datetime import date
31
32 from sqlalchemy import *
33 from sqlalchemy.ext.hybrid import hybrid_property
34 from sqlalchemy.orm import relationship, joinedload, class_mapper, validates
35 from beaker.cache import cache_region, region_invalidate
36
37 from rhodecode.lib.vcs import get_backend
38 from rhodecode.lib.vcs.utils.helpers import get_scm
39 from rhodecode.lib.vcs.exceptions import VCSError
40 from rhodecode.lib.vcs.utils.lazy import LazyProperty
41
42 from rhodecode.lib import str2bool, safe_str, get_changeset_safe, \
43 generate_api_key, safe_unicode
44 from rhodecode.lib.exceptions import UsersGroupsAssignedException
45 from rhodecode.lib.compat import json
46
47 from rhodecode.model.meta import Base, Session
48 from rhodecode.lib.caching_query import FromCache
49
50
51 log = logging.getLogger(__name__)
52
53 #==============================================================================
54 # BASE CLASSES
55 #==============================================================================
56
57 class ModelSerializer(json.JSONEncoder):
58 """
59 Simple Serializer for JSON,
60
61 usage::
62
63 to make object customized for serialization implement a __json__
64 method that will return a dict for serialization into json
65
66 example::
67
68 class Task(object):
69
70 def __init__(self, name, value):
71 self.name = name
72 self.value = value
73
74 def __json__(self):
75 return dict(name=self.name,
76 value=self.value)
77
78 """
79
80 def default(self, obj):
81
82 if hasattr(obj, '__json__'):
83 return obj.__json__()
84 else:
85 return json.JSONEncoder.default(self, obj)
86
87 class BaseModel(object):
88 """Base Model for all classess
89
90 """
91
92 @classmethod
93 def _get_keys(cls):
94 """return column names for this model """
95 return class_mapper(cls).c.keys()
96
97 def get_dict(self):
98 """return dict with keys and values corresponding
99 to this model data """
100
101 d = {}
102 for k in self._get_keys():
103 d[k] = getattr(self, k)
104 return d
105
106 def get_appstruct(self):
107 """return list with keys and values tupples corresponding
108 to this model data """
109
110 l = []
111 for k in self._get_keys():
112 l.append((k, getattr(self, k),))
113 return l
114
115 def populate_obj(self, populate_dict):
116 """populate model with data from given populate_dict"""
117
118 for k in self._get_keys():
119 if k in populate_dict:
120 setattr(self, k, populate_dict[k])
121
122 @classmethod
123 def query(cls):
124 return Session.query(cls)
125
126 @classmethod
127 def get(cls, id_):
128 if id_:
129 return cls.query().get(id_)
130
131 @classmethod
132 def getAll(cls):
133 return cls.query().all()
134
135 @classmethod
136 def delete(cls, id_):
137 obj = cls.query().get(id_)
138 Session.delete(obj)
139 Session.commit()
140
141
142 class RhodeCodeSetting(Base, BaseModel):
143 __tablename__ = 'rhodecode_settings'
144 __table_args__ = (UniqueConstraint('app_settings_name'), {'extend_existing':True})
145 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
146 app_settings_name = Column("app_settings_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
147 _app_settings_value = Column("app_settings_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
148
149 def __init__(self, k='', v=''):
150 self.app_settings_name = k
151 self.app_settings_value = v
152
153
154 @validates('_app_settings_value')
155 def validate_settings_value(self, key, val):
156 assert type(val) == unicode
157 return val
158
159 @hybrid_property
160 def app_settings_value(self):
161 v = self._app_settings_value
162 if v == 'ldap_active':
163 v = str2bool(v)
164 return v
165
166 @app_settings_value.setter
167 def app_settings_value(self, val):
168 """
169 Setter that will always make sure we use unicode in app_settings_value
170
171 :param val:
172 """
173 self._app_settings_value = safe_unicode(val)
174
175 def __repr__(self):
176 return "<%s('%s:%s')>" % (self.__class__.__name__,
177 self.app_settings_name, self.app_settings_value)
178
179
180 @classmethod
181 def get_by_name(cls, ldap_key):
182 return cls.query()\
183 .filter(cls.app_settings_name == ldap_key).scalar()
184
185 @classmethod
186 def get_app_settings(cls, cache=False):
187
188 ret = cls.query()
189
190 if cache:
191 ret = ret.options(FromCache("sql_cache_short", "get_hg_settings"))
192
193 if not ret:
194 raise Exception('Could not get application settings !')
195 settings = {}
196 for each in ret:
197 settings['rhodecode_' + each.app_settings_name] = \
198 each.app_settings_value
199
200 return settings
201
202 @classmethod
203 def get_ldap_settings(cls, cache=False):
204 ret = cls.query()\
205 .filter(cls.app_settings_name.startswith('ldap_')).all()
206 fd = {}
207 for row in ret:
208 fd.update({row.app_settings_name:row.app_settings_value})
209
210 return fd
211
212
213 class RhodeCodeUi(Base, BaseModel):
214 __tablename__ = 'rhodecode_ui'
215 __table_args__ = (UniqueConstraint('ui_key'), {'extend_existing':True})
216
217 HOOK_UPDATE = 'changegroup.update'
218 HOOK_REPO_SIZE = 'changegroup.repo_size'
219 HOOK_PUSH = 'pretxnchangegroup.push_logger'
220 HOOK_PULL = 'preoutgoing.pull_logger'
221
222 ui_id = Column("ui_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
223 ui_section = Column("ui_section", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
224 ui_key = Column("ui_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
225 ui_value = Column("ui_value", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
226 ui_active = Column("ui_active", Boolean(), nullable=True, unique=None, default=True)
227
228
229 @classmethod
230 def get_by_key(cls, key):
231 return cls.query().filter(cls.ui_key == key)
232
233
234 @classmethod
235 def get_builtin_hooks(cls):
236 q = cls.query()
237 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE,
238 cls.HOOK_REPO_SIZE,
239 cls.HOOK_PUSH, cls.HOOK_PULL]))
240 return q.all()
241
242 @classmethod
243 def get_custom_hooks(cls):
244 q = cls.query()
245 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE,
246 cls.HOOK_REPO_SIZE,
247 cls.HOOK_PUSH, cls.HOOK_PULL]))
248 q = q.filter(cls.ui_section == 'hooks')
249 return q.all()
250
251 @classmethod
252 def create_or_update_hook(cls, key, val):
253 new_ui = cls.get_by_key(key).scalar() or cls()
254 new_ui.ui_section = 'hooks'
255 new_ui.ui_active = True
256 new_ui.ui_key = key
257 new_ui.ui_value = val
258
259 Session.add(new_ui)
260 Session.commit()
261
262
263 class User(Base, BaseModel):
264 __tablename__ = 'users'
265 __table_args__ = (UniqueConstraint('username'), UniqueConstraint('email'), {'extend_existing':True})
266 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
267 username = Column("username", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
268 password = Column("password", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
269 active = Column("active", Boolean(), nullable=True, unique=None, default=None)
270 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
271 name = Column("name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
272 lastname = Column("lastname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
273 email = Column("email", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
274 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
275 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
276 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
277
278 user_log = relationship('UserLog', cascade='all')
279 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
280
281 repositories = relationship('Repository')
282 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
283 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
284
285 group_member = relationship('UsersGroupMember', cascade='all')
286
287 @property
288 def full_contact(self):
289 return '%s %s <%s>' % (self.name, self.lastname, self.email)
290
291 @property
292 def short_contact(self):
293 return '%s %s' % (self.name, self.lastname)
294
295 @property
296 def is_admin(self):
297 return self.admin
298
299 def __repr__(self):
300 try:
301 return "<%s('id:%s:%s')>" % (self.__class__.__name__,
302 self.user_id, self.username)
303 except:
304 return self.__class__.__name__
305
306 @classmethod
307 def get_by_username(cls, username, case_insensitive=False):
308 if case_insensitive:
309 return Session.query(cls).filter(cls.username.ilike(username)).scalar()
310 else:
311 return Session.query(cls).filter(cls.username == username).scalar()
312
313 @classmethod
314 def get_by_api_key(cls, api_key):
315 return cls.query().filter(cls.api_key == api_key).one()
316
317 def update_lastlogin(self):
318 """Update user lastlogin"""
319
320 self.last_login = datetime.datetime.now()
321 Session.add(self)
322 Session.commit()
323 log.debug('updated user %s lastlogin' % self.username)
324
325 @classmethod
326 def create(cls, form_data):
327 from rhodecode.lib.auth import get_crypt_password
328
329 try:
330 new_user = cls()
331 for k, v in form_data.items():
332 if k == 'password':
333 v = get_crypt_password(v)
334 setattr(new_user, k, v)
335
336 new_user.api_key = generate_api_key(form_data['username'])
337 Session.add(new_user)
338 Session.commit()
339 return new_user
340 except:
341 log.error(traceback.format_exc())
342 Session.rollback()
343 raise
344
345 class UserLog(Base, BaseModel):
346 __tablename__ = 'user_logs'
347 __table_args__ = {'extend_existing':True}
348 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
349 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
350 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
351 repository_name = Column("repository_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
352 user_ip = Column("user_ip", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
353 action = Column("action", UnicodeText(length=1200000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
354 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
355
356 @property
357 def action_as_day(self):
358 return date(*self.action_date.timetuple()[:3])
359
360 user = relationship('User')
361 repository = relationship('Repository')
362
363
364 class UsersGroup(Base, BaseModel):
365 __tablename__ = 'users_groups'
366 __table_args__ = {'extend_existing':True}
367
368 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
369 users_group_name = Column("users_group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
370 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
371
372 members = relationship('UsersGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
373
374 def __repr__(self):
375 return '<userGroup(%s)>' % (self.users_group_name)
376
377 @classmethod
378 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
379 if case_insensitive:
380 gr = cls.query()\
381 .filter(cls.users_group_name.ilike(group_name))
382 else:
383 gr = cls.query()\
384 .filter(cls.users_group_name == group_name)
385 if cache:
386 gr = gr.options(FromCache("sql_cache_short",
387 "get_user_%s" % group_name))
388 return gr.scalar()
389
390
391 @classmethod
392 def get(cls, users_group_id, cache=False):
393 users_group = cls.query()
394 if cache:
395 users_group = users_group.options(FromCache("sql_cache_short",
396 "get_users_group_%s" % users_group_id))
397 return users_group.get(users_group_id)
398
399 @classmethod
400 def create(cls, form_data):
401 try:
402 new_users_group = cls()
403 for k, v in form_data.items():
404 setattr(new_users_group, k, v)
405
406 Session.add(new_users_group)
407 Session.commit()
408 return new_users_group
409 except:
410 log.error(traceback.format_exc())
411 Session.rollback()
412 raise
413
414 @classmethod
415 def update(cls, users_group_id, form_data):
416
417 try:
418 users_group = cls.get(users_group_id, cache=False)
419
420 for k, v in form_data.items():
421 if k == 'users_group_members':
422 users_group.members = []
423 Session.flush()
424 members_list = []
425 if v:
426 v = [v] if isinstance(v, basestring) else v
427 for u_id in set(v):
428 member = UsersGroupMember(users_group_id, u_id)
429 members_list.append(member)
430 setattr(users_group, 'members', members_list)
431 setattr(users_group, k, v)
432
433 Session.add(users_group)
434 Session.commit()
435 except:
436 log.error(traceback.format_exc())
437 Session.rollback()
438 raise
439
440 @classmethod
441 def delete(cls, users_group_id):
442 try:
443
444 # check if this group is not assigned to repo
445 assigned_groups = UsersGroupRepoToPerm.query()\
446 .filter(UsersGroupRepoToPerm.users_group_id ==
447 users_group_id).all()
448
449 if assigned_groups:
450 raise UsersGroupsAssignedException('RepoGroup assigned to %s' %
451 assigned_groups)
452
453 users_group = cls.get(users_group_id, cache=False)
454 Session.delete(users_group)
455 Session.commit()
456 except:
457 log.error(traceback.format_exc())
458 Session.rollback()
459 raise
460
461 class UsersGroupMember(Base, BaseModel):
462 __tablename__ = 'users_groups_members'
463 __table_args__ = {'extend_existing':True}
464
465 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
466 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
467 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
468
469 user = relationship('User', lazy='joined')
470 users_group = relationship('UsersGroup')
471
472 def __init__(self, gr_id='', u_id=''):
473 self.users_group_id = gr_id
474 self.user_id = u_id
475
476 @staticmethod
477 def add_user_to_group(group, user):
478 ugm = UsersGroupMember()
479 ugm.users_group = group
480 ugm.user = user
481 Session.add(ugm)
482 Session.commit()
483 return ugm
484
485 class Repository(Base, BaseModel):
486 __tablename__ = 'repositories'
487 __table_args__ = (UniqueConstraint('repo_name'), {'extend_existing':True},)
488
489 repo_id = Column("repo_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
490 repo_name = Column("repo_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
491 clone_uri = Column("clone_uri", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=False, default=None)
492 repo_type = Column("repo_type", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=False, default='hg')
493 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
494 private = Column("private", Boolean(), nullable=True, unique=None, default=None)
495 enable_statistics = Column("statistics", Boolean(), nullable=True, unique=None, default=True)
496 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
497 description = Column("description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
498 created_on = Column('created_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
499
500 fork_id = Column("fork_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=False, default=None)
501 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=False, default=None)
502
503
504 user = relationship('User')
505 fork = relationship('Repository', remote_side=repo_id)
506 group = relationship('RepoGroup')
507 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
508 users_group_to_perm = relationship('UsersGroupRepoToPerm', cascade='all')
509 stats = relationship('Statistics', cascade='all', uselist=False)
510
511 followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
512
513 logs = relationship('UserLog', cascade='all')
514
515 def __repr__(self):
516 return "<%s('%s:%s')>" % (self.__class__.__name__,
517 self.repo_id, self.repo_name)
518
519 @classmethod
520 def url_sep(cls):
521 return '/'
522
523 @classmethod
524 def get_by_repo_name(cls, repo_name):
525 q = Session.query(cls).filter(cls.repo_name == repo_name)
526 q = q.options(joinedload(Repository.fork))\
527 .options(joinedload(Repository.user))\
528 .options(joinedload(Repository.group))
529 return q.one()
530
531 @classmethod
532 def get_repo_forks(cls, repo_id):
533 return cls.query().filter(Repository.fork_id == repo_id)
534
535 @classmethod
536 def base_path(cls):
537 """
538 Returns base path when all repos are stored
539
540 :param cls:
541 """
542 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
543 cls.url_sep())
544 q.options(FromCache("sql_cache_short", "repository_repo_path"))
545 return q.one().ui_value
546
547 @property
548 def just_name(self):
549 return self.repo_name.split(Repository.url_sep())[-1]
550
551 @property
552 def groups_with_parents(self):
553 groups = []
554 if self.group is None:
555 return groups
556
557 cur_gr = self.group
558 groups.insert(0, cur_gr)
559 while 1:
560 gr = getattr(cur_gr, 'parent_group', None)
561 cur_gr = cur_gr.parent_group
562 if gr is None:
563 break
564 groups.insert(0, gr)
565
566 return groups
567
568 @property
569 def groups_and_repo(self):
570 return self.groups_with_parents, self.just_name
571
572 @LazyProperty
573 def repo_path(self):
574 """
575 Returns base full path for that repository means where it actually
576 exists on a filesystem
577 """
578 q = Session.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key ==
579 Repository.url_sep())
580 q.options(FromCache("sql_cache_short", "repository_repo_path"))
581 return q.one().ui_value
582
583 @property
584 def repo_full_path(self):
585 p = [self.repo_path]
586 # we need to split the name by / since this is how we store the
587 # names in the database, but that eventually needs to be converted
588 # into a valid system path
589 p += self.repo_name.split(Repository.url_sep())
590 return os.path.join(*p)
591
592 def get_new_name(self, repo_name):
593 """
594 returns new full repository name based on assigned group and new new
595
596 :param group_name:
597 """
598 path_prefix = self.group.full_path_splitted if self.group else []
599 return Repository.url_sep().join(path_prefix + [repo_name])
600
601 @property
602 def _ui(self):
603 """
604 Creates an db based ui object for this repository
605 """
606 from mercurial import ui
607 from mercurial import config
608 baseui = ui.ui()
609
610 #clean the baseui object
611 baseui._ocfg = config.config()
612 baseui._ucfg = config.config()
613 baseui._tcfg = config.config()
614
615
616 ret = RhodeCodeUi.query()\
617 .options(FromCache("sql_cache_short", "repository_repo_ui")).all()
618
619 hg_ui = ret
620 for ui_ in hg_ui:
621 if ui_.ui_active:
622 log.debug('settings ui from db[%s]%s:%s', ui_.ui_section,
623 ui_.ui_key, ui_.ui_value)
624 baseui.setconfig(ui_.ui_section, ui_.ui_key, ui_.ui_value)
625
626 return baseui
627
628 @classmethod
629 def is_valid(cls, repo_name):
630 """
631 returns True if given repo name is a valid filesystem repository
632
633 :param cls:
634 :param repo_name:
635 """
636 from rhodecode.lib.utils import is_valid_repo
637
638 return is_valid_repo(repo_name, cls.base_path())
639
640
641 #==========================================================================
642 # SCM PROPERTIES
643 #==========================================================================
644
645 def get_changeset(self, rev):
646 return get_changeset_safe(self.scm_instance, rev)
647
648 @property
649 def tip(self):
650 return self.get_changeset('tip')
651
652 @property
653 def author(self):
654 return self.tip.author
655
656 @property
657 def last_change(self):
658 return self.scm_instance.last_change
659
660 #==========================================================================
661 # SCM CACHE INSTANCE
662 #==========================================================================
663
664 @property
665 def invalidate(self):
666 return CacheInvalidation.invalidate(self.repo_name)
667
668 def set_invalidate(self):
669 """
670 set a cache for invalidation for this instance
671 """
672 CacheInvalidation.set_invalidate(self.repo_name)
673
674 @LazyProperty
675 def scm_instance(self):
676 return self.__get_instance()
677
678 @property
679 def scm_instance_cached(self):
680 @cache_region('long_term')
681 def _c(repo_name):
682 return self.__get_instance()
683 rn = self.repo_name
684
685 inv = self.invalidate
686 if inv is not None:
687 region_invalidate(_c, None, rn)
688 # update our cache
689 CacheInvalidation.set_valid(inv.cache_key)
690 return _c(rn)
691
692 def __get_instance(self):
693
694 repo_full_path = self.repo_full_path
695
696 try:
697 alias = get_scm(repo_full_path)[0]
698 log.debug('Creating instance of %s repository' % alias)
699 backend = get_backend(alias)
700 except VCSError:
701 log.error(traceback.format_exc())
702 log.error('Perhaps this repository is in db and not in '
703 'filesystem run rescan repositories with '
704 '"destroy old data " option from admin panel')
705 return
706
707 if alias == 'hg':
708
709 repo = backend(safe_str(repo_full_path), create=False,
710 baseui=self._ui)
711 # skip hidden web repository
712 if repo._get_hidden():
713 return
714 else:
715 repo = backend(repo_full_path, create=False)
716
717 return repo
718
719
720 class RepoGroup(Base, BaseModel):
721 __tablename__ = 'groups'
722 __table_args__ = (UniqueConstraint('group_name', 'group_parent_id'),
723 CheckConstraint('group_id != group_parent_id'), {'extend_existing':True},)
724 __mapper_args__ = {'order_by':'group_name'}
725
726 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
727 group_name = Column("group_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=False, unique=True, default=None)
728 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
729 group_description = Column("group_description", String(length=10000, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
730
731 parent_group = relationship('RepoGroup', remote_side=group_id)
732
733
734 def __init__(self, group_name='', parent_group=None):
735 self.group_name = group_name
736 self.parent_group = parent_group
737
738 def __repr__(self):
739 return "<%s('%s:%s')>" % (self.__class__.__name__, self.group_id,
740 self.group_name)
741
742 @classmethod
743 def groups_choices(cls):
744 from webhelpers.html import literal as _literal
745 repo_groups = [('', '')]
746 sep = ' &raquo; '
747 _name = lambda k: _literal(sep.join(k))
748
749 repo_groups.extend([(x.group_id, _name(x.full_path_splitted))
750 for x in cls.query().all()])
751
752 repo_groups = sorted(repo_groups, key=lambda t: t[1].split(sep)[0])
753 return repo_groups
754
755 @classmethod
756 def url_sep(cls):
757 return '/'
758
759 @classmethod
760 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
761 if case_insensitive:
762 gr = cls.query()\
763 .filter(cls.group_name.ilike(group_name))
764 else:
765 gr = cls.query()\
766 .filter(cls.group_name == group_name)
767 if cache:
768 gr = gr.options(FromCache("sql_cache_short",
769 "get_group_%s" % group_name))
770 return gr.scalar()
771
772 @property
773 def parents(self):
774 parents_recursion_limit = 5
775 groups = []
776 if self.parent_group is None:
777 return groups
778 cur_gr = self.parent_group
779 groups.insert(0, cur_gr)
780 cnt = 0
781 while 1:
782 cnt += 1
783 gr = getattr(cur_gr, 'parent_group', None)
784 cur_gr = cur_gr.parent_group
785 if gr is None:
786 break
787 if cnt == parents_recursion_limit:
788 # this will prevent accidental infinit loops
789 log.error('group nested more than %s' %
790 parents_recursion_limit)
791 break
792
793 groups.insert(0, gr)
794 return groups
795
796 @property
797 def children(self):
798 return Group.query().filter(Group.parent_group == self)
799
800 @property
801 def name(self):
802 return self.group_name.split(Group.url_sep())[-1]
803
804 @property
805 def full_path(self):
806 return self.group_name
807
808 @property
809 def full_path_splitted(self):
810 return self.group_name.split(Group.url_sep())
811
812 @property
813 def repositories(self):
814 return Repository.query().filter(Repository.group == self)
815
816 @property
817 def repositories_recursive_count(self):
818 cnt = self.repositories.count()
819
820 def children_count(group):
821 cnt = 0
822 for child in group.children:
823 cnt += child.repositories.count()
824 cnt += children_count(child)
825 return cnt
826
827 return cnt + children_count(self)
828
829
830 def get_new_name(self, group_name):
831 """
832 returns new full group name based on parent and new name
833
834 :param group_name:
835 """
836 path_prefix = (self.parent_group.full_path_splitted if
837 self.parent_group else [])
838 return Group.url_sep().join(path_prefix + [group_name])
839
840
841 class Permission(Base, BaseModel):
842 __tablename__ = 'permissions'
843 __table_args__ = {'extend_existing':True}
844 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
845 permission_name = Column("permission_name", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
846 permission_longname = Column("permission_longname", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
847
848 def __repr__(self):
849 return "<%s('%s:%s')>" % (self.__class__.__name__,
850 self.permission_id, self.permission_name)
851
852 @classmethod
853 def get_by_key(cls, key):
854 return cls.query().filter(cls.permission_name == key).scalar()
855
856 class UserRepoToPerm(Base, BaseModel):
857 __tablename__ = 'repo_to_perm'
858 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'extend_existing':True})
859 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
860 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
861 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
862 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
863
864 user = relationship('User')
865 permission = relationship('Permission')
866 repository = relationship('Repository')
867
868 class UserToPerm(Base, BaseModel):
869 __tablename__ = 'user_to_perm'
870 __table_args__ = (UniqueConstraint('user_id', 'permission_id'), {'extend_existing':True})
871 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
872 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
873 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
874
875 user = relationship('User')
876 permission = relationship('Permission')
877
878 @classmethod
879 def has_perm(cls, user_id, perm):
880 if not isinstance(perm, Permission):
881 raise Exception('perm needs to be an instance of Permission class')
882
883 return cls.query().filter(cls.user_id == user_id)\
884 .filter(cls.permission == perm).scalar() is not None
885
886 @classmethod
887 def grant_perm(cls, user_id, perm):
888 if not isinstance(perm, Permission):
889 raise Exception('perm needs to be an instance of Permission class')
890
891 new = cls()
892 new.user_id = user_id
893 new.permission = perm
894 try:
895 Session.add(new)
896 Session.commit()
897 except:
898 Session.rollback()
899
900
901 @classmethod
902 def revoke_perm(cls, user_id, perm):
903 if not isinstance(perm, Permission):
904 raise Exception('perm needs to be an instance of Permission class')
905
906 try:
907 cls.query().filter(cls.user_id == user_id)\
908 .filter(cls.permission == perm).delete()
909 Session.commit()
910 except:
911 Session.rollback()
912
913 class UsersGroupRepoToPerm(Base, BaseModel):
914 __tablename__ = 'users_group_repo_to_perm'
915 __table_args__ = (UniqueConstraint('repository_id', 'users_group_id', 'permission_id'), {'extend_existing':True})
916 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
917 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
918 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
919 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
920
921 users_group = relationship('UsersGroup')
922 permission = relationship('Permission')
923 repository = relationship('Repository')
924
925 def __repr__(self):
926 return '<userGroup:%s => %s >' % (self.users_group, self.repository)
927
928 class UsersGroupToPerm(Base, BaseModel):
929 __tablename__ = 'users_group_to_perm'
930 __table_args__ = {'extend_existing':True}
931 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
932 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
933 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
934
935 users_group = relationship('UsersGroup')
936 permission = relationship('Permission')
937
938
939 @classmethod
940 def has_perm(cls, users_group_id, perm):
941 if not isinstance(perm, Permission):
942 raise Exception('perm needs to be an instance of Permission class')
943
944 return cls.query().filter(cls.users_group_id ==
945 users_group_id)\
946 .filter(cls.permission == perm)\
947 .scalar() is not None
948
949 @classmethod
950 def grant_perm(cls, users_group_id, perm):
951 if not isinstance(perm, Permission):
952 raise Exception('perm needs to be an instance of Permission class')
953
954 new = cls()
955 new.users_group_id = users_group_id
956 new.permission = perm
957 try:
958 Session.add(new)
959 Session.commit()
960 except:
961 Session.rollback()
962
963
964 @classmethod
965 def revoke_perm(cls, users_group_id, perm):
966 if not isinstance(perm, Permission):
967 raise Exception('perm needs to be an instance of Permission class')
968
969 try:
970 cls.query().filter(cls.users_group_id == users_group_id)\
971 .filter(cls.permission == perm).delete()
972 Session.commit()
973 except:
974 Session.rollback()
975
976
977 class UserRepoGroupToPerm(Base, BaseModel):
978 __tablename__ = 'group_to_perm'
979 __table_args__ = (UniqueConstraint('group_id', 'permission_id'), {'extend_existing':True})
980
981 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
982 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
983 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
984 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
985
986 user = relationship('User')
987 permission = relationship('Permission')
988 group = relationship('RepoGroup')
989
990 class Statistics(Base, BaseModel):
991 __tablename__ = 'statistics'
992 __table_args__ = (UniqueConstraint('repository_id'), {'extend_existing':True})
993 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
994 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
995 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
996 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
997 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
998 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
999
1000 repository = relationship('Repository', single_parent=True)
1001
1002 class UserFollowing(Base, BaseModel):
1003 __tablename__ = 'user_followings'
1004 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
1005 UniqueConstraint('user_id', 'follows_user_id')
1006 , {'extend_existing':True})
1007
1008 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1009 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1010 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
1011 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1012 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
1013
1014 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1015
1016 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1017 follows_repository = relationship('Repository', order_by='Repository.repo_name')
1018
1019
1020 @classmethod
1021 def get_repo_followers(cls, repo_id):
1022 return cls.query().filter(cls.follows_repo_id == repo_id)
1023
1024 class CacheInvalidation(Base, BaseModel):
1025 __tablename__ = 'cache_invalidation'
1026 __table_args__ = (UniqueConstraint('cache_key'), {'extend_existing':True})
1027 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1028 cache_key = Column("cache_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1029 cache_args = Column("cache_args", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
1030 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
1031
1032
1033 def __init__(self, cache_key, cache_args=''):
1034 self.cache_key = cache_key
1035 self.cache_args = cache_args
1036 self.cache_active = False
1037
1038 def __repr__(self):
1039 return "<%s('%s:%s')>" % (self.__class__.__name__,
1040 self.cache_id, self.cache_key)
1041
1042 @classmethod
1043 def invalidate(cls, key):
1044 """
1045 Returns Invalidation object if this given key should be invalidated
1046 None otherwise. `cache_active = False` means that this cache
1047 state is not valid and needs to be invalidated
1048
1049 :param key:
1050 """
1051 return cls.query()\
1052 .filter(CacheInvalidation.cache_key == key)\
1053 .filter(CacheInvalidation.cache_active == False)\
1054 .scalar()
1055
1056 @classmethod
1057 def set_invalidate(cls, key):
1058 """
1059 Mark this Cache key for invalidation
1060
1061 :param key:
1062 """
1063
1064 log.debug('marking %s for invalidation' % key)
1065 inv_obj = Session.query(cls)\
1066 .filter(cls.cache_key == key).scalar()
1067 if inv_obj:
1068 inv_obj.cache_active = False
1069 else:
1070 log.debug('cache key not found in invalidation db -> creating one')
1071 inv_obj = CacheInvalidation(key)
1072
1073 try:
1074 Session.add(inv_obj)
1075 Session.commit()
1076 except Exception:
1077 log.error(traceback.format_exc())
1078 Session.rollback()
1079
1080 @classmethod
1081 def set_valid(cls, key):
1082 """
1083 Mark this cache key as active and currently cached
1084
1085 :param key:
1086 """
1087 inv_obj = Session.query(CacheInvalidation)\
1088 .filter(CacheInvalidation.cache_key == key).scalar()
1089 inv_obj.cache_active = True
1090 Session.add(inv_obj)
1091 Session.commit()
1092
1093 class DbMigrateVersion(Base, BaseModel):
1094 __tablename__ = 'db_migrate_version'
1095 __table_args__ = {'extend_existing':True}
1096 repository_id = Column('repository_id', String(250), primary_key=True)
1097 repository_path = Column('repository_path', Text)
1098 version = Column('version', Integer)
@@ -0,0 +1,28 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.model.db
4 ~~~~~~~~~~~~~~~~~~
5
6 Database Models for RhodeCode
7
8 :created_on: Apr 08, 2010
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
26 #TODO: when branch 1.3 is finished replacem with db.py content
27
28 from rhodecode.model.db import *
@@ -0,0 +1,74 b''
1 import logging
2 import datetime
3
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper
7 from sqlalchemy.orm.session import Session
8
9 from rhodecode.lib.dbmigrate.migrate import *
10 from rhodecode.lib.dbmigrate.migrate.changeset import *
11
12 from rhodecode.model.meta import Base
13
14 log = logging.getLogger(__name__)
15
16
17 def upgrade(migrate_engine):
18 """ Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 #==========================================================================
22 # Add table `users_group_repo_group_to_perm`
23 #==========================================================================
24 from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UsersGroupRepoGroupToPerm
25 UsersGroupRepoGroupToPerm().__table__.create()
26
27 #==========================================================================
28 # Add table `changeset_comments`
29 #==========================================================================
30 from rhodecode.lib.dbmigrate.schema.db_1_3_0 import ChangesetComment
31 ChangesetComment().__table__.create()
32
33 #==========================================================================
34 # Add table `notifications`
35 #==========================================================================
36 from rhodecode.lib.dbmigrate.schema.db_1_3_0 import Notification
37 Notification().__table__.create()
38
39 #==========================================================================
40 # Add table `user_to_notification`
41 #==========================================================================
42 from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UserNotification
43 UserNotification().__table__.create()
44
45 #==========================================================================
46 # Add unique to table `users_group_to_perm`
47 #==========================================================================
48 from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UsersGroupToPerm
49 tbl = UsersGroupToPerm().__table__
50 cons = UniqueConstraint('users_group_id', 'permission_id', table=tbl)
51 cons.create()
52
53 #==========================================================================
54 # Fix unique constrain on table `user_logs`
55 #==========================================================================
56 from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UserLog
57 tbl = UserLog().__table__
58 col = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'),
59 nullable=False, unique=None, default=None)
60 col.alter(nullable=True, table=tbl)
61
62 #==========================================================================
63 # Rename table `group_to_perm` to `user_repo_group_to_perm`
64 #==========================================================================
65 tbl = Table('group_to_perm', MetaData(bind=migrate_engine), autoload=True,
66 autoload_with=migrate_engine)
67 tbl.rename('user_repo_group_to_perm')
68
69 return
70
71
72 def downgrade(migrate_engine):
73 meta = MetaData()
74 meta.bind = migrate_engine
@@ -0,0 +1,65 b''
1 import logging
2 import datetime
3
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper
7 from sqlalchemy.orm.session import Session
8
9 from rhodecode.lib.dbmigrate.migrate import *
10 from rhodecode.lib.dbmigrate.migrate.changeset import *
11
12 from rhodecode.model.meta import Base
13
14 log = logging.getLogger(__name__)
15
16
17 def upgrade(migrate_engine):
18 """ Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21
22 #==========================================================================
23 # Change unique constraints of table `repo_to_perm`
24 #==========================================================================
25 from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UserRepoToPerm
26 tbl = UserRepoToPerm().__table__
27 new_cons = UniqueConstraint('user_id', 'repository_id', 'permission_id', table=tbl)
28 new_cons.create()
29
30 if migrate_engine.name in ['mysql']:
31 old_cons = UniqueConstraint('user_id', 'repository_id', table=tbl, name="user_id")
32 old_cons.drop()
33 elif migrate_engine.name in ['postgresql']:
34 old_cons = UniqueConstraint('user_id', 'repository_id', table=tbl)
35 old_cons.drop()
36 else:
37 # sqlite doesn't support dropping constraints...
38 print """Please manually drop UniqueConstraint('user_id', 'repository_id')"""
39
40 #==========================================================================
41 # fix uniques of table `user_repo_group_to_perm`
42 #==========================================================================
43 from rhodecode.lib.dbmigrate.schema.db_1_3_0 import UserRepoGroupToPerm
44 tbl = UserRepoGroupToPerm().__table__
45 new_cons = UniqueConstraint('group_id', 'permission_id', 'user_id', table=tbl)
46 new_cons.create()
47
48 # fix uniqueConstraints
49 if migrate_engine.name in ['mysql']:
50 #mysql is givinig troubles here...
51 old_cons = UniqueConstraint('group_id', 'permission_id', table=tbl, name="group_id")
52 old_cons.drop()
53 elif migrate_engine.name in ['postgresql']:
54 old_cons = UniqueConstraint('group_id', 'permission_id', table=tbl, name='group_to_perm_group_id_permission_id_key')
55 old_cons.drop()
56 else:
57 # sqlite doesn't support dropping constraints...
58 print """Please manually drop UniqueConstraint('group_id', 'permission_id')"""
59
60 return
61
62
63 def downgrade(migrate_engine):
64 meta = MetaData()
65 meta.bind = migrate_engine
This diff has been collapsed as it changes many lines, (517 lines changed) Show them Hide them
@@ -0,0 +1,517 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.diffs
4 ~~~~~~~~~~~~~~~~~~~
5
6 Set of diffing helpers, previously part of vcs
7
8
9 :created_on: Dec 4, 2011
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 :original copyright: 2007-2008 by Armin Ronacher
13 :license: GPLv3, see COPYING for more details.
14 """
15 # This program is free software: you can redistribute it and/or modify
16 # it under the terms of the GNU General Public License as published by
17 # the Free Software Foundation, either version 3 of the License, or
18 # (at your option) any later version.
19 #
20 # This program is distributed in the hope that it will be useful,
21 # but WITHOUT ANY WARRANTY; without even the implied warranty of
22 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
23 # GNU General Public License for more details.
24 #
25 # You should have received a copy of the GNU General Public License
26 # along with this program. If not, see <http://www.gnu.org/licenses/>.
27
28 import re
29 import difflib
30 import markupsafe
31 from itertools import tee, imap
32
33 from pylons.i18n.translation import _
34
35 from rhodecode.lib.vcs.exceptions import VCSError
36 from rhodecode.lib.vcs.nodes import FileNode
37
38 from rhodecode.lib.utils import EmptyChangeset
39
40
41 def wrap_to_table(str_):
42 return '''<table class="code-difftable">
43 <tr class="line no-comment">
44 <td class="lineno new"></td>
45 <td class="code no-comment"><pre>%s</pre></td>
46 </tr>
47 </table>''' % str_
48
49
50 def wrapped_diff(filenode_old, filenode_new, cut_off_limit=None,
51 ignore_whitespace=True, line_context=3,
52 enable_comments=False):
53 """
54 returns a wrapped diff into a table, checks for cut_off_limit and presents
55 proper message
56 """
57
58 if filenode_old is None:
59 filenode_old = FileNode(filenode_new.path, '', EmptyChangeset())
60
61 if filenode_old.is_binary or filenode_new.is_binary:
62 diff = wrap_to_table(_('binary file'))
63 stats = (0, 0)
64 size = 0
65
66 elif cut_off_limit != -1 and (cut_off_limit is None or
67 (filenode_old.size < cut_off_limit and filenode_new.size < cut_off_limit)):
68
69 f_gitdiff = get_gitdiff(filenode_old, filenode_new,
70 ignore_whitespace=ignore_whitespace,
71 context=line_context)
72 diff_processor = DiffProcessor(f_gitdiff, format='gitdiff')
73
74 diff = diff_processor.as_html(enable_comments=enable_comments)
75 stats = diff_processor.stat()
76 size = len(diff or '')
77 else:
78 diff = wrap_to_table(_('Changeset was to big and was cut off, use '
79 'diff menu to display this diff'))
80 stats = (0, 0)
81 size = 0
82
83 if not diff:
84 diff = wrap_to_table(_('No changes detected'))
85
86 cs1 = filenode_old.last_changeset.raw_id
87 cs2 = filenode_new.last_changeset.raw_id
88
89 return size, cs1, cs2, diff, stats
90
91
92 def get_gitdiff(filenode_old, filenode_new, ignore_whitespace=True, context=3):
93 """
94 Returns git style diff between given ``filenode_old`` and ``filenode_new``.
95
96 :param ignore_whitespace: ignore whitespaces in diff
97 """
98 # make sure we pass in default context
99 context = context or 3
100
101 for filenode in (filenode_old, filenode_new):
102 if not isinstance(filenode, FileNode):
103 raise VCSError("Given object should be FileNode object, not %s"
104 % filenode.__class__)
105
106 repo = filenode_new.changeset.repository
107 old_raw_id = getattr(filenode_old.changeset, 'raw_id', repo.EMPTY_CHANGESET)
108 new_raw_id = getattr(filenode_new.changeset, 'raw_id', repo.EMPTY_CHANGESET)
109
110 vcs_gitdiff = repo.get_diff(old_raw_id, new_raw_id, filenode_new.path,
111 ignore_whitespace, context)
112
113 return vcs_gitdiff
114
115
116 class DiffProcessor(object):
117 """
118 Give it a unified diff and it returns a list of the files that were
119 mentioned in the diff together with a dict of meta information that
120 can be used to render it in a HTML template.
121 """
122 _chunk_re = re.compile(r'@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)')
123
124 def __init__(self, diff, differ='diff', format='udiff'):
125 """
126 :param diff: a text in diff format or generator
127 :param format: format of diff passed, `udiff` or `gitdiff`
128 """
129 if isinstance(diff, basestring):
130 diff = [diff]
131
132 self.__udiff = diff
133 self.__format = format
134 self.adds = 0
135 self.removes = 0
136
137 if isinstance(self.__udiff, basestring):
138 self.lines = iter(self.__udiff.splitlines(1))
139
140 elif self.__format == 'gitdiff':
141 udiff_copy = self.copy_iterator()
142 self.lines = imap(self.escaper, self._parse_gitdiff(udiff_copy))
143 else:
144 udiff_copy = self.copy_iterator()
145 self.lines = imap(self.escaper, udiff_copy)
146
147 # Select a differ.
148 if differ == 'difflib':
149 self.differ = self._highlight_line_difflib
150 else:
151 self.differ = self._highlight_line_udiff
152
153 def escaper(self, string):
154 return markupsafe.escape(string)
155
156 def copy_iterator(self):
157 """
158 make a fresh copy of generator, we should not iterate thru
159 an original as it's needed for repeating operations on
160 this instance of DiffProcessor
161 """
162 self.__udiff, iterator_copy = tee(self.__udiff)
163 return iterator_copy
164
165 def _extract_rev(self, line1, line2):
166 """
167 Extract the filename and revision hint from a line.
168 """
169
170 try:
171 if line1.startswith('--- ') and line2.startswith('+++ '):
172 l1 = line1[4:].split(None, 1)
173 old_filename = (l1[0].replace('a/', '', 1)
174 if len(l1) >= 1 else None)
175 old_rev = l1[1] if len(l1) == 2 else 'old'
176
177 l2 = line2[4:].split(None, 1)
178 new_filename = (l2[0].replace('b/', '', 1)
179 if len(l1) >= 1 else None)
180 new_rev = l2[1] if len(l2) == 2 else 'new'
181
182 filename = (old_filename
183 if old_filename != '/dev/null' else new_filename)
184
185 return filename, new_rev, old_rev
186 except (ValueError, IndexError):
187 pass
188
189 return None, None, None
190
191 def _parse_gitdiff(self, diffiterator):
192 def line_decoder(l):
193 if l.startswith('+') and not l.startswith('+++'):
194 self.adds += 1
195 elif l.startswith('-') and not l.startswith('---'):
196 self.removes += 1
197 return l.decode('utf8', 'replace')
198
199 output = list(diffiterator)
200 size = len(output)
201
202 if size == 2:
203 l = []
204 l.extend([output[0]])
205 l.extend(output[1].splitlines(1))
206 return map(line_decoder, l)
207 elif size == 1:
208 return map(line_decoder, output[0].splitlines(1))
209 elif size == 0:
210 return []
211
212 raise Exception('wrong size of diff %s' % size)
213
214 def _highlight_line_difflib(self, line, next_):
215 """
216 Highlight inline changes in both lines.
217 """
218
219 if line['action'] == 'del':
220 old, new = line, next_
221 else:
222 old, new = next_, line
223
224 oldwords = re.split(r'(\W)', old['line'])
225 newwords = re.split(r'(\W)', new['line'])
226
227 sequence = difflib.SequenceMatcher(None, oldwords, newwords)
228
229 oldfragments, newfragments = [], []
230 for tag, i1, i2, j1, j2 in sequence.get_opcodes():
231 oldfrag = ''.join(oldwords[i1:i2])
232 newfrag = ''.join(newwords[j1:j2])
233 if tag != 'equal':
234 if oldfrag:
235 oldfrag = '<del>%s</del>' % oldfrag
236 if newfrag:
237 newfrag = '<ins>%s</ins>' % newfrag
238 oldfragments.append(oldfrag)
239 newfragments.append(newfrag)
240
241 old['line'] = "".join(oldfragments)
242 new['line'] = "".join(newfragments)
243
244 def _highlight_line_udiff(self, line, next_):
245 """
246 Highlight inline changes in both lines.
247 """
248 start = 0
249 limit = min(len(line['line']), len(next_['line']))
250 while start < limit and line['line'][start] == next_['line'][start]:
251 start += 1
252 end = -1
253 limit -= start
254 while -end <= limit and line['line'][end] == next_['line'][end]:
255 end -= 1
256 end += 1
257 if start or end:
258 def do(l):
259 last = end + len(l['line'])
260 if l['action'] == 'add':
261 tag = 'ins'
262 else:
263 tag = 'del'
264 l['line'] = '%s<%s>%s</%s>%s' % (
265 l['line'][:start],
266 tag,
267 l['line'][start:last],
268 tag,
269 l['line'][last:]
270 )
271 do(line)
272 do(next_)
273
274 def _parse_udiff(self):
275 """
276 Parse the diff an return data for the template.
277 """
278 lineiter = self.lines
279 files = []
280 try:
281 line = lineiter.next()
282 # skip first context
283 skipfirst = True
284 while 1:
285 # continue until we found the old file
286 if not line.startswith('--- '):
287 line = lineiter.next()
288 continue
289
290 chunks = []
291 filename, old_rev, new_rev = \
292 self._extract_rev(line, lineiter.next())
293 files.append({
294 'filename': filename,
295 'old_revision': old_rev,
296 'new_revision': new_rev,
297 'chunks': chunks
298 })
299
300 line = lineiter.next()
301 while line:
302 match = self._chunk_re.match(line)
303 if not match:
304 break
305
306 lines = []
307 chunks.append(lines)
308
309 old_line, old_end, new_line, new_end = \
310 [int(x or 1) for x in match.groups()[:-1]]
311 old_line -= 1
312 new_line -= 1
313 context = len(match.groups()) == 5
314 old_end += old_line
315 new_end += new_line
316
317 if context:
318 if not skipfirst:
319 lines.append({
320 'old_lineno': '...',
321 'new_lineno': '...',
322 'action': 'context',
323 'line': line,
324 })
325 else:
326 skipfirst = False
327
328 line = lineiter.next()
329 while old_line < old_end or new_line < new_end:
330 if line:
331 command, line = line[0], line[1:]
332 else:
333 command = ' '
334 affects_old = affects_new = False
335
336 # ignore those if we don't expect them
337 if command in '#@':
338 continue
339 elif command == '+':
340 affects_new = True
341 action = 'add'
342 elif command == '-':
343 affects_old = True
344 action = 'del'
345 else:
346 affects_old = affects_new = True
347 action = 'unmod'
348
349 old_line += affects_old
350 new_line += affects_new
351 lines.append({
352 'old_lineno': affects_old and old_line or '',
353 'new_lineno': affects_new and new_line or '',
354 'action': action,
355 'line': line
356 })
357 line = lineiter.next()
358
359 except StopIteration:
360 pass
361
362 # highlight inline changes
363 for _ in files:
364 for chunk in chunks:
365 lineiter = iter(chunk)
366 #first = True
367 try:
368 while 1:
369 line = lineiter.next()
370 if line['action'] != 'unmod':
371 nextline = lineiter.next()
372 if nextline['action'] == 'unmod' or \
373 nextline['action'] == line['action']:
374 continue
375 self.differ(line, nextline)
376 except StopIteration:
377 pass
378
379 return files
380
381 def prepare(self):
382 """
383 Prepare the passed udiff for HTML rendering. It'l return a list
384 of dicts
385 """
386 return self._parse_udiff()
387
388 def _safe_id(self, idstring):
389 """Make a string safe for including in an id attribute.
390
391 The HTML spec says that id attributes 'must begin with
392 a letter ([A-Za-z]) and may be followed by any number
393 of letters, digits ([0-9]), hyphens ("-"), underscores
394 ("_"), colons (":"), and periods (".")'. These regexps
395 are slightly over-zealous, in that they remove colons
396 and periods unnecessarily.
397
398 Whitespace is transformed into underscores, and then
399 anything which is not a hyphen or a character that
400 matches \w (alphanumerics and underscore) is removed.
401
402 """
403 # Transform all whitespace to underscore
404 idstring = re.sub(r'\s', "_", '%s' % idstring)
405 # Remove everything that is not a hyphen or a member of \w
406 idstring = re.sub(r'(?!-)\W', "", idstring).lower()
407 return idstring
408
409 def raw_diff(self):
410 """
411 Returns raw string as udiff
412 """
413 udiff_copy = self.copy_iterator()
414 if self.__format == 'gitdiff':
415 udiff_copy = self._parse_gitdiff(udiff_copy)
416 return u''.join(udiff_copy)
417
418 def as_html(self, table_class='code-difftable', line_class='line',
419 new_lineno_class='lineno old', old_lineno_class='lineno new',
420 code_class='code', enable_comments=False):
421 """
422 Return udiff as html table with customized css classes
423 """
424 def _link_to_if(condition, label, url):
425 """
426 Generates a link if condition is meet or just the label if not.
427 """
428
429 if condition:
430 return '''<a href="%(url)s">%(label)s</a>''' % {
431 'url': url,
432 'label': label
433 }
434 else:
435 return label
436 diff_lines = self.prepare()
437 _html_empty = True
438 _html = []
439 _html.append('''<table class="%(table_class)s">\n''' % {
440 'table_class': table_class
441 })
442 for diff in diff_lines:
443 for line in diff['chunks']:
444 _html_empty = False
445 for change in line:
446 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
447 'lc': line_class,
448 'action': change['action']
449 })
450 anchor_old_id = ''
451 anchor_new_id = ''
452 anchor_old = "%(filename)s_o%(oldline_no)s" % {
453 'filename': self._safe_id(diff['filename']),
454 'oldline_no': change['old_lineno']
455 }
456 anchor_new = "%(filename)s_n%(oldline_no)s" % {
457 'filename': self._safe_id(diff['filename']),
458 'oldline_no': change['new_lineno']
459 }
460 cond_old = (change['old_lineno'] != '...' and
461 change['old_lineno'])
462 cond_new = (change['new_lineno'] != '...' and
463 change['new_lineno'])
464 if cond_old:
465 anchor_old_id = 'id="%s"' % anchor_old
466 if cond_new:
467 anchor_new_id = 'id="%s"' % anchor_new
468 ###########################################################
469 # OLD LINE NUMBER
470 ###########################################################
471 _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
472 'a_id': anchor_old_id,
473 'olc': old_lineno_class
474 })
475
476 _html.append('''%(link)s''' % {
477 'link': _link_to_if(True, change['old_lineno'],
478 '#%s' % anchor_old)
479 })
480 _html.append('''</td>\n''')
481 ###########################################################
482 # NEW LINE NUMBER
483 ###########################################################
484
485 _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
486 'a_id': anchor_new_id,
487 'nlc': new_lineno_class
488 })
489
490 _html.append('''%(link)s''' % {
491 'link': _link_to_if(True, change['new_lineno'],
492 '#%s' % anchor_new)
493 })
494 _html.append('''</td>\n''')
495 ###########################################################
496 # CODE
497 ###########################################################
498 comments = '' if enable_comments else 'no-comment'
499 _html.append('''\t<td class="%(cc)s %(inc)s">''' % {
500 'cc': code_class,
501 'inc': comments
502 })
503 _html.append('''\n\t\t<pre>%(code)s</pre>\n''' % {
504 'code': change['line']
505 })
506 _html.append('''\t</td>''')
507 _html.append('''\n</tr>\n''')
508 _html.append('''</table>''')
509 if _html_empty:
510 return None
511 return ''.join(_html)
512
513 def stat(self):
514 """
515 Returns tuple of added, and removed lines for this instance
516 """
517 return self.adds, self.removes
@@ -0,0 +1,137 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.lib.markup_renderer
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6
7 Renderer for markup languages with ability to parse using rst or markdown
8
9 :created_on: Oct 27, 2011
10 :author: marcink
11 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
12 :license: GPLv3, see COPYING for more details.
13 """
14 # This program is free software: you can redistribute it and/or modify
15 # it under the terms of the GNU General Public License as published by
16 # the Free Software Foundation, either version 3 of the License, or
17 # (at your option) any later version.
18 #
19 # This program is distributed in the hope that it will be useful,
20 # but WITHOUT ANY WARRANTY; without even the implied warranty of
21 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 # GNU General Public License for more details.
23 #
24 # You should have received a copy of the GNU General Public License
25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26
27 import re
28 import logging
29
30 from rhodecode.lib import safe_unicode
31
32 log = logging.getLogger(__name__)
33
34
35 class MarkupRenderer(object):
36 RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES = ['include', 'meta', 'raw']
37
38 MARKDOWN_PAT = re.compile(r'md|mkdn?|mdown|markdown', re.IGNORECASE)
39 RST_PAT = re.compile(r're?st', re.IGNORECASE)
40 PLAIN_PAT = re.compile(r'readme', re.IGNORECASE)
41
42 def __detect_renderer(self, source, filename=None):
43 """
44 runs detection of what renderer should be used for generating html
45 from a markup language
46
47 filename can be also explicitly a renderer name
48
49 :param source:
50 :param filename:
51 """
52
53 if MarkupRenderer.MARKDOWN_PAT.findall(filename):
54 detected_renderer = 'markdown'
55 elif MarkupRenderer.RST_PAT.findall(filename):
56 detected_renderer = 'rst'
57 elif MarkupRenderer.PLAIN_PAT.findall(filename):
58 detected_renderer = 'rst'
59 else:
60 detected_renderer = 'plain'
61
62 return getattr(MarkupRenderer, detected_renderer)
63
64 def render(self, source, filename=None):
65 """
66 Renders a given filename using detected renderer
67 it detects renderers based on file extension or mimetype.
68 At last it will just do a simple html replacing new lines with <br/>
69
70 :param file_name:
71 :param source:
72 """
73
74 renderer = self.__detect_renderer(source, filename)
75 readme_data = renderer(source)
76 return readme_data
77
78 @classmethod
79 def plain(cls, source):
80 source = safe_unicode(source)
81
82 def urlify_text(text):
83 url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'
84 '|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
85
86 def url_func(match_obj):
87 url_full = match_obj.groups()[0]
88 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
89
90 return url_pat.sub(url_func, text)
91
92 source = urlify_text(source)
93 return '<br />' + source.replace("\n", '<br />')
94
95 @classmethod
96 def markdown(cls, source):
97 source = safe_unicode(source)
98 try:
99 import markdown as __markdown
100 return __markdown.markdown(source, ['codehilite'])
101 except ImportError:
102 log.warning('Install markdown to use this function')
103 return cls.plain(source)
104
105 @classmethod
106 def rst(cls, source):
107 source = safe_unicode(source)
108 try:
109 from docutils.core import publish_parts
110 from docutils.parsers.rst import directives
111 docutils_settings = dict([(alias, None) for alias in
112 cls.RESTRUCTUREDTEXT_DISALLOWED_DIRECTIVES])
113
114 docutils_settings.update({'input_encoding': 'unicode',
115 'report_level': 4})
116
117 for k, v in docutils_settings.iteritems():
118 directives.register_directive(k, v)
119
120 parts = publish_parts(source=source,
121 writer_name="html4css1",
122 settings_overrides=docutils_settings)
123
124 return parts['html_title'] + parts["fragment"]
125 except ImportError:
126 log.warning('Install docutils to use this function')
127 return cls.plain(source)
128
129 @classmethod
130 def rst_with_mentions(cls, source):
131 mention_pat = re.compile(r'(?:^@|\s@)(\w+)')
132
133 def wrapp(match_obj):
134 uname = match_obj.groups()[0]
135 return ' **@%(uname)s** ' % {'uname':uname}
136 mention_hl = mention_pat.sub(wrapp, source).strip()
137 return cls.rst(mention_hl)
1 NO CONTENT: new file 100644
@@ -0,0 +1,13 b''
1
2
3 class InvalidMessage(RuntimeError):
4 """
5 Raised if message is missing vital headers, such
6 as recipients or sender address.
7 """
8
9
10 class BadHeaders(RuntimeError):
11 """
12 Raised if message contains newlines in headers.
13 """
@@ -0,0 +1,182 b''
1 from rhodecode.lib.rcmail.response import MailResponse
2
3 from rhodecode.lib.rcmail.exceptions import BadHeaders
4 from rhodecode.lib.rcmail.exceptions import InvalidMessage
5
6 class Attachment(object):
7 """
8 Encapsulates file attachment information.
9
10 :param filename: filename of attachment
11 :param content_type: file mimetype
12 :param data: the raw file data, either as string or file obj
13 :param disposition: content-disposition (if any)
14 """
15
16 def __init__(self,
17 filename=None,
18 content_type=None,
19 data=None,
20 disposition=None):
21
22 self.filename = filename
23 self.content_type = content_type
24 self.disposition = disposition or 'attachment'
25 self._data = data
26
27 @property
28 def data(self):
29 if isinstance(self._data, basestring):
30 return self._data
31 self._data = self._data.read()
32 return self._data
33
34
35 class Message(object):
36 """
37 Encapsulates an email message.
38
39 :param subject: email subject header
40 :param recipients: list of email addresses
41 :param body: plain text message
42 :param html: HTML message
43 :param sender: email sender address
44 :param cc: CC list
45 :param bcc: BCC list
46 :param extra_headers: dict of extra email headers
47 :param attachments: list of Attachment instances
48 :param recipients_separator: alternative separator for any of
49 'From', 'To', 'Delivered-To', 'Cc', 'Bcc' fields
50 """
51
52 def __init__(self,
53 subject=None,
54 recipients=None,
55 body=None,
56 html=None,
57 sender=None,
58 cc=None,
59 bcc=None,
60 extra_headers=None,
61 attachments=None,
62 recipients_separator="; "):
63
64 self.subject = subject or ''
65 self.sender = sender
66 self.body = body
67 self.html = html
68
69 self.recipients = recipients or []
70 self.attachments = attachments or []
71 self.cc = cc or []
72 self.bcc = bcc or []
73 self.extra_headers = extra_headers or {}
74
75 self.recipients_separator = recipients_separator
76
77 @property
78 def send_to(self):
79 return set(self.recipients) | set(self.bcc or ()) | set(self.cc or ())
80
81 def to_message(self):
82 """
83 Returns raw email.Message instance.Validates message first.
84 """
85
86 self.validate()
87
88 return self.get_response().to_message()
89
90 def get_response(self):
91 """
92 Creates a Lamson MailResponse instance
93 """
94
95 response = MailResponse(Subject=self.subject,
96 To=self.recipients,
97 From=self.sender,
98 Body=self.body,
99 Html=self.html,
100 separator=self.recipients_separator)
101
102 if self.cc:
103 response.base['Cc'] = self.cc
104
105 for attachment in self.attachments:
106
107 response.attach(attachment.filename,
108 attachment.content_type,
109 attachment.data,
110 attachment.disposition)
111
112 response.update(self.extra_headers)
113
114 return response
115
116 def is_bad_headers(self):
117 """
118 Checks for bad headers i.e. newlines in subject, sender or recipients.
119 """
120
121 headers = [self.subject, self.sender]
122 headers += list(self.send_to)
123 headers += self.extra_headers.values()
124
125 for val in headers:
126 for c in '\r\n':
127 if c in val:
128 return True
129 return False
130
131 def validate(self):
132 """
133 Checks if message is valid and raises appropriate exception.
134 """
135
136 if not self.recipients:
137 raise InvalidMessage, "No recipients have been added"
138
139 if not self.body and not self.html:
140 raise InvalidMessage, "No body has been set"
141
142 if not self.sender:
143 raise InvalidMessage, "No sender address has been set"
144
145 if self.is_bad_headers():
146 raise BadHeaders
147
148 def add_recipient(self, recipient):
149 """
150 Adds another recipient to the message.
151
152 :param recipient: email address of recipient.
153 """
154
155 self.recipients.append(recipient)
156
157 def add_cc(self, recipient):
158 """
159 Adds an email address to the CC list.
160
161 :param recipient: email address of recipient.
162 """
163
164 self.cc.append(recipient)
165
166 def add_bcc(self, recipient):
167 """
168 Adds an email address to the BCC list.
169
170 :param recipient: email address of recipient.
171 """
172
173 self.bcc.append(recipient)
174
175 def attach(self, attachment):
176 """
177 Adds an attachment to the message.
178
179 :param attachment: an **Attachment** instance.
180 """
181
182 self.attachments.append(attachment)
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,6 +1,7 b''
1 1 syntax: glob
2 2 *.pyc
3 3 *.swp
4 *.sqlite
4 5 *.egg-info
5 6 *.egg
6 7
@@ -12,6 +13,9 b' syntax: regexp'
12 13 ^\.settings$
13 14 ^\.project$
14 15 ^\.pydevproject$
16 ^\.coverage$
15 17 ^rhodecode\.db$
16 18 ^test\.db$
17 ^repositories\.config$
19 ^RhodeCode\.egg-info$
20 ^rc\.ini$
21 ^fabfile.py
@@ -13,3 +13,6 b' List of contributors to RhodeCode projec'
13 13 Ankit Solanki <ankit.solanki@gmail.com>
14 14 Liad Shani <liadff@gmail.com>
15 15 Les Peabody <lpeabody@gmail.com>
16 Jonas Oberschweiber <jonas.oberschweiber@d-velop.de>
17 Matt Zuba <matt.zuba@goodwillaz.org>
18 Aras Pranckevicius <aras@unity3d.com> No newline at end of file
@@ -1,22 +1,40 b''
1 =================================================
2 Welcome to RhodeCode (RhodiumCode) documentation!
3 =================================================
1 =========
2 RhodeCode
3 =========
4 4
5 ``RhodeCode`` is a Pylons framework based Mercurial repository
6 browser/management tool with a built in push/pull server and full text search.
5 About
6 -----
7
8 ``RhodeCode`` is a fast and powerful management tool for Mercurial_ and GIT_
9 with a built in push/pull server and full text search and code-review.
7 10 It works on http/https and has a built in permission/authentication system with
8 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also supports
9 simple API so it's easy integrable with existing systems.
11 the ability to authenticate via LDAP or ActiveDirectory. RhodeCode also provides
12 simple API so it's easy integrable with existing external systems.
10 13
11 14 RhodeCode is similar in some respects to github or bitbucket_,
12 however RhodeCode can be run as standalone hosted application on your own server.
15 however RhodeCode can be run as standalone hosted application on your own server.
13 16 It is open source and donation ware and focuses more on providing a customized,
14 self administered interface for Mercurial(and soon GIT) repositories.
17 self administered interface for Mercurial and GIT repositories.
15 18 RhodeCode is powered by a vcs_ library that Lukasz Balcerzak and I created to
16 19 handle multiple different version control systems.
17 20
18 21 RhodeCode uses `Semantic Versioning <http://semver.org/>`_
19 22
23 Installation
24 ------------
25 Stable releases of RhodeCode are best installed via::
26
27 easy_install rhodecode
28
29 Or::
30
31 pip install rhodecode
32
33 Detailed instructions and links may be found on the Installation page.
34
35 Please visit http://packages.python.org/RhodeCode/installation.html for
36 more details
37
20 38 RhodeCode demo
21 39 --------------
22 40
@@ -45,16 +63,11 b' Sources at github_'
45 63
46 64 https://github.com/marcinkuzminski/rhodecode
47 65
48 Installation
49 ------------
50
51 Please visit http://packages.python.org/RhodeCode/installation.html
52
53 66
54 67 RhodeCode Features
55 68 ------------------
56 69
57 - Has it's own middleware to handle mercurial_ protocol requests.
70 - Has its own middleware to handle mercurial_ protocol requests.
58 71 Each request can be logged and authenticated.
59 72 - Runs on threads unlike hgweb. You can make multiple pulls/pushes simultaneous.
60 73 Supports http/https and LDAP
@@ -75,6 +88,9 b' RhodeCode Features'
75 88 - Server side forks. It is possible to fork a project and modify it freely
76 89 without breaking the main repository. You can even write Your own hooks
77 90 and install them
91 - code review with notification system, inline commenting, all parsed using
92 rst syntax
93 - rst and markdown README support for repositories
78 94 - Full text search powered by Whoosh on the source files, and file names.
79 95 Build in indexing daemons, with optional incremental index build
80 96 (no external search servers required all in one application)
@@ -88,20 +104,14 b' RhodeCode Features'
88 104 location
89 105 - Based on pylons / sqlalchemy / sqlite / whoosh / vcs
90 106
91
92 .. include:: ./docs/screenshots.rst
93
94 107
95 108 Incoming / Plans
96 109 ----------------
97 110
98 111 - Finer granular permissions per branch, repo group or subrepo
99 112 - pull requests and web based merges
100 - notification and message system
113 - per line file history
101 114 - SSH based authentication with server side key management
102 - Code review (probably based on hg-review)
103 - Full git_ support, with push/pull server (currently in beta tests)
104 - Redmine and other bugtrackers integration
105 115 - Commit based built in wiki system
106 116 - More statistics and graph (global annotation + some more statistics)
107 117 - Other advancements as development continues (or you can of course make
@@ -113,21 +123,35 b' License'
113 123 ``RhodeCode`` is released under the GPLv3 license.
114 124
115 125
116 Mailing group Q&A
117 -----------------
126 Getting help
127 ------------
118 128
119 Join the `Google group <http://groups.google.com/group/rhodecode>`_
129 Listed bellow are various support resources that should help.
120 130
121 Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
131 .. note::
132
133 Please try to read the documentation before posting any issues
134
135 - Join the `Google group <http://groups.google.com/group/rhodecode>`_ and ask
136 any questions.
122 137
123 Join #rhodecode on FreeNode (irc.freenode.net)
124 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
138 - Open an issue at `issue tracker <http://bitbucket.org/marcinkuzminski/rhodecode/issues>`_
139
140
141 - Join #rhodecode on FreeNode (irc.freenode.net)
142 or use http://webchat.freenode.net/?channels=rhodecode for web access to irc.
143
144 - You can also follow me on twitter @marcinkuzminski where i often post some
145 news about RhodeCode
146
125 147
126 148 Online documentation
127 149 --------------------
128 150
129 151 Online documentation for the current version of RhodeCode is available at
130 http://packages.python.org/RhodeCode/.
152 - http://packages.python.org/RhodeCode/
153 - http://rhodecode.readthedocs.org/en/latest/index.html
154
131 155 You may also build the documentation for yourself - go into ``docs/`` and run::
132 156
133 157 make html
@@ -17,6 +17,7 b' pdebug = false'
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 #email_prefix = [RhodeCode]
20 21
21 22 #smtp_server = mail.server.com
22 23 #smtp_username =
@@ -32,7 +33,7 b' pdebug = false'
32 33 threadpool_workers = 5
33 34
34 35 ##max request before thread respawn
35 threadpool_max_requests = 6
36 threadpool_max_requests = 10
36 37
37 38 ##option to use threads of process
38 39 use_threadpool = true
@@ -45,14 +46,52 b' port = 5000'
45 46 use = egg:rhodecode
46 47 full_stack = true
47 48 static_files = true
48 lang=en
49 lang = en
49 50 cache_dir = %(here)s/data
50 51 index_dir = %(here)s/data/index
51 app_instance_uuid = develop
52 app_instance_uuid = rc-develop
52 53 cut_off_limit = 256000
53 54 force_https = false
54 55 commit_parse_limit = 25
55 56 use_gravatar = true
57 container_auth_enabled = false
58 proxypass_auth_enabled = false
59 default_encoding = utf8
60
61 ## overwrite schema of clone url
62 ## available vars:
63 ## scheme - http/https
64 ## user - current user
65 ## pass - password
66 ## netloc - network location
67 ## path - usually repo_name
68
69 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
70
71 ## issue tracking mapping for commits messages
72 ## comment out issue_pat, issue_server, issue_prefix to enable
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #<numbers> with a regex passive group for `#`
76 ## {id} will be all groups matched from this pattern
77
78 issue_pat = (?:\s*#)(\d+)
79
80 ## server url to the issue, each {id} will be replaced with match
81 ## fetched from the regex and {repo} is replaced with repository name
82
83 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
84
85 ## prefix to add to link to indicate it's an url
86 ## #314 will be replaced by <issue_prefix><id>
87
88 issue_prefix = #
89
90 ## instance-id prefix
91 ## a prefix key for this instance used for cache invalidation when running
92 ## multiple instances of rhodecode, make sure it's globally unique for
93 ## all running rhodecode instances. Leave empty if you don't use it
94 instance_id =
56 95
57 96 ####################################
58 97 ### CELERY CONFIG ####
@@ -91,21 +130,27 b' beaker.cache.regions=super_short_term,sh'
91 130
92 131 beaker.cache.super_short_term.type=memory
93 132 beaker.cache.super_short_term.expire=10
133 beaker.cache.super_short_term.key_length = 256
94 134
95 135 beaker.cache.short_term.type=memory
96 136 beaker.cache.short_term.expire=60
137 beaker.cache.short_term.key_length = 256
97 138
98 139 beaker.cache.long_term.type=memory
99 140 beaker.cache.long_term.expire=36000
141 beaker.cache.long_term.key_length = 256
100 142
101 143 beaker.cache.sql_cache_short.type=memory
102 144 beaker.cache.sql_cache_short.expire=10
145 beaker.cache.sql_cache_short.key_length = 256
103 146
104 147 beaker.cache.sql_cache_med.type=memory
105 148 beaker.cache.sql_cache_med.expire=360
149 beaker.cache.sql_cache_med.key_length = 256
106 150
107 151 beaker.cache.sql_cache_long.type=file
108 152 beaker.cache.sql_cache_long.expire=3600
153 beaker.cache.sql_cache_long.key_length = 256
109 154
110 155 ####################################
111 156 ### BEAKER SESSION ####
@@ -113,12 +158,26 b' beaker.cache.sql_cache_long.expire=3600'
113 158 ## Type of storage used for the session, current types are
114 159 ## dbm, file, memcached, database, and memory.
115 160 ## The storage uses the Container API
116 ##that is also used by the cache system.
117 beaker.session.type = file
161 ## that is also used by the cache system.
162
163 ## db session example
164
165 #beaker.session.type = ext:database
166 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
167 #beaker.session.table_name = db_session
118 168
169 ## encrypted cookie session, good for many instances
170 #beaker.session.type = cookie
171
172 beaker.session.type = file
119 173 beaker.session.key = rhodecode
120 beaker.session.secret = g654dcno0-9873jhgfreyu
174 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
175 #beaker.session.validate_key = 9712sds2212c--zxc123
121 176 beaker.session.timeout = 36000
177 beaker.session.httponly = true
178
179 ## uncomment for https secure cookie
180 beaker.session.secure = false
122 181
123 182 ##auto save the session to not to use .save()
124 183 beaker.session.auto = False
@@ -126,7 +185,7 b' beaker.session.auto = False'
126 185 ##true exire at browser close
127 186 #beaker.session.cookie_expires = 3600
128 187
129
188
130 189 ################################################################################
131 190 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
132 191 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
@@ -26,16 +26,18 b' API ACCESS'
26 26
27 27 All clients are required to send JSON-RPC spec JSON data::
28 28
29 {
29 {
30 "id:<id>,
30 31 "api_key":"<api_key>",
31 32 "method":"<method_name>",
32 33 "args":{"<arg_key>":"<arg_val>"}
33 34 }
34 35
35 36 Example call for autopulling remotes repos using curl::
36 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
37 curl https://server.com/_admin/api -X POST -H 'content-type:text/plain' --data-binary '{"id":1,"api_key":"xe7cdb2v278e4evbdf5vs04v832v0efvcbcve4a3","method":"pull","args":{"repo":"CPython"}}'
37 38
38 39 Simply provide
40 - *id* A value of any type, which is used to match the response with the request that it is replying to.
39 41 - *api_key* for access and permission validation.
40 42 - *method* is name of method to call
41 43 - *args* is an key:value list of arguments to pass to method
@@ -47,7 +49,8 b' Simply provide'
47 49
48 50 RhodeCode API will return always a JSON-RPC response::
49 51
50 {
52 {
53 "id":<id>,
51 54 "result": "<result>",
52 55 "error": null
53 56 }
@@ -72,21 +75,55 b' INPUT::'
72 75 api_key : "<api_key>"
73 76 method : "pull"
74 77 args : {
75 "repo" : "<repo_name>"
78 "repo_name" : "<reponame>"
76 79 }
77 80
78 81 OUTPUT::
79 82
80 result : "Pulled from <repo_name>"
83 result : "Pulled from <reponame>"
81 84 error : null
82 85
83 86
87 get_user
88 --------
89
90 Get's an user by username or user_id, Returns empty result if user is not found.
91 This command can be executed only using api_key belonging to user with admin
92 rights.
93
94
95 INPUT::
96
97 api_key : "<api_key>"
98 method : "get_user"
99 args : {
100 "userid" : "<username or user_id>"
101 }
102
103 OUTPUT::
104
105 result: None if user does not exist or
106 {
107 "id" : "<id>",
108 "username" : "<username>",
109 "firstname": "<firstname>",
110 "lastname" : "<lastname>",
111 "email" : "<email>",
112 "active" : "<bool>",
113 "admin" :  "<bool>",
114 "ldap_dn" : "<ldap_dn>"
115 }
116
117 error: null
118
119
84 120 get_users
85 121 ---------
86 122
87 123 Lists all existing users. This command can be executed only using api_key
88 124 belonging to user with admin rights.
89 125
126
90 127 INPUT::
91 128
92 129 api_key : "<api_key>"
@@ -104,18 +141,20 b' OUTPUT::'
104 141 "email" : "<email>",
105 142 "active" : "<bool>",
106 143 "admin" :  "<bool>",
107 "ldap" : "<ldap_dn>"
144 "ldap_dn" : "<ldap_dn>"
108 145 },
109 146
110 147 ]
111 148 error: null
112 149
150
113 151 create_user
114 152 -----------
115 153
116 Creates new user or updates current one if such user exists. This command can
154 Creates new user. This command can
117 155 be executed only using api_key belonging to user with admin rights.
118 156
157
119 158 INPUT::
120 159
121 160 api_key : "<api_key>"
@@ -123,9 +162,9 b' INPUT::'
123 162 args : {
124 163 "username" : "<username>",
125 164 "password" : "<password>",
126 "firstname" : "<firstname>",
127 "lastname" : "<lastname>",
128 "email" : "<useremail>"
165 "email" : "<useremail>",
166 "firstname" : "<firstname> = None",
167 "lastname" : "<lastname> = None",
129 168 "active" : "<bool> = True",
130 169 "admin" : "<bool> = False",
131 170 "ldap_dn" : "<ldap_dn> = None"
@@ -134,15 +173,88 b' INPUT::'
134 173 OUTPUT::
135 174
136 175 result: {
176 "id" : "<new_user_id>",
137 177 "msg" : "created new user <username>"
138 178 }
139 179 error: null
140 180
181
182 update_user
183 -----------
184
185 updates current one if such user exists. This command can
186 be executed only using api_key belonging to user with admin rights.
187
188
189 INPUT::
190
191 api_key : "<api_key>"
192 method : "update_user"
193 args : {
194 "userid" : "<user_id or username>",
195 "username" : "<username>",
196 "password" : "<password>",
197 "email" : "<useremail>",
198 "firstname" : "<firstname>",
199 "lastname" : "<lastname>",
200 "active" : "<bool>",
201 "admin" : "<bool>",
202 "ldap_dn" : "<ldap_dn>"
203 }
204
205 OUTPUT::
206
207 result: {
208 "id" : "<edited_user_id>",
209 "msg" : "updated user <username>"
210 }
211 error: null
212
213
214 get_users_group
215 ---------------
216
217 Gets an existing users group. This command can be executed only using api_key
218 belonging to user with admin rights.
219
220
221 INPUT::
222
223 api_key : "<api_key>"
224 method : "get_users_group"
225 args : {
226 "group_name" : "<name>"
227 }
228
229 OUTPUT::
230
231 result : None if group not exist
232 {
233 "id" : "<id>",
234 "group_name" : "<groupname>",
235 "active": "<bool>",
236 "members" : [
237 { "id" : "<userid>",
238 "username" : "<username>",
239 "firstname": "<firstname>",
240 "lastname" : "<lastname>",
241 "email" : "<email>",
242 "active" : "<bool>",
243 "admin" :  "<bool>",
244 "ldap" : "<ldap_dn>"
245 },
246
247 ]
248 }
249 error : null
250
251
141 252 get_users_groups
142 253 ----------------
143 254
144 Lists all existing users groups. This command can be executed only using api_key
145 belonging to user with admin rights.
255 Lists all existing users groups. This command can be executed only using
256 api_key belonging to user with admin rights.
257
146 258
147 259 INPUT::
148 260
@@ -154,9 +266,9 b' OUTPUT::'
154 266
155 267 result : [
156 268 {
157 "id" : "<id>",
158 "name" : "<name>",
159 "active": "<bool>",
269 "id" : "<id>",
270 "group_name" : "<groupname>",
271 "active": "<bool>",
160 272 "members" : [
161 273 {
162 274 "id" : "<userid>",
@@ -174,41 +286,6 b' OUTPUT::'
174 286 ]
175 287 error : null
176 288
177 get_users_group
178 ---------------
179
180 Gets an existing users group. This command can be executed only using api_key
181 belonging to user with admin rights.
182
183 INPUT::
184
185 api_key : "<api_key>"
186 method : "get_users_group"
187 args : {
188 "group_name" : "<name>"
189 }
190
191 OUTPUT::
192
193 result : None if group not exist
194 {
195 "id" : "<id>",
196 "name" : "<name>",
197 "active": "<bool>",
198 "members" : [
199 { "id" : "<userid>",
200 "username" : "<username>",
201 "firstname": "<firstname>",
202 "lastname" : "<lastname>",
203 "email" : "<email>",
204 "active" : "<bool>",
205 "admin" :  "<bool>",
206 "ldap" : "<ldap_dn>"
207 },
208
209 ]
210 }
211 error : null
212 289
213 290 create_users_group
214 291 ------------------
@@ -216,12 +293,13 b' create_users_group'
216 293 Creates new users group. This command can be executed only using api_key
217 294 belonging to user with admin rights
218 295
296
219 297 INPUT::
220 298
221 299 api_key : "<api_key>"
222 300 method : "create_users_group"
223 301 args: {
224 "name": "<name>",
302 "group_name": "<groupname>",
225 303 "active":"<bool> = True"
226 304 }
227 305
@@ -229,39 +307,120 b' OUTPUT::'
229 307
230 308 result: {
231 309 "id": "<newusersgroupid>",
232 "msg": "created new users group <name>"
310 "msg": "created new users group <groupname>"
233 311 }
234 312 error: null
235 313
314
236 315 add_user_to_users_group
237 316 -----------------------
238 317
239 Adds a user to a users group. This command can be executed only using api_key
318 Adds a user to a users group. If user exists in that group success will be
319 `false`. This command can be executed only using api_key
240 320 belonging to user with admin rights
241 321
322
242 323 INPUT::
243 324
244 325 api_key : "<api_key>"
245 326 method : "add_user_users_group"
246 327 args: {
247 328 "group_name" : "<groupname>",
248 "user_name" : "<username>"
329 "username" : "<username>"
249 330 }
250 331
251 332 OUTPUT::
252 333
253 334 result: {
254 335 "id": "<newusersgroupmemberid>",
255 "msg": "created new users group member"
336 "success": True|False # depends on if member is in group
337 "msg": "added member <username> to users group <groupname> |
338 User is already in that group"
339 }
340 error: null
341
342
343 remove_user_from_users_group
344 ----------------------------
345
346 Removes a user from a users group. If user is not in given group success will
347 be `false`. This command can be executed only
348 using api_key belonging to user with admin rights
349
350
351 INPUT::
352
353 api_key : "<api_key>"
354 method : "remove_user_from_users_group"
355 args: {
356 "group_name" : "<groupname>",
357 "username" : "<username>"
358 }
359
360 OUTPUT::
361
362 result: {
363 "success": True|False, # depends on if member is in group
364 "msg": "removed member <username> from users group <groupname> |
365 User wasn't in group"
256 366 }
257 367 error: null
258 368
369
370 get_repo
371 --------
372
373 Gets an existing repository by it's name or repository_id. This command can
374 be executed only using api_key belonging to user with admin rights.
375
376
377 INPUT::
378
379 api_key : "<api_key>"
380 method : "get_repo"
381 args: {
382 "repoid" : "<reponame or repo_id>"
383 }
384
385 OUTPUT::
386
387 result: None if repository does not exist or
388 {
389 "id" : "<id>",
390 "repo_name" : "<reponame>"
391 "type" : "<type>",
392 "description" : "<description>",
393 "members" : [
394 { "id" : "<userid>",
395 "username" : "<username>",
396 "firstname": "<firstname>",
397 "lastname" : "<lastname>",
398 "email" : "<email>",
399 "active" : "<bool>",
400 "admin" :  "<bool>",
401 "ldap" : "<ldap_dn>",
402 "permission" : "repository.(read|write|admin)"
403 },
404
405 {
406 "id" : "<usersgroupid>",
407 "name" : "<usersgroupname>",
408 "active": "<bool>",
409 "permission" : "repository.(read|write|admin)"
410 },
411
412 ]
413 }
414 error: null
415
416
259 417 get_repos
260 418 ---------
261 419
262 420 Lists all existing repositories. This command can be executed only using api_key
263 421 belonging to user with admin rights
264 422
423
265 424 INPUT::
266 425
267 426 api_key : "<api_key>"
@@ -273,7 +432,7 b' OUTPUT::'
273 432 result: [
274 433 {
275 434 "id" : "<id>",
276 "name" : "<name>"
435 "repo_name" : "<reponame>"
277 436 "type" : "<type>",
278 437 "description" : "<description>"
279 438 },
@@ -281,51 +440,39 b' OUTPUT::'
281 440 ]
282 441 error: null
283 442
284 get_repo
285 --------
443
444 get_repo_nodes
445 --------------
286 446
287 Gets an existing repository. This command can be executed only using api_key
288 belonging to user with admin rights
447 returns a list of nodes and it's children in a flat list for a given path
448 at given revision. It's possible to specify ret_type to show only `files` or
449 `dirs`. This command can be executed only using api_key belonging to user
450 with admin rights
451
289 452
290 453 INPUT::
291 454
292 455 api_key : "<api_key>"
293 method : "get_repo"
456 method : "get_repo_nodes"
294 457 args: {
295 "name" : "<name>"
458 "repo_name" : "<reponame>",
459 "revision" : "<revision>",
460 "root_path" : "<root_path>",
461 "ret_type" : "<ret_type>" = 'all'
296 462 }
297 463
298 464 OUTPUT::
299 465
300 result: None if repository not exist
301 {
302 "id" : "<id>",
466 result: [
467 {
303 468 "name" : "<name>"
304 469 "type" : "<type>",
305 "description" : "<description>",
306 "members" : [
307 { "id" : "<userid>",
308 "username" : "<username>",
309 "firstname": "<firstname>",
310 "lastname" : "<lastname>",
311 "email" : "<email>",
312 "active" : "<bool>",
313 "admin" :  "<bool>",
314 "ldap" : "<ldap_dn>",
315 "permission" : "repository.(read|write|admin)"
316 },
317
318 {
319 "id" : "<usersgroupid>",
320 "name" : "<usersgroupname>",
321 "active": "<bool>",
322 "permission" : "repository.(read|write|admin)"
323 },
324
325 ]
326 }
470 },
471
472 ]
327 473 error: null
328 474
475
329 476 create_repo
330 477 -----------
331 478
@@ -335,58 +482,146 b' If repository name contains "/", all nee'
335 482 For example "foo/bar/baz" will create groups "foo", "bar" (with "foo" as parent),
336 483 and create "baz" repository with "bar" as group.
337 484
485
338 486 INPUT::
339 487
340 488 api_key : "<api_key>"
341 489 method : "create_repo"
342 490 args: {
343 "name" : "<name>",
491 "repo_name" : "<reponame>",
344 492 "owner_name" : "<ownername>",
345 493 "description" : "<description> = ''",
346 494 "repo_type" : "<type> = 'hg'",
347 "private" : "<bool> = False"
495 "private" : "<bool> = False",
496 "clone_uri" : "<clone_uri> = None",
348 497 }
349 498
350 499 OUTPUT::
351 500
352 result: None
501 result: {
502 "id": "<newrepoid>",
503 "msg": "Created new repository <reponame>",
504 }
353 505 error: null
354 506
355 add_user_to_repo
356 ----------------
507
508 delete_repo
509 -----------
510
511 Deletes a repository. This command can be executed only using api_key
512 belonging to user with admin rights.
513
514
515 INPUT::
516
517 api_key : "<api_key>"
518 method : "delete_repo"
519 args: {
520 "repo_name" : "<reponame>",
521 }
357 522
358 Add a user to a repository. This command can be executed only using api_key
359 belonging to user with admin rights.
360 If "perm" is None, user will be removed from the repository.
523 OUTPUT::
524
525 result: {
526 "msg": "Deleted repository <reponame>",
527 }
528 error: null
529
530
531 grant_user_permission
532 ---------------------
533
534 Grant permission for user on given repository, or update existing one
535 if found. This command can be executed only using api_key belonging to user
536 with admin rights.
537
361 538
362 539 INPUT::
363 540
364 541 api_key : "<api_key>"
365 method : "add_user_to_repo"
542 method : "grant_user_permission"
366 543 args: {
367 544 "repo_name" : "<reponame>",
368 "user_name" : "<username>",
369 "perm" : "(None|repository.(read|write|admin))",
545 "username" : "<username>",
546 "perm" : "(repository.(none|read|write|admin))",
547 }
548
549 OUTPUT::
550
551 result: {
552 "msg" : "Granted perm: <perm> for user: <username> in repo: <reponame>"
553 }
554 error: null
555
556
557 revoke_user_permission
558 ----------------------
559
560 Revoke permission for user on given repository. This command can be executed
561 only using api_key belonging to user with admin rights.
562
563
564 INPUT::
565
566 api_key : "<api_key>"
567 method : "revoke_user_permission"
568 args: {
569 "repo_name" : "<reponame>",
570 "username" : "<username>",
370 571 }
371 572
372 573 OUTPUT::
373 574
374 result: None
575 result: {
576 "msg" : "Revoked perm for user: <suername> in repo: <reponame>"
577 }
375 578 error: null
376 579
377 add_users_group_to_repo
378 -----------------------
580
581 grant_users_group_permission
582 ----------------------------
379 583
380 Add a users group to a repository. This command can be executed only using
381 api_key belonging to user with admin rights. If "perm" is None, group will
382 be removed from the repository.
584 Grant permission for users group on given repository, or update
585 existing one if found. This command can be executed only using
586 api_key belonging to user with admin rights.
587
383 588
384 589 INPUT::
385 590
386 591 api_key : "<api_key>"
387 method : "add_users_group_to_repo"
592 method : "grant_users_group_permission"
593 args: {
594 "repo_name" : "<reponame>",
595 "group_name" : "<usersgroupname>",
596 "perm" : "(repository.(none|read|write|admin))",
597 }
598
599 OUTPUT::
600
601 result: {
602 "msg" : "Granted perm: <perm> for group: <usersgroupname> in repo: <reponame>"
603 }
604 error: null
605
606
607 revoke_users_group_permission
608 -----------------------------
609
610 Revoke permission for users group on given repository.This command can be
611 executed only using api_key belonging to user with admin rights.
612
613 INPUT::
614
615 api_key : "<api_key>"
616 method : "revoke_users_group_permission"
388 617 args: {
389 618 "repo_name" : "<reponame>",
390 "group_name" : "<groupname>",
391 "perm" : "(None|repository.(read|write|admin))",
392 } No newline at end of file
619 "users_group" : "<usersgroupname>",
620 }
621
622 OUTPUT::
623
624 result: {
625 "msg" : "Revoked perm for group: <usersgroupname> in repo: <reponame>"
626 }
627 error: null No newline at end of file
@@ -6,14 +6,29 b' The :mod:`models` Module'
6 6 .. automodule:: rhodecode.model
7 7 :members:
8 8
9 .. automodule:: rhodecode.model.comment
10 :members:
11
12 .. automodule:: rhodecode.model.notification
13 :members:
14
9 15 .. automodule:: rhodecode.model.permission
10 16 :members:
11
17
18 .. automodule:: rhodecode.model.repo_permission
19 :members:
20
12 21 .. automodule:: rhodecode.model.repo
13 22 :members:
14 23
24 .. automodule:: rhodecode.model.repos_group
25 :members:
26
15 27 .. automodule:: rhodecode.model.scm
16 28 :members:
17
29
18 30 .. automodule:: rhodecode.model.user
19 31 :members:
32
33 .. automodule:: rhodecode.model.users_group
34 :members: No newline at end of file
@@ -4,14 +4,73 b' Changelog'
4 4 =========
5 5
6 6
7 1.2.5 (**2012-01-28**)
8 ======================
7 1.3.0 (**2012-02-XX**)
8 ----------------------
9
10 :status: in-progress
11 :branch: beta
9 12
10 13 news
11 ----
14 ++++
15
16 - code review, inspired by github code-comments
17 - #215 rst and markdown README files support
18 - #252 Container-based and proxy pass-through authentication support
19 - #44 branch browser. Filtering of changelog by branches
20 - mercurial bookmarks support
21 - new hover top menu, optimized to add maximum size for important views
22 - configurable clone url template with possibility to specify protocol like
23 ssh:// or http:// and also manually alter other parts of clone_url.
24 - enabled largefiles extension by default
25 - optimized summary file pages and saved a lot of unused space in them
26 - #239 option to manually mark repository as fork
27 - #320 mapping of commit authors to RhodeCode users
28 - #304 hashes are displayed using monospace font
29 - diff configuration, toggle white lines and context lines
30 - #307 configurable diffs, whitespace toggle, increasing context lines
31 - sorting on branches, tags and bookmarks using YUI datatable
32 - improved file filter on files page
33 - implements #330 api method for listing nodes ar particular revision
34 - #73 added linking issues in commit messages to chosen issue tracker url
35 based on user defined regular expression
36 - added linking of changesets in commit messages
37 - new compact changelog with expandable commit messages
38 - firstname and lastname are optional in user creation
39 - #348 added post-create repository hook
40 - #212 global encoding settings is now configurable from .ini files
41 - #227 added repository groups permissions
42 - markdown gets codehilite extensions
43 - new API methods, delete_repositories, grante/revoke permissions for groups
44 and repos
45
46
47 fixes
48 +++++
49
50 - rewrote dbsession management for atomic operations, and better error handling
51 - fixed sorting of repo tables
52 - #326 escape of special html entities in diffs
53 - normalized user_name => username in api attributes
54 - fixes #298 ldap created users with mixed case emails created conflicts
55 on saving a form
56 - fixes issue when owner of a repo couldn't revoke permissions for users
57 and groups
58 - fixes #271 rare JSON serialization problem with statistics
59 - fixes #337 missing validation check for conflicting names of a group with a
60 repositories group
61 - #340 fixed session problem for mysql and celery tasks
62 - fixed #331 RhodeCode mangles repository names if the a repository group
63 contains the "full path" to the repositories
64 - #355 RhodeCode doesn't store encrypted LDAP passwords
65
66 1.2.5 (**2012-01-28**)
67 ----------------------
68
69 news
70 ++++
12 71
13 72 fixes
14 -----
73 +++++
15 74
16 75 - #340 Celery complains about MySQL server gone away, added session cleanup
17 76 for celery tasks
@@ -24,10 +83,10 b' fixes'
24 83 forking on windows impossible
25 84
26 85 1.2.4 (**2012-01-19**)
27 ======================
86 ----------------------
28 87
29 88 news
30 ----
89 ++++
31 90
32 91 - RhodeCode is bundled with mercurial series 2.0.X by default, with
33 92 full support to largefiles extension. Enabled by default in new installations
@@ -35,7 +94,7 b' news'
35 94 - added requires.txt file with requirements
36 95
37 96 fixes
38 -----
97 +++++
39 98
40 99 - fixes db session issues with celery when emailing admins
41 100 - #331 RhodeCode mangles repository names if the a repository group
@@ -52,10 +111,10 b' fixes'
52 111 - #316 fixes issues with web description in hgrc files
53 112
54 113 1.2.3 (**2011-11-02**)
55 ======================
114 ----------------------
56 115
57 116 news
58 ----
117 ++++
59 118
60 119 - added option to manage repos group for non admin users
61 120 - added following API methods for get_users, create_user, get_users_groups,
@@ -67,24 +126,23 b' news'
67 126 administrator users, and global config email.
68 127
69 128 fixes
70 -----
129 +++++
71 130
72 131 - added option for passing auth method for smtp mailer
73 132 - #276 issue with adding a single user with id>10 to usergroups
74 133 - #277 fixes windows LDAP settings in which missing values breaks the ldap auth
75 134 - #288 fixes managing of repos in a group for non admin user
76 135
77
78 136 1.2.2 (**2011-10-17**)
79 ======================
137 ----------------------
80 138
81 139 news
82 ----
140 ++++
83 141
84 142 - #226 repo groups are available by path instead of numerical id
85 143
86 144 fixes
87 -----
145 +++++
88 146
89 147 - #259 Groups with the same name but with different parent group
90 148 - #260 Put repo in group, then move group to another group -> repo becomes unavailable
@@ -98,27 +156,25 b' fixes'
98 156 - fixes #248 cannot edit repos inside a group on windows
99 157 - fixes #219 forking problems on windows
100 158
101
102 159 1.2.1 (**2011-10-08**)
103 ======================
160 ----------------------
104 161
105 162 news
106 ----
163 ++++
107 164
108 165
109 166 fixes
110 -----
167 +++++
111 168
112 169 - fixed problems with basic auth and push problems
113 170 - gui fixes
114 171 - fixed logger
115 172
116
117 173 1.2.0 (**2011-10-07**)
118 ======================
174 ----------------------
119 175
120 176 news
121 ----
177 ++++
122 178
123 179 - implemented #47 repository groups
124 180 - implemented #89 Can setup google analytics code from settings menu
@@ -158,7 +214,7 b' news'
158 214 - Implemented advanced hook management
159 215
160 216 fixes
161 -----
217 +++++
162 218
163 219 - fixed file browser bug, when switching into given form revision the url was
164 220 not changing
@@ -185,18 +241,17 b' fixes'
185 241 - fixes #218 os.kill patch for windows was missing sig param
186 242 - improved rendering of dag (they are not trimmed anymore when number of
187 243 heads exceeds 5)
188
189
244
190 245 1.1.8 (**2011-04-12**)
191 ======================
246 ----------------------
192 247
193 248 news
194 ----
249 ++++
195 250
196 251 - improved windows support
197 252
198 253 fixes
199 -----
254 +++++
200 255
201 256 - fixed #140 freeze of python dateutil library, since new version is python2.x
202 257 incompatible
@@ -219,40 +274,40 b' fixes'
219 274
220 275
221 276 1.1.7 (**2011-03-23**)
222 ======================
277 ----------------------
223 278
224 279 news
225 ----
280 ++++
226 281
227 282 fixes
228 -----
283 +++++
229 284
230 285 - fixed (again) #136 installation support for FreeBSD
231 286
232 287
233 288 1.1.6 (**2011-03-21**)
234 ======================
289 ----------------------
235 290
236 291 news
237 ----
292 ++++
238 293
239 294 fixes
240 -----
295 +++++
241 296
242 297 - fixed #136 installation support for FreeBSD
243 298 - RhodeCode will check for python version during installation
244 299
245 300 1.1.5 (**2011-03-17**)
246 ======================
301 ----------------------
247 302
248 303 news
249 ----
304 ++++
250 305
251 306 - basic windows support, by exchanging pybcrypt into sha256 for windows only
252 307 highly inspired by idea of mantis406
253 308
254 309 fixes
255 -----
310 +++++
256 311
257 312 - fixed sorting by author in main page
258 313 - fixed crashes with diffs on binary files
@@ -264,13 +319,13 b' fixes'
264 319 - cleaned out docs, big thanks to Jason Harris
265 320
266 321 1.1.4 (**2011-02-19**)
267 ======================
322 ----------------------
268 323
269 324 news
270 ----
325 ++++
271 326
272 327 fixes
273 -----
328 +++++
274 329
275 330 - fixed formencode import problem on settings page, that caused server crash
276 331 when that page was accessed as first after server start
@@ -278,17 +333,17 b' fixes'
278 333 - fixed option to access repository just by entering http://server/<repo_name>
279 334
280 335 1.1.3 (**2011-02-16**)
281 ======================
336 ----------------------
282 337
283 338 news
284 ----
339 ++++
285 340
286 341 - implemented #102 allowing the '.' character in username
287 342 - added option to access repository just by entering http://server/<repo_name>
288 343 - celery task ignores result for better performance
289 344
290 345 fixes
291 -----
346 +++++
292 347
293 348 - fixed ehlo command and non auth mail servers on smtp_lib. Thanks to
294 349 apollo13 and Johan Walles
@@ -304,31 +359,31 b' fixes'
304 359 - fixed static files paths links to use of url() method
305 360
306 361 1.1.2 (**2011-01-12**)
307 ======================
362 ----------------------
308 363
309 364 news
310 ----
365 ++++
311 366
312 367
313 368 fixes
314 -----
369 +++++
315 370
316 371 - fixes #98 protection against float division of percentage stats
317 372 - fixed graph bug
318 373 - forced webhelpers version since it was making troubles during installation
319 374
320 375 1.1.1 (**2011-01-06**)
321 ======================
376 ----------------------
322 377
323 378 news
324 ----
379 ++++
325 380
326 381 - added force https option into ini files for easier https usage (no need to
327 382 set server headers with this options)
328 383 - small css updates
329 384
330 385 fixes
331 -----
386 +++++
332 387
333 388 - fixed #96 redirect loop on files view on repositories without changesets
334 389 - fixed #97 unicode string passed into server header in special cases (mod_wsgi)
@@ -337,16 +392,16 b' fixes'
337 392 - fixed #92 whoosh indexer is more error proof
338 393
339 394 1.1.0 (**2010-12-18**)
340 ======================
395 ----------------------
341 396
342 397 news
343 ----
398 ++++
344 399
345 400 - rewrite of internals for vcs >=0.1.10
346 401 - uses mercurial 1.7 with dotencode disabled for maintaining compatibility
347 402 with older clients
348 403 - anonymous access, authentication via ldap
349 - performance upgrade for cached repos list - each repository has it's own
404 - performance upgrade for cached repos list - each repository has its own
350 405 cache that's invalidated when needed.
351 406 - performance upgrades on repositories with large amount of commits (20K+)
352 407 - main page quick filter for filtering repositories
@@ -365,7 +420,7 b' news'
365 420 - other than sqlite database backends can be used
366 421
367 422 fixes
368 -----
423 +++++
369 424
370 425 - fixes #61 forked repo was showing only after cache expired
371 426 - fixes #76 no confirmation on user deletes
@@ -385,16 +440,16 b' fixes'
385 440
386 441
387 442 1.0.2 (**2010-11-12**)
388 ======================
443 ----------------------
389 444
390 445 news
391 ----
446 ++++
392 447
393 448 - tested under python2.7
394 449 - bumped sqlalchemy and celery versions
395 450
396 451 fixes
397 -----
452 +++++
398 453
399 454 - fixed #59 missing graph.js
400 455 - fixed repo_size crash when repository had broken symlinks
@@ -402,15 +457,15 b' fixes'
402 457
403 458
404 459 1.0.1 (**2010-11-10**)
405 ======================
460 ----------------------
406 461
407 462 news
408 ----
463 ++++
409 464
410 465 - small css updated
411 466
412 467 fixes
413 -----
468 +++++
414 469
415 470 - fixed #53 python2.5 incompatible enumerate calls
416 471 - fixed #52 disable mercurial extension for web
@@ -418,7 +473,7 b' fixes'
418 473
419 474
420 475 1.0.0 (**2010-11-02**)
421 ======================
476 ----------------------
422 477
423 478 - security bugfix simplehg wasn't checking for permissions on commands
424 479 other than pull or push.
@@ -428,7 +483,7 b' 1.0.0 (**2010-11-02**)'
428 483 - permissions cached queries
429 484
430 485 1.0.0rc4 (**2010-10-12**)
431 ==========================
486 --------------------------
432 487
433 488 - fixed python2.5 missing simplejson imports (thanks to Jens Bäckman)
434 489 - removed cache_manager settings from sqlalchemy meta
@@ -438,12 +493,12 b' 1.0.0rc4 (**2010-10-12**)'
438 493
439 494
440 495 1.0.0rc3 (**2010-10-11**)
441 =========================
496 -------------------------
442 497
443 498 - fixed i18n during installation.
444 499
445 500 1.0.0rc2 (**2010-10-11**)
446 =========================
501 -------------------------
447 502
448 503 - Disabled dirsize in file browser, it's causing nasty bug when dir renames
449 504 occure. After vcs is fixed it'll be put back again.
@@ -2,8 +2,8 b''
2 2
3 3 .. include:: ./../README.rst
4 4
5 Documentation
6 -------------
5 Users Guide
6 -----------
7 7
8 8 **Installation:**
9 9
@@ -20,10 +20,9 b' Documentation'
20 20 :maxdepth: 1
21 21
22 22 usage/general
23 usage/enable_git
23 usage/git_support
24 24 usage/statistics
25 25 usage/backup
26 usage/api_key_access
27 26
28 27 **Develop**
29 28
@@ -36,9 +35,10 b' Documentation'
36 35 **API**
37 36
38 37 .. toctree::
39 :maxdepth: 2
38 :maxdepth: 1
40 39
41 api/index
40 api/api
41 api/models
42 42
43 43
44 44 Other topics
@@ -346,6 +346,106 b' All other LDAP settings will likely be s'
346 346 appropriately configured.
347 347
348 348
349 Authentication by container or reverse-proxy
350 --------------------------------------------
351
352 Starting with version 1.3, RhodeCode supports delegating the authentication
353 of users to its WSGI container, or to a reverse-proxy server through which all
354 clients access the application.
355
356 When these authentication methods are enabled in RhodeCode, it uses the
357 username that the container/proxy (Apache/Nginx/etc) authenticated and doesn't
358 perform the authentication itself. The authorization, however, is still done by
359 RhodeCode according to its settings.
360
361 When a user logs in for the first time using these authentication methods,
362 a matching user account is created in RhodeCode with default permissions. An
363 administrator can then modify it using RhodeCode's admin interface.
364 It's also possible for an administrator to create accounts and configure their
365 permissions before the user logs in for the first time.
366
367 Container-based authentication
368 ''''''''''''''''''''''''''''''
369
370 In a container-based authentication setup, RhodeCode reads the user name from
371 the ``REMOTE_USER`` server variable provided by the WSGI container.
372
373 After setting up your container (see `Apache's WSGI config`_), you'd need
374 to configure it to require authentication on the location configured for
375 RhodeCode.
376
377 In order for RhodeCode to start using the provided username, you should set the
378 following in the [app:main] section of your .ini file::
379
380 container_auth_enabled = true
381
382
383 Proxy pass-through authentication
384 '''''''''''''''''''''''''''''''''
385
386 In a proxy pass-through authentication setup, RhodeCode reads the user name
387 from the ``X-Forwarded-User`` request header, which should be configured to be
388 sent by the reverse-proxy server.
389
390 After setting up your proxy solution (see `Apache virtual host reverse proxy example`_,
391 `Apache as subdirectory`_ or `Nginx virtual host example`_), you'd need to
392 configure the authentication and add the username in a request header named
393 ``X-Forwarded-User``.
394
395 For example, the following config section for Apache sets a subdirectory in a
396 reverse-proxy setup with basic auth::
397
398 <Location /<someprefix> >
399 ProxyPass http://127.0.0.1:5000/<someprefix>
400 ProxyPassReverse http://127.0.0.1:5000/<someprefix>
401 SetEnvIf X-Url-Scheme https HTTPS=1
402
403 AuthType Basic
404 AuthName "RhodeCode authentication"
405 AuthUserFile /home/web/rhodecode/.htpasswd
406 require valid-user
407
408 RequestHeader unset X-Forwarded-User
409
410 RewriteEngine On
411 RewriteCond %{LA-U:REMOTE_USER} (.+)
412 RewriteRule .* - [E=RU:%1]
413 RequestHeader set X-Forwarded-User %{RU}e
414 </Location>
415
416 In order for RhodeCode to start using the forwarded username, you should set
417 the following in the [app:main] section of your .ini file::
418
419 proxypass_auth_enabled = true
420
421 .. note::
422 If you enable proxy pass-through authentication, make sure your server is
423 only accessible through the proxy. Otherwise, any client would be able to
424 forge the authentication header and could effectively become authenticated
425 using any account of their liking.
426
427 Integration with Issue trackers
428 -------------------------------
429
430 RhodeCode provides a simple integration with issue trackers. It's possible
431 to define a regular expression that will fetch issue id stored in commit
432 messages and replace that with an url to this issue. To enable this simply
433 uncomment following variables in the ini file::
434
435 url_pat = (?:^#|\s#)(\w+)
436 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
437 issue_prefix = #
438
439 `url_pat` is the regular expression that will fetch issues from commit messages.
440 Default regex will match issues in format of #<number> eg. #300.
441
442 Matched issues will be replace with the link specified as `issue_server_link`
443 {id} will be replaced with issue id, and {repo} with repository name.
444 Since the # is striped `issue_prefix` is added as a prefix to url.
445 `issue_prefix` can be something different than # if you pass
446 ISSUE- as issue prefix this will generate an url in format::
447
448 <a href="https://myissueserver.com/example_repo/issue/300">ISSUE-300</a>
349 449
350 450 Hook management
351 451 ---------------
@@ -361,6 +461,17 b' To add another custom hook simply fill i'
361 461 can be found at *rhodecode.lib.hooks*.
362 462
363 463
464 Changing default encoding
465 -------------------------
466
467 By default RhodeCode uses utf8 encoding, starting from 1.3 series this
468 can be changed, simply edit default_encoding in .ini file to desired one.
469 This affects many parts in rhodecode including commiters names, filenames,
470 encoding of commit messages. In addition RhodeCode can detect if `chardet`
471 library is installed. If `chardet` is detected RhodeCode will fallback to it
472 when there are encode/decode errors.
473
474
364 475 Setting Up Celery
365 476 -----------------
366 477
@@ -397,27 +508,36 b' Nginx virtual host example'
397 508
398 509 Sample config for nginx using proxy::
399 510
511 upstream rc {
512 server 127.0.0.1:5000;
513 # add more instances for load balancing
514 #server 127.0.0.1:5001;
515 #server 127.0.0.1:5002;
516 }
517
400 518 server {
401 519 listen 80;
402 520 server_name hg.myserver.com;
403 521 access_log /var/log/nginx/rhodecode.access.log;
404 522 error_log /var/log/nginx/rhodecode.error.log;
523
405 524 location / {
406 root /var/www/rhodecode/rhodecode/public/;
407 if (!-f $request_filename){
408 proxy_pass http://127.0.0.1:5000;
409 }
410 #this is important if you want to use https !!!
411 proxy_set_header X-Url-Scheme $scheme;
412 include /etc/nginx/proxy.conf;
525 try_files $uri @rhode;
413 526 }
527
528 location @rhode {
529 proxy_pass http://rc;
530 include /etc/nginx/proxy.conf;
531 }
532
414 533 }
415 534
416 535 Here's the proxy.conf. It's tuned so it will not timeout on long
417 536 pushes or large pushes::
418
537
419 538 proxy_redirect off;
420 539 proxy_set_header Host $host;
540 proxy_set_header X-Url-Scheme $scheme;
421 541 proxy_set_header X-Host $http_host;
422 542 proxy_set_header X-Real-IP $remote_addr;
423 543 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@@ -1,12 +1,12 b''
1 1 {% extends "basic/layout.html" %}
2 2
3 3 {% block sidebarlogo %}
4 <h3>Support my development effort.</h3>
4 <h3>Support RhodeCode development.</h3>
5 5 <div style="text-align:center">
6 6 <form action="https://www.paypal.com/cgi-bin/webscr" method="post">
7 7 <input type="hidden" name="cmd" value="_s-xclick">
8 8 <input type="hidden" name="hosted_button_id" value="8U2LLRPLBKWDU">
9 <input style="border:0px !important" type="image" src="https://www.paypal.com/en_US/i/btn/btn_donate_SM.gif"
9 <input style="border:0px !important" type="image" src="https://www.paypal.com/en_US/i/btn/btn_donate_SM.gif"
10 10 border="0" name="submit" alt="PayPal - The safer, easier way to pay online!">
11 11 <img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1">
12 12 </form>
@@ -36,6 +36,31 b' Compare view is also available from the '
36 36 one changeset
37 37
38 38
39 Non changeable repository urls
40 ------------------------------
41
42 Due to complicated nature of repository grouping, often urls of repositories
43 can change.
44
45 example::
46
47 #before
48 http://server.com/repo_name
49 # after insertion to test_group group the url will be
50 http://server.com/test_group/repo_name
51
52 This can be an issue for build systems and any other hardcoded scripts, moving
53 repository to a group leads to a need for changing external systems. To
54 overcome this RhodeCode introduces a non changable replacement url. It's
55 simply an repository ID prefixed with `_` above urls are also accessible as::
56
57 http://server.com/_<ID>
58
59 Since ID are always the same moving the repository will not affect such url.
60 the _<ID> syntax can be used anywhere in the system so urls with repo_name
61 for changelogs, files and other can be exchanged with _<ID> syntax.
62
63
39 64
40 65 Mailing
41 66 -------
@@ -1,13 +1,41 b''
1 .. _enable_git:
1 .. _git_support:
2 2
3 Enabling GIT support (beta)
4 ===========================
3 GIT support
4 ===========
5 5
6 6
7 Git support in RhodeCode 1.1 was disabled due to current instability issues.
8 However,if you would like to test git support please feel free to re-enable it.
9 To re-enable GIT support just uncomment the git line in the
10 file **rhodecode/__init__.py**
7 Git support in RhodeCode 1.3 was enabled by default.
8 Although There are some limitations on git usage.
9
10 - No hooks are runned for git push/pull actions.
11 - logs in action journals don't have git operations
12 - large pushes needs http server with chunked encoding support.
13
14 if you plan to use git you need to run RhodeCode with some
15 http server that supports chunked encoding which git http protocol uses,
16 i recommend using waitress_ or gunicorn_ (linux only) for `paste` wsgi app
17 replacement.
18
19 To use waitress simply change change the following in the .ini file::
20
21 use = egg:Paste#http
22
23 To::
24
25 use = egg:waitress#main
26
27 And comment out bellow options::
28
29 threadpool_workers =
30 threadpool_max_requests =
31 use_threadpool =
32
33
34 You can simply run `paster serve` as usual.
35
36
37 You can always disable git/hg support by editing a
38 file **rhodecode/__init__.py** and commenting out backends
11 39
12 40 .. code-block:: python
13 41
@@ -16,9 +44,5 b' file **rhodecode/__init__.py**'
16 44 #'git': 'Git repository',
17 45 }
18 46
19 .. note::
20 Please note that the git support provided by RhodeCode is not yet fully
21 stable and RhodeCode might crash while using git repositories. (That is why
22 it is currently disabled.) Thus be careful about enabling git support, and
23 certainly don't use it in a production setting!
24 No newline at end of file
47 .. _waitress: http://pypi.python.org/pypi/waitress
48 .. _gunicorn: http://pypi.python.org/pypi/gunicorn No newline at end of file
1 NO CONTENT: modified file chmod 100755 => 100644
@@ -17,6 +17,7 b' pdebug = false'
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 #email_prefix = [RhodeCode]
20 21
21 22 #smtp_server = mail.server.com
22 23 #smtp_username =
@@ -45,14 +46,52 b' port = 8001'
45 46 use = egg:rhodecode
46 47 full_stack = true
47 48 static_files = true
48 lang=en
49 lang = en
49 50 cache_dir = %(here)s/data
50 51 index_dir = %(here)s/data/index
51 app_instance_uuid = prod1234
52 app_instance_uuid = rc-production
52 53 cut_off_limit = 256000
53 force_https = false
54 force_https = false
54 55 commit_parse_limit = 50
55 56 use_gravatar = true
57 container_auth_enabled = false
58 proxypass_auth_enabled = false
59 default_encoding = utf8
60
61 ## overwrite schema of clone url
62 ## available vars:
63 ## scheme - http/https
64 ## user - current user
65 ## pass - password
66 ## netloc - network location
67 ## path - usually repo_name
68
69 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
70
71 ## issue tracking mapping for commits messages
72 ## comment out issue_pat, issue_server, issue_prefix to enable
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #<numbers> with a regex passive group for `#`
76 ## {id} will be all groups matched from this pattern
77
78 issue_pat = (?:\s*#)(\d+)
79
80 ## server url to the issue, each {id} will be replaced with match
81 ## fetched from the regex and {repo} is replaced with repository name
82
83 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
84
85 ## prefix to add to link to indicate it's an url
86 ## #314 will be replaced by <issue_prefix><id>
87
88 issue_prefix = #
89
90 ## instance-id prefix
91 ## a prefix key for this instance used for cache invalidation when running
92 ## multiple instances of rhodecode, make sure it's globally unique for
93 ## all running rhodecode instances. Leave empty if you don't use it
94 instance_id =
56 95
57 96 ####################################
58 97 ### CELERY CONFIG ####
@@ -91,21 +130,27 b' beaker.cache.regions=super_short_term,sh'
91 130
92 131 beaker.cache.super_short_term.type=memory
93 132 beaker.cache.super_short_term.expire=10
133 beaker.cache.super_short_term.key_length = 256
94 134
95 135 beaker.cache.short_term.type=memory
96 136 beaker.cache.short_term.expire=60
137 beaker.cache.short_term.key_length = 256
97 138
98 139 beaker.cache.long_term.type=memory
99 140 beaker.cache.long_term.expire=36000
141 beaker.cache.long_term.key_length = 256
100 142
101 143 beaker.cache.sql_cache_short.type=memory
102 144 beaker.cache.sql_cache_short.expire=10
145 beaker.cache.sql_cache_short.key_length = 256
103 146
104 147 beaker.cache.sql_cache_med.type=memory
105 148 beaker.cache.sql_cache_med.expire=360
149 beaker.cache.sql_cache_med.key_length = 256
106 150
107 151 beaker.cache.sql_cache_long.type=file
108 152 beaker.cache.sql_cache_long.expire=3600
153 beaker.cache.sql_cache_long.key_length = 256
109 154
110 155 ####################################
111 156 ### BEAKER SESSION ####
@@ -113,12 +158,27 b' beaker.cache.sql_cache_long.expire=3600'
113 158 ## Type of storage used for the session, current types are
114 159 ## dbm, file, memcached, database, and memory.
115 160 ## The storage uses the Container API
116 ##that is also used by the cache system.
117 beaker.session.type = file
161 ## that is also used by the cache system.
162
163 ## db session example
164
165 #beaker.session.type = ext:database
166 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
167 #beaker.session.table_name = db_session
168
169 ## encrypted cookie session, good for many instances
170 #beaker.session.type = cookie
118 171
172 beaker.session.type = file
119 173 beaker.session.key = rhodecode
120 beaker.session.secret = g654dcno0-9873jhgfreyu
174 # secure cookie requires AES python libraries
175 #beaker.session.encrypt_key = g654dcno0-9873jhgfreyu
176 #beaker.session.validate_key = 9712sds2212c--zxc123
121 177 beaker.session.timeout = 36000
178 beaker.session.httponly = true
179
180 ## uncomment for https secure cookie
181 beaker.session.secure = false
122 182
123 183 ##auto save the session to not to use .save()
124 184 beaker.session.auto = False
@@ -126,7 +186,7 b' beaker.session.auto = False'
126 186 ##true exire at browser close
127 187 #beaker.session.cookie_expires = 3600
128 188
129
189
130 190 ################################################################################
131 191 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
132 192 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
@@ -232,4 +292,4 b' datefmt = %Y-%m-%d %H:%M:%S'
232 292 [formatter_color_formatter_sql]
233 293 class=rhodecode.lib.colored_formatter.ColorFormatterSql
234 294 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
235 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
295 datefmt = %Y-%m-%d %H:%M:%S
@@ -1,16 +1,17 b''
1 1 Pylons==1.0.0
2 Beaker==1.5.4
2 Beaker==1.6.2
3 3 WebHelpers>=1.2
4 4 formencode==1.2.4
5 5 SQLAlchemy==0.7.4
6 6 Mako==0.5.0
7 7 pygments>=1.4
8 whoosh<1.8
8 whoosh>=2.3.0,<2.4
9 9 celery>=2.2.5,<2.3
10 10 babel
11 11 python-dateutil>=1.5.0,<2.0.0
12 12 dulwich>=0.8.0,<0.9.0
13 vcs==0.2.2
14 13 webob==1.0.8
14 markdown==2.1.1
15 docutils==0.8.1
15 16 py-bcrypt
16 mercurial==2.0.2 No newline at end of file
17 mercurial>=2.1,<2.2 No newline at end of file
@@ -26,9 +26,9 b''
26 26 import sys
27 27 import platform
28 28
29 VERSION = (1, 2, 5)
29 VERSION = (1, 3, 0)
30 30 __version__ = '.'.join((str(each) for each in VERSION[:4]))
31 __dbversion__ = 3 # defines current db version for migrations
31 __dbversion__ = 5 # defines current db version for migrations
32 32 __platform__ = platform.system()
33 33 __license__ = 'GPLv3'
34 34 __py_version__ = sys.version_info
@@ -38,19 +38,20 b" PLATFORM_OTHERS = ('Linux', 'Darwin', 'F"
38 38
39 39 requirements = [
40 40 "Pylons==1.0.0",
41 "Beaker==1.5.4",
41 "Beaker==1.6.2",
42 42 "WebHelpers>=1.2",
43 43 "formencode==1.2.4",
44 44 "SQLAlchemy==0.7.4",
45 45 "Mako==0.5.0",
46 46 "pygments>=1.4",
47 "whoosh<1.8",
47 "whoosh>=2.3.0,<2.4",
48 48 "celery>=2.2.5,<2.3",
49 49 "babel",
50 50 "python-dateutil>=1.5.0,<2.0.0",
51 51 "dulwich>=0.8.0,<0.9.0",
52 "vcs==0.2.2",
53 "webob==1.0.8"
52 "webob==1.0.8",
53 "markdown==2.1.1",
54 "docutils==0.8.1",
54 55 ]
55 56
56 57 if __py_version__ < (2, 6):
@@ -58,15 +59,15 b' if __py_version__ < (2, 6):'
58 59 requirements.append("pysqlite")
59 60
60 61 if __platform__ in PLATFORM_WIN:
61 requirements.append("mercurial==2.0.1")
62 requirements.append("mercurial>=2.1,<2.2")
62 63 else:
63 64 requirements.append("py-bcrypt")
64 requirements.append("mercurial==2.0.2")
65 requirements.append("mercurial>=2.1,<2.2")
65 66
66 67
67 68 try:
68 69 from rhodecode.lib import get_current_revision
69 _rev = get_current_revision(quiet=True)
70 _rev = get_current_revision()
70 71 except ImportError:
71 72 # this is needed when doing some setup.py operations
72 73 _rev = False
@@ -82,5 +83,10 b' def get_version():'
82 83
83 84 BACKENDS = {
84 85 'hg': 'Mercurial repository',
85 #'git': 'Git repository',
86 'git': 'Git repository',
86 87 }
88
89 CELERY_ON = False
90
91 # link to config for pylons
92 CONFIG = {}
@@ -17,6 +17,7 b' pdebug = false'
17 17 #error_email_from = paste_error@localhost
18 18 #app_email_from = rhodecode-noreply@localhost
19 19 #error_message =
20 #email_prefix = [RhodeCode]
20 21
21 22 #smtp_server = mail.server.com
22 23 #smtp_username =
@@ -45,14 +46,52 b' port = 5000'
45 46 use = egg:rhodecode
46 47 full_stack = true
47 48 static_files = true
48 lang=en
49 lang = en
49 50 cache_dir = %(here)s/data
50 51 index_dir = %(here)s/data/index
51 52 app_instance_uuid = ${app_instance_uuid}
52 53 cut_off_limit = 256000
53 force_https = false
54 force_https = false
54 55 commit_parse_limit = 50
55 56 use_gravatar = true
57 container_auth_enabled = false
58 proxypass_auth_enabled = false
59 default_encoding = utf8
60
61 ## overwrite schema of clone url
62 ## available vars:
63 ## scheme - http/https
64 ## user - current user
65 ## pass - password
66 ## netloc - network location
67 ## path - usually repo_name
68
69 #clone_uri = {scheme}://{user}{pass}{netloc}{path}
70
71 ## issue tracking mapping for commits messages
72 ## comment out issue_pat, issue_server, issue_prefix to enable
73
74 ## pattern to get the issues from commit messages
75 ## default one used here is #<numbers> with a regex passive group for `#`
76 ## {id} will be all groups matched from this pattern
77
78 issue_pat = (?:\s*#)(\d+)
79
80 ## server url to the issue, each {id} will be replaced with match
81 ## fetched from the regex and {repo} is replaced with repository name
82
83 issue_server_link = https://myissueserver.com/{repo}/issue/{id}
84
85 ## prefix to add to link to indicate it's an url
86 ## #314 will be replaced by <issue_prefix><id>
87
88 issue_prefix = #
89
90 ## instance-id prefix
91 ## a prefix key for this instance used for cache invalidation when running
92 ## multiple instances of rhodecode, make sure it's globally unique for
93 ## all running rhodecode instances. Leave empty if you don't use it
94 instance_id =
56 95
57 96 ####################################
58 97 ### CELERY CONFIG ####
@@ -91,21 +130,27 b' beaker.cache.regions=super_short_term,sh'
91 130
92 131 beaker.cache.super_short_term.type=memory
93 132 beaker.cache.super_short_term.expire=10
133 beaker.cache.super_short_term.key_length = 256
94 134
95 135 beaker.cache.short_term.type=memory
96 136 beaker.cache.short_term.expire=60
137 beaker.cache.short_term.key_length = 256
97 138
98 139 beaker.cache.long_term.type=memory
99 140 beaker.cache.long_term.expire=36000
141 beaker.cache.long_term.key_length = 256
100 142
101 143 beaker.cache.sql_cache_short.type=memory
102 144 beaker.cache.sql_cache_short.expire=10
145 beaker.cache.sql_cache_short.key_length = 256
103 146
104 147 beaker.cache.sql_cache_med.type=memory
105 148 beaker.cache.sql_cache_med.expire=360
149 beaker.cache.sql_cache_med.key_length = 256
106 150
107 151 beaker.cache.sql_cache_long.type=file
108 152 beaker.cache.sql_cache_long.expire=3600
153 beaker.cache.sql_cache_long.key_length = 256
109 154
110 155 ####################################
111 156 ### BEAKER SESSION ####
@@ -113,12 +158,27 b' beaker.cache.sql_cache_long.expire=3600'
113 158 ## Type of storage used for the session, current types are
114 159 ## dbm, file, memcached, database, and memory.
115 160 ## The storage uses the Container API
116 ##that is also used by the cache system.
117 beaker.session.type = file
161 ## that is also used by the cache system.
162
163 ## db session example
164
165 #beaker.session.type = ext:database
166 #beaker.session.sa.url = postgresql://postgres:qwe@localhost/rhodecode
167 #beaker.session.table_name = db_session
168
169 ## encrypted cookie session, good for many instances
170 #beaker.session.type = cookie
118 171
172 beaker.session.type = file
119 173 beaker.session.key = rhodecode
120 beaker.session.secret = ${app_instance_secret}
174 # secure cookie requires AES python libraries
175 #beaker.session.encrypt_key = ${app_instance_secret}
176 #beaker.session.validate_key = ${app_instance_secret}
121 177 beaker.session.timeout = 36000
178 beaker.session.httponly = true
179
180 ## uncomment for https secure cookie
181 beaker.session.secure = false
122 182
123 183 ##auto save the session to not to use .save()
124 184 beaker.session.auto = False
@@ -126,7 +186,7 b' beaker.session.auto = False'
126 186 ##true exire at browser close
127 187 #beaker.session.cookie_expires = 3600
128 188
129
189
130 190 ################################################################################
131 191 ## WARNING: *THE LINE BELOW MUST BE UNCOMMENTED ON A PRODUCTION ENVIRONMENT* ##
132 192 ## Debug mode will enable the interactive debugging tool, allowing ANYONE to ##
@@ -154,6 +214,7 b' sqlalchemy.db1.url = sqlite:///%(here)s/'
154 214 # MySQL
155 215 # sqlalchemy.db1.url = mysql://user:pass@localhost/rhodecode
156 216
217 # see sqlalchemy docs for others
157 218
158 219 sqlalchemy.db1.echo = false
159 220 sqlalchemy.db1.pool_recycle = 3600
@@ -217,13 +278,13 b' propagate = 0'
217 278 class = StreamHandler
218 279 args = (sys.stderr,)
219 280 level = INFO
220 formatter = color_formatter
281 formatter = generic
221 282
222 283 [handler_console_sql]
223 284 class = StreamHandler
224 285 args = (sys.stderr,)
225 286 level = WARN
226 formatter = color_formatter_sql
287 formatter = generic
227 288
228 289 ################
229 290 ## FORMATTERS ##
@@ -241,4 +302,4 b' datefmt = %Y-%m-%d %H:%M:%S'
241 302 [formatter_color_formatter_sql]
242 303 class=rhodecode.lib.colored_formatter.ColorFormatterSql
243 304 format= %(asctime)s.%(msecs)03d %(levelname)-5.5s [%(name)s] %(message)s
244 datefmt = %Y-%m-%d %H:%M:%S No newline at end of file
305 datefmt = %Y-%m-%d %H:%M:%S
@@ -7,13 +7,14 b' from mako.lookup import TemplateLookup'
7 7 from pylons.configuration import PylonsConfig
8 8 from pylons.error import handle_mako_error
9 9
10 import rhodecode
10 11 import rhodecode.lib.app_globals as app_globals
11 12 import rhodecode.lib.helpers
12 13
13 14 from rhodecode.config.routing import make_map
14 from rhodecode.lib import celerypylons
15 # don't remove this import it does magic for celery
16 from rhodecode.lib import celerypylons, str2bool
15 17 from rhodecode.lib import engine_from_config
16 from rhodecode.lib.timerproxy import TimerProxy
17 18 from rhodecode.lib.auth import set_available_permissions
18 19 from rhodecode.lib.utils import repo2db_mapper, make_ui, set_rhodecode_config
19 20 from rhodecode.model import init_model
@@ -38,10 +39,13 b' def load_environment(global_conf, app_co'
38 39 # Initialize config with the basic options
39 40 config.init_app(global_conf, app_conf, package='rhodecode', paths=paths)
40 41
42 # store some globals into rhodecode
43 rhodecode.CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
44
41 45 config['routes.map'] = make_map(config)
42 46 config['pylons.app_globals'] = app_globals.Globals(config)
43 47 config['pylons.h'] = rhodecode.lib.helpers
44
48 rhodecode.CONFIG = config
45 49 # Setup cache object as early as possible
46 50 import pylons
47 51 pylons.cache._push_object(config['pylons.app_globals'].cache)
@@ -54,7 +58,7 b' def load_environment(global_conf, app_co'
54 58 input_encoding='utf-8', default_filters=['escape'],
55 59 imports=['from webhelpers.html import escape'])
56 60
57 #sets the c attribute access when don't existing attribute are accessed
61 # sets the c attribute access when don't existing attribute are accessed
58 62 config['pylons.strict_tmpl_context'] = True
59 63 test = os.path.split(config['__file__'])[-1] == 'test.ini'
60 64 if test:
@@ -63,7 +67,7 b' def load_environment(global_conf, app_co'
63 67 create_test_env(TESTS_TMP_PATH, config)
64 68 create_test_index(TESTS_TMP_PATH, config, True)
65 69
66 #MULTIPLE DB configs
70 # MULTIPLE DB configs
67 71 # Setup the SQLAlchemy database engine
68 72 sa_engine_db1 = engine_from_config(config, 'sqlalchemy.db1.')
69 73
@@ -77,4 +81,7 b' def load_environment(global_conf, app_co'
77 81 # CONFIGURATION OPTIONS HERE (note: all config options will override
78 82 # any Pylons config options)
79 83
84 # store config reference into our module to skip import magic of
85 # pylons
86 rhodecode.CONFIG.update(config)
80 87 return config
@@ -51,15 +51,16 b' def make_app(global_conf, full_stack=Tru'
51 51 from rhodecode.lib.profiler import ProfilingMiddleware
52 52 app = ProfilingMiddleware(app)
53 53
54 # we want our low level middleware to get to the request ASAP. We don't
55 # need any pylons stack middleware in them
56 app = SimpleHg(app, config)
57 app = SimpleGit(app, config)
54 if asbool(full_stack):
58 55
59 if asbool(full_stack):
60 56 # Handle Python exceptions
61 57 app = ErrorHandler(app, global_conf, **config['pylons.errorware'])
62 58
59 # we want our low level middleware to get to the request ASAP. We don't
60 # need any pylons stack middleware in them
61 app = SimpleHg(app, config)
62 app = SimpleGit(app, config)
63
63 64 # Display error documents for 401, 403, 404 status codes (and
64 65 # 500 when debug is disabled)
65 66 if asbool(config['debug']):
@@ -8,7 +8,6 b' refer to the routes manual at http://rou'
8 8 from __future__ import with_statement
9 9 from routes import Mapper
10 10
11
12 11 # prefix for non repository related links needs to be prefixed with `/`
13 12 ADMIN_PREFIX = '/_admin'
14 13
@@ -26,18 +25,27 b' def make_map(config):'
26 25 def check_repo(environ, match_dict):
27 26 """
28 27 check for valid repository for proper 404 handling
29
28
30 29 :param environ:
31 30 :param match_dict:
32 31 """
32 from rhodecode.model.db import Repository
33 repo_name = match_dict.get('repo_name')
33 34
34 repo_name = match_dict.get('repo_name')
35 try:
36 by_id = repo_name.split('_')
37 if len(by_id) == 2 and by_id[1].isdigit():
38 repo_name = Repository.get(by_id[1]).repo_name
39 match_dict['repo_name'] = repo_name
40 except:
41 pass
42
35 43 return is_valid_repo(repo_name, config['base_path'])
36 44
37 45 def check_group(environ, match_dict):
38 46 """
39 47 check for valid repositories group for proper 404 handling
40
48
41 49 :param environ:
42 50 :param match_dict:
43 51 """
@@ -45,7 +53,6 b' def make_map(config):'
45 53
46 54 return is_valid_repos_group(repos_group_name, config['base_path'])
47 55
48
49 56 def check_int(environ, match_dict):
50 57 return match_dict.get('id').isdigit()
51 58
@@ -62,9 +69,14 b' def make_map(config):'
62 69 rmap.connect('home', '/', controller='home', action='index')
63 70 rmap.connect('repo_switcher', '/repos', controller='home',
64 71 action='repo_switcher')
72 rmap.connect('branch_tag_switcher', '/branches-tags/{repo_name:.*}',
73 controller='home', action='branch_tag_switcher')
65 74 rmap.connect('bugtracker',
66 75 "http://bitbucket.org/marcinkuzminski/rhodecode/issues",
67 76 _static=True)
77 rmap.connect('rst_help',
78 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
79 _static=True)
68 80 rmap.connect('rhodecode_official', "http://rhodecode.org", _static=True)
69 81
70 82 #ADMIN REPOSITORY REST ROUTES
@@ -101,8 +113,9 b' def make_map(config):'
101 113 function=check_repo))
102 114 #ajax delete repo perm user
103 115 m.connect('delete_repo_user', "/repos_delete_user/{repo_name:.*}",
104 action="delete_perm_user", conditions=dict(method=["DELETE"],
105 function=check_repo))
116 action="delete_perm_user",
117 conditions=dict(method=["DELETE"], function=check_repo))
118
106 119 #ajax delete repo perm users_group
107 120 m.connect('delete_repo_users_group',
108 121 "/repos_delete_users_group/{repo_name:.*}",
@@ -111,18 +124,20 b' def make_map(config):'
111 124
112 125 #settings actions
113 126 m.connect('repo_stats', "/repos_stats/{repo_name:.*}",
114 action="repo_stats", conditions=dict(method=["DELETE"],
115 function=check_repo))
127 action="repo_stats", conditions=dict(method=["DELETE"],
128 function=check_repo))
116 129 m.connect('repo_cache', "/repos_cache/{repo_name:.*}",
117 action="repo_cache", conditions=dict(method=["DELETE"],
130 action="repo_cache", conditions=dict(method=["DELETE"],
131 function=check_repo))
132 m.connect('repo_public_journal', "/repos_public_journal/{repo_name:.*}",
133 action="repo_public_journal", conditions=dict(method=["PUT"],
118 134 function=check_repo))
119 m.connect('repo_public_journal',
120 "/repos_public_journal/{repo_name:.*}",
121 action="repo_public_journal", conditions=dict(method=["PUT"],
122 function=check_repo))
123 135 m.connect('repo_pull', "/repo_pull/{repo_name:.*}",
124 action="repo_pull", conditions=dict(method=["PUT"],
125 function=check_repo))
136 action="repo_pull", conditions=dict(method=["PUT"],
137 function=check_repo))
138 m.connect('repo_as_fork', "/repo_as_fork/{repo_name:.*}",
139 action="repo_as_fork", conditions=dict(method=["PUT"],
140 function=check_repo))
126 141
127 142 with rmap.submapper(path_prefix=ADMIN_PREFIX,
128 143 controller='admin/repos_groups') as m:
@@ -155,6 +170,17 b' def make_map(config):'
155 170 m.connect("formatted_repos_group", "/repos_groups/{id}.{format}",
156 171 action="show", conditions=dict(method=["GET"],
157 172 function=check_int))
173 # ajax delete repos group perm user
174 m.connect('delete_repos_group_user_perm',
175 "/delete_repos_group_user_perm/{group_name:.*}",
176 action="delete_repos_group_user_perm",
177 conditions=dict(method=["DELETE"], function=check_group))
178
179 # ajax delete repos group perm users_group
180 m.connect('delete_repos_group_users_group_perm',
181 "/delete_repos_group_users_group_perm/{group_name:.*}",
182 action="delete_repos_group_users_group_perm",
183 conditions=dict(method=["DELETE"], function=check_group))
158 184
159 185 #ADMIN USER REST ROUTES
160 186 with rmap.submapper(path_prefix=ADMIN_PREFIX,
@@ -267,6 +293,34 b' def make_map(config):'
267 293 m.connect("admin_settings_create_repository", "/create_repository",
268 294 action="create_repository", conditions=dict(method=["GET"]))
269 295
296 #NOTIFICATION REST ROUTES
297 with rmap.submapper(path_prefix=ADMIN_PREFIX,
298 controller='admin/notifications') as m:
299 m.connect("notifications", "/notifications",
300 action="create", conditions=dict(method=["POST"]))
301 m.connect("notifications", "/notifications",
302 action="index", conditions=dict(method=["GET"]))
303 m.connect("notifications_mark_all_read", "/notifications/mark_all_read",
304 action="mark_all_read", conditions=dict(method=["GET"]))
305 m.connect("formatted_notifications", "/notifications.{format}",
306 action="index", conditions=dict(method=["GET"]))
307 m.connect("new_notification", "/notifications/new",
308 action="new", conditions=dict(method=["GET"]))
309 m.connect("formatted_new_notification", "/notifications/new.{format}",
310 action="new", conditions=dict(method=["GET"]))
311 m.connect("/notification/{notification_id}",
312 action="update", conditions=dict(method=["PUT"]))
313 m.connect("/notification/{notification_id}",
314 action="delete", conditions=dict(method=["DELETE"]))
315 m.connect("edit_notification", "/notification/{notification_id}/edit",
316 action="edit", conditions=dict(method=["GET"]))
317 m.connect("formatted_edit_notification",
318 "/notification/{notification_id}.{format}/edit",
319 action="edit", conditions=dict(method=["GET"]))
320 m.connect("notification", "/notification/{notification_id}",
321 action="show", conditions=dict(method=["GET"]))
322 m.connect("formatted_notification", "/notifications/{notification_id}.{format}",
323 action="show", conditions=dict(method=["GET"]))
270 324
271 325 #ADMIN MAIN PAGES
272 326 with rmap.submapper(path_prefix=ADMIN_PREFIX,
@@ -276,13 +330,12 b' def make_map(config):'
276 330 action='add_repo')
277 331
278 332 #==========================================================================
279 # API V1
333 # API V2
280 334 #==========================================================================
281 335 with rmap.submapper(path_prefix=ADMIN_PREFIX,
282 336 controller='api/api') as m:
283 337 m.connect('api', '/api')
284 338
285
286 339 #USER JOURNAL
287 340 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX, controller='journal')
288 341
@@ -344,6 +397,16 b' def make_map(config):'
344 397 controller='changeset', revision='tip',
345 398 conditions=dict(function=check_repo))
346 399
400 rmap.connect('changeset_comment',
401 '/{repo_name:.*}/changeset/{revision}/comment',
402 controller='changeset', revision='tip', action='comment',
403 conditions=dict(function=check_repo))
404
405 rmap.connect('changeset_comment_delete',
406 '/{repo_name:.*}/changeset/comment/{comment_id}/delete',
407 controller='changeset', action='delete_comment',
408 conditions=dict(function=check_repo, method=["DELETE"]))
409
347 410 rmap.connect('raw_changeset_home',
348 411 '/{repo_name:.*}/raw-changeset/{revision}',
349 412 controller='changeset', action='raw_changeset',
@@ -361,6 +424,9 b' def make_map(config):'
361 424 rmap.connect('tags_home', '/{repo_name:.*}/tags',
362 425 controller='tags', conditions=dict(function=check_repo))
363 426
427 rmap.connect('bookmarks_home', '/{repo_name:.*}/bookmarks',
428 controller='bookmarks', conditions=dict(function=check_repo))
429
364 430 rmap.connect('changelog_home', '/{repo_name:.*}/changelog',
365 431 controller='changelog', conditions=dict(function=check_repo))
366 432
@@ -423,19 +489,19 b' def make_map(config):'
423 489 conditions=dict(function=check_repo))
424 490
425 491 rmap.connect('repo_fork_create_home', '/{repo_name:.*}/fork',
426 controller='settings', action='fork_create',
492 controller='forks', action='fork_create',
427 493 conditions=dict(function=check_repo, method=["POST"]))
428 494
429 495 rmap.connect('repo_fork_home', '/{repo_name:.*}/fork',
430 controller='settings', action='fork',
496 controller='forks', action='fork',
431 497 conditions=dict(function=check_repo))
432 498
499 rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
500 controller='forks', action='forks',
501 conditions=dict(function=check_repo))
502
433 503 rmap.connect('repo_followers_home', '/{repo_name:.*}/followers',
434 504 controller='followers', action='followers',
435 505 conditions=dict(function=check_repo))
436 506
437 rmap.connect('repo_forks_home', '/{repo_name:.*}/forks',
438 controller='forks', action='forks',
439 conditions=dict(function=check_repo))
440
441 507 return rmap
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 7, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Nov 26, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -39,7 +39,7 b' from rhodecode.lib import helpers as h'
39 39 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
40 40 from rhodecode.lib.exceptions import LdapImportError
41 41 from rhodecode.model.forms import LdapSettingsForm
42 from rhodecode.model.db import RhodeCodeSettings
42 from rhodecode.model.db import RhodeCodeSetting
43 43
44 44 log = logging.getLogger(__name__)
45 45
@@ -83,7 +83,7 b' class LdapSettingsController(BaseControl'
83 83 super(LdapSettingsController, self).__before__()
84 84
85 85 def index(self):
86 defaults = RhodeCodeSettings.get_ldap_settings()
86 defaults = RhodeCodeSetting.get_ldap_settings()
87 87 c.search_scope_cur = defaults.get('ldap_search_scope')
88 88 c.tls_reqcert_cur = defaults.get('ldap_tls_reqcert')
89 89 c.tls_kind_cur = defaults.get('ldap_tls_kind')
@@ -107,7 +107,7 b' class LdapSettingsController(BaseControl'
107 107
108 108 for k, v in form_result.items():
109 109 if k.startswith('ldap_'):
110 setting = RhodeCodeSettings.get_by_name(k)
110 setting = RhodeCodeSetting.get_by_name(k)
111 111 setting.app_settings_value = v
112 112 self.sa.add(setting)
113 113
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 27, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -38,6 +38,7 b' from rhodecode.lib.base import BaseContr'
38 38 from rhodecode.model.forms import DefaultPermissionsForm
39 39 from rhodecode.model.permission import PermissionModel
40 40 from rhodecode.model.db import User
41 from rhodecode.model.meta import Session
41 42
42 43 log = logging.getLogger(__name__)
43 44
@@ -101,6 +102,7 b' class PermissionsController(BaseControll'
101 102 form_result = _form.to_python(dict(request.POST))
102 103 form_result.update({'perm_user_name': id})
103 104 permission_model.update(form_result)
105 Session.commit()
104 106 h.flash(_('Default permissions updated successfully'),
105 107 category='success')
106 108
@@ -3,11 +3,11 b''
3 3 rhodecode.controllers.admin.repos
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 Admin controller for RhodeCode
6 Repositories controller for RhodeCode
7 7
8 8 :created_on: Apr 7, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -29,9 +29,10 b' import formencode'
29 29 from formencode import htmlfill
30 30
31 31 from paste.httpexceptions import HTTPInternalServerError
32 from pylons import request, response, session, tmpl_context as c, url
33 from pylons.controllers.util import abort, redirect
32 from pylons import request, session, tmpl_context as c, url
33 from pylons.controllers.util import redirect
34 34 from pylons.i18n.translation import _
35 from sqlalchemy.exc import IntegrityError
35 36
36 37 from rhodecode.lib import helpers as h
37 38 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator, \
@@ -39,11 +40,11 b' from rhodecode.lib.auth import LoginRequ'
39 40 from rhodecode.lib.base import BaseController, render
40 41 from rhodecode.lib.utils import invalidate_cache, action_logger, repo_name_slug
41 42 from rhodecode.lib.helpers import get_token
42 from rhodecode.model.db import User, Repository, UserFollowing, Group
43 from rhodecode.model.meta import Session
44 from rhodecode.model.db import User, Repository, UserFollowing, RepoGroup
43 45 from rhodecode.model.forms import RepoForm
44 46 from rhodecode.model.scm import ScmModel
45 47 from rhodecode.model.repo import RepoModel
46 from sqlalchemy.exc import IntegrityError
47 48
48 49 log = logging.getLogger(__name__)
49 50
@@ -63,9 +64,9 b' class ReposController(BaseController):'
63 64 super(ReposController, self).__before__()
64 65
65 66 def __load_defaults(self):
66 c.repo_groups = Group.groups_choices()
67 c.repo_groups = RepoGroup.groups_choices()
67 68 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
68
69
69 70 repo_model = RepoModel()
70 71 c.users_array = repo_model.get_users_js()
71 72 c.users_groups_array = repo_model.get_users_groups_js()
@@ -96,12 +97,13 b' class ReposController(BaseController):'
96 97 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
97 98
98 99 if c.repo_info.stats:
99 last_rev = c.repo_info.stats.stat_on_revision
100 # this is on what revision we ended up so we add +1 for count
101 last_rev = c.repo_info.stats.stat_on_revision + 1
100 102 else:
101 103 last_rev = 0
102 104 c.stats_revision = last_rev
103 105
104 c.repo_last_rev = repo.count() - 1 if repo.revisions else 0
106 c.repo_last_rev = repo.count() if repo.revisions else 0
105 107
106 108 if last_rev == 0 or c.repo_last_rev == 0:
107 109 c.stats_percentage = 0
@@ -110,6 +112,10 b' class ReposController(BaseController):'
110 112 c.repo_last_rev) * 100)
111 113
112 114 defaults = RepoModel()._get_defaults(repo_name)
115
116 c.repos_list = [('', _('--REMOVE FORK--'))]
117 c.repos_list += [(x.repo_id, x.repo_name) for x in
118 Repository.query().order_by(Repository.repo_name).all()]
113 119 return defaults
114 120
115 121 @HasPermissionAllDecorator('hg.admin')
@@ -127,13 +133,13 b' class ReposController(BaseController):'
127 133 """
128 134 POST /repos: Create a new item"""
129 135 # url('repos')
130 repo_model = RepoModel()
136
131 137 self.__load_defaults()
132 138 form_result = {}
133 139 try:
134 140 form_result = RepoForm(repo_groups=c.repo_groups_choices)()\
135 141 .to_python(dict(request.POST))
136 repo_model.create(form_result, self.rhodecode_user)
142 RepoModel().create(form_result, self.rhodecode_user)
137 143 if form_result['clone_uri']:
138 144 h.flash(_('created repository %s from %s') \
139 145 % (form_result['repo_name'], form_result['clone_uri']),
@@ -143,13 +149,13 b' class ReposController(BaseController):'
143 149 category='success')
144 150
145 151 if request.POST.get('user_created'):
146 #created by regular non admin user
152 # created by regular non admin user
147 153 action_logger(self.rhodecode_user, 'user_created_repo',
148 154 form_result['repo_name_full'], '', self.sa)
149 155 else:
150 156 action_logger(self.rhodecode_user, 'admin_created_repo',
151 157 form_result['repo_name_full'], '', self.sa)
152
158 Session.commit()
153 159 except formencode.Invalid, errors:
154 160
155 161 c.new_repo = errors.value['repo_name']
@@ -207,7 +213,7 b' class ReposController(BaseController):'
207 213 changed_name = repo.repo_name
208 214 action_logger(self.rhodecode_user, 'admin_updated_repo',
209 215 changed_name, '', self.sa)
210
216 Session.commit()
211 217 except formencode.Invalid, errors:
212 218 defaults = self.__load_data(repo_name)
213 219 defaults.update(errors.value)
@@ -251,9 +257,9 b' class ReposController(BaseController):'
251 257 repo_model.delete(repo)
252 258 invalidate_cache('get_repo_cached_%s' % repo_name)
253 259 h.flash(_('deleted repository %s') % repo_name, category='success')
254
260 Session.commit()
255 261 except IntegrityError, e:
256 if e.message.find('repositories_fork_id_fkey'):
262 if e.message.find('repositories_fork_id_fkey') != -1:
257 263 log.error(traceback.format_exc())
258 264 h.flash(_('Cannot delete %s it still contains attached '
259 265 'forks') % repo_name,
@@ -271,8 +277,7 b' class ReposController(BaseController):'
271 277
272 278 return redirect(url('repos'))
273 279
274
275 @HasRepoPermissionAllDecorator('repository.admin')
280 @HasRepoPermissionAllDecorator('repository.admin')
276 281 def delete_perm_user(self, repo_name):
277 282 """
278 283 DELETE an existing repository permission user
@@ -281,9 +286,11 b' class ReposController(BaseController):'
281 286 """
282 287
283 288 try:
284 repo_model = RepoModel()
285 repo_model.delete_perm_user(request.POST, repo_name)
286 except Exception, e:
289 RepoModel().revoke_user_permission(repo=repo_name,
290 user=request.POST['user_id'])
291 Session.commit()
292 except Exception:
293 log.error(traceback.format_exc())
287 294 h.flash(_('An error occurred during deletion of repository user'),
288 295 category='error')
289 296 raise HTTPInternalServerError()
@@ -295,10 +302,14 b' class ReposController(BaseController):'
295 302
296 303 :param repo_name:
297 304 """
305
298 306 try:
299 repo_model = RepoModel()
300 repo_model.delete_perm_users_group(request.POST, repo_name)
301 except Exception, e:
307 RepoModel().revoke_users_group_permission(
308 repo=repo_name, group_name=request.POST['users_group_id']
309 )
310 Session.commit()
311 except Exception:
312 log.error(traceback.format_exc())
302 313 h.flash(_('An error occurred during deletion of repository'
303 314 ' users groups'),
304 315 category='error')
@@ -313,8 +324,8 b' class ReposController(BaseController):'
313 324 """
314 325
315 326 try:
316 repo_model = RepoModel()
317 repo_model.delete_stats(repo_name)
327 RepoModel().delete_stats(repo_name)
328 Session.commit()
318 329 except Exception, e:
319 330 h.flash(_('An error occurred during deletion of repository stats'),
320 331 category='error')
@@ -330,6 +341,7 b' class ReposController(BaseController):'
330 341
331 342 try:
332 343 ScmModel().mark_for_invalidation(repo_name)
344 Session.commit()
333 345 except Exception, e:
334 346 h.flash(_('An error occurred during cache invalidation'),
335 347 category='error')
@@ -353,6 +365,7 b' class ReposController(BaseController):'
353 365 self.scm_model.toggle_following_repo(repo_id, user_id)
354 366 h.flash(_('Updated repository visibility in public journal'),
355 367 category='success')
368 Session.commit()
356 369 except:
357 370 h.flash(_('An error occurred during setting this'
358 371 ' repository in public journal'),
@@ -380,6 +393,28 b' class ReposController(BaseController):'
380 393 return redirect(url('edit_repo', repo_name=repo_name))
381 394
382 395 @HasPermissionAllDecorator('hg.admin')
396 def repo_as_fork(self, repo_name):
397 """
398 Mark given repository as a fork of another
399
400 :param repo_name:
401 """
402 try:
403 fork_id = request.POST.get('id_fork_of')
404 repo = ScmModel().mark_as_fork(repo_name, fork_id,
405 self.rhodecode_user.username)
406 fork = repo.fork.repo_name if repo.fork else _('Nothing')
407 Session.commit()
408 h.flash(_('Marked repo %s as fork of %s' % (repo_name,fork)),
409 category='success')
410 except Exception, e:
411 raise
412 h.flash(_('An error occurred during this operation'),
413 category='error')
414
415 return redirect(url('edit_repo', repo_name=repo_name))
416
417 @HasPermissionAllDecorator('hg.admin')
383 418 def show(self, repo_name, format='html'):
384 419 """GET /repos/repo_name: Show a specific item"""
385 420 # url('repo', repo_name=ID)
@@ -1,22 +1,50 b''
1 # -*- coding: utf-8 -*-
2 """
3 rhodecode.controllers.admin.repos_groups
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
6 Repositories groups controller for RhodeCode
7
8 :created_on: Mar 23, 2010
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 :license: GPLv3, see COPYING for more details.
12 """
13 # This program is free software: you can redistribute it and/or modify
14 # it under the terms of the GNU General Public License as published by
15 # the Free Software Foundation, either version 3 of the License, or
16 # (at your option) any later version.
17 #
18 # This program is distributed in the hope that it will be useful,
19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 # GNU General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25
1 26 import logging
2 27 import traceback
3 28 import formencode
4 29
5 30 from formencode import htmlfill
6 from operator import itemgetter
7 31
8 from pylons import request, response, session, tmpl_context as c, url
9 from pylons.controllers.util import abort, redirect
32 from pylons import request, tmpl_context as c, url
33 from pylons.controllers.util import redirect
10 34 from pylons.i18n.translation import _
11 35
12 36 from sqlalchemy.exc import IntegrityError
13 37
14 38 from rhodecode.lib import helpers as h
15 from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator
39 from rhodecode.lib.auth import LoginRequired, HasPermissionAnyDecorator,\
40 HasReposGroupPermissionAnyDecorator
16 41 from rhodecode.lib.base import BaseController, render
17 from rhodecode.model.db import Group
42 from rhodecode.model.db import RepoGroup
18 43 from rhodecode.model.repos_group import ReposGroupModel
19 44 from rhodecode.model.forms import ReposGroupForm
45 from rhodecode.model.meta import Session
46 from rhodecode.model.repo import RepoModel
47 from webob.exc import HTTPInternalServerError
20 48
21 49 log = logging.getLogger(__name__)
22 50
@@ -32,9 +60,13 b' class ReposGroupsController(BaseControll'
32 60 super(ReposGroupsController, self).__before__()
33 61
34 62 def __load_defaults(self):
35 c.repo_groups = Group.groups_choices()
63 c.repo_groups = RepoGroup.groups_choices()
36 64 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
37 65
66 repo_model = RepoModel()
67 c.users_array = repo_model.get_users_js()
68 c.users_groups_array = repo_model.get_users_groups_js()
69
38 70 def __load_data(self, group_id):
39 71 """
40 72 Load defaults settings for edit, and update
@@ -43,21 +75,30 b' class ReposGroupsController(BaseControll'
43 75 """
44 76 self.__load_defaults()
45 77
46 repo_group = Group.get(group_id)
78 repo_group = RepoGroup.get(group_id)
47 79
48 80 data = repo_group.get_dict()
49 81
50 82 data['group_name'] = repo_group.name
51 83
84 # fill repository users
85 for p in repo_group.repo_group_to_perm:
86 data.update({'u_perm_%s' % p.user.username:
87 p.permission.permission_name})
88
89 # fill repository groups
90 for p in repo_group.users_group_to_perm:
91 data.update({'g_perm_%s' % p.users_group.users_group_name:
92 p.permission.permission_name})
93
52 94 return data
53 95
54 96 @HasPermissionAnyDecorator('hg.admin')
55 97 def index(self, format='html'):
56 98 """GET /repos_groups: All items in the collection"""
57 99 # url('repos_groups')
58
59 sk = lambda g:g.parents[0].group_name if g.parents else g.group_name
60 c.groups = sorted(Group.query().all(), key=sk)
100 sk = lambda g: g.parents[0].group_name if g.parents else g.group_name
101 c.groups = sorted(RepoGroup.query().all(), key=sk)
61 102 return render('admin/repos_groups/repos_groups_show.html')
62 103
63 104 @HasPermissionAnyDecorator('hg.admin')
@@ -65,12 +106,16 b' class ReposGroupsController(BaseControll'
65 106 """POST /repos_groups: Create a new item"""
66 107 # url('repos_groups')
67 108 self.__load_defaults()
68 repos_group_model = ReposGroupModel()
69 repos_group_form = ReposGroupForm(available_groups=
109 repos_group_form = ReposGroupForm(available_groups =
70 110 c.repo_groups_choices)()
71 111 try:
72 112 form_result = repos_group_form.to_python(dict(request.POST))
73 repos_group_model.create(form_result)
113 ReposGroupModel().create(
114 group_name=form_result['group_name'],
115 group_description=form_result['group_description'],
116 parent=form_result['group_parent_id']
117 )
118 Session.commit()
74 119 h.flash(_('created repos group %s') \
75 120 % form_result['group_name'], category='success')
76 121 #TODO: in futureaction_logger(, '', '', '', self.sa)
@@ -89,7 +134,6 b' class ReposGroupsController(BaseControll'
89 134
90 135 return redirect(url('repos_groups'))
91 136
92
93 137 @HasPermissionAnyDecorator('hg.admin')
94 138 def new(self, format='html'):
95 139 """GET /repos_groups/new: Form to create a new item"""
@@ -108,16 +152,17 b' class ReposGroupsController(BaseControll'
108 152 # url('repos_group', id=ID)
109 153
110 154 self.__load_defaults()
111 c.repos_group = Group.get(id)
155 c.repos_group = RepoGroup.get(id)
112 156
113 repos_group_model = ReposGroupModel()
114 repos_group_form = ReposGroupForm(edit=True,
115 old_data=c.repos_group.get_dict(),
116 available_groups=
117 c.repo_groups_choices)()
157 repos_group_form = ReposGroupForm(
158 edit=True,
159 old_data=c.repos_group.get_dict(),
160 available_groups=c.repo_groups_choices
161 )()
118 162 try:
119 163 form_result = repos_group_form.to_python(dict(request.POST))
120 repos_group_model.update(id, form_result)
164 ReposGroupModel().update(id, form_result)
165 Session.commit()
121 166 h.flash(_('updated repos group %s') \
122 167 % form_result['group_name'], category='success')
123 168 #TODO: in futureaction_logger(, '', '', '', self.sa)
@@ -136,7 +181,6 b' class ReposGroupsController(BaseControll'
136 181
137 182 return redirect(url('repos_groups'))
138 183
139
140 184 @HasPermissionAnyDecorator('hg.admin')
141 185 def delete(self, id):
142 186 """DELETE /repos_groups/id: Delete an existing item"""
@@ -147,8 +191,7 b' class ReposGroupsController(BaseControll'
147 191 # method='delete')
148 192 # url('repos_group', id=ID)
149 193
150 repos_group_model = ReposGroupModel()
151 gr = Group.get(id)
194 gr = RepoGroup.get(id)
152 195 repos = gr.repositories.all()
153 196 if repos:
154 197 h.flash(_('This group contains %s repositores and cannot be '
@@ -157,11 +200,12 b' class ReposGroupsController(BaseControll'
157 200 return redirect(url('repos_groups'))
158 201
159 202 try:
160 repos_group_model.delete(id)
203 ReposGroupModel().delete(id)
204 Session.commit()
161 205 h.flash(_('removed repos group %s' % gr.group_name), category='success')
162 206 #TODO: in future action_logger(, '', '', '', self.sa)
163 207 except IntegrityError, e:
164 if e.message.find('groups_group_parent_id_fkey'):
208 if e.message.find('groups_group_parent_id_fkey') != -1:
165 209 log.error(traceback.format_exc())
166 210 h.flash(_('Cannot delete this group it still contains '
167 211 'subgroups'),
@@ -178,15 +222,57 b' class ReposGroupsController(BaseControll'
178 222
179 223 return redirect(url('repos_groups'))
180 224
225 @HasReposGroupPermissionAnyDecorator('group.admin')
226 def delete_repos_group_user_perm(self, group_name):
227 """
228 DELETE an existing repositories group permission user
229
230 :param group_name:
231 """
232
233 try:
234 ReposGroupModel().revoke_user_permission(
235 repos_group=group_name, user=request.POST['user_id']
236 )
237 Session.commit()
238 except Exception:
239 log.error(traceback.format_exc())
240 h.flash(_('An error occurred during deletion of group user'),
241 category='error')
242 raise HTTPInternalServerError()
243
244 @HasReposGroupPermissionAnyDecorator('group.admin')
245 def delete_repos_group_users_group_perm(self, group_name):
246 """
247 DELETE an existing repositories group permission users group
248
249 :param group_name:
250 """
251
252 try:
253 ReposGroupModel().revoke_users_group_permission(
254 repos_group=group_name,
255 group_name=request.POST['users_group_id']
256 )
257 Session.commit()
258 except Exception:
259 log.error(traceback.format_exc())
260 h.flash(_('An error occurred during deletion of group'
261 ' users groups'),
262 category='error')
263 raise HTTPInternalServerError()
264
181 265 def show_by_name(self, group_name):
182 id_ = Group.get_by_group_name(group_name).group_id
266 id_ = RepoGroup.get_by_group_name(group_name).group_id
183 267 return self.show(id_)
184 268
269 @HasReposGroupPermissionAnyDecorator('group.read', 'group.write',
270 'group.admin')
185 271 def show(self, id, format='html'):
186 272 """GET /repos_groups/id: Show a specific item"""
187 273 # url('repos_group', id=ID)
188 274
189 c.group = Group.get(id)
275 c.group = RepoGroup.get(id)
190 276
191 277 if c.group:
192 278 c.group_repos = c.group.repositories.all()
@@ -201,8 +287,8 b' class ReposGroupsController(BaseControll'
201 287
202 288 c.repo_cnt = 0
203 289
204 c.groups = self.sa.query(Group).order_by(Group.group_name)\
205 .filter(Group.group_parent_id == id).all()
290 c.groups = self.sa.query(RepoGroup).order_by(RepoGroup.group_name)\
291 .filter(RepoGroup.group_parent_id == id).all()
206 292
207 293 return render('admin/repos_groups/repos_groups.html')
208 294
@@ -213,11 +299,11 b' class ReposGroupsController(BaseControll'
213 299
214 300 id_ = int(id)
215 301
216 c.repos_group = Group.get(id_)
302 c.repos_group = RepoGroup.get(id_)
217 303 defaults = self.__load_data(id_)
218 304
219 305 # we need to exclude this group from the group list for editing
220 c.repo_groups = filter(lambda x:x[0] != id_, c.repo_groups)
306 c.repo_groups = filter(lambda x: x[0] != id_, c.repo_groups)
221 307
222 308 return htmlfill.render(
223 309 render('admin/repos_groups/repos_groups_edit.html'),
@@ -225,5 +311,3 b' class ReposGroupsController(BaseControll'
225 311 encoding="UTF-8",
226 312 force_defaults=False
227 313 )
228
229
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Jul 14, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -40,13 +40,15 b' from rhodecode.lib.base import BaseContr'
40 40 from rhodecode.lib.celerylib import tasks, run_task
41 41 from rhodecode.lib.utils import repo2db_mapper, invalidate_cache, \
42 42 set_rhodecode_config, repo_name_slug
43 from rhodecode.model.db import RhodeCodeUi, Repository, Group, \
44 RhodeCodeSettings
43 from rhodecode.model.db import RhodeCodeUi, Repository, RepoGroup, \
44 RhodeCodeSetting
45 45 from rhodecode.model.forms import UserForm, ApplicationSettingsForm, \
46 46 ApplicationUiSettingsForm
47 47 from rhodecode.model.scm import ScmModel
48 48 from rhodecode.model.user import UserModel
49 49 from rhodecode.model.db import User
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.meta import Session
50 52
51 53 log = logging.getLogger(__name__)
52 54
@@ -69,7 +71,7 b' class SettingsController(BaseController)'
69 71 """GET /admin/settings: All items in the collection"""
70 72 # url('admin_settings')
71 73
72 defaults = RhodeCodeSettings.get_app_settings()
74 defaults = RhodeCodeSetting.get_app_settings()
73 75 defaults.update(self.get_hg_ui_settings())
74 76 return htmlfill.render(
75 77 render('admin/settings/settings.html'),
@@ -99,7 +101,7 b' class SettingsController(BaseController)'
99 101 # url('admin_setting', setting_id=ID)
100 102 if setting_id == 'mapping':
101 103 rm_obsolete = request.POST.get('destroy', False)
102 log.debug('Rescanning directories with destroy=%s', rm_obsolete)
104 log.debug('Rescanning directories with destroy=%s' % rm_obsolete)
103 105 initial = ScmModel().repo_scan()
104 106 log.debug('invalidating all repositories')
105 107 for repo_name in initial.keys():
@@ -124,15 +126,15 b' class SettingsController(BaseController)'
124 126 form_result = application_form.to_python(dict(request.POST))
125 127
126 128 try:
127 hgsettings1 = RhodeCodeSettings.get_by_name('title')
129 hgsettings1 = RhodeCodeSetting.get_by_name('title')
128 130 hgsettings1.app_settings_value = \
129 131 form_result['rhodecode_title']
130 132
131 hgsettings2 = RhodeCodeSettings.get_by_name('realm')
133 hgsettings2 = RhodeCodeSetting.get_by_name('realm')
132 134 hgsettings2.app_settings_value = \
133 135 form_result['rhodecode_realm']
134 136
135 hgsettings3 = RhodeCodeSettings.get_by_name('ga_code')
137 hgsettings3 = RhodeCodeSetting.get_by_name('ga_code')
136 138 hgsettings3.app_settings_value = \
137 139 form_result['rhodecode_ga_code']
138 140
@@ -226,12 +228,11 b' class SettingsController(BaseController)'
226 228 prefix_error=False,
227 229 encoding="UTF-8")
228 230
229
230 231 if setting_id == 'hooks':
231 232 ui_key = request.POST.get('new_hook_ui_key')
232 233 ui_value = request.POST.get('new_hook_ui_value')
233 234 try:
234
235
235 236 if ui_value and ui_key:
236 237 RhodeCodeUi.create_or_update_hook(ui_key, ui_value)
237 238 h.flash(_('Added new hook'),
@@ -240,13 +241,14 b' class SettingsController(BaseController)'
240 241 # check for edits
241 242 update = False
242 243 _d = request.POST.dict_of_lists()
243 for k, v in zip(_d.get('hook_ui_key',[]), _d.get('hook_ui_value_new',[])):
244 for k, v in zip(_d.get('hook_ui_key', []),
245 _d.get('hook_ui_value_new', [])):
244 246 RhodeCodeUi.create_or_update_hook(k, v)
245 247 update = True
246 248
247 249 if update:
248 250 h.flash(_('Updated hooks'), category='success')
249
251 Session.commit()
250 252 except:
251 253 log.error(traceback.format_exc())
252 254 h.flash(_('error occurred during hook creation'),
@@ -254,6 +256,21 b' class SettingsController(BaseController)'
254 256
255 257 return redirect(url('admin_edit_setting', setting_id='hooks'))
256 258
259 if setting_id == 'email':
260 test_email = request.POST.get('test_email')
261 test_email_subj = 'RhodeCode TestEmail'
262 test_email_body = 'RhodeCode Email test'
263
264 test_email_html_body = EmailNotificationModel()\
265 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
266 body=test_email_body)
267
268 recipients = [test_email] if [test_email] else None
269
270 run_task(tasks.send_email, recipients, test_email_subj,
271 test_email_body, test_email_html_body)
272
273 h.flash(_('Email task created'), category='success')
257 274 return redirect(url('admin_settings'))
258 275
259 276 @HasPermissionAllDecorator('hg.admin')
@@ -268,8 +285,8 b' class SettingsController(BaseController)'
268 285 if setting_id == 'hooks':
269 286 hook_id = request.POST.get('hook_id')
270 287 RhodeCodeUi.delete(hook_id)
271
272
288
289
273 290 @HasPermissionAllDecorator('hg.admin')
274 291 def show(self, setting_id, format='html'):
275 292 """
@@ -339,7 +356,7 b' class SettingsController(BaseController)'
339 356 user_model.update_my_account(uid, form_result)
340 357 h.flash(_('Your account was updated successfully'),
341 358 category='success')
342
359 Session.commit()
343 360 except formencode.Invalid, errors:
344 361 c.user = User.get(self.rhodecode_user.user_id)
345 362 all_repos = self.sa.query(Repository)\
@@ -366,7 +383,7 b' class SettingsController(BaseController)'
366 383 def create_repository(self):
367 384 """GET /_admin/create_repository: Form to create a new item"""
368 385
369 c.repo_groups = Group.groups_choices()
386 c.repo_groups = RepoGroup.groups_choices()
370 387 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
371 388
372 389 new_repo = request.GET.get('repo', '')
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 4, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -29,7 +29,7 b' import formencode'
29 29
30 30 from formencode import htmlfill
31 31 from pylons import request, session, tmpl_context as c, url, config
32 from pylons.controllers.util import abort, redirect
32 from pylons.controllers.util import redirect
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib.exceptions import DefaultUserException, \
@@ -38,9 +38,10 b' from rhodecode.lib import helpers as h'
38 38 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
39 39 from rhodecode.lib.base import BaseController, render
40 40
41 from rhodecode.model.db import User, RepoToPerm, UserToPerm, Permission
41 from rhodecode.model.db import User, Permission
42 42 from rhodecode.model.forms import UserForm
43 43 from rhodecode.model.user import UserModel
44 from rhodecode.model.meta import Session
44 45
45 46 log = logging.getLogger(__name__)
46 47
@@ -71,12 +72,13 b' class UsersController(BaseController):'
71 72 # url('users')
72 73
73 74 user_model = UserModel()
74 login_form = UserForm()()
75 user_form = UserForm()()
75 76 try:
76 form_result = login_form.to_python(dict(request.POST))
77 form_result = user_form.to_python(dict(request.POST))
77 78 user_model.create(form_result)
78 79 h.flash(_('created user %s') % form_result['username'],
79 80 category='success')
81 Session.commit()
80 82 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
81 83 except formencode.Invalid, errors:
82 84 return htmlfill.render(
@@ -114,11 +116,11 b' class UsersController(BaseController):'
114 116 form_result = _form.to_python(dict(request.POST))
115 117 user_model.update(id, form_result)
116 118 h.flash(_('User updated successfully'), category='success')
117
119 Session.commit()
118 120 except formencode.Invalid, errors:
119 121 e = errors.error_dict or {}
120 122 perm = Permission.get_by_key('hg.create.repository')
121 e.update({'create_repo_perm': UserToPerm.has_perm(id, perm)})
123 e.update({'create_repo_perm': user_model.has_perm(id, perm)})
122 124 return htmlfill.render(
123 125 render('admin/users/user_edit.html'),
124 126 defaults=errors.value,
@@ -144,6 +146,7 b' class UsersController(BaseController):'
144 146 try:
145 147 user_model.delete(id)
146 148 h.flash(_('successfully deleted user'), category='success')
149 Session.commit()
147 150 except (UserOwnsReposException, DefaultUserException), e:
148 151 h.flash(str(e), category='warning')
149 152 except Exception:
@@ -158,20 +161,19 b' class UsersController(BaseController):'
158 161 def edit(self, id, format='html'):
159 162 """GET /users/id/edit: Form to edit an existing item"""
160 163 # url('edit_user', id=ID)
161 user_model = UserModel()
162 c.user = user_model.get(id)
164 c.user = User.get(id)
163 165 if not c.user:
164 166 return redirect(url('users'))
165 167 if c.user.username == 'default':
166 168 h.flash(_("You can't edit this user"), category='warning')
167 169 return redirect(url('users'))
168 170 c.user.permissions = {}
169 c.granted_permissions = user_model.fill_perms(c.user)\
171 c.granted_permissions = UserModel().fill_perms(c.user)\
170 172 .permissions['global']
171 173
172 174 defaults = c.user.get_dict()
173 175 perm = Permission.get_by_key('hg.create.repository')
174 defaults.update({'create_repo_perm': UserToPerm.has_perm(id, perm)})
176 defaults.update({'create_repo_perm': UserModel().has_perm(id, perm)})
175 177
176 178 return htmlfill.render(
177 179 render('admin/users/user_edit.html'),
@@ -185,23 +187,24 b' class UsersController(BaseController):'
185 187 # url('user_perm', id=ID, method='put')
186 188
187 189 grant_perm = request.POST.get('create_repo_perm', False)
190 user_model = UserModel()
188 191
189 192 if grant_perm:
190 193 perm = Permission.get_by_key('hg.create.none')
191 UserToPerm.revoke_perm(id, perm)
194 user_model.revoke_perm(id, perm)
192 195
193 196 perm = Permission.get_by_key('hg.create.repository')
194 UserToPerm.grant_perm(id, perm)
197 user_model.grant_perm(id, perm)
195 198 h.flash(_("Granted 'repository create' permission to user"),
196 199 category='success')
197
200 Session.commit()
198 201 else:
199 202 perm = Permission.get_by_key('hg.create.repository')
200 UserToPerm.revoke_perm(id, perm)
203 user_model.revoke_perm(id, perm)
201 204
202 205 perm = Permission.get_by_key('hg.create.none')
203 UserToPerm.grant_perm(id, perm)
206 user_model.grant_perm(id, perm)
204 207 h.flash(_("Revoked 'repository create' permission to user"),
205 208 category='success')
206
209 Session.commit()
207 210 return redirect(url('edit_user', id=id))
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Jan 25, 2011
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -33,12 +33,15 b' from pylons.controllers.util import abor'
33 33 from pylons.i18n.translation import _
34 34
35 35 from rhodecode.lib.exceptions import UsersGroupsAssignedException
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h, safe_unicode
37 37 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
38 38 from rhodecode.lib.base import BaseController, render
39 39
40 from rhodecode.model.users_group import UsersGroupModel
41
40 42 from rhodecode.model.db import User, UsersGroup, Permission, UsersGroupToPerm
41 from rhodecode.model.forms import UserForm, UsersGroupForm
43 from rhodecode.model.forms import UsersGroupForm
44 from rhodecode.model.meta import Session
42 45
43 46 log = logging.getLogger(__name__)
44 47
@@ -70,10 +73,12 b' class UsersGroupsController(BaseControll'
70 73 users_group_form = UsersGroupForm()()
71 74 try:
72 75 form_result = users_group_form.to_python(dict(request.POST))
73 UsersGroup.create(form_result)
76 UsersGroupModel().create(name=form_result['users_group_name'],
77 active=form_result['users_group_active'])
74 78 h.flash(_('created users group %s') \
75 79 % form_result['users_group_name'], category='success')
76 80 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
81 Session.commit()
77 82 except formencode.Invalid, errors:
78 83 return htmlfill.render(
79 84 render('admin/users_groups/users_group_add.html'),
@@ -103,29 +108,33 b' class UsersGroupsController(BaseControll'
103 108 # url('users_group', id=ID)
104 109
105 110 c.users_group = UsersGroup.get(id)
106 c.group_members = [(x.user_id, x.user.username) for x in
107 c.users_group.members]
111 c.group_members_obj = [x.user for x in c.users_group.members]
112 c.group_members = [(x.user_id, x.username) for x in
113 c.group_members_obj]
108 114
109 115 c.available_members = [(x.user_id, x.username) for x in
110 116 self.sa.query(User).all()]
117
118 available_members = [safe_unicode(x[0]) for x in c.available_members]
119
111 120 users_group_form = UsersGroupForm(edit=True,
112 121 old_data=c.users_group.get_dict(),
113 available_members=[str(x[0]) for x
114 in c.available_members])()
122 available_members=available_members)()
115 123
116 124 try:
117 125 form_result = users_group_form.to_python(request.POST)
118 UsersGroup.update(id, form_result)
126 UsersGroupModel().update(c.users_group, form_result)
119 127 h.flash(_('updated users group %s') \
120 128 % form_result['users_group_name'],
121 129 category='success')
122 130 #action_logger(self.rhodecode_user, 'new_user', '', '', self.sa)
131 Session.commit()
123 132 except formencode.Invalid, errors:
124 133 e = errors.error_dict or {}
125 134
126 135 perm = Permission.get_by_key('hg.create.repository')
127 136 e.update({'create_repo_perm':
128 UsersGroupToPerm.has_perm(id, perm)})
137 UsersGroupModel().has_perm(id, perm)})
129 138
130 139 return htmlfill.render(
131 140 render('admin/users_groups/users_group_edit.html'),
@@ -150,8 +159,9 b' class UsersGroupsController(BaseControll'
150 159 # url('users_group', id=ID)
151 160
152 161 try:
153 UsersGroup.delete(id)
162 UsersGroupModel().delete(id)
154 163 h.flash(_('successfully deleted users group'), category='success')
164 Session.commit()
155 165 except UsersGroupsAssignedException, e:
156 166 h.flash(e, category='error')
157 167 except Exception:
@@ -172,14 +182,15 b' class UsersGroupsController(BaseControll'
172 182 return redirect(url('users_groups'))
173 183
174 184 c.users_group.permissions = {}
175 c.group_members = [(x.user_id, x.user.username) for x in
176 c.users_group.members]
185 c.group_members_obj = [x.user for x in c.users_group.members]
186 c.group_members = [(x.user_id, x.username) for x in
187 c.group_members_obj]
177 188 c.available_members = [(x.user_id, x.username) for x in
178 189 self.sa.query(User).all()]
179 190 defaults = c.users_group.get_dict()
180 191 perm = Permission.get_by_key('hg.create.repository')
181 192 defaults.update({'create_repo_perm':
182 UsersGroupToPerm.has_perm(id, perm)})
193 UsersGroupModel().has_perm(c.users_group, perm)})
183 194 return htmlfill.render(
184 195 render('admin/users_groups/users_group_edit.html'),
185 196 defaults=defaults,
@@ -195,20 +206,21 b' class UsersGroupsController(BaseControll'
195 206
196 207 if grant_perm:
197 208 perm = Permission.get_by_key('hg.create.none')
198 UsersGroupToPerm.revoke_perm(id, perm)
209 UsersGroupModel().revoke_perm(id, perm)
199 210
200 211 perm = Permission.get_by_key('hg.create.repository')
201 UsersGroupToPerm.grant_perm(id, perm)
212 UsersGroupModel().grant_perm(id, perm)
202 213 h.flash(_("Granted 'repository create' permission to user"),
203 214 category='success')
204 215
216 Session.commit()
205 217 else:
206 218 perm = Permission.get_by_key('hg.create.repository')
207 UsersGroupToPerm.revoke_perm(id, perm)
219 UsersGroupModel().revoke_perm(id, perm)
208 220
209 221 perm = Permission.get_by_key('hg.create.none')
210 UsersGroupToPerm.grant_perm(id, perm)
222 UsersGroupModel().grant_perm(id, perm)
211 223 h.flash(_("Revoked 'repository create' permission to user"),
212 224 category='success')
213
225 Session.commit()
214 226 return redirect(url('edit_users_group', id=id))
@@ -7,19 +7,19 b''
7 7
8 8 :created_on: Aug 20, 2011
9 9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software; you can redistribute it and/or
14 14 # modify it under the terms of the GNU General Public License
15 15 # as published by the Free Software Foundation; version 2
16 16 # of the License or (at your opinion) any later version of the license.
17 #
17 #
18 18 # This program is distributed in the hope that it will be useful,
19 19 # but WITHOUT ANY WARRANTY; without even the implied warranty of
20 20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 21 # GNU General Public License for more details.
22 #
22 #
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program; if not, write to the Free Software
25 25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
@@ -62,7 +62,7 b' def jsonrpc_error(message, code=None):'
62 62 Generate a Response object with a JSON-RPC error body
63 63 """
64 64 from pylons.controllers.util import Response
65 resp = Response(body=json.dumps(dict(result=None, error=message)),
65 resp = Response(body=json.dumps(dict(id=None, result=None, error=message)),
66 66 status=code,
67 67 content_type='application/json')
68 68 return resp
@@ -100,7 +100,7 b' class JSONRPCController(WSGIController):'
100 100 else:
101 101 length = environ['CONTENT_LENGTH'] or 0
102 102 length = int(environ['CONTENT_LENGTH'])
103 log.debug('Content-Length: %s', length)
103 log.debug('Content-Length: %s' % length)
104 104
105 105 if length == 0:
106 106 log.debug("Content-Length is 0")
@@ -118,11 +118,13 b' class JSONRPCController(WSGIController):'
118 118 # check AUTH based on API KEY
119 119 try:
120 120 self._req_api_key = json_body['api_key']
121 self._req_id = json_body['id']
121 122 self._req_method = json_body['method']
122 123 self._request_params = json_body['args']
123 log.debug('method: %s, params: %s',
124 self._req_method,
125 self._request_params)
124 log.debug(
125 'method: %s, params: %s' % (self._req_method,
126 self._request_params)
127 )
126 128 except KeyError, e:
127 129 return jsonrpc_error(message='Incorrect JSON query missing %s' % e)
128 130
@@ -225,21 +227,26 b' class JSONRPCController(WSGIController):'
225 227 if self._error is not None:
226 228 raw_response = None
227 229
228 response = dict(result=raw_response,
230 response = dict(id=self._req_id, result=raw_response,
229 231 error=self._error)
230 232
231 233 try:
232 234 return json.dumps(response)
233 235 except TypeError, e:
234 log.debug('Error encoding response: %s', e)
235 return json.dumps(dict(result=None,
236 error="Error encoding response"))
236 log.debug('Error encoding response: %s' % e)
237 return json.dumps(
238 dict(
239 self._req_id,
240 result=None,
241 error="Error encoding response"
242 )
243 )
237 244
238 245 def _find_method(self):
239 246 """
240 247 Return method named by `self._req_method` in controller if able
241 248 """
242 log.debug('Trying to find JSON-RPC method: %s', self._req_method)
249 log.debug('Trying to find JSON-RPC method: %s' % self._req_method)
243 250 if self._req_method.startswith('_'):
244 251 raise AttributeError("Method not allowed")
245 252
@@ -253,4 +260,3 b' class JSONRPCController(WSGIController):'
253 260 return func
254 261 else:
255 262 raise AttributeError("No such method: %s" % self._req_method)
256
@@ -30,17 +30,15 b' import logging'
30 30
31 31 from rhodecode.controllers.api import JSONRPCController, JSONRPCError
32 32 from rhodecode.lib.auth import HasPermissionAllDecorator, \
33 HasPermissionAnyDecorator
33 HasPermissionAnyDecorator, PasswordGenerator
34
35 from rhodecode.model.meta import Session
34 36 from rhodecode.model.scm import ScmModel
35
36 from rhodecode.model.db import User, UsersGroup, Group, Repository
37 from rhodecode.model.db import User, UsersGroup, RepoGroup, Repository
37 38 from rhodecode.model.repo import RepoModel
38 39 from rhodecode.model.user import UserModel
39 from rhodecode.model.repo_permission import RepositoryPermissionModel
40 40 from rhodecode.model.users_group import UsersGroupModel
41 from rhodecode.model import users_group
42 41 from rhodecode.model.repos_group import ReposGroupModel
43 from sqlalchemy.orm.exc import NoResultFound
44 42
45 43
46 44 log = logging.getLogger(__name__)
@@ -63,26 +61,26 b' class ApiController(JSONRPCController):'
63 61 """
64 62
65 63 @HasPermissionAllDecorator('hg.admin')
66 def pull(self, apiuser, repo):
64 def pull(self, apiuser, repo_name):
67 65 """
68 66 Dispatch pull action on given repo
69 67
70 68
71 69 :param user:
72 :param repo:
70 :param repo_name:
73 71 """
74 72
75 if Repository.is_valid(repo) is False:
76 raise JSONRPCError('Unknown repo "%s"' % repo)
73 if Repository.is_valid(repo_name) is False:
74 raise JSONRPCError('Unknown repo "%s"' % repo_name)
77 75
78 76 try:
79 ScmModel().pull_changes(repo, self.rhodecode_user.username)
80 return 'Pulled from %s' % repo
77 ScmModel().pull_changes(repo_name, self.rhodecode_user.username)
78 return 'Pulled from %s' % repo_name
81 79 except Exception:
82 raise JSONRPCError('Unable to pull changes from "%s"' % repo)
80 raise JSONRPCError('Unable to pull changes from "%s"' % repo_name)
83 81
84 82 @HasPermissionAllDecorator('hg.admin')
85 def get_user(self, apiuser, username):
83 def get_user(self, apiuser, userid):
86 84 """"
87 85 Get a user by username
88 86
@@ -90,9 +88,9 b' class ApiController(JSONRPCController):'
90 88 :param username:
91 89 """
92 90
93 user = User.get_by_username(username)
94 if not user:
95 return None
91 user = UserModel().get_user(userid)
92 if user is None:
93 return user
96 94
97 95 return dict(
98 96 id=user.user_id,
@@ -102,7 +100,7 b' class ApiController(JSONRPCController):'
102 100 email=user.email,
103 101 active=user.active,
104 102 admin=user.admin,
105 ldap=user.ldap_dn
103 ldap_dn=user.ldap_dn
106 104 )
107 105
108 106 @HasPermissionAllDecorator('hg.admin')
@@ -124,47 +122,85 b' class ApiController(JSONRPCController):'
124 122 email=user.email,
125 123 active=user.active,
126 124 admin=user.admin,
127 ldap=user.ldap_dn
125 ldap_dn=user.ldap_dn
128 126 )
129 127 )
130 128 return result
131 129
132 130 @HasPermissionAllDecorator('hg.admin')
133 def create_user(self, apiuser, username, password, firstname,
134 lastname, email, active=True, admin=False, ldap_dn=None):
131 def create_user(self, apiuser, username, email, password, firstname=None,
132 lastname=None, active=True, admin=False, ldap_dn=None):
135 133 """
136 134 Create new user
137 135
138 136 :param apiuser:
139 137 :param username:
140 138 :param password:
139 :param email:
141 140 :param name:
142 141 :param lastname:
143 :param email:
144 142 :param active:
145 143 :param admin:
146 144 :param ldap_dn:
147 145 """
148
149 146 if User.get_by_username(username):
150 147 raise JSONRPCError("user %s already exist" % username)
151 148
149 if User.get_by_email(email, case_insensitive=True):
150 raise JSONRPCError("email %s already exist" % email)
151
152 if ldap_dn:
153 # generate temporary password if ldap_dn
154 password = PasswordGenerator().gen_password(length=8)
155
152 156 try:
153 form_data = dict(username=username,
154 password=password,
155 active=active,
156 admin=admin,
157 name=firstname,
158 lastname=lastname,
159 email=email,
160 ldap_dn=ldap_dn)
161 UserModel().create_ldap(username, password, ldap_dn, form_data)
162 return dict(msg='created new user %s' % username)
157 usr = UserModel().create_or_update(
158 username, password, email, firstname,
159 lastname, active, admin, ldap_dn
160 )
161 Session.commit()
162 return dict(
163 id=usr.user_id,
164 msg='created new user %s' % username
165 )
163 166 except Exception:
164 167 log.error(traceback.format_exc())
165 168 raise JSONRPCError('failed to create user %s' % username)
166 169
167 170 @HasPermissionAllDecorator('hg.admin')
171 def update_user(self, apiuser, userid, username, password, email,
172 firstname, lastname, active, admin, ldap_dn):
173 """
174 Updates given user
175
176 :param apiuser:
177 :param username:
178 :param password:
179 :param email:
180 :param name:
181 :param lastname:
182 :param active:
183 :param admin:
184 :param ldap_dn:
185 """
186 if not UserModel().get_user(userid):
187 raise JSONRPCError("user %s does not exist" % username)
188
189 try:
190 usr = UserModel().create_or_update(
191 username, password, email, firstname,
192 lastname, active, admin, ldap_dn
193 )
194 Session.commit()
195 return dict(
196 id=usr.user_id,
197 msg='updated user %s' % username
198 )
199 except Exception:
200 log.error(traceback.format_exc())
201 raise JSONRPCError('failed to update user %s' % username)
202
203 @HasPermissionAllDecorator('hg.admin')
168 204 def get_users_group(self, apiuser, group_name):
169 205 """"
170 206 Get users group by name
@@ -190,7 +226,7 b' class ApiController(JSONRPCController):'
190 226 ldap=user.ldap_dn))
191 227
192 228 return dict(id=users_group.users_group_id,
193 name=users_group.users_group_name,
229 group_name=users_group.users_group_name,
194 230 active=users_group.users_group_active,
195 231 members=members)
196 232
@@ -217,41 +253,40 b' class ApiController(JSONRPCController):'
217 253 ldap=user.ldap_dn))
218 254
219 255 result.append(dict(id=users_group.users_group_id,
220 name=users_group.users_group_name,
256 group_name=users_group.users_group_name,
221 257 active=users_group.users_group_active,
222 258 members=members))
223 259 return result
224 260
225 261 @HasPermissionAllDecorator('hg.admin')
226 def create_users_group(self, apiuser, name, active=True):
262 def create_users_group(self, apiuser, group_name, active=True):
227 263 """
228 264 Creates an new usergroup
229 265
230 :param name:
266 :param group_name:
231 267 :param active:
232 268 """
233 269
234 if self.get_users_group(apiuser, name):
235 raise JSONRPCError("users group %s already exist" % name)
270 if self.get_users_group(apiuser, group_name):
271 raise JSONRPCError("users group %s already exist" % group_name)
236 272
237 273 try:
238 form_data = dict(users_group_name=name,
239 users_group_active=active)
240 ug = UsersGroup.create(form_data)
274 ug = UsersGroupModel().create(name=group_name, active=active)
275 Session.commit()
241 276 return dict(id=ug.users_group_id,
242 msg='created new users group %s' % name)
277 msg='created new users group %s' % group_name)
243 278 except Exception:
244 279 log.error(traceback.format_exc())
245 raise JSONRPCError('failed to create group %s' % name)
280 raise JSONRPCError('failed to create group %s' % group_name)
246 281
247 282 @HasPermissionAllDecorator('hg.admin')
248 def add_user_to_users_group(self, apiuser, group_name, user_name):
283 def add_user_to_users_group(self, apiuser, group_name, username):
249 284 """"
250 285 Add a user to a group
251 286
252 :param apiuser
253 :param group_name
254 :param user_name
287 :param apiuser:
288 :param group_name:
289 :param username:
255 290 """
256 291
257 292 try:
@@ -259,32 +294,65 b' class ApiController(JSONRPCController):'
259 294 if not users_group:
260 295 raise JSONRPCError('unknown users group %s' % group_name)
261 296
262 try:
263 user = User.get_by_username(user_name)
264 except NoResultFound:
265 raise JSONRPCError('unknown user %s' % user_name)
297 user = User.get_by_username(username)
298 if user is None:
299 raise JSONRPCError('unknown user %s' % username)
266 300
267 301 ugm = UsersGroupModel().add_user_to_group(users_group, user)
302 success = True if ugm != True else False
303 msg = 'added member %s to users group %s' % (username, group_name)
304 msg = msg if success else 'User is already in that group'
305 Session.commit()
268 306
269 return dict(id=ugm.users_group_member_id,
270 msg='created new users group member')
307 return dict(
308 id=ugm.users_group_member_id if ugm != True else None,
309 success=success,
310 msg=msg
311 )
271 312 except Exception:
272 313 log.error(traceback.format_exc())
273 raise JSONRPCError('failed to create users group member')
314 raise JSONRPCError('failed to add users group member')
315
316 @HasPermissionAllDecorator('hg.admin')
317 def remove_user_from_users_group(self, apiuser, group_name, username):
318 """
319 Remove user from a group
320
321 :param apiuser
322 :param group_name
323 :param username
324 """
325
326 try:
327 users_group = UsersGroup.get_by_group_name(group_name)
328 if not users_group:
329 raise JSONRPCError('unknown users group %s' % group_name)
330
331 user = User.get_by_username(username)
332 if user is None:
333 raise JSONRPCError('unknown user %s' % username)
334
335 success = UsersGroupModel().remove_user_from_group(users_group, user)
336 msg = 'removed member %s from users group %s' % (username, group_name)
337 msg = msg if success else "User wasn't in group"
338 Session.commit()
339 return dict(success=success, msg=msg)
340 except Exception:
341 log.error(traceback.format_exc())
342 raise JSONRPCError('failed to remove user from group')
274 343
275 344 @HasPermissionAnyDecorator('hg.admin')
276 def get_repo(self, apiuser, name):
345 def get_repo(self, apiuser, repoid):
277 346 """"
278 347 Get repository by name
279 348
280 :param apiuser
281 :param repo_name
349 :param apiuser:
350 :param repo_name:
282 351 """
283 352
284 try:
285 repo = Repository.get_by_repo_name(name)
286 except NoResultFound:
287 return None
353 repo = RepoModel().get_repo(repoid)
354 if repo is None:
355 raise JSONRPCError('unknown repository %s' % repo)
288 356
289 357 members = []
290 358 for user in repo.repo_to_perm:
@@ -319,7 +387,7 b' class ApiController(JSONRPCController):'
319 387
320 388 return dict(
321 389 id=repo.repo_id,
322 name=repo.repo_name,
390 repo_name=repo.repo_name,
323 391 type=repo.repo_type,
324 392 description=repo.description,
325 393 members=members
@@ -330,7 +398,7 b' class ApiController(JSONRPCController):'
330 398 """"
331 399 Get all repositories
332 400
333 :param apiuser
401 :param apiuser:
334 402 """
335 403
336 404 result = []
@@ -338,85 +406,255 b' class ApiController(JSONRPCController):'
338 406 result.append(
339 407 dict(
340 408 id=repository.repo_id,
341 name=repository.repo_name,
409 repo_name=repository.repo_name,
342 410 type=repository.repo_type,
343 411 description=repository.description
344 412 )
345 413 )
346 414 return result
347 415
348 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
349 def create_repo(self, apiuser, name, owner_name, description='',
350 repo_type='hg', private=False):
416 @HasPermissionAnyDecorator('hg.admin')
417 def get_repo_nodes(self, apiuser, repo_name, revision, root_path,
418 ret_type='all'):
351 419 """
352 Create a repository
420 returns a list of nodes and it's children
421 for a given path at given revision. It's possible to specify ret_type
422 to show only files or dirs
353 423
354 :param apiuser
355 :param name
356 :param description
357 :param type
358 :param private
359 :param owner_name
424 :param apiuser:
425 :param repo_name: name of repository
426 :param revision: revision for which listing should be done
427 :param root_path: path from which start displaying
428 :param ret_type: return type 'all|files|dirs' nodes
429 """
430 try:
431 _d, _f = ScmModel().get_nodes(repo_name, revision, root_path,
432 flat=False)
433 _map = {
434 'all': _d + _f,
435 'files': _f,
436 'dirs': _d,
437 }
438 return _map[ret_type]
439 except KeyError:
440 raise JSONRPCError('ret_type must be one of %s' % _map.keys())
441 except Exception, e:
442 raise JSONRPCError(e)
443
444 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
445 def create_repo(self, apiuser, repo_name, owner_name, description='',
446 repo_type='hg', private=False, clone_uri=None):
447 """
448 Create repository, if clone_url is given it makes a remote clone
449
450 :param apiuser:
451 :param repo_name:
452 :param owner_name:
453 :param description:
454 :param repo_type:
455 :param private:
456 :param clone_uri:
360 457 """
361 458
362 459 try:
363 try:
364 owner = User.get_by_username(owner_name)
365 except NoResultFound:
366 raise JSONRPCError('unknown user %s' % owner)
460 owner = User.get_by_username(owner_name)
461 if owner is None:
462 raise JSONRPCError('unknown user %s' % owner_name)
367 463
368 if self.get_repo(apiuser, name):
369 raise JSONRPCError("repo %s already exist" % name)
464 if Repository.get_by_repo_name(repo_name):
465 raise JSONRPCError("repo %s already exist" % repo_name)
370 466
371 groups = name.split('/')
467 groups = repo_name.split('/')
372 468 real_name = groups[-1]
373 469 groups = groups[:-1]
374 470 parent_id = None
375 471 for g in groups:
376 group = Group.get_by_group_name(g)
472 group = RepoGroup.get_by_group_name(g)
377 473 if not group:
378 group = ReposGroupModel().create(dict(group_name=g,
379 group_description='',
380 group_parent_id=parent_id))
474 group = ReposGroupModel().create(g, '', parent_id)
381 475 parent_id = group.group_id
382 476
383 RepoModel().create(dict(repo_name=real_name,
384 repo_name_full=name,
385 description=description,
386 private=private,
387 repo_type=repo_type,
388 repo_group=parent_id,
389 clone_uri=None), owner)
477 repo = RepoModel().create(
478 dict(
479 repo_name=real_name,
480 repo_name_full=repo_name,
481 description=description,
482 private=private,
483 repo_type=repo_type,
484 repo_group=parent_id,
485 clone_uri=clone_uri
486 ),
487 owner
488 )
489 Session.commit()
490
491 return dict(
492 id=repo.repo_id,
493 msg="Created new repository %s" % repo.repo_name
494 )
495
496 except Exception:
497 log.error(traceback.format_exc())
498 raise JSONRPCError('failed to create repository %s' % repo_name)
499
500 @HasPermissionAnyDecorator('hg.admin')
501 def delete_repo(self, apiuser, repo_name):
502 """
503 Deletes a given repository
504
505 :param repo_name:
506 """
507 if not Repository.get_by_repo_name(repo_name):
508 raise JSONRPCError("repo %s does not exist" % repo_name)
509 try:
510 RepoModel().delete(repo_name)
511 Session.commit()
512 return dict(
513 msg='Deleted repository %s' % repo_name
514 )
390 515 except Exception:
391 516 log.error(traceback.format_exc())
392 raise JSONRPCError('failed to create repository %s' % name)
517 raise JSONRPCError('failed to delete repository %s' % repo_name)
393 518
394 519 @HasPermissionAnyDecorator('hg.admin')
395 def add_user_to_repo(self, apiuser, repo_name, user_name, perm):
520 def grant_user_permission(self, apiuser, repo_name, username, perm):
521 """
522 Grant permission for user on given repository, or update existing one
523 if found
524
525 :param repo_name:
526 :param username:
527 :param perm:
396 528 """
397 Add permission for a user to a repository
529
530 try:
531 repo = Repository.get_by_repo_name(repo_name)
532 if repo is None:
533 raise JSONRPCError('unknown repository %s' % repo)
534
535 user = User.get_by_username(username)
536 if user is None:
537 raise JSONRPCError('unknown user %s' % username)
538
539 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
398 540
399 :param apiuser
400 :param repo_name
401 :param user_name
402 :param perm
541 Session.commit()
542 return dict(
543 msg='Granted perm: %s for user: %s in repo: %s' % (
544 perm, username, repo_name
545 )
546 )
547 except Exception:
548 log.error(traceback.format_exc())
549 raise JSONRPCError(
550 'failed to edit permission %(repo)s for %(user)s' % dict(
551 user=username, repo=repo_name
552 )
553 )
554
555 @HasPermissionAnyDecorator('hg.admin')
556 def revoke_user_permission(self, apiuser, repo_name, username):
557 """
558 Revoke permission for user on given repository
559
560 :param repo_name:
561 :param username:
403 562 """
404 563
405 564 try:
406 try:
407 repo = Repository.get_by_repo_name(repo_name)
408 except NoResultFound:
565 repo = Repository.get_by_repo_name(repo_name)
566 if repo is None:
567 raise JSONRPCError('unknown repository %s' % repo)
568
569 user = User.get_by_username(username)
570 if user is None:
571 raise JSONRPCError('unknown user %s' % username)
572
573 RepoModel().revoke_user_permission(repo=repo_name, user=username)
574
575 Session.commit()
576 return dict(
577 msg='Revoked perm for user: %s in repo: %s' % (
578 username, repo_name
579 )
580 )
581 except Exception:
582 log.error(traceback.format_exc())
583 raise JSONRPCError(
584 'failed to edit permission %(repo)s for %(user)s' % dict(
585 user=username, repo=repo_name
586 )
587 )
588
589 @HasPermissionAnyDecorator('hg.admin')
590 def grant_users_group_permission(self, apiuser, repo_name, group_name, perm):
591 """
592 Grant permission for users group on given repository, or update
593 existing one if found
594
595 :param repo_name:
596 :param group_name:
597 :param perm:
598 """
599
600 try:
601 repo = Repository.get_by_repo_name(repo_name)
602 if repo is None:
409 603 raise JSONRPCError('unknown repository %s' % repo)
410 604
411 try:
412 user = User.get_by_username(user_name)
413 except NoResultFound:
414 raise JSONRPCError('unknown user %s' % user)
605 user_group = UsersGroup.get_by_group_name(group_name)
606 if user_group is None:
607 raise JSONRPCError('unknown users group %s' % user_group)
415 608
416 RepositoryPermissionModel()\
417 .update_or_delete_user_permission(repo, user, perm)
609 RepoModel().grant_users_group_permission(repo=repo_name,
610 group_name=group_name,
611 perm=perm)
612
613 Session.commit()
614 return dict(
615 msg='Granted perm: %s for group: %s in repo: %s' % (
616 perm, group_name, repo_name
617 )
618 )
418 619 except Exception:
419 620 log.error(traceback.format_exc())
420 raise JSONRPCError('failed to edit permission %(repo)s for %(user)s'
421 % dict(user=user_name, repo=repo_name))
621 raise JSONRPCError(
622 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
623 usersgr=group_name, repo=repo_name
624 )
625 )
626
627 @HasPermissionAnyDecorator('hg.admin')
628 def revoke_users_group_permission(self, apiuser, repo_name, group_name):
629 """
630 Revoke permission for users group on given repository
631
632 :param repo_name:
633 :param group_name:
634 """
635
636 try:
637 repo = Repository.get_by_repo_name(repo_name)
638 if repo is None:
639 raise JSONRPCError('unknown repository %s' % repo)
422 640
641 user_group = UsersGroup.get_by_group_name(group_name)
642 if user_group is None:
643 raise JSONRPCError('unknown users group %s' % user_group)
644
645 RepoModel().revoke_users_group_permission(repo=repo_name,
646 group_name=group_name)
647
648 Session.commit()
649 return dict(
650 msg='Revoked perm for group: %s in repo: %s' % (
651 group_name, repo_name
652 )
653 )
654 except Exception:
655 log.error(traceback.format_exc())
656 raise JSONRPCError(
657 'failed to edit permission %(repo)s for %(usersgr)s' % dict(
658 usersgr=group_name, repo=repo_name
659 )
660 )
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -46,33 +46,30 b' class BranchesController(BaseRepoControl'
46 46 def index(self):
47 47
48 48 def _branchtags(localrepo):
49
50 bt = {}
51 49 bt_closed = {}
52
53 50 for bn, heads in localrepo.branchmap().iteritems():
54 51 tip = heads[-1]
55 if 'close' not in localrepo.changelog.read(tip)[5]:
56 bt[bn] = tip
57 else:
52 if 'close' in localrepo.changelog.read(tip)[5]:
58 53 bt_closed[bn] = tip
59 return bt, bt_closed
54 return bt_closed
60 55
56 cs_g = c.rhodecode_repo.get_changeset
61 57
62 bt, bt_closed = _branchtags(c.rhodecode_repo._repo)
63 cs_g = c.rhodecode_repo.get_changeset
64 _branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),) for n, h in
65 bt.items()]
58 c.repo_closed_branches = {}
59 if c.rhodecode_db_repo.repo_type == 'hg':
60 bt_closed = _branchtags(c.rhodecode_repo._repo)
61 _closed_branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),)
62 for n, h in bt_closed.items()]
66 63
67 _closed_branches = [(safe_unicode(n), cs_g(binascii.hexlify(h)),) for n, h in
68 bt_closed.items()]
64 c.repo_closed_branches = OrderedDict(sorted(_closed_branches,
65 key=lambda ctx: ctx[0],
66 reverse=False))
69 67
68 _branches = [(safe_unicode(n), cs_g(h))
69 for n, h in c.rhodecode_repo.branches.items()]
70 70 c.repo_branches = OrderedDict(sorted(_branches,
71 71 key=lambda ctx: ctx[0],
72 72 reverse=False))
73 c.repo_closed_branches = OrderedDict(sorted(_closed_branches,
74 key=lambda ctx: ctx[0],
75 reverse=False))
76 73
77 74
78 75 return render('branches/branches.html')
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -24,15 +24,22 b''
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 import traceback
27 28
28 29 from mercurial import graphmod
29 from pylons import request, session, tmpl_context as c
30 from pylons import request, url, session, tmpl_context as c
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
30 33
34 import rhodecode.lib.helpers as h
31 35 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
32 36 from rhodecode.lib.base import BaseRepoController, render
33 37 from rhodecode.lib.helpers import RepoPage
34 38 from rhodecode.lib.compat import json
35 39
40 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError
41 from rhodecode.model.db import Repository
42
36 43 log = logging.getLogger(__name__)
37 44
38 45
@@ -62,12 +69,32 b' class ChangelogController(BaseRepoContro'
62 69
63 70 p = int(request.params.get('page', 1))
64 71 branch_name = request.params.get('branch', None)
65 c.total_cs = len(c.rhodecode_repo)
66 c.pagination = RepoPage(c.rhodecode_repo, page=p,
67 item_count=c.total_cs, items_per_page=c.size,
68 branch_name=branch_name)
72 try:
73 if branch_name:
74 collection = [z for z in
75 c.rhodecode_repo.get_changesets(start=0,
76 branch_name=branch_name)]
77 c.total_cs = len(collection)
78 else:
79 collection = c.rhodecode_repo
80 c.total_cs = len(c.rhodecode_repo)
69 81
70 self._graph(c.rhodecode_repo, c.total_cs, c.size, p)
82 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
83 items_per_page=c.size, branch=branch_name)
84 collection = list(c.pagination)
85 page_revisions = [x.raw_id for x in collection]
86 c.comments = c.rhodecode_db_repo.comments(page_revisions)
87
88 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
89 log.error(traceback.format_exc())
90 h.flash(str(e), category='warning')
91 return redirect(url('home'))
92
93 self._graph(c.rhodecode_repo, collection, c.total_cs, c.size, p)
94
95 c.branch_name = branch_name
96 c.branch_filters = [('', _('All Branches'))] + \
97 [(k, k) for k in c.rhodecode_repo.branches.keys()]
71 98
72 99 return render('changelog/changelog.html')
73 100
@@ -76,7 +103,7 b' class ChangelogController(BaseRepoContro'
76 103 c.cs = c.rhodecode_repo.get_changeset(cs)
77 104 return render('changelog/changelog_details.html')
78 105
79 def _graph(self, repo, repo_size, size, p):
106 def _graph(self, repo, collection, repo_size, size, p):
80 107 """
81 108 Generates a DAG graph for mercurial
82 109
@@ -84,29 +111,20 b' class ChangelogController(BaseRepoContro'
84 111 :param size: number of commits to show
85 112 :param p: page number
86 113 """
87 if not repo.revisions:
114 if not collection:
88 115 c.jsdata = json.dumps([])
89 116 return
90 117
91 revcount = min(repo_size, size)
92 offset = 1 if p == 1 else ((p - 1) * revcount + 1)
93 try:
94 rev_end = repo.revisions.index(repo.revisions[(-1 * offset)])
95 except IndexError:
96 rev_end = repo.revisions.index(repo.revisions[-1])
97 rev_start = max(0, rev_end - revcount)
98
99 118 data = []
100 rev_end += 1
119 revs = [x.revision for x in collection]
101 120
102 121 if repo.alias == 'git':
103 for _ in xrange(rev_start, rev_end):
122 for _ in revs:
104 123 vtx = [0, 1]
105 124 edges = [[0, 0, 1]]
106 125 data.append(['', vtx, edges])
107 126
108 127 elif repo.alias == 'hg':
109 revs = list(reversed(xrange(rev_start, rev_end)))
110 128 c.dag = graphmod.colored(graphmod.dagwalker(repo._repo, revs))
111 129 for (id, type, ctx, vtx, edges) in c.dag:
112 130 if type != graphmod.CHANGESET:
@@ -8,7 +8,7 b''
8 8
9 9 :created_on: Apr 25, 2010
10 10 :author: marcink
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
@@ -25,25 +25,129 b''
25 25 # along with this program. If not, see <http://www.gnu.org/licenses/>.
26 26 import logging
27 27 import traceback
28 from collections import defaultdict
29 from webob.exc import HTTPForbidden
28 30
29 31 from pylons import tmpl_context as c, url, request, response
30 32 from pylons.i18n.translation import _
31 33 from pylons.controllers.util import redirect
34 from pylons.decorators import jsonify
35
36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetError, \
37 ChangesetDoesNotExistError
38 from rhodecode.lib.vcs.nodes import FileNode
32 39
33 40 import rhodecode.lib.helpers as h
34 41 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
35 42 from rhodecode.lib.base import BaseRepoController, render
36 43 from rhodecode.lib.utils import EmptyChangeset
37 44 from rhodecode.lib.compat import OrderedDict
38
39 from vcs.exceptions import RepositoryError, ChangesetError, \
40 ChangesetDoesNotExistError
41 from vcs.nodes import FileNode
42 from vcs.utils import diffs as differ
45 from rhodecode.lib import diffs
46 from rhodecode.model.db import ChangesetComment
47 from rhodecode.model.comment import ChangesetCommentsModel
48 from rhodecode.model.meta import Session
49 from rhodecode.lib.diffs import wrapped_diff
43 50
44 51 log = logging.getLogger(__name__)
45 52
46 53
54 def anchor_url(revision, path):
55 fid = h.FID(revision, path)
56 return h.url.current(anchor=fid, **request.GET)
57
58
59 def get_ignore_ws(fid, GET):
60 ig_ws_global = request.GET.get('ignorews')
61 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
62 if ig_ws:
63 try:
64 return int(ig_ws[0].split(':')[-1])
65 except:
66 pass
67 return ig_ws_global
68
69
70 def _ignorews_url(fileid=None):
71
72 params = defaultdict(list)
73 lbl = _('show white space')
74 ig_ws = get_ignore_ws(fileid, request.GET)
75 ln_ctx = get_line_ctx(fileid, request.GET)
76 # global option
77 if fileid is None:
78 if ig_ws is None:
79 params['ignorews'] += [1]
80 lbl = _('ignore white space')
81 ctx_key = 'context'
82 ctx_val = ln_ctx
83 # per file options
84 else:
85 if ig_ws is None:
86 params[fileid] += ['WS:1']
87 lbl = _('ignore white space')
88
89 ctx_key = fileid
90 ctx_val = 'C:%s' % ln_ctx
91 # if we have passed in ln_ctx pass it along to our params
92 if ln_ctx:
93 params[ctx_key] += [ctx_val]
94
95 params['anchor'] = fileid
96 img = h.image('/images/icons/text_strikethrough.png', lbl, class_='icon')
97 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
98
99
100 def get_line_ctx(fid, GET):
101 ln_ctx_global = request.GET.get('context')
102 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
103
104 if ln_ctx:
105 retval = ln_ctx[0].split(':')[-1]
106 else:
107 retval = ln_ctx_global
108
109 try:
110 return int(retval)
111 except:
112 return
113
114
115 def _context_url(fileid=None):
116 """
117 Generates url for context lines
118
119 :param fileid:
120 """
121 ig_ws = get_ignore_ws(fileid, request.GET)
122 ln_ctx = (get_line_ctx(fileid, request.GET) or 3) * 2
123
124 params = defaultdict(list)
125
126 # global option
127 if fileid is None:
128 if ln_ctx > 0:
129 params['context'] += [ln_ctx]
130
131 if ig_ws:
132 ig_ws_key = 'ignorews'
133 ig_ws_val = 1
134
135 # per file option
136 else:
137 params[fileid] += ['C:%s' % ln_ctx]
138 ig_ws_key = fileid
139 ig_ws_val = 'WS:%s' % 1
140
141 if ig_ws:
142 params[ig_ws_key] += [ig_ws_val]
143
144 lbl = _('%s line context') % ln_ctx
145
146 params['anchor'] = fileid
147 img = h.image('/images/icons/table_add.png', lbl, class_='icon')
148 return h.link_to(img, h.url.current(**params), title=lbl, class_='tooltip')
149
150
47 151 class ChangesetController(BaseRepoController):
48 152
49 153 @LoginRequired()
@@ -55,20 +159,16 b' class ChangesetController(BaseRepoContro'
55 159
56 160 def index(self, revision):
57 161
58 def wrap_to_table(str):
59
60 return '''<table class="code-difftable">
61 <tr class="line">
62 <td class="lineno new"></td>
63 <td class="code"><pre>%s</pre></td>
64 </tr>
65 </table>''' % str
162 c.anchor_url = anchor_url
163 c.ignorews_url = _ignorews_url
164 c.context_url = _context_url
66 165
67 166 #get ranges of revisions if preset
68 167 rev_range = revision.split('...')[:2]
69
168 enable_comments = True
70 169 try:
71 170 if len(rev_range) == 2:
171 enable_comments = False
72 172 rev_start = rev_range[0]
73 173 rev_end = rev_range[1]
74 174 rev_ranges = c.rhodecode_repo.get_changesets(start=rev_start,
@@ -77,6 +177,8 b' class ChangesetController(BaseRepoContro'
77 177 rev_ranges = [c.rhodecode_repo.get_changeset(revision)]
78 178
79 179 c.cs_ranges = list(rev_ranges)
180 if not c.cs_ranges:
181 raise RepositoryError('Changeset range returned empty result')
80 182
81 183 except (RepositoryError, ChangesetDoesNotExistError, Exception), e:
82 184 log.error(traceback.format_exc())
@@ -84,14 +186,25 b' class ChangesetController(BaseRepoContro'
84 186 return redirect(url('home'))
85 187
86 188 c.changes = OrderedDict()
87 c.sum_added = 0
88 c.sum_removed = 0
89 c.lines_added = 0
90 c.lines_deleted = 0
189
190 c.lines_added = 0 # count of lines added
191 c.lines_deleted = 0 # count of lines removes
192
193 cumulative_diff = 0
91 194 c.cut_off = False # defines if cut off limit is reached
92 195
196 c.comments = []
197 c.inline_comments = []
198 c.inline_cnt = 0
93 199 # Iterate over ranges (default changeset view is always one changeset)
94 200 for changeset in c.cs_ranges:
201 c.comments.extend(ChangesetCommentsModel()\
202 .get_comments(c.rhodecode_db_repo.repo_id,
203 changeset.raw_id))
204 inlines = ChangesetCommentsModel()\
205 .get_inline_comments(c.rhodecode_db_repo.repo_id,
206 changeset.raw_id)
207 c.inline_comments.extend(inlines)
95 208 c.changes[changeset.raw_id] = []
96 209 try:
97 210 changeset_parent = changeset.parents[0]
@@ -102,32 +215,19 b' class ChangesetController(BaseRepoContro'
102 215 # ADDED FILES
103 216 #==================================================================
104 217 for node in changeset.added:
105
106 filenode_old = FileNode(node.path, '', EmptyChangeset())
107 if filenode_old.is_binary or node.is_binary:
108 diff = wrap_to_table(_('binary file'))
109 st = (0, 0)
110 else:
111 # in this case node.size is good parameter since those are
112 # added nodes and their size defines how many changes were
113 # made
114 c.sum_added += node.size
115 if c.sum_added < self.cut_off_limit:
116 f_gitdiff = differ.get_gitdiff(filenode_old, node)
117 d = differ.DiffProcessor(f_gitdiff, format='gitdiff')
118
119 st = d.stat()
120 diff = d.as_html()
121
122 else:
123 diff = wrap_to_table(_('Changeset is to big and '
124 'was cut off, see raw '
125 'changeset instead'))
126 c.cut_off = True
127 break
128
129 cs1 = None
130 cs2 = node.last_changeset.raw_id
218 fid = h.FID(revision, node.path)
219 line_context_lcl = get_line_ctx(fid, request.GET)
220 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
221 lim = self.cut_off_limit
222 if cumulative_diff > self.cut_off_limit:
223 lim = -1
224 size, cs1, cs2, diff, st = wrapped_diff(filenode_old=None,
225 filenode_new=node,
226 cut_off_limit=lim,
227 ignore_whitespace=ign_whitespace_lcl,
228 line_context=line_context_lcl,
229 enable_comments=enable_comments)
230 cumulative_diff += size
131 231 c.lines_added += st[0]
132 232 c.lines_deleted += st[1]
133 233 c.changes[changeset.raw_id].append(('added', node, diff,
@@ -136,55 +236,42 b' class ChangesetController(BaseRepoContro'
136 236 #==================================================================
137 237 # CHANGED FILES
138 238 #==================================================================
139 if not c.cut_off:
140 for node in changeset.changed:
141 try:
142 filenode_old = changeset_parent.get_node(node.path)
143 except ChangesetError:
144 log.warning('Unable to fetch parent node for diff')
145 filenode_old = FileNode(node.path, '',
146 EmptyChangeset())
147
148 if filenode_old.is_binary or node.is_binary:
149 diff = wrap_to_table(_('binary file'))
150 st = (0, 0)
151 else:
239 for node in changeset.changed:
240 try:
241 filenode_old = changeset_parent.get_node(node.path)
242 except ChangesetError:
243 log.warning('Unable to fetch parent node for diff')
244 filenode_old = FileNode(node.path, '', EmptyChangeset())
152 245
153 if c.sum_removed < self.cut_off_limit:
154 f_gitdiff = differ.get_gitdiff(filenode_old, node)
155 d = differ.DiffProcessor(f_gitdiff,
156 format='gitdiff')
157 st = d.stat()
158 if (st[0] + st[1]) * 256 > self.cut_off_limit:
159 diff = wrap_to_table(_('Diff is to big '
160 'and was cut off, see '
161 'raw diff instead'))
162 else:
163 diff = d.as_html()
164
165 if diff:
166 c.sum_removed += len(diff)
167 else:
168 diff = wrap_to_table(_('Changeset is to big and '
169 'was cut off, see raw '
170 'changeset instead'))
171 c.cut_off = True
172 break
173
174 cs1 = filenode_old.last_changeset.raw_id
175 cs2 = node.last_changeset.raw_id
176 c.lines_added += st[0]
177 c.lines_deleted += st[1]
178 c.changes[changeset.raw_id].append(('changed', node, diff,
179 cs1, cs2, st))
246 fid = h.FID(revision, node.path)
247 line_context_lcl = get_line_ctx(fid, request.GET)
248 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
249 lim = self.cut_off_limit
250 if cumulative_diff > self.cut_off_limit:
251 lim = -1
252 size, cs1, cs2, diff, st = wrapped_diff(filenode_old=filenode_old,
253 filenode_new=node,
254 cut_off_limit=lim,
255 ignore_whitespace=ign_whitespace_lcl,
256 line_context=line_context_lcl,
257 enable_comments=enable_comments)
258 cumulative_diff += size
259 c.lines_added += st[0]
260 c.lines_deleted += st[1]
261 c.changes[changeset.raw_id].append(('changed', node, diff,
262 cs1, cs2, st))
180 263
181 264 #==================================================================
182 265 # REMOVED FILES
183 266 #==================================================================
184 if not c.cut_off:
185 for node in changeset.removed:
186 c.changes[changeset.raw_id].append(('removed', node, None,
187 None, None, (0, 0)))
267 for node in changeset.removed:
268 c.changes[changeset.raw_id].append(('removed', node, None,
269 None, None, (0, 0)))
270
271 # count inline comments
272 for path, lines in c.inline_comments:
273 for comments in lines.values():
274 c.inline_cnt += len(comments)
188 275
189 276 if len(c.cs_ranges) == 1:
190 277 c.changeset = c.cs_ranges[0]
@@ -197,6 +284,8 b' class ChangesetController(BaseRepoContro'
197 284 def raw_changeset(self, revision):
198 285
199 286 method = request.GET.get('diff', 'show')
287 ignore_whitespace = request.GET.get('ignorews') == '1'
288 line_context = request.GET.get('context', 3)
200 289 try:
201 290 c.scm_type = c.rhodecode_repo.alias
202 291 c.changeset = c.rhodecode_repo.get_changeset(revision)
@@ -215,8 +304,10 b' class ChangesetController(BaseRepoContro'
215 304 if filenode_old.is_binary or node.is_binary:
216 305 diff = _('binary file') + '\n'
217 306 else:
218 f_gitdiff = differ.get_gitdiff(filenode_old, node)
219 diff = differ.DiffProcessor(f_gitdiff,
307 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
308 ignore_whitespace=ignore_whitespace,
309 context=line_context)
310 diff = diffs.DiffProcessor(f_gitdiff,
220 311 format='gitdiff').raw_diff()
221 312
222 313 cs1 = None
@@ -228,8 +319,10 b' class ChangesetController(BaseRepoContro'
228 319 if filenode_old.is_binary or node.is_binary:
229 320 diff = _('binary file')
230 321 else:
231 f_gitdiff = differ.get_gitdiff(filenode_old, node)
232 diff = differ.DiffProcessor(f_gitdiff,
322 f_gitdiff = diffs.get_gitdiff(filenode_old, node,
323 ignore_whitespace=ignore_whitespace,
324 context=line_context)
325 diff = diffs.DiffProcessor(f_gitdiff,
233 326 format='gitdiff').raw_diff()
234 327
235 328 cs1 = filenode_old.last_changeset.raw_id
@@ -250,3 +343,25 b' class ChangesetController(BaseRepoContro'
250 343 c.diffs += x[2]
251 344
252 345 return render('changeset/raw_changeset.html')
346
347 def comment(self, repo_name, revision):
348 ChangesetCommentsModel().create(text=request.POST.get('text'),
349 repo_id=c.rhodecode_db_repo.repo_id,
350 user_id=c.rhodecode_user.user_id,
351 revision=revision,
352 f_path=request.POST.get('f_path'),
353 line_no=request.POST.get('line'))
354 Session.commit()
355 return redirect(h.url('changeset_home', repo_name=repo_name,
356 revision=revision))
357
358 @jsonify
359 def delete_comment(self, repo_name, comment_id):
360 co = ChangesetComment.get(comment_id)
361 owner = lambda: co.author.user_id == c.rhodecode_user.user_id
362 if h.HasPermissionAny('hg.admin', 'repository.admin')() or owner:
363 ChangesetCommentsModel().delete(comment=co)
364 Session.commit()
365 return True
366 else:
367 raise HTTPForbidden()
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Dec 8, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -54,7 +54,7 b' class ErrorController(BaseController):'
54 54 resp = request.environ.get('pylons.original_response')
55 55 c.rhodecode_name = config.get('rhodecode_title')
56 56
57 log.debug('### %s ###', resp.status)
57 log.debug('### %s ###' % resp.status)
58 58
59 59 e = request.environ
60 60 c.serv_p = r'%(protocol)s://%(host)s/' \
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 23, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -51,6 +51,11 b' class FeedController(BaseRepoController)'
51 51 self.ttl = "5"
52 52 self.feed_nr = 10
53 53
54 def _get_title(self, cs):
55 return "R%s:%s - %s" % (
56 cs.revision, cs.short_id, cs.message
57 )
58
54 59 def __changes(self, cs):
55 60 changes = []
56 61
@@ -72,18 +77,21 b' class FeedController(BaseRepoController)'
72 77
73 78 def atom(self, repo_name):
74 79 """Produce an atom-1.0 feed via feedgenerator module"""
75 feed = Atom1Feed(title=self.title % repo_name,
76 link=url('summary_home', repo_name=repo_name,
77 qualified=True),
78 description=self.description % repo_name,
79 language=self.language,
80 ttl=self.ttl)
81 desc_msg = []
80 feed = Atom1Feed(
81 title=self.title % repo_name,
82 link=url('summary_home', repo_name=repo_name,
83 qualified=True),
84 description=self.description % repo_name,
85 language=self.language,
86 ttl=self.ttl
87 )
88
82 89 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
90 desc_msg = []
83 91 desc_msg.append('%s - %s<br/><pre>' % (cs.author, cs.date))
84 92 desc_msg.append(self.__changes(cs))
85 93
86 feed.add_item(title=cs.message,
94 feed.add_item(title=self._get_title(cs),
87 95 link=url('changeset_home', repo_name=repo_name,
88 96 revision=cs.raw_id, qualified=True),
89 97 author_name=cs.author,
@@ -94,18 +102,21 b' class FeedController(BaseRepoController)'
94 102
95 103 def rss(self, repo_name):
96 104 """Produce an rss2 feed via feedgenerator module"""
97 feed = Rss201rev2Feed(title=self.title % repo_name,
98 link=url('summary_home', repo_name=repo_name,
99 qualified=True),
100 description=self.description % repo_name,
101 language=self.language,
102 ttl=self.ttl)
103 desc_msg = []
105 feed = Rss201rev2Feed(
106 title=self.title % repo_name,
107 link=url('summary_home', repo_name=repo_name,
108 qualified=True),
109 description=self.description % repo_name,
110 language=self.language,
111 ttl=self.ttl
112 )
113
104 114 for cs in reversed(list(c.rhodecode_repo[-self.feed_nr:])):
115 desc_msg = []
105 116 desc_msg.append('%s - %s<br/><pre>' % (cs.author, cs.date))
106 117 desc_msg.append(self.__changes(cs))
107 118
108 feed.add_item(title=cs.message,
119 feed.add_item(title=self._get_title(cs),
109 120 link=url('changeset_home', repo_name=repo_name,
110 121 revision=cs.raw_id, qualified=True),
111 122 author_name=cs.author,
@@ -27,25 +27,29 b' import os'
27 27 import logging
28 28 import traceback
29 29
30 from os.path import join as jn
31
32 from pylons import request, response, session, tmpl_context as c, url
30 from pylons import request, response, tmpl_context as c, url
33 31 from pylons.i18n.translation import _
34 32 from pylons.controllers.util import redirect
35 33 from pylons.decorators import jsonify
36 34
37 from vcs.conf import settings
38 from vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
39 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError
40 from vcs.nodes import FileNode, NodeKind
41 from vcs.utils import diffs as differ
35 from rhodecode.lib.vcs.conf import settings
36 from rhodecode.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
37 EmptyRepositoryError, ImproperArchiveTypeError, VCSError, \
38 NodeAlreadyExistsError
39 from rhodecode.lib.vcs.nodes import FileNode
42 40
41 from rhodecode.lib.compat import OrderedDict
43 42 from rhodecode.lib import convert_line_endings, detect_mode, safe_str
44 43 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
45 44 from rhodecode.lib.base import BaseRepoController, render
46 45 from rhodecode.lib.utils import EmptyChangeset
46 from rhodecode.lib import diffs
47 47 import rhodecode.lib.helpers as h
48 48 from rhodecode.model.repo import RepoModel
49 from rhodecode.controllers.changeset import anchor_url, _ignorews_url,\
50 _context_url, get_line_ctx, get_ignore_ws
51 from rhodecode.lib.diffs import wrapped_diff
52 from rhodecode.model.scm import ScmModel
49 53
50 54 log = logging.getLogger(__name__)
51 55
@@ -104,26 +108,6 b' class FilesController(BaseRepoController'
104 108
105 109 return file_node
106 110
107
108 def __get_paths(self, changeset, starting_path):
109 """recursive walk in root dir and return a set of all path in that dir
110 based on repository walk function
111 """
112 _files = list()
113 _dirs = list()
114
115 try:
116 tip = changeset
117 for topnode, dirs, files in tip.walk(starting_path):
118 for f in files:
119 _files.append(f.path)
120 for d in dirs:
121 _dirs.append(d.path)
122 except RepositoryError, e:
123 log.debug(traceback.format_exc())
124 pass
125 return _dirs, _files
126
127 111 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
128 112 'repository.admin')
129 113 def index(self, repo_name, revision, f_path):
@@ -162,9 +146,9 b' class FilesController(BaseRepoController'
162 146
163 147 # files or dirs
164 148 try:
165 c.files_list = c.changeset.get_node(f_path)
149 c.file = c.changeset.get_node(f_path)
166 150
167 if c.files_list.is_file():
151 if c.file.is_file():
168 152 c.file_history = self._get_node_history(c.changeset, f_path)
169 153 else:
170 154 c.file_history = []
@@ -405,13 +389,19 b' class FilesController(BaseRepoController'
405 389 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
406 390 'repository.admin')
407 391 def diff(self, repo_name, f_path):
408 diff1 = request.GET.get('diff1')
409 diff2 = request.GET.get('diff2')
392 ignore_whitespace = request.GET.get('ignorews') == '1'
393 line_context = request.GET.get('context', 3)
394 diff1 = request.GET.get('diff1', '')
395 diff2 = request.GET.get('diff2', '')
410 396 c.action = request.GET.get('diff')
411 397 c.no_changes = diff1 == diff2
412 398 c.f_path = f_path
413 399 c.big_diff = False
414
400 c.anchor_url = anchor_url
401 c.ignorews_url = _ignorews_url
402 c.context_url = _context_url
403 c.changes = OrderedDict()
404 c.changes[diff2] = []
415 405 try:
416 406 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
417 407 c.changeset_1 = c.rhodecode_repo.get_changeset(diff1)
@@ -427,12 +417,14 b' class FilesController(BaseRepoController'
427 417 c.changeset_2 = EmptyChangeset(repo=c.rhodecode_repo)
428 418 node2 = FileNode('.', '', changeset=c.changeset_2)
429 419 except RepositoryError:
430 return redirect(url('files_home',
431 repo_name=c.repo_name, f_path=f_path))
420 return redirect(url('files_home', repo_name=c.repo_name,
421 f_path=f_path))
432 422
433 423 if c.action == 'download':
434 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
435 format='gitdiff')
424 _diff = diffs.get_gitdiff(node1, node2,
425 ignore_whitespace=ignore_whitespace,
426 context=line_context)
427 diff = diffs.DiffProcessor(_diff, format='gitdiff')
436 428
437 429 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
438 430 response.content_type = 'text/plain'
@@ -441,39 +433,28 b' class FilesController(BaseRepoController'
441 433 return diff.raw_diff()
442 434
443 435 elif c.action == 'raw':
444 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
445 format='gitdiff')
436 _diff = diffs.get_gitdiff(node1, node2,
437 ignore_whitespace=ignore_whitespace,
438 context=line_context)
439 diff = diffs.DiffProcessor(_diff, format='gitdiff')
446 440 response.content_type = 'text/plain'
447 441 return diff.raw_diff()
448 442
449 elif c.action == 'diff':
450 if node1.is_binary or node2.is_binary:
451 c.cur_diff = _('Binary file')
452 elif node1.size > self.cut_off_limit or \
453 node2.size > self.cut_off_limit:
454 c.cur_diff = ''
455 c.big_diff = True
456 else:
457 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
458 format='gitdiff')
459 c.cur_diff = diff.as_html()
460 443 else:
444 fid = h.FID(diff2, node2.path)
445 line_context_lcl = get_line_ctx(fid, request.GET)
446 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
461 447
462 #default option
463 if node1.is_binary or node2.is_binary:
464 c.cur_diff = _('Binary file')
465 elif node1.size > self.cut_off_limit or \
466 node2.size > self.cut_off_limit:
467 c.cur_diff = ''
468 c.big_diff = True
448 lim = request.GET.get('fulldiff') or self.cut_off_limit
449 _, cs1, cs2, diff, st = wrapped_diff(filenode_old=node1,
450 filenode_new=node2,
451 cut_off_limit=lim,
452 ignore_whitespace=ign_whitespace_lcl,
453 line_context=line_context_lcl,
454 enable_comments=False)
469 455
470 else:
471 diff = differ.DiffProcessor(differ.get_gitdiff(node1, node2),
472 format='gitdiff')
473 c.cur_diff = diff.as_html()
456 c.changes = [('', node2, diff, cs1, cs2, st,)]
474 457
475 if not c.cur_diff and not c.big_diff:
476 c.no_changes = True
477 458 return render('files/file_diff.html')
478 459
479 460 def _get_node_history(self, cs, f_path):
@@ -485,18 +466,16 b' class FilesController(BaseRepoController'
485 466 tags_group = ([], _("Tags"))
486 467
487 468 for chs in changesets:
488 n_desc = 'r%s:%s' % (chs.revision, chs.short_id)
469 n_desc = 'r%s:%s (%s)' % (chs.revision, chs.short_id, chs.branch)
489 470 changesets_group[0].append((chs.raw_id, n_desc,))
490 471
491 472 hist_l.append(changesets_group)
492 473
493 474 for name, chs in c.rhodecode_repo.branches.items():
494 #chs = chs.split(':')[-1]
495 475 branches_group[0].append((chs, name),)
496 476 hist_l.append(branches_group)
497 477
498 478 for name, chs in c.rhodecode_repo.tags.items():
499 #chs = chs.split(':')[-1]
500 479 tags_group[0].append((chs, name),)
501 480 hist_l.append(tags_group)
502 481
@@ -508,6 +487,6 b' class FilesController(BaseRepoController'
508 487 def nodelist(self, repo_name, revision, f_path):
509 488 if request.environ.get('HTTP_X_PARTIAL_XHR'):
510 489 cs = self.__get_cs_or_redirect(revision, repo_name)
511 _d, _f = self.__get_paths(cs, f_path)
490 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
491 flat=False)
512 492 return _d + _f
513
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 23, 2011
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 23, 2011
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2011-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -23,13 +23,23 b''
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 import formencode
27 import traceback
28 from formencode import htmlfill
26 29
27 from pylons import tmpl_context as c, request
30 from pylons import tmpl_context as c, request, url
31 from pylons.controllers.util import redirect
32 from pylons.i18n.translation import _
33
34 import rhodecode.lib.helpers as h
28 35
29 36 from rhodecode.lib.helpers import Page
30 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
38 NotAnonymous
31 39 from rhodecode.lib.base import BaseRepoController, render
32 from rhodecode.model.db import Repository, User, UserFollowing
40 from rhodecode.model.db import Repository, RepoGroup, UserFollowing, User
41 from rhodecode.model.repo import RepoModel
42 from rhodecode.model.forms import RepoForkForm
33 43
34 44 log = logging.getLogger(__name__)
35 45
@@ -37,11 +47,59 b' log = logging.getLogger(__name__)'
37 47 class ForksController(BaseRepoController):
38 48
39 49 @LoginRequired()
40 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
41 'repository.admin')
42 50 def __before__(self):
43 51 super(ForksController, self).__before__()
44 52
53 def __load_defaults(self):
54 c.repo_groups = RepoGroup.groups_choices()
55 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
56
57 def __load_data(self, repo_name=None):
58 """
59 Load defaults settings for edit, and update
60
61 :param repo_name:
62 """
63 self.__load_defaults()
64
65 c.repo_info = db_repo = Repository.get_by_repo_name(repo_name)
66 repo = db_repo.scm_instance
67
68 if c.repo_info is None:
69 h.flash(_('%s repository is not mapped to db perhaps'
70 ' it was created or renamed from the filesystem'
71 ' please run the application again'
72 ' in order to rescan repositories') % repo_name,
73 category='error')
74
75 return redirect(url('repos'))
76
77 c.default_user_id = User.get_by_username('default').user_id
78 c.in_public_journal = UserFollowing.query()\
79 .filter(UserFollowing.user_id == c.default_user_id)\
80 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
81
82 if c.repo_info.stats:
83 last_rev = c.repo_info.stats.stat_on_revision+1
84 else:
85 last_rev = 0
86 c.stats_revision = last_rev
87
88 c.repo_last_rev = repo.count() if repo.revisions else 0
89
90 if last_rev == 0 or c.repo_last_rev == 0:
91 c.stats_percentage = 0
92 else:
93 c.stats_percentage = '%.2f' % ((float((last_rev)) /
94 c.repo_last_rev) * 100)
95
96 defaults = RepoModel()._get_defaults(repo_name)
97 # add prefix to fork
98 defaults['repo_name'] = 'fork-' + defaults['repo_name']
99 return defaults
100
101 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
102 'repository.admin')
45 103 def forks(self, repo_name):
46 104 p = int(request.params.get('page', 1))
47 105 repo_id = c.rhodecode_db_repo.repo_id
@@ -54,3 +112,63 b' class ForksController(BaseRepoController'
54 112 return c.forks_data
55 113
56 114 return render('/forks/forks.html')
115
116 @NotAnonymous()
117 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
118 'repository.admin')
119 def fork(self, repo_name):
120 c.repo_info = Repository.get_by_repo_name(repo_name)
121 if not c.repo_info:
122 h.flash(_('%s repository is not mapped to db perhaps'
123 ' it was created or renamed from the file system'
124 ' please run the application again'
125 ' in order to rescan repositories') % repo_name,
126 category='error')
127
128 return redirect(url('home'))
129
130 defaults = self.__load_data(repo_name)
131
132 return htmlfill.render(
133 render('forks/fork.html'),
134 defaults=defaults,
135 encoding="UTF-8",
136 force_defaults=False
137 )
138
139
140 @NotAnonymous()
141 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
142 'repository.admin')
143 def fork_create(self, repo_name):
144 self.__load_defaults()
145 c.repo_info = Repository.get_by_repo_name(repo_name)
146 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
147 repo_groups=c.repo_groups_choices,)()
148 form_result = {}
149 try:
150 form_result = _form.to_python(dict(request.POST))
151 # add org_path of repo so we can do a clone from it later
152 form_result['org_path'] = c.repo_info.repo_name
153
154 # create fork is done sometimes async on celery, db transaction
155 # management is handled there.
156 RepoModel().create_fork(form_result, self.rhodecode_user)
157 h.flash(_('forked %s repository as %s') \
158 % (repo_name, form_result['repo_name']),
159 category='success')
160 except formencode.Invalid, errors:
161 c.new_repo = errors.value['repo_name']
162
163 return htmlfill.render(
164 render('forks/fork.html'),
165 defaults=errors.value,
166 errors=errors.error_dict or {},
167 prefix_error=False,
168 encoding="UTF-8")
169 except Exception:
170 log.error(traceback.format_exc())
171 h.flash(_('An error occurred during repository forking %s') %
172 repo_name, category='error')
173
174 return redirect(url('home'))
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Feb 18, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -24,14 +24,13 b''
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import logging
27 from operator import itemgetter
28 27
29 28 from pylons import tmpl_context as c, request
30 29 from paste.httpexceptions import HTTPBadRequest
31 30
32 31 from rhodecode.lib.auth import LoginRequired
33 32 from rhodecode.lib.base import BaseController, render
34 from rhodecode.model.db import Group, Repository
33 from rhodecode.model.db import Repository
35 34
36 35 log = logging.getLogger(__name__)
37 36
@@ -43,10 +42,8 b' class HomeController(BaseController):'
43 42 super(HomeController, self).__before__()
44 43
45 44 def index(self):
46
47 45 c.repos_list = self.scm_model.get_repos()
48
49 c.groups = Group.query().filter(Group.group_parent_id == None).all()
46 c.groups = self.scm_model.get_repos_groups()
50 47
51 48 return render('/index.html')
52 49
@@ -58,3 +55,11 b' class HomeController(BaseController):'
58 55 return render('/repo_switcher_list.html')
59 56 else:
60 57 return HTTPBadRequest()
58
59 def branch_tag_switcher(self, repo_name):
60 if request.is_xhr:
61 c.rhodecode_db_repo = Repository.get_by_repo_name(c.repo_name)
62 c.rhodecode_repo = c.rhodecode_db_repo.scm_instance
63 return render('/switch_to_list.html')
64 else:
65 return HTTPBadRequest()
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Nov 21, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -23,21 +23,24 b''
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25 import logging
26 from itertools import groupby
26 27
27 28 from sqlalchemy import or_
28 from sqlalchemy.orm import joinedload, make_transient
29 from sqlalchemy.orm import joinedload
29 30 from webhelpers.paginate import Page
30 from itertools import groupby
31 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
31 32
32 33 from paste.httpexceptions import HTTPBadRequest
33 34 from pylons import request, tmpl_context as c, response, url
34 35 from pylons.i18n.translation import _
35 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
36 36
37 37 import rhodecode.lib.helpers as h
38 38 from rhodecode.lib.auth import LoginRequired, NotAnonymous
39 39 from rhodecode.lib.base import BaseController, render
40 from rhodecode.model.db import UserLog, UserFollowing
40 from rhodecode.model.db import UserLog, UserFollowing, Repository, User
41 from rhodecode.model.meta import Session
42 from sqlalchemy.sql.expression import func
43 from rhodecode.model.scm import ScmModel
41 44
42 45 log = logging.getLogger(__name__)
43 46
@@ -58,6 +61,13 b' class JournalController(BaseController):'
58 61 # Return a rendered template
59 62 p = int(request.params.get('page', 1))
60 63
64 c.user = User.get(self.rhodecode_user.user_id)
65 all_repos = self.sa.query(Repository)\
66 .filter(Repository.user_id == c.user.user_id)\
67 .order_by(func.lower(Repository.repo_name)).all()
68
69 c.user_repos = ScmModel().get_repos(all_repos)
70
61 71 c.following = self.sa.query(UserFollowing)\
62 72 .filter(UserFollowing.user_id == self.rhodecode_user.user_id)\
63 73 .options(joinedload(UserFollowing.follows_repository))\
@@ -124,6 +134,7 b' class JournalController(BaseController):'
124 134 try:
125 135 self.scm_model.toggle_following_user(user_id,
126 136 self.rhodecode_user.user_id)
137 Session.commit()
127 138 return 'ok'
128 139 except:
129 140 raise HTTPBadRequest()
@@ -133,11 +144,12 b' class JournalController(BaseController):'
133 144 try:
134 145 self.scm_model.toggle_following_repo(repo_id,
135 146 self.rhodecode_user.user_id)
147 Session.commit()
136 148 return 'ok'
137 149 except:
138 150 raise HTTPBadRequest()
139 151
140 log.debug('token mismatch %s vs %s', cur_token, token)
152 log.debug('token mismatch %s vs %s' % (cur_token, token))
141 153 raise HTTPBadRequest()
142 154
143 155 @LoginRequired()
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 22, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -38,6 +38,7 b' from rhodecode.lib.base import BaseContr'
38 38 from rhodecode.model.db import User
39 39 from rhodecode.model.forms import LoginForm, RegisterForm, PasswordResetForm
40 40 from rhodecode.model.user import UserModel
41 from rhodecode.model.meta import Session
41 42
42 43
43 44 log = logging.getLogger(__name__)
@@ -49,7 +50,7 b' class LoginController(BaseController):'
49 50 super(LoginController, self).__before__()
50 51
51 52 def index(self):
52 #redirect if already logged in
53 # redirect if already logged in
53 54 c.came_from = request.GET.get('came_from', None)
54 55
55 56 if self.rhodecode_user.is_authenticated \
@@ -58,21 +59,28 b' class LoginController(BaseController):'
58 59 return redirect(url('home'))
59 60
60 61 if request.POST:
61 #import Login Form validator class
62 # import Login Form validator class
62 63 login_form = LoginForm()
63 64 try:
64 65 c.form_result = login_form.to_python(dict(request.POST))
65 #form checks for username/password, now we're authenticated
66 # form checks for username/password, now we're authenticated
66 67 username = c.form_result['username']
67 68 user = User.get_by_username(username, case_insensitive=True)
68 69 auth_user = AuthUser(user.user_id)
69 70 auth_user.set_authenticated()
70 session['rhodecode_user'] = auth_user
71 cs = auth_user.get_cookie_store()
72 session['rhodecode_user'] = cs
73 # If they want to be remembered, update the cookie
74 if c.form_result['remember'] is not False:
75 session.cookie_expires = False
76 session._set_cookie_values()
77 session._update_cookie_out()
71 78 session.save()
72 79
73 log.info('user %s is now authenticated and stored in session',
74 username)
80 log.info('user %s is now authenticated and stored in '
81 'session, session attrs %s' % (username, cs))
75 82 user.update_lastlogin()
83 Session.commit()
76 84
77 85 if c.came_from:
78 86 return redirect(c.came_from)
@@ -92,7 +100,6 b' class LoginController(BaseController):'
92 100 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
93 101 'hg.register.manual_activate')
94 102 def register(self):
95 user_model = UserModel()
96 103 c.auto_active = False
97 104 for perm in User.get_by_username('default').user_perms:
98 105 if perm.permission.permission_name == 'hg.register.auto_activate':
@@ -105,9 +112,10 b' class LoginController(BaseController):'
105 112 try:
106 113 form_result = register_form.to_python(dict(request.POST))
107 114 form_result['active'] = c.auto_active
108 user_model.create_registration(form_result)
115 UserModel().create_registration(form_result)
109 116 h.flash(_('You have successfully registered into rhodecode'),
110 117 category='success')
118 Session.commit()
111 119 return redirect(url('login_home'))
112 120
113 121 except formencode.Invalid, errors:
@@ -121,13 +129,11 b' class LoginController(BaseController):'
121 129 return render('/register.html')
122 130
123 131 def password_reset(self):
124 user_model = UserModel()
125 132 if request.POST:
126
127 133 password_reset_form = PasswordResetForm()()
128 134 try:
129 135 form_result = password_reset_form.to_python(dict(request.POST))
130 user_model.reset_password_link(form_result)
136 UserModel().reset_password_link(form_result)
131 137 h.flash(_('Your password reset link was sent'),
132 138 category='success')
133 139 return redirect(url('login_home'))
@@ -143,13 +149,11 b' class LoginController(BaseController):'
143 149 return render('/password_reset.html')
144 150
145 151 def password_reset_confirmation(self):
146
147 152 if request.GET and request.GET.get('key'):
148 153 try:
149 user_model = UserModel()
150 154 user = User.get_by_api_key(request.GET.get('key'))
151 155 data = dict(email=user.email)
152 user_model.reset_password(data)
156 UserModel().reset_password(data)
153 157 h.flash(_('Your password reset was successful, '
154 158 'new password has been sent to your email'),
155 159 category='success')
@@ -160,7 +164,6 b' class LoginController(BaseController):'
160 164 return redirect(url('login_home'))
161 165
162 166 def logout(self):
163 del session['rhodecode_user']
164 session.save()
165 log.info('Logging out and setting user as Empty')
167 session.delete()
168 log.info('Logging out and deleting session for user')
166 169 redirect(url('home'))
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Aug 7, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -26,7 +26,7 b' import logging'
26 26 import traceback
27 27
28 28 from pylons.i18n.translation import _
29 from pylons import request, config, session, tmpl_context as c
29 from pylons import request, config, tmpl_context as c
30 30
31 31 from rhodecode.lib.auth import LoginRequired
32 32 from rhodecode.lib.base import BaseController, render
@@ -76,7 +76,7 b' class SearchController(BaseController):'
76 76 cur_query = u'repository:%s %s' % (c.repo_name, cur_query)
77 77 try:
78 78 query = qp.parse(unicode(cur_query))
79
79 # extract words for highlight
80 80 if isinstance(query, Phrase):
81 81 highlight_items.update(query.words)
82 82 elif isinstance(query, Prefix):
@@ -92,18 +92,22 b' class SearchController(BaseController):'
92 92 log.debug(highlight_items)
93 93 results = searcher.search(query)
94 94 res_ln = len(results)
95 c.runtime = '%s results (%.3f seconds)' \
96 % (res_ln, results.runtime)
95 c.runtime = '%s results (%.3f seconds)' % (
96 res_ln, results.runtime
97 )
97 98
98 99 def url_generator(**kw):
99 100 return update_params("?q=%s&type=%s" \
100 101 % (c.cur_query, c.cur_search), **kw)
101 102
102 103 c.formated_results = Page(
103 ResultWrapper(search_type, searcher, matcher,
104 highlight_items),
105 page=p, item_count=res_ln,
106 items_per_page=10, url=url_generator)
104 ResultWrapper(search_type, searcher, matcher,
105 highlight_items),
106 page=p,
107 item_count=res_ln,
108 items_per_page=10,
109 url=url_generator
110 )
107 111
108 112 except QueryParserError:
109 113 c.runtime = _('Invalid search query. Try quoting it.')
@@ -117,5 +121,6 b' class SearchController(BaseController):'
117 121 log.error(traceback.format_exc())
118 122 c.runtime = _('An error occurred during this search operation')
119 123
124
120 125 # Return a rendered template
121 126 return render('/search/search.html')
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Jun 30, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -35,14 +35,14 b' from pylons.i18n.translation import _'
35 35
36 36 import rhodecode.lib.helpers as h
37 37
38 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAllDecorator, \
39 HasRepoPermissionAnyDecorator, NotAnonymous
38 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAllDecorator
40 39 from rhodecode.lib.base import BaseRepoController, render
41 40 from rhodecode.lib.utils import invalidate_cache, action_logger
42 41
43 from rhodecode.model.forms import RepoSettingsForm, RepoForkForm
42 from rhodecode.model.forms import RepoSettingsForm
44 43 from rhodecode.model.repo import RepoModel
45 from rhodecode.model.db import Group
44 from rhodecode.model.db import RepoGroup
45 from rhodecode.model.meta import Session
46 46
47 47 log = logging.getLogger(__name__)
48 48
@@ -52,15 +52,15 b' class SettingsController(BaseRepoControl'
52 52 @LoginRequired()
53 53 def __before__(self):
54 54 super(SettingsController, self).__before__()
55
55
56 56 def __load_defaults(self):
57 c.repo_groups = Group.groups_choices()
57 c.repo_groups = RepoGroup.groups_choices()
58 58 c.repo_groups_choices = map(lambda k: unicode(k[0]), c.repo_groups)
59
59
60 60 repo_model = RepoModel()
61 61 c.users_array = repo_model.get_users_js()
62 62 c.users_groups_array = repo_model.get_users_groups_js()
63
63
64 64 @HasRepoPermissionAllDecorator('repository.admin')
65 65 def index(self, repo_name):
66 66 repo_model = RepoModel()
@@ -89,15 +89,15 b' class SettingsController(BaseRepoControl'
89 89 def update(self, repo_name):
90 90 repo_model = RepoModel()
91 91 changed_name = repo_name
92
92
93 93 self.__load_defaults()
94
94
95 95 _form = RepoSettingsForm(edit=True,
96 96 old_data={'repo_name': repo_name},
97 97 repo_groups=c.repo_groups_choices)()
98 98 try:
99 99 form_result = _form.to_python(dict(request.POST))
100
100
101 101 repo_model.update(repo_name, form_result)
102 102 invalidate_cache('get_repo_cached_%s' % repo_name)
103 103 h.flash(_('Repository %s updated successfully' % repo_name),
@@ -105,6 +105,7 b' class SettingsController(BaseRepoControl'
105 105 changed_name = form_result['repo_name_full']
106 106 action_logger(self.rhodecode_user, 'user_updated_repo',
107 107 changed_name, '', self.sa)
108 Session.commit()
108 109 except formencode.Invalid, errors:
109 110 c.repo_info = repo_model.get_by_repo_name(repo_name)
110 111 c.users_array = repo_model.get_users_js()
@@ -148,61 +149,10 b' class SettingsController(BaseRepoControl'
148 149 repo_model.delete(repo)
149 150 invalidate_cache('get_repo_cached_%s' % repo_name)
150 151 h.flash(_('deleted repository %s') % repo_name, category='success')
152 Session.commit()
151 153 except Exception:
152 154 log.error(traceback.format_exc())
153 155 h.flash(_('An error occurred during deletion of %s') % repo_name,
154 156 category='error')
155 157
156 158 return redirect(url('home'))
157
158 @NotAnonymous()
159 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
160 'repository.admin')
161 def fork(self, repo_name):
162 repo_model = RepoModel()
163 c.repo_info = repo = repo_model.get_by_repo_name(repo_name)
164 if not repo:
165 h.flash(_('%s repository is not mapped to db perhaps'
166 ' it was created or renamed from the file system'
167 ' please run the application again'
168 ' in order to rescan repositories') % repo_name,
169 category='error')
170
171 return redirect(url('home'))
172
173 return render('settings/repo_fork.html')
174
175 @NotAnonymous()
176 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
177 'repository.admin')
178 def fork_create(self, repo_name):
179 repo_model = RepoModel()
180 c.repo_info = repo_model.get_by_repo_name(repo_name)
181 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type})()
182 form_result = {}
183 try:
184 form_result = _form.to_python(dict(request.POST))
185 form_result.update({'repo_name': repo_name})
186 repo_model.create_fork(form_result, self.rhodecode_user)
187 h.flash(_('forked %s repository as %s') \
188 % (repo_name, form_result['fork_name']),
189 category='success')
190 action_logger(self.rhodecode_user,
191 'user_forked_repo:%s' % form_result['fork_name'],
192 repo_name, '', self.sa)
193 except formencode.Invalid, errors:
194 c.new_repo = errors.value['fork_name']
195 r = render('settings/repo_fork.html')
196
197 return htmlfill.render(
198 r,
199 defaults=errors.value,
200 errors=errors.error_dict or {},
201 prefix_error=False,
202 encoding="UTF-8")
203 except Exception:
204 log.error(traceback.format_exc())
205 h.flash(_('An error occurred during repository forking %s') %
206 repo_name, category='error')
207
208 return redirect(url('home'))
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -30,6 +30,7 b' from pylons import tmpl_context as c, re'
30 30 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
31 31 from rhodecode.lib.base import BaseRepoController, render
32 32 from rhodecode.lib.helpers import RepoPage
33 from pylons.controllers.util import redirect
33 34
34 35 log = logging.getLogger(__name__)
35 36
@@ -50,8 +51,11 b' class ShortlogController(BaseRepoControl'
50 51 return url('shortlog_home', repo_name=repo_name, size=size, **kw)
51 52
52 53 c.repo_changesets = RepoPage(c.rhodecode_repo, page=p,
53 items_per_page=size,
54 url=url_generator)
54 items_per_page=size, url=url_generator)
55
56 if not c.repo_changesets:
57 return redirect(url('summary_home', repo_name=repo_name))
58
55 59 c.shortlog_data = render('shortlog/shortlog_data.html')
56 60 if request.environ.get('HTTP_X_PARTIAL_XHR'):
57 61 return c.shortlog_data
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 18, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -23,23 +23,28 b''
23 23 # You should have received a copy of the GNU General Public License
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 import traceback
26 27 import calendar
27 28 import logging
28 29 from time import mktime
29 from datetime import datetime, timedelta, date
30 from datetime import timedelta, date
31 from itertools import product
32 from urlparse import urlparse
30 33
31 from vcs.exceptions import ChangesetError
34 from rhodecode.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
35 NodeDoesNotExistError
32 36
33 from pylons import tmpl_context as c, request, url
37 from pylons import tmpl_context as c, request, url, config
34 38 from pylons.i18n.translation import _
35 39
36 from rhodecode.model.db import Statistics, Repository
37 from rhodecode.model.repo import RepoModel
40 from beaker.cache import cache_region, region_invalidate
38 41
42 from rhodecode.model.db import Statistics, CacheInvalidation
43 from rhodecode.lib import ALL_READMES, ALL_EXTS
39 44 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
40 45 from rhodecode.lib.base import BaseRepoController, render
41 46 from rhodecode.lib.utils import EmptyChangeset
42
47 from rhodecode.lib.markup_renderer import MarkupRenderer
43 48 from rhodecode.lib.celerylib import run_task
44 49 from rhodecode.lib.celerylib.tasks import get_commits_stats, \
45 50 LANGUAGES_EXTENSIONS_MAP
@@ -48,6 +53,10 b' from rhodecode.lib.compat import json, O'
48 53
49 54 log = logging.getLogger(__name__)
50 55
56 README_FILES = [''.join([x[0][0], x[1][0]]) for x in
57 sorted(list(product(ALL_READMES, ALL_EXTS)),
58 key=lambda y:y[0][1] + y[1][1])]
59
51 60
52 61 class SummaryController(BaseRepoController):
53 62
@@ -58,10 +67,7 b' class SummaryController(BaseRepoControll'
58 67 super(SummaryController, self).__before__()
59 68
60 69 def index(self, repo_name):
61
62 e = request.environ
63 70 c.dbrepo = dbrepo = c.rhodecode_db_repo
64
65 71 c.following = self.scm_model.is_following_repo(repo_name,
66 72 self.rhodecode_user.user_id)
67 73
@@ -72,26 +78,34 b' class SummaryController(BaseRepoControll'
72 78 items_per_page=10, url=url_generator)
73 79
74 80 if self.rhodecode_user.username == 'default':
75 #for default(anonymous) user we don't need to pass credentials
81 # for default(anonymous) user we don't need to pass credentials
76 82 username = ''
77 83 password = ''
78 84 else:
79 85 username = str(self.rhodecode_user.username)
80 86 password = '@'
81 87
82 if e.get('wsgi.url_scheme') == 'https':
83 split_s = 'https://'
84 else:
85 split_s = 'http://'
88 parsed_url = urlparse(url.current(qualified=True))
89
90 default_clone_uri = '{scheme}://{user}{pass}{netloc}{path}'
91
92 uri_tmpl = config.get('clone_uri', default_clone_uri)
93 uri_tmpl = uri_tmpl.replace('{', '%(').replace('}', ')s')
86 94
87 qualified_uri = [split_s] + [url.current(qualified=True)\
88 .split(split_s)[-1]]
89 uri = u'%(proto)s%(user)s%(pass)s%(rest)s' \
90 % {'user': username,
91 'pass': password,
92 'proto': qualified_uri[0],
93 'rest': qualified_uri[1]}
95 uri_dict = {
96 'user': username,
97 'pass': password,
98 'scheme': parsed_url.scheme,
99 'netloc': parsed_url.netloc,
100 'path': parsed_url.path
101 }
102 uri = uri_tmpl % uri_dict
103 # generate another clone url by id
104 uri_dict.update({'path': '/_%s' % c.dbrepo.repo_id})
105 uri_id = uri_tmpl % uri_dict
106
94 107 c.clone_repo_url = uri
108 c.clone_repo_url_id = uri_id
95 109 c.repo_tags = OrderedDict()
96 110 for name, hash in c.rhodecode_repo.tags.items()[:10]:
97 111 try:
@@ -161,8 +175,44 b' class SummaryController(BaseRepoControll'
161 175 if c.enable_downloads:
162 176 c.download_options = self._get_download_links(c.rhodecode_repo)
163 177
178 c.readme_data, c.readme_file = self.__get_readme_data(c.rhodecode_repo)
164 179 return render('summary/summary.html')
165 180
181 def __get_readme_data(self, repo):
182
183 @cache_region('long_term')
184 def _get_readme_from_cache(key):
185 readme_data = None
186 readme_file = None
187 log.debug('Fetching readme file')
188 try:
189 cs = repo.get_changeset('tip')
190 renderer = MarkupRenderer()
191 for f in README_FILES:
192 try:
193 readme = cs.get_node(f)
194 readme_file = f
195 readme_data = renderer.render(readme.content, f)
196 log.debug('Found readme %s' % readme_file)
197 break
198 except NodeDoesNotExistError:
199 continue
200 except ChangesetError:
201 pass
202 except EmptyRepositoryError:
203 pass
204 except Exception:
205 log.error(traceback.format_exc())
206
207 return readme_data, readme_file
208
209 key = repo.name + '_README'
210 inv = CacheInvalidation.invalidate(key)
211 if inv is not None:
212 region_invalidate(_get_readme_from_cache, None, key)
213 CacheInvalidation.set_valid(inv.cache_key)
214 return _get_readme_from_cache(key)
215
166 216 def _get_download_links(self, repo):
167 217
168 218 download_l = []
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Apr 21, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -24,6 +24,9 b''
24 24 # along with this program. If not, see <http://www.gnu.org/licenses/>.
25 25
26 26 import os
27 import re
28 from rhodecode.lib.vcs.utils.lazy import LazyProperty
29
27 30
28 31 def __get_lem():
29 32 from pygments import lexers
@@ -66,6 +69,34 b" ADDITIONAL_MAPPINGS = {'xaml': 'XAML'}"
66 69
67 70 LANGUAGES_EXTENSIONS_MAP.update(ADDITIONAL_MAPPINGS)
68 71
72 # list of readme files to search in file tree and display in summary
73 # attached weights defines the search order lower is first
74 ALL_READMES = [
75 ('readme', 0), ('README', 0), ('Readme', 0),
76 ('doc/readme', 1), ('doc/README', 1), ('doc/Readme', 1),
77 ('Docs/readme', 2), ('Docs/README', 2), ('Docs/Readme', 2),
78 ('DOCS/readme', 2), ('DOCS/README', 2), ('DOCS/Readme', 2),
79 ('docs/readme', 2), ('docs/README', 2), ('docs/Readme', 2),
80 ]
81
82 # extension together with weights to search lower is first
83 RST_EXTS = [
84 ('', 0), ('.rst', 1), ('.rest', 1),
85 ('.RST', 2), ('.REST', 2),
86 ('.txt', 3), ('.TXT', 3)
87 ]
88
89 MARKDOWN_EXTS = [
90 ('.md', 1), ('.MD', 1),
91 ('.mkdn', 2), ('.MKDN', 2),
92 ('.mdown', 3), ('.MDOWN', 3),
93 ('.markdown', 4), ('.MARKDOWN', 4)
94 ]
95
96 PLAIN_EXTS = [('.text', 2), ('.TEXT', 2)]
97
98 ALL_EXTS = MARKDOWN_EXTS + RST_EXTS + PLAIN_EXTS
99
69 100
70 101 def str2bool(_str):
71 102 """
@@ -107,7 +138,6 b' def convert_line_endings(line, mode):'
107 138 line = replace(line, '\r\n', '\r')
108 139 line = replace(line, '\n', '\r')
109 140 elif mode == 2:
110 import re
111 141 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
112 142 return line
113 143
@@ -151,7 +181,7 b' def generate_api_key(username, salt=None'
151 181 return hashlib.sha1(username + salt).hexdigest()
152 182
153 183
154 def safe_unicode(str_, from_encoding='utf8'):
184 def safe_unicode(str_, from_encoding=None):
155 185 """
156 186 safe unicode function. Does few trick to turn str_ into unicode
157 187
@@ -165,6 +195,11 b" def safe_unicode(str_, from_encoding='ut"
165 195 if isinstance(str_, unicode):
166 196 return str_
167 197
198 if not from_encoding:
199 import rhodecode
200 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
201 from_encoding = DEFAULT_ENCODING
202
168 203 try:
169 204 return unicode(str_)
170 205 except UnicodeDecodeError:
@@ -184,10 +219,11 b" def safe_unicode(str_, from_encoding='ut"
184 219 except (ImportError, UnicodeDecodeError, Exception):
185 220 return unicode(str_, from_encoding, 'replace')
186 221
187 def safe_str(unicode_, to_encoding='utf8'):
222
223 def safe_str(unicode_, to_encoding=None):
188 224 """
189 225 safe str function. Does few trick to turn unicode_ into string
190
226
191 227 In case of UnicodeEncodeError we try to return it with encoding detected
192 228 by chardet library if it fails fallback to string with errors replaced
193 229
@@ -199,9 +235,17 b" def safe_str(unicode_, to_encoding='utf8"
199 235 if not isinstance(unicode_, basestring):
200 236 return str(unicode_)
201 237
238 if not isinstance(unicode_, basestring):
239 return str(unicode_)
240
202 241 if isinstance(unicode_, str):
203 242 return unicode_
204 243
244 if not to_encoding:
245 import rhodecode
246 DEFAULT_ENCODING = rhodecode.CONFIG.get('default_encoding','utf8')
247 to_encoding = DEFAULT_ENCODING
248
205 249 try:
206 250 return unicode_.encode(to_encoding)
207 251 except UnicodeEncodeError:
@@ -221,11 +265,10 b" def safe_str(unicode_, to_encoding='utf8"
221 265 return safe_str
222 266
223 267
224
225 268 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
226 269 """
227 270 Custom engine_from_config functions that makes sure we use NullPool for
228 file based sqlite databases. This prevents errors on sqlite. This only
271 file based sqlite databases. This prevents errors on sqlite. This only
229 272 applies to sqlalchemy versions < 0.7.0
230 273
231 274 """
@@ -284,7 +327,7 b' def engine_from_config(configuration, pr'
284 327 def age(curdate):
285 328 """
286 329 turns a datetime into an age string.
287
330
288 331 :param curdate: datetime object
289 332 :rtype: unicode
290 333 :returns: unicode words describing age
@@ -293,7 +336,7 b' def age(curdate):'
293 336 from datetime import datetime
294 337 from webhelpers.date import time_ago_in_words
295 338
296 _ = lambda s:s
339 _ = lambda s: s
297 340
298 341 if not curdate:
299 342 return ''
@@ -310,7 +353,8 b' def age(curdate):'
310 353 pos = 1
311 354 for scale in agescales:
312 355 if scale[1] <= age_seconds:
313 if pos == 6:pos = 5
356 if pos == 6:
357 pos = 5
314 358 return '%s %s' % (time_ago_in_words(curdate,
315 359 agescales[pos][0]), _('ago'))
316 360 pos += 1
@@ -321,10 +365,10 b' def age(curdate):'
321 365 def uri_filter(uri):
322 366 """
323 367 Removes user:password from given url string
324
368
325 369 :param uri:
326 370 :rtype: unicode
327 :returns: filtered list of strings
371 :returns: filtered list of strings
328 372 """
329 373 if not uri:
330 374 return ''
@@ -353,7 +397,7 b' def uri_filter(uri):'
353 397 def credentials_filter(uri):
354 398 """
355 399 Returns a url with removed credentials
356
400
357 401 :param uri:
358 402 """
359 403
@@ -364,16 +408,17 b' def credentials_filter(uri):'
364 408
365 409 return ''.join(uri)
366 410
411
367 412 def get_changeset_safe(repo, rev):
368 413 """
369 Safe version of get_changeset if this changeset doesn't exists for a
414 Safe version of get_changeset if this changeset doesn't exists for a
370 415 repo it returns a Dummy one instead
371
416
372 417 :param repo:
373 418 :param rev:
374 419 """
375 from vcs.backends.base import BaseRepository
376 from vcs.exceptions import RepositoryError
420 from rhodecode.lib.vcs.backends.base import BaseRepository
421 from rhodecode.lib.vcs.exceptions import RepositoryError
377 422 if not isinstance(repo, BaseRepository):
378 423 raise Exception('You must pass an Repository '
379 424 'object as first argument got %s', type(repo))
@@ -390,13 +435,13 b' def get_current_revision(quiet=False):'
390 435 """
391 436 Returns tuple of (number, id) from repository containing this package
392 437 or None if repository could not be found.
393
438
394 439 :param quiet: prints error for fetching revision if True
395 440 """
396 441
397 442 try:
398 from vcs import get_repo
399 from vcs.utils.helpers import get_scm
443 from rhodecode.lib.vcs import get_repo
444 from rhodecode.lib.vcs.utils.helpers import get_scm
400 445 repopath = os.path.join(os.path.dirname(__file__), '..', '..')
401 446 scm = get_scm(repopath)[0]
402 447 repo = get_repo(path=repopath, alias=scm)
@@ -408,3 +453,15 b' def get_current_revision(quiet=False):'
408 453 "was: %s" % err)
409 454 return None
410 455
456
457 def extract_mentioned_users(s):
458 """
459 Returns unique usernames from given string s that have @mention
460
461 :param s: string to get mentions
462 """
463 usrs = {}
464 for username in re.findall(r'(?:^@|\s@)(\w+)', s):
465 usrs[username] = username
466
467 return sorted(usrs.keys())
@@ -6,7 +6,8 b''
6 6 authentication and permission libraries
7 7
8 8 :created_on: Apr 4, 2010
9 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 11 :license: GPLv3, see COPYING for more details.
11 12 """
12 13 # This program is free software: you can redistribute it and/or modify
@@ -30,11 +31,12 b' import hashlib'
30 31 from tempfile import _RandomNameSequence
31 32 from decorator import decorator
32 33
33 from pylons import config, session, url, request
34 from pylons import config, url, request
34 35 from pylons.controllers.util import abort, redirect
35 36 from pylons.i18n.translation import _
36 37
37 38 from rhodecode import __platform__, PLATFORM_WIN, PLATFORM_OTHERS
39 from rhodecode.model.meta import Session
38 40
39 41 if __platform__ in PLATFORM_WIN:
40 42 from hashlib import sha256
@@ -43,20 +45,22 b' if __platform__ in PLATFORM_OTHERS:'
43 45
44 46 from rhodecode.lib import str2bool, safe_unicode
45 47 from rhodecode.lib.exceptions import LdapPasswordError, LdapUsernameError
46 from rhodecode.lib.utils import get_repo_slug
48 from rhodecode.lib.utils import get_repo_slug, get_repos_group_slug
47 49 from rhodecode.lib.auth_ldap import AuthLdap
48 50
49 51 from rhodecode.model import meta
50 52 from rhodecode.model.user import UserModel
51 from rhodecode.model.db import Permission, RhodeCodeSettings, User
53 from rhodecode.model.db import Permission, RhodeCodeSetting, User
52 54
53 55 log = logging.getLogger(__name__)
54 56
55 57
56 58 class PasswordGenerator(object):
57 """This is a simple class for generating password from
58 different sets of characters
59 usage:
59 """
60 This is a simple class for generating password from different sets of
61 characters
62 usage::
63
60 64 passwd_gen = PasswordGenerator()
61 65 #print 8-letter password containing only big and small letters
62 66 of alphabet
@@ -128,15 +132,24 b' def check_password(password, hashed):'
128 132 return RhodeCodeCrypto.hash_check(password, hashed)
129 133
130 134
131 def generate_api_key(username, salt=None):
135 def generate_api_key(str_, salt=None):
136 """
137 Generates API KEY from given string
138
139 :param str_:
140 :param salt:
141 """
142
132 143 if salt is None:
133 144 salt = _RandomNameSequence().next()
134 145
135 return hashlib.sha1(username + salt).hexdigest()
146 return hashlib.sha1(str_ + salt).hexdigest()
136 147
137 148
138 149 def authfunc(environ, username, password):
139 """Dummy authentication function used in Mercurial/Git/ and access control,
150 """
151 Dummy authentication wrapper function used in Mercurial and Git for
152 access control.
140 153
141 154 :param environ: needed only for using in Basic auth
142 155 """
@@ -144,7 +157,8 b' def authfunc(environ, username, password'
144 157
145 158
146 159 def authenticate(username, password):
147 """Authentication function used for access control,
160 """
161 Authentication function used for access control,
148 162 firstly checks for db authentication then if ldap is enabled for ldap
149 163 authentication, also creates ldap user if not in database
150 164
@@ -159,16 +173,16 b' def authenticate(username, password):'
159 173 if user is not None and not user.ldap_dn:
160 174 if user.active:
161 175 if user.username == 'default' and user.active:
162 log.info('user %s authenticated correctly as anonymous user',
176 log.info('user %s authenticated correctly as anonymous user' %
163 177 username)
164 178 return True
165 179
166 180 elif user.username == username and check_password(password,
167 181 user.password):
168 log.info('user %s authenticated correctly', username)
182 log.info('user %s authenticated correctly' % username)
169 183 return True
170 184 else:
171 log.warning('user %s is disabled', username)
185 log.warning('user %s tried auth but is disabled' % username)
172 186
173 187 else:
174 188 log.debug('Regular authentication failed')
@@ -178,7 +192,7 b' def authenticate(username, password):'
178 192 log.debug('this user already exists as non ldap')
179 193 return False
180 194
181 ldap_settings = RhodeCodeSettings.get_ldap_settings()
195 ldap_settings = RhodeCodeSetting.get_ldap_settings()
182 196 #======================================================================
183 197 # FALLBACK TO LDAP AUTH IF ENABLE
184 198 #======================================================================
@@ -202,7 +216,7 b' def authenticate(username, password):'
202 216 aldap = AuthLdap(**kwargs)
203 217 (user_dn, ldap_attrs) = aldap.authenticate_ldap(username,
204 218 password)
205 log.debug('Got ldap DN response %s', user_dn)
219 log.debug('Got ldap DN response %s' % user_dn)
206 220
207 221 get_ldap_attr = lambda k: ldap_attrs.get(ldap_settings\
208 222 .get(k), [''])[0]
@@ -222,6 +236,7 b' def authenticate(username, password):'
222 236 user_attrs):
223 237 log.info('created new ldap user %s' % username)
224 238
239 Session.commit()
225 240 return True
226 241 except (LdapUsernameError, LdapPasswordError,):
227 242 pass
@@ -231,6 +246,64 b' def authenticate(username, password):'
231 246 return False
232 247
233 248
249 def login_container_auth(username):
250 user = User.get_by_username(username)
251 if user is None:
252 user_attrs = {
253 'name': username,
254 'lastname': None,
255 'email': None,
256 }
257 user = UserModel().create_for_container_auth(username, user_attrs)
258 if not user:
259 return None
260 log.info('User %s was created by container authentication' % username)
261
262 if not user.active:
263 return None
264
265 user.update_lastlogin()
266 Session.commit()
267
268 log.debug('User %s is now logged in by container authentication',
269 user.username)
270 return user
271
272
273 def get_container_username(environ, config):
274 username = None
275
276 if str2bool(config.get('container_auth_enabled', False)):
277 from paste.httpheaders import REMOTE_USER
278 username = REMOTE_USER(environ)
279
280 if not username and str2bool(config.get('proxypass_auth_enabled', False)):
281 username = environ.get('HTTP_X_FORWARDED_USER')
282
283 if username:
284 # Removing realm and domain from username
285 username = username.partition('@')[0]
286 username = username.rpartition('\\')[2]
287 log.debug('Received username %s from container' % username)
288
289 return username
290
291
292 class CookieStoreWrapper(object):
293
294 def __init__(self, cookie_store):
295 self.cookie_store = cookie_store
296
297 def __repr__(self):
298 return 'CookieStore<%s>' % (self.cookie_store)
299
300 def get(self, key, other=None):
301 if isinstance(self.cookie_store, dict):
302 return self.cookie_store.get(key, other)
303 elif isinstance(self.cookie_store, AuthUser):
304 return self.cookie_store.__dict__.get(key, other)
305
306
234 307 class AuthUser(object):
235 308 """
236 309 A simple object that handles all attributes of user in RhodeCode
@@ -241,12 +314,12 b' class AuthUser(object):'
241 314 in
242 315 """
243 316
244 def __init__(self, user_id=None, api_key=None):
317 def __init__(self, user_id=None, api_key=None, username=None):
245 318
246 319 self.user_id = user_id
247 320 self.api_key = None
321 self.username = username
248 322
249 self.username = 'None'
250 323 self.name = ''
251 324 self.lastname = ''
252 325 self.email = ''
@@ -255,51 +328,85 b' class AuthUser(object):'
255 328 self.permissions = {}
256 329 self._api_key = api_key
257 330 self.propagate_data()
331 self._instance = None
258 332
259 333 def propagate_data(self):
260 334 user_model = UserModel()
261 self.anonymous_user = User.get_by_username('default')
335 self.anonymous_user = User.get_by_username('default', cache=True)
336 is_user_loaded = False
337
338 # try go get user by api key
262 339 if self._api_key and self._api_key != self.anonymous_user.api_key:
263 #try go get user by api key
264 log.debug('Auth User lookup by API KEY %s', self._api_key)
265 user_model.fill_data(self, api_key=self._api_key)
266 else:
267 log.debug('Auth User lookup by USER ID %s', self.user_id)
268 if self.user_id is not None \
269 and self.user_id != self.anonymous_user.user_id:
270 user_model.fill_data(self, user_id=self.user_id)
340 log.debug('Auth User lookup by API KEY %s' % self._api_key)
341 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
342 # lookup by userid
343 elif (self.user_id is not None and
344 self.user_id != self.anonymous_user.user_id):
345 log.debug('Auth User lookup by USER ID %s' % self.user_id)
346 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
347 # lookup by username
348 elif self.username and \
349 str2bool(config.get('container_auth_enabled', False)):
350
351 log.debug('Auth User lookup by USER NAME %s' % self.username)
352 dbuser = login_container_auth(self.username)
353 if dbuser is not None:
354 for k, v in dbuser.get_dict().items():
355 setattr(self, k, v)
356 self.set_authenticated()
357 is_user_loaded = True
358
359 if not is_user_loaded:
360 # if we cannot authenticate user try anonymous
361 if self.anonymous_user.active is True:
362 user_model.fill_data(self, user_id=self.anonymous_user.user_id)
363 # then we set this user is logged in
364 self.is_authenticated = True
271 365 else:
272 if self.anonymous_user.active is True:
273 user_model.fill_data(self,
274 user_id=self.anonymous_user.user_id)
275 #then we set this user is logged in
276 self.is_authenticated = True
277 else:
278 self.is_authenticated = False
366 self.user_id = None
367 self.username = None
368 self.is_authenticated = False
279 369
280 log.debug('Auth User is now %s', self)
370 if not self.username:
371 self.username = 'None'
372
373 log.debug('Auth User is now %s' % self)
281 374 user_model.fill_perms(self)
282 375
283 376 @property
284 377 def is_admin(self):
285 378 return self.admin
286 379
287 @property
288 def full_contact(self):
289 return '%s %s <%s>' % (self.name, self.lastname, self.email)
290
291 380 def __repr__(self):
292 381 return "<AuthUser('id:%s:%s|%s')>" % (self.user_id, self.username,
293 382 self.is_authenticated)
294 383
295 384 def set_authenticated(self, authenticated=True):
296
297 385 if self.user_id != self.anonymous_user.user_id:
298 386 self.is_authenticated = authenticated
299 387
388 def get_cookie_store(self):
389 return {'username': self.username,
390 'user_id': self.user_id,
391 'is_authenticated': self.is_authenticated}
392
393 @classmethod
394 def from_cookie_store(cls, cookie_store):
395 """
396 Creates AuthUser from a cookie store
397
398 :param cls:
399 :param cookie_store:
400 """
401 user_id = cookie_store.get('user_id')
402 username = cookie_store.get('username')
403 api_key = cookie_store.get('api_key')
404 return AuthUser(user_id, api_key, username)
405
300 406
301 407 def set_available_permissions(config):
302 """This function will propagate pylons globals with all available defined
408 """
409 This function will propagate pylons globals with all available defined
303 410 permission given in db. We don't want to check each time from db for new
304 411 permissions since adding a new permission also requires application restart
305 412 ie. to decorate new views with the newly created permission
@@ -309,9 +416,9 b' def set_available_permissions(config):'
309 416 """
310 417 log.info('getting information about all available permissions')
311 418 try:
312 sa = meta.Session()
419 sa = meta.Session
313 420 all_perms = sa.query(Permission).all()
314 except:
421 except Exception:
315 422 pass
316 423 finally:
317 424 meta.Session.remove()
@@ -343,26 +450,31 b' class LoginRequired(object):'
343 450
344 451 api_access_ok = False
345 452 if self.api_access:
346 log.debug('Checking API KEY access for %s', cls)
453 log.debug('Checking API KEY access for %s' % cls)
347 454 if user.api_key == request.GET.get('api_key'):
348 455 api_access_ok = True
349 456 else:
350 457 log.debug("API KEY token not valid")
351
352 log.debug('Checking if %s is authenticated @ %s', user.username, cls)
458 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
459 log.debug('Checking if %s is authenticated @ %s' % (user.username, loc))
353 460 if user.is_authenticated or api_access_ok:
354 log.debug('user %s is authenticated', user.username)
461 log.info('user %s is authenticated and granted access to %s' % (
462 user.username, loc)
463 )
355 464 return func(*fargs, **fkwargs)
356 465 else:
357 log.warn('user %s NOT authenticated', user.username)
466 log.warn('user %s NOT authenticated on func: %s' % (
467 user, loc)
468 )
358 469 p = url.current()
359 470
360 log.debug('redirecting to login page with %s', p)
471 log.debug('redirecting to login page with %s' % p)
361 472 return redirect(url('login_home', came_from=p))
362 473
363 474
364 475 class NotAnonymous(object):
365 """Must be logged in to execute this function else
476 """
477 Must be logged in to execute this function else
366 478 redirect to login page"""
367 479
368 480 def __call__(self, func):
@@ -372,7 +484,7 b' class NotAnonymous(object):'
372 484 cls = fargs[0]
373 485 self.user = cls.rhodecode_user
374 486
375 log.debug('Checking if user is not anonymous @%s', cls)
487 log.debug('Checking if user is not anonymous @%s' % cls)
376 488
377 489 anonymous = self.user.username == 'default'
378 490
@@ -411,13 +523,11 b' class PermsDecorator(object):'
411 523 self.user)
412 524
413 525 if self.check_permissions():
414 log.debug('Permission granted for %s %s', cls, self.user)
526 log.debug('Permission granted for %s %s' % (cls, self.user))
415 527 return func(*fargs, **fkwargs)
416 528
417 529 else:
418 log.warning('Permission denied for %s %s', cls, self.user)
419
420
530 log.debug('Permission denied for %s %s' % (cls, self.user))
421 531 anonymous = self.user.username == 'default'
422 532
423 533 if anonymous:
@@ -430,7 +540,7 b' class PermsDecorator(object):'
430 540 return redirect(url('login_home', came_from=p))
431 541
432 542 else:
433 #redirect with forbidden ret code
543 # redirect with forbidden ret code
434 544 return abort(403)
435 545
436 546 def check_permissions(self):
@@ -439,7 +549,8 b' class PermsDecorator(object):'
439 549
440 550
441 551 class HasPermissionAllDecorator(PermsDecorator):
442 """Checks for access permission for all given predicates. All of them
552 """
553 Checks for access permission for all given predicates. All of them
443 554 have to be meet in order to fulfill the request
444 555 """
445 556
@@ -450,7 +561,8 b' class HasPermissionAllDecorator(PermsDec'
450 561
451 562
452 563 class HasPermissionAnyDecorator(PermsDecorator):
453 """Checks for access permission for any of given predicates. In order to
564 """
565 Checks for access permission for any of given predicates. In order to
454 566 fulfill the request any of predicates must be meet
455 567 """
456 568
@@ -461,7 +573,8 b' class HasPermissionAnyDecorator(PermsDec'
461 573
462 574
463 575 class HasRepoPermissionAllDecorator(PermsDecorator):
464 """Checks for access permission for all given predicates for specific
576 """
577 Checks for access permission for all given predicates for specific
465 578 repository. All of them have to be meet in order to fulfill the request
466 579 """
467 580
@@ -477,7 +590,8 b' class HasRepoPermissionAllDecorator(Perm'
477 590
478 591
479 592 class HasRepoPermissionAnyDecorator(PermsDecorator):
480 """Checks for access permission for any of given predicates for specific
593 """
594 Checks for access permission for any of given predicates for specific
481 595 repository. In order to fulfill the request any of predicates must be meet
482 596 """
483 597
@@ -493,6 +607,41 b' class HasRepoPermissionAnyDecorator(Perm'
493 607 return False
494 608
495 609
610 class HasReposGroupPermissionAllDecorator(PermsDecorator):
611 """
612 Checks for access permission for all given predicates for specific
613 repository. All of them have to be meet in order to fulfill the request
614 """
615
616 def check_permissions(self):
617 group_name = get_repos_group_slug(request)
618 try:
619 user_perms = set([self.user_perms['repositories_groups'][group_name]])
620 except KeyError:
621 return False
622 if self.required_perms.issubset(user_perms):
623 return True
624 return False
625
626
627 class HasReposGroupPermissionAnyDecorator(PermsDecorator):
628 """
629 Checks for access permission for any of given predicates for specific
630 repository. In order to fulfill the request any of predicates must be meet
631 """
632
633 def check_permissions(self):
634 group_name = get_repos_group_slug(request)
635
636 try:
637 user_perms = set([self.user_perms['repositories_groups'][group_name]])
638 except KeyError:
639 return False
640 if self.required_perms.intersection(user_perms):
641 return True
642 return False
643
644
496 645 #==============================================================================
497 646 # CHECK FUNCTIONS
498 647 #==============================================================================
@@ -511,7 +660,7 b' class PermsFunction(object):'
511 660 self.repo_name = None
512 661
513 662 def __call__(self, check_Location=''):
514 user = session.get('rhodecode_user', False)
663 user = request.user
515 664 if not user:
516 665 return False
517 666 self.user_perms = user.permissions
@@ -525,7 +674,7 b' class PermsFunction(object):'
525 674 return True
526 675
527 676 else:
528 log.warning('Permission denied for %s @ %s', self.granted_for,
677 log.debug('Permission denied for %s @ %s', self.granted_for,
529 678 check_Location or 'unspecified location')
530 679 return False
531 680
@@ -559,8 +708,9 b' class HasRepoPermissionAll(PermsFunction'
559 708 self.repo_name = get_repo_slug(request)
560 709
561 710 try:
562 self.user_perms = set([self.user_perms['reposit'
563 'ories'][self.repo_name]])
711 self.user_perms = set(
712 [self.user_perms['repositories'][self.repo_name]]
713 )
564 714 except KeyError:
565 715 return False
566 716 self.granted_for = self.repo_name
@@ -580,8 +730,9 b' class HasRepoPermissionAny(PermsFunction'
580 730 self.repo_name = get_repo_slug(request)
581 731
582 732 try:
583 self.user_perms = set([self.user_perms['reposi'
584 'tories'][self.repo_name]])
733 self.user_perms = set(
734 [self.user_perms['repositories'][self.repo_name]]
735 )
585 736 except KeyError:
586 737 return False
587 738 self.granted_for = self.repo_name
@@ -590,6 +741,42 b' class HasRepoPermissionAny(PermsFunction'
590 741 return False
591 742
592 743
744 class HasReposGroupPermissionAny(PermsFunction):
745 def __call__(self, group_name=None, check_Location=''):
746 self.group_name = group_name
747 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
748
749 def check_permissions(self):
750 try:
751 self.user_perms = set(
752 [self.user_perms['repositories_groups'][self.group_name]]
753 )
754 except KeyError:
755 return False
756 self.granted_for = self.repo_name
757 if self.required_perms.intersection(self.user_perms):
758 return True
759 return False
760
761
762 class HasReposGroupPermissionAll(PermsFunction):
763 def __call__(self, group_name=None, check_Location=''):
764 self.group_name = group_name
765 return super(HasReposGroupPermissionAny, self).__call__(check_Location)
766
767 def check_permissions(self):
768 try:
769 self.user_perms = set(
770 [self.user_perms['repositories_groups'][self.group_name]]
771 )
772 except KeyError:
773 return False
774 self.granted_for = self.repo_name
775 if self.required_perms.issubset(self.user_perms):
776 return True
777 return False
778
779
593 780 #==============================================================================
594 781 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
595 782 #==============================================================================
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Created on Nov 17, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -43,8 +43,7 b' class AuthLdap(object):'
43 43 def __init__(self, server, base_dn, port=389, bind_dn='', bind_pass='',
44 44 tls_kind='PLAIN', tls_reqcert='DEMAND', ldap_version=3,
45 45 ldap_filter='(&(objectClass=user)(!(objectClass=computer)))',
46 search_scope='SUBTREE',
47 attr_login='uid'):
46 search_scope='SUBTREE', attr_login='uid'):
48 47 self.ldap_version = ldap_version
49 48 ldap_server_type = 'ldap'
50 49
@@ -53,14 +52,14 b' class AuthLdap(object):'
53 52 if self.TLS_KIND == 'LDAPS':
54 53 port = port or 689
55 54 ldap_server_type = ldap_server_type + 's'
56
55
57 56 OPT_X_TLS_DEMAND = 2
58 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
57 self.TLS_REQCERT = getattr(ldap, 'OPT_X_TLS_%s' % tls_reqcert,
59 58 OPT_X_TLS_DEMAND)
60 59 self.LDAP_SERVER_ADDRESS = server
61 60 self.LDAP_SERVER_PORT = port
62 61
63 #USE FOR READ ONLY BIND TO LDAP SERVER
62 # USE FOR READ ONLY BIND TO LDAP SERVER
64 63 self.LDAP_BIND_DN = bind_dn
65 64 self.LDAP_BIND_PASS = bind_pass
66 65
@@ -74,7 +73,8 b' class AuthLdap(object):'
74 73 self.attr_login = attr_login
75 74
76 75 def authenticate_ldap(self, username, password):
77 """Authenticate a user via LDAP and return his/her LDAP properties.
76 """
77 Authenticate a user via LDAP and return his/her LDAP properties.
78 78
79 79 Raises AuthenticationError if the credentials are rejected, or
80 80 EnvironmentError if the LDAP server can't be reached.
@@ -87,11 +87,15 b' class AuthLdap(object):'
87 87
88 88 uid = chop_at(username, "@%s" % self.LDAP_SERVER_ADDRESS)
89 89
90 if not password:
91 log.debug("Attempt to authenticate LDAP user "
92 "with blank password rejected.")
93 raise LdapPasswordError()
90 94 if "," in username:
91 95 raise LdapUsernameError("invalid character in username: ,")
92 96 try:
93 if hasattr(ldap,'OPT_X_TLS_CACERTDIR'):
94 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
97 if hasattr(ldap, 'OPT_X_TLS_CACERTDIR'):
98 ldap.set_option(ldap.OPT_X_TLS_CACERTDIR,
95 99 '/etc/openldap/cacerts')
96 100 ldap.set_option(ldap.OPT_REFERRALS, ldap.OPT_OFF)
97 101 ldap.set_option(ldap.OPT_RESTART, ldap.OPT_ON)
@@ -112,12 +116,12 b' class AuthLdap(object):'
112 116 if self.LDAP_BIND_DN and self.LDAP_BIND_PASS:
113 117 server.simple_bind_s(self.LDAP_BIND_DN, self.LDAP_BIND_PASS)
114 118
115 filt = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login,
119 filter_ = '(&%s(%s=%s))' % (self.LDAP_FILTER, self.attr_login,
116 120 username)
117 log.debug("Authenticating %r filt %s at %s", self.BASE_DN,
118 filt, self.LDAP_SERVER)
121 log.debug("Authenticating %r filter %s at %s", self.BASE_DN,
122 filter_, self.LDAP_SERVER)
119 123 lobjects = server.search_ext_s(self.BASE_DN, self.SEARCH_SCOPE,
120 filt)
124 filter_)
121 125
122 126 if not lobjects:
123 127 raise ldap.NO_SUCH_OBJECT()
@@ -127,24 +131,28 b' class AuthLdap(object):'
127 131 continue
128 132
129 133 try:
134 log.debug('Trying simple bind with %s' % dn)
130 135 server.simple_bind_s(dn, password)
131 136 attrs = server.search_ext_s(dn, ldap.SCOPE_BASE,
132 137 '(objectClass=*)')[0][1]
133 138 break
134 139
135 except ldap.INVALID_CREDENTIALS, e:
136 log.debug("LDAP rejected password for user '%s' (%s): %s",
137 uid, username, dn)
140 except ldap.INVALID_CREDENTIALS:
141 log.debug(
142 "LDAP rejected password for user '%s' (%s): %s" % (
143 uid, username, dn
144 )
145 )
138 146
139 147 else:
140 148 log.debug("No matching LDAP objects for authentication "
141 149 "of '%s' (%s)", uid, username)
142 150 raise LdapPasswordError()
143 151
144 except ldap.NO_SUCH_OBJECT, e:
145 log.debug("LDAP says no such user '%s' (%s)", uid, username)
152 except ldap.NO_SUCH_OBJECT:
153 log.debug("LDAP says no such user '%s' (%s)" % (uid, username))
146 154 raise LdapUsernameError()
147 except ldap.SERVER_DOWN, e:
155 except ldap.SERVER_DOWN:
148 156 raise LdapConnectionError("LDAP can't access "
149 157 "authentication server")
150 158
@@ -3,11 +3,12 b''
3 3 rhodecode.lib.backup_manager
4 4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 5
6 Mercurial repositories backup manager, it allows to backups all
6 Mercurial repositories backup manager, it allows to backups all
7 7 repositories and send it to backup server using RSA key via ssh.
8 8
9 9 :created_on: Feb 28, 2010
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :author: marcink
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 12 :license: GPLv3, see COPYING for more details.
12 13 """
13 14 # This program is free software: you can redistribute it and/or modify
@@ -3,35 +3,125 b''
3 3 Provides the BaseController class for subclassing.
4 4 """
5 5 import logging
6 import time
7 import traceback
8
9 from paste.auth.basic import AuthBasicAuthenticator
6 10
7 11 from pylons import config, tmpl_context as c, request, session, url
8 12 from pylons.controllers import WSGIController
9 13 from pylons.controllers.util import redirect
10 14 from pylons.templating import render_mako as render
11 15
12 from rhodecode import __version__
13 from rhodecode.lib import str2bool
14 from rhodecode.lib.auth import AuthUser
15 from rhodecode.lib.utils import get_repo_slug
16 from rhodecode import __version__, BACKENDS
17
18 from rhodecode.lib import str2bool, safe_unicode
19 from rhodecode.lib.auth import AuthUser, get_container_username, authfunc,\
20 HasPermissionAnyMiddleware, CookieStoreWrapper
21 from rhodecode.lib.utils import get_repo_slug, invalidate_cache
16 22 from rhodecode.model import meta
23
24 from rhodecode.model.db import Repository
25 from rhodecode.model.notification import NotificationModel
17 26 from rhodecode.model.scm import ScmModel
18 from rhodecode import BACKENDS
19 from rhodecode.model.db import Repository
20 27
21 28 log = logging.getLogger(__name__)
22 29
30
31 class BaseVCSController(object):
32
33 def __init__(self, application, config):
34 self.application = application
35 self.config = config
36 # base path of repo locations
37 self.basepath = self.config['base_path']
38 #authenticate this mercurial request using authfunc
39 self.authenticate = AuthBasicAuthenticator('', authfunc)
40 self.ipaddr = '0.0.0.0'
41
42 def _handle_request(self, environ, start_response):
43 raise NotImplementedError()
44
45 def _get_by_id(self, repo_name):
46 """
47 Get's a special pattern _<ID> from clone url and tries to replace it
48 with a repository_name for support of _<ID> non changable urls
49
50 :param repo_name:
51 """
52 try:
53 data = repo_name.split('/')
54 if len(data) >= 2:
55 by_id = data[1].split('_')
56 if len(by_id) == 2 and by_id[1].isdigit():
57 _repo_name = Repository.get(by_id[1]).repo_name
58 data[1] = _repo_name
59 except:
60 log.debug('Failed to extract repo_name from id %s' % (
61 traceback.format_exc()
62 )
63 )
64
65 return '/'.join(data)
66
67 def _invalidate_cache(self, repo_name):
68 """
69 Set's cache for this repository for invalidation on next access
70
71 :param repo_name: full repo name, also a cache key
72 """
73 invalidate_cache('get_repo_cached_%s' % repo_name)
74
75 def _check_permission(self, action, user, repo_name):
76 """
77 Checks permissions using action (push/pull) user and repository
78 name
79
80 :param action: push or pull action
81 :param user: user instance
82 :param repo_name: repository name
83 """
84 if action == 'push':
85 if not HasPermissionAnyMiddleware('repository.write',
86 'repository.admin')(user,
87 repo_name):
88 return False
89
90 else:
91 #any other action need at least read permission
92 if not HasPermissionAnyMiddleware('repository.read',
93 'repository.write',
94 'repository.admin')(user,
95 repo_name):
96 return False
97
98 return True
99
100 def __call__(self, environ, start_response):
101 start = time.time()
102 try:
103 return self._handle_request(environ, start_response)
104 finally:
105 log = logging.getLogger('rhodecode.' + self.__class__.__name__)
106 log.debug('Request time: %.3fs' % (time.time() - start))
107 meta.Session.remove()
108
109
23 110 class BaseController(WSGIController):
24 111
25 112 def __before__(self):
26 113 c.rhodecode_version = __version__
114 c.rhodecode_instanceid = config.get('instance_id')
27 115 c.rhodecode_name = config.get('rhodecode_title')
28 116 c.use_gravatar = str2bool(config.get('use_gravatar'))
29 117 c.ga_code = config.get('rhodecode_ga_code')
30 118 c.repo_name = get_repo_slug(request)
31 119 c.backends = BACKENDS.keys()
120 c.unread_notifications = NotificationModel()\
121 .get_unread_cnt_for_user(c.rhodecode_user.user_id)
32 122 self.cut_off_limit = int(config.get('cut_off_limit'))
33 123
34 self.sa = meta.Session()
124 self.sa = meta.Session
35 125 self.scm_model = ScmModel(self.sa)
36 126
37 127 def __call__(self, environ, start_response):
@@ -39,18 +129,30 b' class BaseController(WSGIController):'
39 129 # WSGIController.__call__ dispatches to the Controller method
40 130 # the request is routed to. This routing information is
41 131 # available in environ['pylons.routes_dict']
132 start = time.time()
42 133 try:
43 # putting this here makes sure that we update permissions each time
134 # make sure that we update permissions each time we call controller
44 135 api_key = request.GET.get('api_key')
45 user_id = getattr(session.get('rhodecode_user'), 'user_id', None)
46 self.rhodecode_user = c.rhodecode_user = AuthUser(user_id, api_key)
47 self.rhodecode_user.set_authenticated(
48 getattr(session.get('rhodecode_user'),
49 'is_authenticated', False))
50 session['rhodecode_user'] = self.rhodecode_user
51 session.save()
136 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
137 user_id = cookie_store.get('user_id', None)
138 username = get_container_username(environ, config)
139
140 auth_user = AuthUser(user_id, api_key, username)
141 request.user = auth_user
142 self.rhodecode_user = c.rhodecode_user = auth_user
143 if not self.rhodecode_user.is_authenticated and \
144 self.rhodecode_user.user_id is not None:
145 self.rhodecode_user.set_authenticated(
146 cookie_store.get('is_authenticated')
147 )
148 log.info('User: %s accessed %s' % (
149 auth_user, safe_unicode(environ.get('PATH_INFO')))
150 )
52 151 return WSGIController.__call__(self, environ, start_response)
53 152 finally:
153 log.info('Request to %s time: %.3fs' % (
154 safe_unicode(environ.get('PATH_INFO')), time.time() - start)
155 )
54 156 meta.Session.remove()
55 157
56 158
@@ -80,4 +182,3 b' class BaseRepoController(BaseController)'
80 182
81 183 c.repository_followers = self.scm_model.get_followers(c.repo_name)
82 184 c.repository_forks = self.scm_model.get_forks(c.repo_name)
83
@@ -137,8 +137,13 b' def _get_cache_parameters(query):'
137 137
138 138 if cache_key is None:
139 139 # cache key - the value arguments from this query's parameters.
140 args = _params_from_query(query)
141 cache_key = " ".join([str(x) for x in args])
140 args = [str(x) for x in _params_from_query(query)]
141 args.extend(filter(lambda k:k not in ['None', None, u'None'],
142 [str(query._limit), str(query._offset)]))
143 cache_key = " ".join(args)
144
145 if cache_key is None:
146 raise Exception('Cache key cannot be None')
142 147
143 148 # get cache
144 149 #cache = query.cache_manager.get_cache_region(namespace, region)
@@ -275,15 +280,20 b' def _params_from_query(query):'
275 280 """
276 281 v = []
277 282 def visit_bindparam(bind):
278 value = query._params.get(bind.key, bind.value)
279 283
280 # lazyloader may dig a callable in here, intended
281 # to late-evaluate params after autoflush is called.
282 # convert to a scalar value.
283 if callable(value):
284 value = value()
284 if bind.key in query._params:
285 value = query._params[bind.key]
286 elif bind.callable:
287 # lazyloader may dig a callable in here, intended
288 # to late-evaluate params after autoflush is called.
289 # convert to a scalar value.
290 value = bind.callable()
291 else:
292 value = bind.value
285 293
286 294 v.append(value)
287 295 if query._criterion is not None:
288 296 visitors.traverse(query._criterion, {}, {'bindparam':visit_bindparam})
297 for f in query._from_obj:
298 visitors.traverse(f, {}, {'bindparam':visit_bindparam})
289 299 return v
@@ -34,8 +34,8 b' from pylons import config'
34 34 from hashlib import md5
35 35 from decorator import decorator
36 36
37 from vcs.utils.lazy import LazyProperty
38
37 from rhodecode.lib.vcs.utils.lazy import LazyProperty
38 from rhodecode import CELERY_ON
39 39 from rhodecode.lib import str2bool, safe_str
40 40 from rhodecode.lib.pidlock import DaemonLock, LockHeld
41 41 from rhodecode.model import init_model
@@ -48,11 +48,6 b' from celery.messaging import establish_c'
48 48
49 49 log = logging.getLogger(__name__)
50 50
51 try:
52 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
53 except KeyError:
54 CELERY_ON = False
55
56 51
57 52 class ResultWrapper(object):
58 53 def __init__(self, task):
@@ -67,7 +62,7 b' def run_task(task, *args, **kwargs):'
67 62 if CELERY_ON:
68 63 try:
69 64 t = task.apply_async(args=args, kwargs=kwargs)
70 log.info('running task %s:%s', t.task_id, task)
65 log.info('running task %s:%s' % (t.task_id, task))
71 66 return t
72 67
73 68 except socket.error, e:
@@ -80,7 +75,7 b' def run_task(task, *args, **kwargs):'
80 75 except Exception, e:
81 76 log.error(traceback.format_exc())
82 77
83 log.debug('executing task %s in sync mode', task)
78 log.debug('executing task %s in sync mode' % task)
84 79 return ResultWrapper(task(*args, **kwargs))
85 80
86 81
@@ -100,7 +95,7 b' def locked_task(func):'
100 95 lockkey = __get_lockkey(func, *fargs, **fkwargs)
101 96 lockkey_path = config['here']
102 97
103 log.info('running task with lockkey %s', lockkey)
98 log.info('running task with lockkey %s' % lockkey)
104 99 try:
105 100 l = DaemonLock(file_=jn(lockkey_path, lockkey))
106 101 ret = func(*fargs, **fkwargs)
@@ -28,7 +28,7 b' from celery.decorators import task'
28 28 import os
29 29 import traceback
30 30 import logging
31 from os.path import dirname as dn, join as jn
31 from os.path import join as jn
32 32
33 33 from time import mktime
34 34 from operator import itemgetter
@@ -37,69 +37,76 b' from string import lower'
37 37 from pylons import config, url
38 38 from pylons.i18n.translation import _
39 39
40 from rhodecode.lib.vcs import get_backend
41
42 from rhodecode import CELERY_ON
40 43 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, safe_str
41 from rhodecode.lib.celerylib import run_task, locked_task, str2bool, \
42 __get_lockkey, LockHeld, DaemonLock, get_session, dbsession
44 from rhodecode.lib.celerylib import run_task, locked_task, dbsession, \
45 str2bool, __get_lockkey, LockHeld, DaemonLock, get_session
43 46 from rhodecode.lib.helpers import person
44 from rhodecode.lib.smtp_mailer import SmtpMailer
45 from rhodecode.lib.utils import add_cache
47 from rhodecode.lib.rcmail.smtp_mailer import SmtpMailer
48 from rhodecode.lib.utils import add_cache, action_logger
46 49 from rhodecode.lib.compat import json, OrderedDict
47 50
48 from rhodecode.model.db import RhodeCodeUi, Statistics, Repository, User
51 from rhodecode.model.db import Statistics, Repository, User
49 52
50 from vcs.backends import get_repo
51 from vcs import get_backend
52 53
53 54 add_cache(config)
54 55
55 56 __all__ = ['whoosh_index', 'get_commits_stats',
56 57 'reset_user_password', 'send_email']
57 58
58 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
59
60 59
61 def get_repos_path():
62 sa = get_session()
63 q = sa.query(RhodeCodeUi).filter(RhodeCodeUi.ui_key == '/').one()
64 return q.ui_value
60 def get_logger(cls):
61 if CELERY_ON:
62 try:
63 log = cls.get_logger()
64 except:
65 log = logging.getLogger(__name__)
66 else:
67 log = logging.getLogger(__name__)
68
69 return log
65 70
66 71
67 72 @task(ignore_result=True)
68 73 @locked_task
69 74 @dbsession
70 75 def whoosh_index(repo_location, full_index):
71 #log = whoosh_index.get_logger()
72 76 from rhodecode.lib.indexers.daemon import WhooshIndexingDaemon
77 log = whoosh_index.get_logger(whoosh_index)
78 DBS = get_session()
79
73 80 index_location = config['index_dir']
74 81 WhooshIndexingDaemon(index_location=index_location,
75 repo_location=repo_location, sa=get_session())\
82 repo_location=repo_location, sa=DBS)\
76 83 .run(full_index=full_index)
77 84
78 85
79 86 @task(ignore_result=True)
80 87 @dbsession
81 88 def get_commits_stats(repo_name, ts_min_y, ts_max_y):
82 try:
83 log = get_commits_stats.get_logger()
84 except:
85 log = logging.getLogger(__name__)
86
89 log = get_logger(get_commits_stats)
90 DBS = get_session()
87 91 lockkey = __get_lockkey('get_commits_stats', repo_name, ts_min_y,
88 92 ts_max_y)
89 93 lockkey_path = config['here']
90 94
91 log.info('running task with lockkey %s', lockkey)
95 log.info('running task with lockkey %s' % lockkey)
96
92 97 try:
93 sa = get_session()
94 98 lock = l = DaemonLock(file_=jn(lockkey_path, lockkey))
95 99
96 # for js data compatibilty cleans the key for person from '
100 # for js data compatibility cleans the key for person from '
97 101 akc = lambda k: person(k).replace('"', "")
98 102
99 103 co_day_auth_aggr = {}
100 104 commits_by_day_aggregate = {}
101 repos_path = get_repos_path()
102 repo = get_repo(safe_str(os.path.join(repos_path, repo_name)))
105 repo = Repository.get_by_repo_name(repo_name)
106 if repo is None:
107 return True
108
109 repo = repo.scm_instance
103 110 repo_size = repo.count()
104 111 # return if repo have no revisions
105 112 if repo_size < 1:
@@ -112,9 +119,9 b' def get_commits_stats(repo_name, ts_min_'
112 119 last_cs = None
113 120 timegetter = itemgetter('time')
114 121
115 dbrepo = sa.query(Repository)\
122 dbrepo = DBS.query(Repository)\
116 123 .filter(Repository.repo_name == repo_name).scalar()
117 cur_stats = sa.query(Statistics)\
124 cur_stats = DBS.query(Statistics)\
118 125 .filter(Statistics.repository == dbrepo).scalar()
119 126
120 127 if cur_stats is not None:
@@ -132,7 +139,7 b' def get_commits_stats(repo_name, ts_min_'
132 139 cur_stats.commit_activity_combined))
133 140 co_day_auth_aggr = json.loads(cur_stats.commit_activity)
134 141
135 log.debug('starting parsing %s', parse_limit)
142 log.debug('starting parsing %s' % parse_limit)
136 143 lmktime = mktime
137 144
138 145 last_rev = last_rev + 1 if last_rev >= 0 else 0
@@ -207,9 +214,9 b' def get_commits_stats(repo_name, ts_min_'
207 214 stats.commit_activity = json.dumps(co_day_auth_aggr)
208 215 stats.commit_activity_combined = json.dumps(overview_data)
209 216
210 log.debug('last revison %s', last_rev)
217 log.debug('last revison %s' % last_rev)
211 218 leftovers = len(repo.revisions[last_rev:])
212 log.debug('revisions to parse %s', leftovers)
219 log.debug('revisions to parse %s' % leftovers)
213 220
214 221 if last_rev == 0 or leftovers < parse_limit:
215 222 log.debug('getting code trending stats')
@@ -218,18 +225,18 b' def get_commits_stats(repo_name, ts_min_'
218 225 try:
219 226 stats.repository = dbrepo
220 227 stats.stat_on_revision = last_cs.revision if last_cs else 0
221 sa.add(stats)
222 sa.commit()
228 DBS.add(stats)
229 DBS.commit()
223 230 except:
224 231 log.error(traceback.format_exc())
225 sa.rollback()
232 DBS.rollback()
226 233 lock.release()
227 234 return False
228 235
229 # final release
236 #final release
230 237 lock.release()
231 238
232 # execute another task if celery is enabled
239 #execute another task if celery is enabled
233 240 if len(repo.revisions) > 1 and CELERY_ON:
234 241 run_task(get_commits_stats, repo_name, ts_min_y, ts_max_y)
235 242 return True
@@ -240,38 +247,28 b' def get_commits_stats(repo_name, ts_min_'
240 247 @task(ignore_result=True)
241 248 @dbsession
242 249 def send_password_link(user_email):
243 try:
244 log = reset_user_password.get_logger()
245 except:
246 log = logging.getLogger(__name__)
250 from rhodecode.model.notification import EmailNotificationModel
247 251
248 from rhodecode.lib import auth
252 log = get_logger(send_password_link)
253 DBS = get_session()
249 254
250 255 try:
251 sa = get_session()
252 user = sa.query(User).filter(User.email == user_email).scalar()
253
256 user = User.get_by_email(user_email)
254 257 if user:
258 log.debug('password reset user found %s' % user)
255 259 link = url('reset_password_confirmation', key=user.api_key,
256 260 qualified=True)
257 tmpl = """
258 Hello %s
259
260 We received a request to create a new password for your account.
261
262 You can generate it by clicking following URL:
263
264 %s
265
266 If you didn't request new password please ignore this email.
267 """
261 reg_type = EmailNotificationModel.TYPE_PASSWORD_RESET
262 body = EmailNotificationModel().get_email_tmpl(reg_type,
263 **{'user':user.short_contact,
264 'reset_url':link})
265 log.debug('sending email')
268 266 run_task(send_email, user_email,
269 "RhodeCode password reset link",
270 tmpl % (user.short_contact, link))
271 log.info('send new password mail to %s', user_email)
272
267 _("password reset link"), body)
268 log.info('send new password mail to %s' % user_email)
269 else:
270 log.debug("password reset email %s not found" % user_email)
273 271 except:
274 log.error('Failed to update user password')
275 272 log.error(traceback.format_exc())
276 273 return False
277 274
@@ -280,36 +277,32 b" If you didn't request new password pleas"
280 277 @task(ignore_result=True)
281 278 @dbsession
282 279 def reset_user_password(user_email):
283 try:
284 log = reset_user_password.get_logger()
285 except:
286 log = logging.getLogger(__name__)
280 from rhodecode.lib import auth
287 281
288 from rhodecode.lib import auth
282 log = get_logger(reset_user_password)
283 DBS = get_session()
289 284
290 285 try:
291 286 try:
292 sa = get_session()
293 user = sa.query(User).filter(User.email == user_email).scalar()
287 user = User.get_by_email(user_email)
294 288 new_passwd = auth.PasswordGenerator().gen_password(8,
295 289 auth.PasswordGenerator.ALPHABETS_BIG_SMALL)
296 290 if user:
297 291 user.password = auth.get_crypt_password(new_passwd)
298 292 user.api_key = auth.generate_api_key(user.username)
299 sa.add(user)
300 sa.commit()
301 log.info('change password for %s', user_email)
293 DBS.add(user)
294 DBS.commit()
295 log.info('change password for %s' % user_email)
302 296 if new_passwd is None:
303 297 raise Exception('unable to generate new password')
304
305 298 except:
306 299 log.error(traceback.format_exc())
307 sa.rollback()
300 DBS.rollback()
308 301
309 302 run_task(send_email, user_email,
310 "Your new RhodeCode password",
303 'Your new password',
311 304 'Your new RhodeCode password:%s' % (new_passwd))
312 log.info('send new password mail to %s', user_email)
305 log.info('send new password mail to %s' % user_email)
313 306
314 307 except:
315 308 log.error('Failed to update user password')
@@ -320,7 +313,7 b' def reset_user_password(user_email):'
320 313
321 314 @task(ignore_result=True)
322 315 @dbsession
323 def send_email(recipients, subject, body):
316 def send_email(recipients, subject, body, html_body=''):
324 317 """
325 318 Sends an email with defined parameters from the .ini files.
326 319
@@ -328,23 +321,20 b' def send_email(recipients, subject, body'
328 321 address from field 'email_to' is used instead
329 322 :param subject: subject of the mail
330 323 :param body: body of the mail
324 :param html_body: html version of body
331 325 """
332 try:
333 log = send_email.get_logger()
334 except:
335 log = logging.getLogger(__name__)
336
337 sa = get_session()
326 log = get_logger(send_email)
327 DBS = get_session()
328
338 329 email_config = config
339
330 subject = "%s %s" % (email_config.get('email_prefix'), subject)
340 331 if not recipients:
341 332 # if recipients are not defined we send to email_config + all admins
342 admins = [
343 u.email for u in sa.query(User).filter(User.admin==True).all()
344 ]
333 admins = [u.email for u in User.query()
334 .filter(User.admin == True).all()]
345 335 recipients = [email_config.get('email_to')] + admins
346 336
347 mail_from = email_config.get('app_email_from')
337 mail_from = email_config.get('app_email_from', 'RhodeCode')
348 338 user = email_config.get('smtp_username')
349 339 passwd = email_config.get('smtp_password')
350 340 mail_server = email_config.get('smtp_server')
@@ -355,9 +345,9 b' def send_email(recipients, subject, body'
355 345 smtp_auth = email_config.get('smtp_auth')
356 346
357 347 try:
358 m = SmtpMailer(mail_from, user, passwd, mail_server,smtp_auth,
348 m = SmtpMailer(mail_from, user, passwd, mail_server, smtp_auth,
359 349 mail_port, ssl, tls, debug=debug)
360 m.send(recipients, subject, body)
350 m.send(recipients, subject, body, html_body)
361 351 except:
362 352 log.error('Mail sending failed')
363 353 log.error(traceback.format_exc())
@@ -368,29 +358,45 b' def send_email(recipients, subject, body'
368 358 @task(ignore_result=True)
369 359 @dbsession
370 360 def create_repo_fork(form_data, cur_user):
361 """
362 Creates a fork of repository using interval VCS methods
363
364 :param form_data:
365 :param cur_user:
366 """
371 367 from rhodecode.model.repo import RepoModel
372 368
373 try:
374 log = create_repo_fork.get_logger()
375 except:
376 log = logging.getLogger(__name__)
369 log = get_logger(create_repo_fork)
370 DBS = get_session()
371
372 base_path = Repository.base_path()
373
374 RepoModel(DBS).create(form_data, cur_user, just_db=True, fork=True)
375
376 alias = form_data['repo_type']
377 org_repo_name = form_data['org_path']
378 fork_name = form_data['repo_name_full']
379 update_after_clone = form_data['update_after_clone']
380 source_repo_path = os.path.join(base_path, org_repo_name)
381 destination_fork_path = os.path.join(base_path, fork_name)
377 382
378 repo_model = RepoModel(get_session())
379 repo_model.create(form_data, cur_user, just_db=True, fork=True)
380 repo_name = form_data['repo_name']
381 repos_path = get_repos_path()
382 repo_path = os.path.join(repos_path, repo_name)
383 repo_fork_path = os.path.join(repos_path, form_data['fork_name'])
384 alias = form_data['repo_type']
383 log.info('creating fork of %s as %s', source_repo_path,
384 destination_fork_path)
385 backend = get_backend(alias)
386 backend(safe_str(destination_fork_path), create=True,
387 src_url=safe_str(source_repo_path),
388 update_after_clone=update_after_clone)
389 action_logger(cur_user, 'user_forked_repo:%s' % fork_name,
390 org_repo_name, '', DBS)
385 391
386 log.info('creating repo fork %s as %s', repo_name, repo_path)
387 backend = get_backend(alias)
388 backend(str(repo_fork_path), create=True, src_url=str(repo_path))
389
392 action_logger(cur_user, 'user_created_fork:%s' % fork_name,
393 fork_name, '', DBS)
394 # finally commit at latest possible stage
395 DBS.commit()
390 396
391 397 def __get_codes_stats(repo_name):
392 repos_path = get_repos_path()
393 repo = get_repo(safe_str(os.path.join(repos_path, repo_name)))
398 repo = Repository.get_by_repo_name(repo_name).scm_instance
399
394 400 tip = repo.get_changeset()
395 401 code_stats = {}
396 402
@@ -1,7 +1,10 b''
1 import rhodecode
1 2 from rhodecode.lib.utils import BasePasterCommand, Command
2 3 from celery.app import app_or_default
3 4 from celery.bin import camqadm, celerybeat, celeryd, celeryev
4 5
6 from rhodecode.lib import str2bool
7
5 8 __all__ = ['CeleryDaemonCommand', 'CeleryBeatCommand',
6 9 'CAMQPAdminCommand', 'CeleryEventCommand']
7 10
@@ -26,6 +29,16 b' class CeleryCommand(BasePasterCommand):'
26 29 self.parser.add_option(x)
27 30
28 31 def command(self):
32 from pylons import config
33 try:
34 CELERY_ON = str2bool(config['app_conf'].get('use_celery'))
35 except KeyError:
36 CELERY_ON = False
37
38 if CELERY_ON == False:
39 raise Exception('Please enable celery_on in .ini config '
40 'file before running celeryd')
41 rhodecode.CELERY_ON = CELERY_ON
29 42 cmd = self.celery_command(app_or_default())
30 43 return cmd.run(**vars(self.options))
31 44
@@ -4,11 +4,11 b''
4 4 ~~~~~~~~~~~~~~~~~~~~
5 5
6 6 Python backward compatibility functions and common libs
7
8
7
8
9 9 :created_on: Oct 7, 2011
10 10 :author: marcink
11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2010 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
@@ -87,10 +87,11 b' class _Nil(object):'
87 87
88 88 _nil = _Nil()
89 89
90
90 91 class _odict(object):
91 92 """Ordered dict data structure, with O(1) complexity for dict operations
92 93 that modify one element.
93
94
94 95 Overwriting values doesn't change their original sequential order.
95 96 """
96 97
@@ -146,7 +147,7 b' class _odict(object):'
146 147 dict_impl = self._dict_impl()
147 148 try:
148 149 dict_impl.__getitem__(self, key)[1] = val
149 except KeyError, e:
150 except KeyError:
150 151 new = [dict_impl.__getattribute__(self, 'lt'), val, _nil]
151 152 dict_impl.__setitem__(self, key, new)
152 153 if dict_impl.__getattribute__(self, 'lt') == _nil:
@@ -158,7 +159,7 b' class _odict(object):'
158 159
159 160 def __delitem__(self, key):
160 161 dict_impl = self._dict_impl()
161 pred, _ , succ = self._dict_impl().__getitem__(self, key)
162 pred, _, succ = self._dict_impl().__getitem__(self, key)
162 163 if pred == _nil:
163 164 dict_impl.__setattr__(self, 'lh', succ)
164 165 else:
@@ -351,6 +352,7 b' class _odict(object):'
351 352 dict_impl.__getattribute__(self, 'lt'),
352 353 dict_impl.__repr__(self))
353 354
355
354 356 class OrderedDict(_odict, dict):
355 357
356 358 def _dict_impl(self):
@@ -8,7 +8,7 b''
8 8
9 9 :created_on: Apr 10, 2010
10 10 :author: marcink
11 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
@@ -33,13 +33,15 b' from os.path import dirname as dn, join '
33 33 from rhodecode import __dbversion__
34 34 from rhodecode.model import meta
35 35
36 from rhodecode.lib.auth import get_crypt_password, generate_api_key
36 from rhodecode.model.user import UserModel
37 37 from rhodecode.lib.utils import ask_ok
38 38 from rhodecode.model import init_model
39 39 from rhodecode.model.db import User, Permission, RhodeCodeUi, \
40 RhodeCodeSettings, UserToPerm, DbMigrateVersion
40 RhodeCodeSetting, UserToPerm, DbMigrateVersion, RepoGroup,\
41 UserRepoGroupToPerm
41 42
42 43 from sqlalchemy.engine import create_engine
44 from rhodecode.model.repos_group import ReposGroupModel
43 45
44 46 log = logging.getLogger(__name__)
45 47
@@ -57,10 +59,11 b' class DbManage(object):'
57 59 def init_db(self):
58 60 engine = create_engine(self.dburi, echo=self.log_sql)
59 61 init_model(engine)
60 self.sa = meta.Session()
62 self.sa = meta.Session
61 63
62 64 def create_tables(self, override=False):
63 """Create a auth database
65 """
66 Create a auth database
64 67 """
65 68
66 69 log.info("Any existing database is going to be destroyed")
@@ -75,23 +78,19 b' class DbManage(object):'
75 78
76 79 checkfirst = not override
77 80 meta.Base.metadata.create_all(checkfirst=checkfirst)
78 log.info('Created tables for %s', self.dbname)
81 log.info('Created tables for %s' % self.dbname)
79 82
80 83 def set_db_version(self):
81 try:
82 ver = DbMigrateVersion()
83 ver.version = __dbversion__
84 ver.repository_id = 'rhodecode_db_migrations'
85 ver.repository_path = 'versions'
86 self.sa.add(ver)
87 self.sa.commit()
88 except:
89 self.sa.rollback()
90 raise
91 log.info('db version set to: %s', __dbversion__)
84 ver = DbMigrateVersion()
85 ver.version = __dbversion__
86 ver.repository_id = 'rhodecode_db_migrations'
87 ver.repository_path = 'versions'
88 self.sa.add(ver)
89 log.info('db version set to: %s' % __dbversion__)
92 90
93 91 def upgrade(self):
94 """Upgrades given database schema to given revision following
92 """
93 Upgrades given database schema to given revision following
95 94 all needed steps, to perform the upgrade
96 95
97 96 """
@@ -146,7 +145,7 b' class DbManage(object):'
146 145 self.klass = klass
147 146
148 147 def step_0(self):
149 #step 0 is the schema upgrade, and than follow proper upgrades
148 # step 0 is the schema upgrade, and than follow proper upgrades
150 149 print ('attempting to do database upgrade to version %s' \
151 150 % __dbversion__)
152 151 api.upgrade(db_uri, repository_path, __dbversion__)
@@ -170,16 +169,26 b' class DbManage(object):'
170 169 self.klass.fix_settings()
171 170 print ('Adding ldap defaults')
172 171 self.klass.create_ldap_options(skip_existing=True)
173
172
173 def step_4(self):
174 print ('create permissions and fix groups')
175 self.klass.create_permissions()
176 self.klass.fixup_groups()
177
178 def step_5(self):
179 pass
180
174 181 upgrade_steps = [0] + range(curr_version + 1, __dbversion__ + 1)
175 182
176 #CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
183 # CALL THE PROPER ORDER OF STEPS TO PERFORM FULL UPGRADE
177 184 for step in upgrade_steps:
178 185 print ('performing upgrade step %s' % step)
179 186 getattr(UpgradeSteps(self), 'step_%s' % step)()
187 self.sa.commit()
180 188
181 189 def fix_repo_paths(self):
182 """Fixes a old rhodecode version path into new one without a '*'
190 """
191 Fixes a old rhodecode version path into new one without a '*'
183 192 """
184 193
185 194 paths = self.sa.query(RhodeCodeUi)\
@@ -196,7 +205,8 b' class DbManage(object):'
196 205 raise
197 206
198 207 def fix_default_user(self):
199 """Fixes a old default user with some 'nicer' default values,
208 """
209 Fixes a old default user with some 'nicer' default values,
200 210 used mostly for anonymous access
201 211 """
202 212 def_user = self.sa.query(User)\
@@ -215,10 +225,11 b' class DbManage(object):'
215 225 raise
216 226
217 227 def fix_settings(self):
218 """Fixes rhodecode settings adds ga_code key for google analytics
228 """
229 Fixes rhodecode settings adds ga_code key for google analytics
219 230 """
220 231
221 hgsettings3 = RhodeCodeSettings('ga_code', '')
232 hgsettings3 = RhodeCodeSetting('ga_code', '')
222 233
223 234 try:
224 235 self.sa.add(hgsettings3)
@@ -258,18 +269,27 b' class DbManage(object):'
258 269 self.create_user(username, password, email, True)
259 270 else:
260 271 log.info('creating admin and regular test users')
261 self.create_user('test_admin', 'test12',
262 'test_admin@mail.com', True)
263 self.create_user('test_regular', 'test12',
264 'test_regular@mail.com', False)
265 self.create_user('test_regular2', 'test12',
266 'test_regular2@mail.com', False)
272 from rhodecode.tests import TEST_USER_ADMIN_LOGIN,\
273 TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL,\
274 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,\
275 TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
276 TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
277
278 self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
279 TEST_USER_ADMIN_EMAIL, True)
280
281 self.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
282 TEST_USER_REGULAR_EMAIL, False)
283
284 self.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS,
285 TEST_USER_REGULAR2_EMAIL, False)
267 286
268 287 def create_ui_settings(self):
269 """Creates ui settings, fills out hooks
288 """
289 Creates ui settings, fills out hooks
270 290 and disables dotencode
291 """
271 292
272 """
273 293 #HOOKS
274 294 hooks1_key = RhodeCodeUi.HOOK_UPDATE
275 295 hooks1_ = self.sa.query(RhodeCodeUi)\
@@ -300,7 +320,7 b' class DbManage(object):'
300 320 hooks4.ui_key = RhodeCodeUi.HOOK_PULL
301 321 hooks4.ui_value = 'python:rhodecode.lib.hooks.log_pull_action'
302 322
303 #For mercurial 1.7 set backward comapatibility with format
323 # For mercurial 1.7 set backward comapatibility with format
304 324 dotencode_disable = RhodeCodeUi()
305 325 dotencode_disable.ui_section = 'format'
306 326 dotencode_disable.ui_key = 'dotencode'
@@ -312,39 +332,43 b' class DbManage(object):'
312 332 largefiles.ui_key = 'largefiles'
313 333 largefiles.ui_value = ''
314 334
315 try:
316 self.sa.add(hooks1)
317 self.sa.add(hooks2)
318 self.sa.add(hooks3)
319 self.sa.add(hooks4)
320 self.sa.add(dotencode_disable)
321 self.sa.add(largefiles)
322 self.sa.commit()
323 except:
324 self.sa.rollback()
325 raise
335 self.sa.add(hooks1)
336 self.sa.add(hooks2)
337 self.sa.add(hooks3)
338 self.sa.add(hooks4)
339 self.sa.add(largefiles)
326 340
327 def create_ldap_options(self,skip_existing=False):
341 def create_ldap_options(self, skip_existing=False):
328 342 """Creates ldap settings"""
329 343
330 try:
331 for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
332 ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
333 ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
334 ('ldap_dn_pass', ''), ('ldap_base_dn', ''),
335 ('ldap_filter', ''), ('ldap_search_scope', ''),
336 ('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
337 ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
344 for k, v in [('ldap_active', 'false'), ('ldap_host', ''),
345 ('ldap_port', '389'), ('ldap_tls_kind', 'PLAIN'),
346 ('ldap_tls_reqcert', ''), ('ldap_dn_user', ''),
347 ('ldap_dn_pass', ''), ('ldap_base_dn', ''),
348 ('ldap_filter', ''), ('ldap_search_scope', ''),
349 ('ldap_attr_login', ''), ('ldap_attr_firstname', ''),
350 ('ldap_attr_lastname', ''), ('ldap_attr_email', '')]:
351
352 if skip_existing and RhodeCodeSetting.get_by_name(k) != None:
353 log.debug('Skipping option %s' % k)
354 continue
355 setting = RhodeCodeSetting(k, v)
356 self.sa.add(setting)
338 357
339 if skip_existing and RhodeCodeSettings.get_by_name(k) != None:
340 log.debug('Skipping option %s' % k)
341 continue
342 setting = RhodeCodeSettings(k, v)
343 self.sa.add(setting)
344 self.sa.commit()
345 except:
346 self.sa.rollback()
347 raise
358 def fixup_groups(self):
359 def_usr = User.get_by_username('default')
360 for g in RepoGroup.query().all():
361 g.group_name = g.get_new_name(g.name)
362 self.sa.add(g)
363 # get default perm
364 default = UserRepoGroupToPerm.query()\
365 .filter(UserRepoGroupToPerm.group == g)\
366 .filter(UserRepoGroupToPerm.user == def_usr)\
367 .scalar()
368
369 if default is None:
370 log.debug('missing default permission for group %s adding' % g)
371 ReposGroupModel()._create_default_perms(g)
348 372
349 373 def config_prompt(self, test_repo_path='', retries=3):
350 374 if retries == 3:
@@ -359,16 +383,15 b' class DbManage(object):'
359 383 path = test_repo_path
360 384 path_ok = True
361 385
362 #check proper dir
386 # check proper dir
363 387 if not os.path.isdir(path):
364 388 path_ok = False
365 log.error('Given path %s is not a valid directory', path)
389 log.error('Given path %s is not a valid directory' % path)
366 390
367 #check write access
391 # check write access
368 392 if not os.access(path, os.W_OK) and path_ok:
369 393 path_ok = False
370 log.error('No write permission to given path %s', path)
371
394 log.error('No write permission to given path %s' % path)
372 395
373 396 if retries == 0:
374 397 sys.exit('max retries reached')
@@ -408,85 +431,68 b' class DbManage(object):'
408 431 paths.ui_key = '/'
409 432 paths.ui_value = path
410 433
411 hgsettings1 = RhodeCodeSettings('realm', 'RhodeCode authentication')
412 hgsettings2 = RhodeCodeSettings('title', 'RhodeCode')
413 hgsettings3 = RhodeCodeSettings('ga_code', '')
434 hgsettings1 = RhodeCodeSetting('realm', 'RhodeCode authentication')
435 hgsettings2 = RhodeCodeSetting('title', 'RhodeCode')
436 hgsettings3 = RhodeCodeSetting('ga_code', '')
414 437
415 try:
416 self.sa.add(web1)
417 self.sa.add(web2)
418 self.sa.add(web3)
419 self.sa.add(web4)
420 self.sa.add(paths)
421 self.sa.add(hgsettings1)
422 self.sa.add(hgsettings2)
423 self.sa.add(hgsettings3)
424
425 self.sa.commit()
426 except:
427 self.sa.rollback()
428 raise
438 self.sa.add(web1)
439 self.sa.add(web2)
440 self.sa.add(web3)
441 self.sa.add(web4)
442 self.sa.add(paths)
443 self.sa.add(hgsettings1)
444 self.sa.add(hgsettings2)
445 self.sa.add(hgsettings3)
429 446
430 447 self.create_ldap_options()
431 448
432 449 log.info('created ui config')
433 450
434 451 def create_user(self, username, password, email='', admin=False):
435 log.info('creating administrator user %s', username)
436
437 form_data = dict(username=username,
438 password=password,
439 active=True,
440 admin=admin,
441 name='RhodeCode',
442 lastname='Admin',
443 email=email)
444 User.create(form_data)
445
452 log.info('creating user %s' % username)
453 UserModel().create_or_update(username, password, email,
454 name='RhodeCode', lastname='Admin',
455 active=True, admin=admin)
446 456
447 457 def create_default_user(self):
448 458 log.info('creating default user')
449 #create default user for handling default permissions.
459 # create default user for handling default permissions.
460 UserModel().create_or_update(username='default',
461 password=str(uuid.uuid1())[:8],
462 email='anonymous@rhodecode.org',
463 name='Anonymous', lastname='User')
450 464
451 form_data = dict(username='default',
452 password=str(uuid.uuid1())[:8],
453 active=False,
454 admin=False,
455 name='Anonymous',
456 lastname='User',
457 email='anonymous@rhodecode.org')
458 User.create(form_data)
459
460 465 def create_permissions(self):
461 #module.(access|create|change|delete)_[name]
462 #module.(read|write|owner)
463 perms = [('repository.none', 'Repository no access'),
464 ('repository.read', 'Repository read access'),
465 ('repository.write', 'Repository write access'),
466 ('repository.admin', 'Repository admin access'),
467 ('hg.admin', 'Hg Administrator'),
468 ('hg.create.repository', 'Repository create'),
469 ('hg.create.none', 'Repository creation disabled'),
470 ('hg.register.none', 'Register disabled'),
471 ('hg.register.manual_activate', 'Register new user with '
472 'RhodeCode without manual'
473 'activation'),
466 # module.(access|create|change|delete)_[name]
467 # module.(none|read|write|admin)
468 perms = [
469 ('repository.none', 'Repository no access'),
470 ('repository.read', 'Repository read access'),
471 ('repository.write', 'Repository write access'),
472 ('repository.admin', 'Repository admin access'),
474 473
475 ('hg.register.auto_activate', 'Register new user with '
476 'RhodeCode without auto '
477 'activation'),
478 ]
474 ('group.none', 'Repositories Group no access'),
475 ('group.read', 'Repositories Group read access'),
476 ('group.write', 'Repositories Group write access'),
477 ('group.admin', 'Repositories Group admin access'),
478
479 ('hg.admin', 'Hg Administrator'),
480 ('hg.create.repository', 'Repository create'),
481 ('hg.create.none', 'Repository creation disabled'),
482 ('hg.register.none', 'Register disabled'),
483 ('hg.register.manual_activate', 'Register new user with RhodeCode '
484 'without manual activation'),
485
486 ('hg.register.auto_activate', 'Register new user with RhodeCode '
487 'without auto activation'),
488 ]
479 489
480 490 for p in perms:
481 new_perm = Permission()
482 new_perm.permission_name = p[0]
483 new_perm.permission_longname = p[1]
484 try:
491 if not Permission.get_by_key(p[0]):
492 new_perm = Permission()
493 new_perm.permission_name = p[0]
494 new_perm.permission_longname = p[1]
485 495 self.sa.add(new_perm)
486 self.sa.commit()
487 except:
488 self.sa.rollback()
489 raise
490 496
491 497 def populate_default_permissions(self):
492 498 log.info('creating default user permissions')
@@ -512,11 +518,6 b' class DbManage(object):'
512 518 .filter(Permission.permission_name == 'repository.read')\
513 519 .scalar()
514 520
515 try:
516 self.sa.add(reg_perm)
517 self.sa.add(create_repo_perm)
518 self.sa.add(default_repo_perm)
519 self.sa.commit()
520 except:
521 self.sa.rollback()
522 raise
521 self.sa.add(reg_perm)
522 self.sa.add(create_repo_perm)
523 self.sa.add(default_repo_perm)
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Dec 11, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -8,4 +8,4 b''
8 8 from rhodecode.lib.dbmigrate.migrate.versioning import *
9 9 from rhodecode.lib.dbmigrate.migrate.changeset import *
10 10
11 __version__ = '0.7.2.dev' No newline at end of file
11 __version__ = '0.7.3.dev'
@@ -12,7 +12,7 b' from sqlalchemy import __version__ as _s'
12 12
13 13 warnings.simplefilter('always', DeprecationWarning)
14 14
15 _sa_version = tuple(int(re.match("\d+", x).group(0))
15 _sa_version = tuple(int(re.match("\d+", x).group(0))
16 16 for x in _sa_version.split("."))
17 17 SQLA_06 = _sa_version >= (0, 6)
18 18 SQLA_07 = _sa_version >= (0, 7)
@@ -17,23 +17,19 b' from sqlalchemy.schema import (ForeignKe'
17 17 Index)
18 18
19 19 from rhodecode.lib.dbmigrate.migrate import exceptions
20 from rhodecode.lib.dbmigrate.migrate.changeset import constraint, SQLA_06
20 from rhodecode.lib.dbmigrate.migrate.changeset import constraint
21 21
22 if not SQLA_06:
23 from sqlalchemy.sql.compiler import SchemaGenerator, SchemaDropper
24 else:
25 from sqlalchemy.schema import AddConstraint, DropConstraint
26 from sqlalchemy.sql.compiler import DDLCompiler
27 SchemaGenerator = SchemaDropper = DDLCompiler
22 from sqlalchemy.schema import AddConstraint, DropConstraint
23 from sqlalchemy.sql.compiler import DDLCompiler
24 SchemaGenerator = SchemaDropper = DDLCompiler
28 25
29 26
30 27 class AlterTableVisitor(SchemaVisitor):
31 28 """Common operations for ``ALTER TABLE`` statements."""
32 29
33 if SQLA_06:
34 # engine.Compiler looks for .statement
35 # when it spawns off a new compiler
36 statement = ClauseElement()
30 # engine.Compiler looks for .statement
31 # when it spawns off a new compiler
32 statement = ClauseElement()
37 33
38 34 def append(self, s):
39 35 """Append content to the SchemaIterator's query buffer."""
@@ -123,9 +119,8 b' class ANSIColumnGenerator(AlterTableVisi'
123 119 name=column.primary_key_name)
124 120 cons.create()
125 121
126 if SQLA_06:
127 def add_foreignkey(self, fk):
128 self.connection.execute(AddConstraint(fk))
122 def add_foreignkey(self, fk):
123 self.connection.execute(AddConstraint(fk))
129 124
130 125 class ANSIColumnDropper(AlterTableVisitor, SchemaDropper):
131 126 """Extends ANSI SQL dropper for column dropping (``ALTER TABLE
@@ -232,10 +227,7 b' class ANSISchemaChanger(AlterTableVisito'
232 227
233 228 def _visit_column_type(self, table, column, delta):
234 229 type_ = delta['type']
235 if SQLA_06:
236 type_text = str(type_.compile(dialect=self.dialect))
237 else:
238 type_text = type_.dialect_impl(self.dialect).get_col_spec()
230 type_text = str(type_.compile(dialect=self.dialect))
239 231 self.append("TYPE %s" % type_text)
240 232
241 233 def _visit_column_name(self, table, column, delta):
@@ -279,75 +271,17 b' class ANSIConstraintCommon(AlterTableVis'
279 271 def visit_migrate_unique_constraint(self, *p, **k):
280 272 self._visit_constraint(*p, **k)
281 273
282 if SQLA_06:
283 class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
284 def _visit_constraint(self, constraint):
285 constraint.name = self.get_constraint_name(constraint)
286 self.append(self.process(AddConstraint(constraint)))
287 self.execute()
288
289 class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
290 def _visit_constraint(self, constraint):
291 constraint.name = self.get_constraint_name(constraint)
292 self.append(self.process(DropConstraint(constraint, cascade=constraint.cascade)))
293 self.execute()
294
295 else:
296 class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
297
298 def get_constraint_specification(self, cons, **kwargs):
299 """Constaint SQL generators.
300
301 We cannot use SA visitors because they append comma.
302 """
274 class ANSIConstraintGenerator(ANSIConstraintCommon, SchemaGenerator):
275 def _visit_constraint(self, constraint):
276 constraint.name = self.get_constraint_name(constraint)
277 self.append(self.process(AddConstraint(constraint)))
278 self.execute()
303 279
304 if isinstance(cons, PrimaryKeyConstraint):
305 if cons.name is not None:
306 self.append("CONSTRAINT %s " % self.preparer.format_constraint(cons))
307 self.append("PRIMARY KEY ")
308 self.append("(%s)" % ', '.join(self.preparer.quote(c.name, c.quote)
309 for c in cons))
310 self.define_constraint_deferrability(cons)
311 elif isinstance(cons, ForeignKeyConstraint):
312 self.define_foreign_key(cons)
313 elif isinstance(cons, CheckConstraint):
314 if cons.name is not None:
315 self.append("CONSTRAINT %s " %
316 self.preparer.format_constraint(cons))
317 self.append("CHECK (%s)" % cons.sqltext)
318 self.define_constraint_deferrability(cons)
319 elif isinstance(cons, UniqueConstraint):
320 if cons.name is not None:
321 self.append("CONSTRAINT %s " %
322 self.preparer.format_constraint(cons))
323 self.append("UNIQUE (%s)" % \
324 (', '.join(self.preparer.quote(c.name, c.quote) for c in cons)))
325 self.define_constraint_deferrability(cons)
326 else:
327 raise exceptions.InvalidConstraintError(cons)
328
329 def _visit_constraint(self, constraint):
330
331 table = self.start_alter_table(constraint)
332 constraint.name = self.get_constraint_name(constraint)
333 self.append("ADD ")
334 self.get_constraint_specification(constraint)
335 self.execute()
336
337
338 class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
339
340 def _visit_constraint(self, constraint):
341 self.start_alter_table(constraint)
342 self.append("DROP CONSTRAINT ")
343 constraint.name = self.get_constraint_name(constraint)
344 self.append(self.preparer.format_constraint(constraint))
345 if constraint.cascade:
346 self.cascade_constraint(constraint)
347 self.execute()
348
349 def cascade_constraint(self, constraint):
350 self.append(" CASCADE")
280 class ANSIConstraintDropper(ANSIConstraintCommon, SchemaDropper):
281 def _visit_constraint(self, constraint):
282 constraint.name = self.get_constraint_name(constraint)
283 self.append(self.process(DropConstraint(constraint, cascade=constraint.cascade)))
284 self.execute()
351 285
352 286
353 287 class ANSIDialect(DefaultDialect):
@@ -4,7 +4,7 b''
4 4 from sqlalchemy import schema
5 5
6 6 from rhodecode.lib.dbmigrate.migrate.exceptions import *
7 from rhodecode.lib.dbmigrate.migrate.changeset import SQLA_06
7
8 8
9 9 class ConstraintChangeset(object):
10 10 """Base class for Constraint classes."""
@@ -85,7 +85,6 b' class PrimaryKeyConstraint(ConstraintCha'
85 85 if table is not None:
86 86 self._set_parent(table)
87 87
88
89 88 def autoname(self):
90 89 """Mimic the database's automatic constraint names"""
91 90 return "%s_pkey" % self.table.name
@@ -111,8 +110,9 b' class ForeignKeyConstraint(ConstraintCha'
111 110 table = kwargs.pop('table', table)
112 111 refcolnames, reftable = self._normalize_columns(refcolumns,
113 112 table_name=True)
114 super(ForeignKeyConstraint, self).__init__(colnames, refcolnames, *args,
115 **kwargs)
113 super(ForeignKeyConstraint, self).__init__(
114 colnames, refcolnames, *args,**kwargs
115 )
116 116 if table is not None:
117 117 self._set_parent(table)
118 118
@@ -165,8 +165,6 b' class CheckConstraint(ConstraintChangese'
165 165 table = kwargs.pop('table', table)
166 166 schema.CheckConstraint.__init__(self, sqltext, *args, **kwargs)
167 167 if table is not None:
168 if not SQLA_06:
169 self.table = table
170 168 self._set_parent(table)
171 169 self.colnames = colnames
172 170
@@ -199,4 +197,4 b' class UniqueConstraint(ConstraintChanges'
199 197
200 198 def autoname(self):
201 199 """Mimic the database's automatic constraint names"""
202 return "%s_%s_key" % (self.table.name, self.colnames[0])
200 return "%s_%s_key" % (self.table.name, '_'.join(self.colnames))
@@ -4,13 +4,10 b''
4 4 from sqlalchemy.databases import firebird as sa_base
5 5 from sqlalchemy.schema import PrimaryKeyConstraint
6 6 from rhodecode.lib.dbmigrate.migrate import exceptions
7 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
7 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql
8 8
9 9
10 if SQLA_06:
11 FBSchemaGenerator = sa_base.FBDDLCompiler
12 else:
13 FBSchemaGenerator = sa_base.FBSchemaGenerator
10 FBSchemaGenerator = sa_base.FBDDLCompiler
14 11
15 12 class FBColumnGenerator(FBSchemaGenerator, ansisql.ANSIColumnGenerator):
16 13 """Firebird column generator implementation."""
@@ -41,10 +38,7 b' class FBColumnDropper(ansisql.ANSIColumn'
41 38 # is deleted!
42 39 continue
43 40
44 if SQLA_06:
45 should_drop = column.name in cons.columns
46 else:
47 should_drop = cons.contains_column(column) and cons.name
41 should_drop = column.name in cons.columns
48 42 if should_drop:
49 43 self.start_alter_table(column)
50 44 self.append("DROP CONSTRAINT ")
@@ -6,13 +6,10 b' from sqlalchemy.databases import mysql a'
6 6 from sqlalchemy import types as sqltypes
7 7
8 8 from rhodecode.lib.dbmigrate.migrate import exceptions
9 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
9 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql
10 10
11 11
12 if not SQLA_06:
13 MySQLSchemaGenerator = sa_base.MySQLSchemaGenerator
14 else:
15 MySQLSchemaGenerator = sa_base.MySQLDDLCompiler
12 MySQLSchemaGenerator = sa_base.MySQLDDLCompiler
16 13
17 14 class MySQLColumnGenerator(MySQLSchemaGenerator, ansisql.ANSIColumnGenerator):
18 15 pass
@@ -53,37 +50,11 b' class MySQLSchemaChanger(MySQLSchemaGene'
53 50 class MySQLConstraintGenerator(ansisql.ANSIConstraintGenerator):
54 51 pass
55 52
56 if SQLA_06:
57 class MySQLConstraintDropper(MySQLSchemaGenerator, ansisql.ANSIConstraintDropper):
58 def visit_migrate_check_constraint(self, *p, **k):
59 raise exceptions.NotSupportedError("MySQL does not support CHECK"
60 " constraints, use triggers instead.")
61 53
62 else:
63 class MySQLConstraintDropper(ansisql.ANSIConstraintDropper):
64
65 def visit_migrate_primary_key_constraint(self, constraint):
66 self.start_alter_table(constraint)
67 self.append("DROP PRIMARY KEY")
68 self.execute()
69
70 def visit_migrate_foreign_key_constraint(self, constraint):
71 self.start_alter_table(constraint)
72 self.append("DROP FOREIGN KEY ")
73 constraint.name = self.get_constraint_name(constraint)
74 self.append(self.preparer.format_constraint(constraint))
75 self.execute()
76
77 def visit_migrate_check_constraint(self, *p, **k):
78 raise exceptions.NotSupportedError("MySQL does not support CHECK"
79 " constraints, use triggers instead.")
80
81 def visit_migrate_unique_constraint(self, constraint, *p, **k):
82 self.start_alter_table(constraint)
83 self.append('DROP INDEX ')
84 constraint.name = self.get_constraint_name(constraint)
85 self.append(self.preparer.format_constraint(constraint))
86 self.execute()
54 class MySQLConstraintDropper(MySQLSchemaGenerator, ansisql.ANSIConstraintDropper):
55 def visit_migrate_check_constraint(self, *p, **k):
56 raise exceptions.NotSupportedError("MySQL does not support CHECK"
57 " constraints, use triggers instead.")
87 58
88 59
89 60 class MySQLDialect(ansisql.ANSIDialect):
@@ -3,14 +3,11 b''
3 3
4 4 .. _`PostgreSQL`: http://www.postgresql.org/
5 5 """
6 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
6 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql
7
7 8
8 if not SQLA_06:
9 from sqlalchemy.databases import postgres as sa_base
10 PGSchemaGenerator = sa_base.PGSchemaGenerator
11 else:
12 from sqlalchemy.databases import postgresql as sa_base
13 PGSchemaGenerator = sa_base.PGDDLCompiler
9 from sqlalchemy.databases import postgresql as sa_base
10 PGSchemaGenerator = sa_base.PGDDLCompiler
14 11
15 12
16 13 class PGColumnGenerator(PGSchemaGenerator, ansisql.ANSIColumnGenerator):
@@ -11,11 +11,8 b' from sqlalchemy.databases import sqlite '
11 11 from rhodecode.lib.dbmigrate.migrate import exceptions
12 12 from rhodecode.lib.dbmigrate.migrate.changeset import ansisql, SQLA_06
13 13
14 SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
14 15
15 if not SQLA_06:
16 SQLiteSchemaGenerator = sa_base.SQLiteSchemaGenerator
17 else:
18 SQLiteSchemaGenerator = sa_base.SQLiteDDLCompiler
19 16
20 17 class SQLiteCommon(object):
21 18
@@ -39,7 +36,7 b' class SQLiteHelper(SQLiteCommon):'
39 36
40 37 insertion_string = self._modify_table(table, column, delta)
41 38
42 table.create()
39 table.create(bind=self.connection)
43 40 self.append(insertion_string % {'table_name': table_name})
44 41 self.execute()
45 42 self.append('DROP TABLE migration_tmp')
@@ -349,10 +349,7 b' class ColumnDelta(DictMixin, sqlalchemy.'
349 349 def process_column(self, column):
350 350 """Processes default values for column"""
351 351 # XXX: this is a snippet from SA processing of positional parameters
352 if not SQLA_06 and column.args:
353 toinit = list(column.args)
354 else:
355 toinit = list()
352 toinit = list()
356 353
357 354 if column.server_default is not None:
358 355 if isinstance(column.server_default, sqlalchemy.FetchedValue):
@@ -368,9 +365,6 b' class ColumnDelta(DictMixin, sqlalchemy.'
368 365 if toinit:
369 366 column._init_items(*toinit)
370 367
371 if not SQLA_06:
372 column.args = []
373
374 368 def _get_table(self):
375 369 return getattr(self, '_table', None)
376 370
@@ -469,14 +463,18 b' class ChangesetTable(object):'
469 463 self._set_parent(self.metadata)
470 464
471 465 def _meta_key(self):
466 """Get the meta key for this table."""
472 467 return sqlalchemy.schema._get_table_key(self.name, self.schema)
473 468
474 469 def deregister(self):
475 470 """Remove this table from its metadata"""
476 key = self._meta_key()
477 meta = self.metadata
478 if key in meta.tables:
479 del meta.tables[key]
471 if SQLA_07:
472 self.metadata._remove_table(self.name, self.schema)
473 else:
474 key = self._meta_key()
475 meta = self.metadata
476 if key in meta.tables:
477 del meta.tables[key]
480 478
481 479
482 480 class ChangesetColumn(object):
@@ -83,6 +83,5 b' class NotSupportedError(Error):'
83 83 class InvalidConstraintError(Error):
84 84 """Invalid constraint error"""
85 85
86
87 86 class MigrateDeprecationWarning(DeprecationWarning):
88 87 """Warning for deprecated features in Migrate"""
@@ -119,7 +119,7 b' def script_sql(database, description, re'
119 119
120 120 For instance, manage.py script_sql postgresql description creates:
121 121 repository/versions/001_description_postgresql_upgrade.sql and
122 repository/versions/001_description_postgresql_postgres.sql
122 repository/versions/001_description_postgresql_downgrade.sql
123 123 """
124 124 repo = Repository(repository)
125 125 repo.create_script_sql(database, description, **opts)
@@ -212,14 +212,15 b' def test(url, repository, **opts):'
212 212 """
213 213 engine = opts.pop('engine')
214 214 repos = Repository(repository)
215 script = repos.version(None).script()
216 215
217 216 # Upgrade
218 217 log.info("Upgrading...")
218 script = repos.version(None).script(engine.name, 'upgrade')
219 219 script.run(engine, 1)
220 220 log.info("done")
221 221
222 222 log.info("Downgrading...")
223 script = repos.version(None).script(engine.name, 'downgrade')
223 224 script.run(engine, -1)
224 225 log.info("done")
225 226 log.info("Success")
@@ -282,4 +282,3 b' class ModelGenerator(object):'
282 282 except:
283 283 trans.rollback()
284 284 raise
285
@@ -115,7 +115,7 b' class Repository(pathed.Pathed):'
115 115 options.setdefault('version_table', 'migrate_version')
116 116 options.setdefault('repository_id', name)
117 117 options.setdefault('required_dbs', [])
118 options.setdefault('use_timestamp_numbering', '0')
118 options.setdefault('use_timestamp_numbering', False)
119 119
120 120 tmpl = open(os.path.join(tmpl_dir, cls._config)).read()
121 121 ret = TempitaTemplate(tmpl).substitute(options)
@@ -153,7 +153,7 b' class Repository(pathed.Pathed):'
153 153
154 154 def create_script(self, description, **k):
155 155 """API to :meth:`migrate.versioning.version.Collection.create_new_python_version`"""
156
156
157 157 k['use_timestamp_numbering'] = self.use_timestamp_numbering
158 158 self.versions.create_new_python_version(description, **k)
159 159
@@ -180,9 +180,9 b' class Repository(pathed.Pathed):'
180 180 @property
181 181 def use_timestamp_numbering(self):
182 182 """Returns use_timestamp_numbering specified in config"""
183 ts_numbering = self.config.get('db_settings', 'use_timestamp_numbering', raw=True)
184
185 return ts_numbering
183 if self.config.has_option('db_settings', 'use_timestamp_numbering'):
184 return self.config.getboolean('db_settings', 'use_timestamp_numbering')
185 return False
186 186
187 187 def version(self, *p, **k):
188 188 """API to :attr:`migrate.versioning.version.Collection.version`"""
@@ -17,8 +17,16 b' def getDiffOfModelAgainstDatabase(metada'
17 17 :return: object which will evaluate to :keyword:`True` if there \
18 18 are differences else :keyword:`False`.
19 19 """
20 return SchemaDiff(metadata,
21 sqlalchemy.MetaData(engine, reflect=True),
20 db_metadata = sqlalchemy.MetaData(engine, reflect=True)
21
22 # sqlite will include a dynamically generated 'sqlite_sequence' table if
23 # there are autoincrement sequences in the database; this should not be
24 # compared.
25 if engine.dialect.name == 'sqlite':
26 if 'sqlite_sequence' in db_metadata.tables:
27 db_metadata.remove(db_metadata.tables['sqlite_sequence'])
28
29 return SchemaDiff(metadata, db_metadata,
22 30 labelA='model',
23 31 labelB='database',
24 32 excludeTables=excludeTables)
@@ -31,7 +39,7 b' def getDiffOfModelAgainstModel(metadataA'
31 39 :return: object which will evaluate to :keyword:`True` if there \
32 40 are differences else :keyword:`False`.
33 41 """
34 return SchemaDiff(metadataA, metadataB, excludeTables)
42 return SchemaDiff(metadataA, metadataB, excludeTables=excludeTables)
35 43
36 44
37 45 class ColDiff(object):
@@ -7,4 +7,6 b" del _vars['__template_name__']"
7 7 _vars.pop('repository_name', None)
8 8 defaults = ", ".join(["%s='%s'" % var for var in _vars.iteritems()])
9 9 }}
10 main({{ defaults }})
10
11 if __name__ == '__main__':
12 main({{ defaults }})
@@ -26,4 +26,5 b' conf_dict = ConfigLoader(conf_path).pars'
26 26
27 27 # migrate supports passing url as an existing Engine instance (since 0.6.0)
28 28 # usage: migrate -c path/to/config.ini COMMANDS
29 main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }})
29 if __name__ == '__main__':
30 main(url=engine_from_config(conf_dict), repository=migrations.__path__[0],{{ defaults }})
@@ -158,7 +158,7 b' def with_engine(f, *a, **kw):'
158 158 kw['engine'] = engine
159 159 return f(*a, **kw)
160 160 finally:
161 if isinstance(engine, Engine):
161 if isinstance(engine, Engine) and engine is not url:
162 162 log.debug('Disposing SQLAlchemy engine %s', engine)
163 163 engine.dispose()
164 164
@@ -60,7 +60,7 b' class Collection(pathed.Pathed):'
60 60 and store them in self.versions
61 61 """
62 62 super(Collection, self).__init__(path)
63
63
64 64 # Create temporary list of files, allowing skipped version numbers.
65 65 files = os.listdir(path)
66 66 if '1' in files:
@@ -90,9 +90,7 b' class Collection(pathed.Pathed):'
90 90 return max([VerNum(0)] + self.versions.keys())
91 91
92 92 def _next_ver_num(self, use_timestamp_numbering):
93 print use_timestamp_numbering
94 93 if use_timestamp_numbering == True:
95 print "Creating new timestamp version!"
96 94 return VerNum(int(datetime.utcnow().strftime('%Y%m%d%H%M%S')))
97 95 else:
98 96 return self.latest + 1
@@ -113,7 +111,7 b' class Collection(pathed.Pathed):'
113 111
114 112 script.PythonScript.create(filepath, **k)
115 113 self.versions[ver] = Version(ver, self.path, [filename])
116
114
117 115 def create_new_sql_version(self, database, description, **k):
118 116 """Create SQL files for new version"""
119 117 ver = self._next_ver_num(k.pop('use_timestamp_numbering', False))
@@ -133,7 +131,7 b' class Collection(pathed.Pathed):'
133 131 filepath = self._version_path(filename)
134 132 script.SqlScript.create(filepath, **k)
135 133 self.versions[ver].add_script(filepath)
136
134
137 135 def version(self, vernum=None):
138 136 """Returns latest Version if vernum is not given.
139 137 Otherwise, returns wanted version"""
@@ -152,7 +150,7 b' class Collection(pathed.Pathed):'
152 150
153 151 class Version(object):
154 152 """A single version in a collection
155 :param vernum: Version Number
153 :param vernum: Version Number
156 154 :param path: Path to script files
157 155 :param filelist: List of scripts
158 156 :type vernum: int, VerNum
@@ -169,7 +167,7 b' class Version(object):'
169 167
170 168 for script in filelist:
171 169 self.add_script(os.path.join(path, script))
172
170
173 171 def script(self, database=None, operation=None):
174 172 """Returns SQL or Python Script"""
175 173 for db in (database, 'default'):
@@ -198,7 +196,7 b' class Version(object):'
198 196 def _add_script_sql(self, path):
199 197 basename = os.path.basename(path)
200 198 match = self.SQL_FILENAME.match(basename)
201
199
202 200 if match:
203 201 basename = basename.replace('.sql', '')
204 202 parts = basename.split('_')
@@ -14,7 +14,7 b' from rhodecode.lib.dbmigrate.migrate imp'
14 14
15 15 log = logging.getLogger(__name__)
16 16
17 class RhodeCodeSettings(Base):
17 class RhodeCodeSetting(Base):
18 18 __tablename__ = 'rhodecode_settings'
19 19 __table_args__ = (UniqueConstraint('app_settings_name'), {'useexisting':True})
20 20 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
@@ -74,7 +74,7 b' class User(Base):'
74 74 self.last_login = datetime.datetime.now()
75 75 session.add(self)
76 76 session.commit()
77 log.debug('updated user %s lastlogin', self.username)
77 log.debug('updated user %s lastlogin' % self.username)
78 78 except (DatabaseError,):
79 79 session.rollback()
80 80
@@ -107,7 +107,7 b' class Repository(Base):'
107 107
108 108 user = relation('User')
109 109 fork = relation('Repository', remote_side=repo_id)
110 repo_to_perm = relation('RepoToPerm', cascade='all')
110 repo_to_perm = relation('UserRepoToPerm', cascade='all')
111 111 stats = relation('Statistics', cascade='all', uselist=False)
112 112
113 113 repo_followers = relation('UserFollowing', primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id', cascade='all')
@@ -126,7 +126,7 b' class Permission(Base):'
126 126 def __repr__(self):
127 127 return "<Permission('%s:%s')>" % (self.permission_id, self.permission_name)
128 128
129 class RepoToPerm(Base):
129 class UserRepoToPerm(Base):
130 130 __tablename__ = 'repo_to_perm'
131 131 __table_args__ = (UniqueConstraint('user_id', 'repository_id'), {'useexisting':True})
132 132 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
@@ -12,6 +12,7 b' from rhodecode.lib.dbmigrate.migrate.cha'
12 12
13 13 log = logging.getLogger(__name__)
14 14
15
15 16 def upgrade(migrate_engine):
16 17 """ Upgrade operations go here.
17 18 Don't create your own engine; bind migrate_engine to your metadata
@@ -44,8 +45,6 b' def upgrade(migrate_engine):'
44 45 nullable=True, unique=None, default=None)
45 46 revision.create(tbl)
46 47
47
48
49 48 #==========================================================================
50 49 # Upgrade of `repositories` table
51 50 #==========================================================================
@@ -69,47 +68,18 b' def upgrade(migrate_engine):'
69 68 #==========================================================================
70 69 # Add table `user_followings`
71 70 #==========================================================================
72 class UserFollowing(Base, BaseModel):
73 __tablename__ = 'user_followings'
74 __table_args__ = (UniqueConstraint('user_id', 'follows_repository_id'),
75 UniqueConstraint('user_id', 'follows_user_id')
76 , {'useexisting':True})
77
78 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
79 user_id = Column("user_id", Integer(), ForeignKey(u'users.user_id'), nullable=False, unique=None, default=None)
80 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey(u'repositories.repo_id'), nullable=True, unique=None, default=None)
81 follows_user_id = Column("follows_user_id", Integer(), ForeignKey(u'users.user_id'), nullable=True, unique=None, default=None)
82
83 user = relation('User', primaryjoin='User.user_id==UserFollowing.user_id')
84
85 follows_user = relation('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
86 follows_repository = relation('Repository')
87
71 from rhodecode.lib.dbmigrate.schema.db_1_1_0 import UserFollowing
88 72 UserFollowing().__table__.create()
89 73
90 74 #==========================================================================
91 75 # Add table `cache_invalidation`
92 76 #==========================================================================
93 class CacheInvalidation(Base, BaseModel):
94 __tablename__ = 'cache_invalidation'
95 __table_args__ = (UniqueConstraint('cache_key'), {'useexisting':True})
96 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
97 cache_key = Column("cache_key", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
98 cache_args = Column("cache_args", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
99 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
100
101
102 def __init__(self, cache_key, cache_args=''):
103 self.cache_key = cache_key
104 self.cache_args = cache_args
105 self.cache_active = False
106
107 def __repr__(self):
108 return "<CacheInvalidation('%s:%s')>" % (self.cache_id, self.cache_key)
77 from rhodecode.lib.dbmigrate.schema.db_1_1_0 import CacheInvalidation
109 78 CacheInvalidation().__table__.create()
110 79
111 80 return
112 81
82
113 83 def downgrade(migrate_engine):
114 84 meta = MetaData()
115 85 meta.bind = migrate_engine
@@ -13,6 +13,7 b' from rhodecode.model.meta import Base'
13 13
14 14 log = logging.getLogger(__name__)
15 15
16
16 17 def upgrade(migrate_engine):
17 18 """ Upgrade operations go here.
18 19 Don't create your own engine; bind migrate_engine to your metadata
@@ -21,46 +22,46 b' def upgrade(migrate_engine):'
21 22 #==========================================================================
22 23 # Add table `groups``
23 24 #==========================================================================
24 from rhodecode.model.db import Group
25 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import RepoGroup as Group
25 26 Group().__table__.create()
26 27
27 28 #==========================================================================
28 29 # Add table `group_to_perm`
29 30 #==========================================================================
30 from rhodecode.model.db import GroupToPerm
31 GroupToPerm().__table__.create()
31 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UserRepoGroupToPerm
32 UserRepoGroupToPerm().__table__.create()
32 33
33 34 #==========================================================================
34 35 # Add table `users_groups`
35 36 #==========================================================================
36 from rhodecode.model.db import UsersGroup
37 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroup
37 38 UsersGroup().__table__.create()
38 39
39 40 #==========================================================================
40 41 # Add table `users_groups_members`
41 42 #==========================================================================
42 from rhodecode.model.db import UsersGroupMember
43 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupMember
43 44 UsersGroupMember().__table__.create()
44 45
45 46 #==========================================================================
46 47 # Add table `users_group_repo_to_perm`
47 48 #==========================================================================
48 from rhodecode.model.db import UsersGroupRepoToPerm
49 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupRepoToPerm
49 50 UsersGroupRepoToPerm().__table__.create()
50 51
51 52 #==========================================================================
52 53 # Add table `users_group_to_perm`
53 54 #==========================================================================
54 from rhodecode.model.db import UsersGroupToPerm
55 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UsersGroupToPerm
55 56 UsersGroupToPerm().__table__.create()
56 57
57 58 #==========================================================================
58 59 # Upgrade of `users` table
59 60 #==========================================================================
60 from rhodecode.model.db import User
61 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import User
61 62
62 63 #add column
63 ldap_dn = Column("ldap_dn", String(length=None, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
64 ldap_dn = Column("ldap_dn", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
64 65 ldap_dn.create(User().__table__)
65 66
66 67 api_key = Column("api_key", String(length=255, convert_unicode=False, assert_unicode=None), nullable=True, unique=None, default=None)
@@ -74,7 +75,7 b' def upgrade(migrate_engine):'
74 75 #==========================================================================
75 76 # Upgrade of `repositories` table
76 77 #==========================================================================
77 from rhodecode.model.db import Repository
78 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import Repository
78 79
79 80 #ADD clone_uri column#
80 81
@@ -83,7 +84,7 b' def upgrade(migrate_engine):'
83 84 nullable=True, unique=False, default=None)
84 85
85 86 clone_uri.create(Repository().__table__)
86
87
87 88 #ADD downloads column#
88 89 enable_downloads = Column("downloads", Boolean(), nullable=True, unique=None, default=True)
89 90 enable_downloads.create(Repository().__table__)
@@ -104,10 +105,10 b' def upgrade(migrate_engine):'
104 105 # Upgrade of `user_followings` table
105 106 #==========================================================================
106 107
107 from rhodecode.model.db import UserFollowing
108 from rhodecode.lib.dbmigrate.schema.db_1_2_0 import UserFollowing
108 109
109 follows_from = Column('follows_from', DateTime(timezone=False),
110 nullable=True, unique=None,
110 follows_from = Column('follows_from', DateTime(timezone=False),
111 nullable=True, unique=None,
111 112 default=datetime.datetime.now)
112 113 follows_from.create(UserFollowing().__table__)
113 114
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Dec 11, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -6,7 +6,8 b''
6 6 Set of custom exceptions used in RhodeCode
7 7
8 8 :created_on: Nov 17, 2010
9 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
9 :author: marcink
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
10 11 :license: GPLv3, see COPYING for more details.
11 12 """
12 13 # This program is free software: you can redistribute it and/or modify
@@ -46,5 +47,6 b' class DefaultUserException(Exception):'
46 47 class UserOwnsReposException(Exception):
47 48 pass
48 49
50
49 51 class UsersGroupsAssignedException(Exception):
50 52 pass
@@ -8,22 +8,24 b' import hashlib'
8 8 import StringIO
9 9 import urllib
10 10 import math
11 import logging
11 12
12 13 from datetime import datetime
13 from pygments.formatters import HtmlFormatter
14 from pygments.formatters.html import HtmlFormatter
14 15 from pygments import highlight as code_highlight
15 16 from pylons import url, request, config
16 17 from pylons.i18n.translation import _, ungettext
18 from hashlib import md5
17 19
18 20 from webhelpers.html import literal, HTML, escape
19 21 from webhelpers.html.tools import *
20 22 from webhelpers.html.builder import make_tag
21 23 from webhelpers.html.tags import auto_discovery_link, checkbox, css_classes, \
22 end_form, file, form, hidden, image, javascript_link, link_to, link_to_if, \
23 link_to_unless, ol, required_legend, select, stylesheet_link, submit, text, \
24 password, textarea, title, ul, xml_declaration, radio
25 from webhelpers.html.tools import auto_link, button_to, highlight, js_obfuscate, \
26 mail_to, strip_links, strip_tags, tag_re
24 end_form, file, form, hidden, image, javascript_link, link_to, \
25 link_to_if, link_to_unless, ol, required_legend, select, stylesheet_link, \
26 submit, text, password, textarea, title, ul, xml_declaration, radio
27 from webhelpers.html.tools import auto_link, button_to, highlight, \
28 js_obfuscate, mail_to, strip_links, strip_tags, tag_re
27 29 from webhelpers.number import format_byte_size, format_bit_size
28 30 from webhelpers.pylonslib import Flash as _Flash
29 31 from webhelpers.pylonslib.secure_form import secure_form
@@ -33,11 +35,15 b' from webhelpers.text import chop_at, col'
33 35 from webhelpers.date import time_ago_in_words
34 36 from webhelpers.paginate import Page
35 37 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
36 convert_boolean_attrs, NotGiven
38 convert_boolean_attrs, NotGiven, _make_safe_id_component
37 39
38 from vcs.utils.annotate import annotate_highlight
40 from rhodecode.lib.annotate import annotate_highlight
39 41 from rhodecode.lib.utils import repo_name_slug
40 from rhodecode.lib import str2bool, safe_unicode, safe_str,get_changeset_safe
42 from rhodecode.lib import str2bool, safe_unicode, safe_str, get_changeset_safe
43 from rhodecode.lib.markup_renderer import MarkupRenderer
44
45 log = logging.getLogger(__name__)
46
41 47
42 48 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
43 49 """
@@ -49,6 +55,19 b' def _reset(name, value=None, id=NotGiven'
49 55 return HTML.input(**attrs)
50 56
51 57 reset = _reset
58 safeid = _make_safe_id_component
59
60
61 def FID(raw_id, path):
62 """
63 Creates a uniqe ID for filenode based on it's hash of path and revision
64 it's safe to use in urls
65
66 :param raw_id:
67 :param path:
68 """
69
70 return 'C-%s-%s' % (short_id(raw_id), md5(path).hexdigest()[:12])
52 71
53 72
54 73 def get_token():
@@ -104,10 +123,14 b' class _FilesBreadCrumbs(object):'
104 123 paths_l = paths.split('/')
105 124 for cnt, p in enumerate(paths_l):
106 125 if p != '':
107 url_l.append(link_to(p, url('files_home',
108 repo_name=repo_name,
109 revision=rev,
110 f_path='/'.join(paths_l[:cnt + 1]))))
126 url_l.append(link_to(p,
127 url('files_home',
128 repo_name=repo_name,
129 revision=rev,
130 f_path='/'.join(paths_l[:cnt + 1])
131 )
132 )
133 )
111 134
112 135 return literal('/'.join(url_l))
113 136
@@ -198,13 +221,16 b' def pygmentize(filenode, **kwargs):'
198 221 return literal(code_highlight(filenode.content,
199 222 filenode.lexer, CodeHtmlFormatter(**kwargs)))
200 223
224
201 225 def pygmentize_annotation(repo_name, filenode, **kwargs):
202 """pygmentize function for annotation
226 """
227 pygmentize function for annotation
203 228
204 229 :param filenode:
205 230 """
206 231
207 232 color_dict = {}
233
208 234 def gen_color(n=10000):
209 235 """generator for getting n of evenly distributed colors using
210 236 hsv color and golden ratio. It always return same order of colors
@@ -213,19 +239,26 b' def pygmentize_annotation(repo_name, fil'
213 239 """
214 240
215 241 def hsv_to_rgb(h, s, v):
216 if s == 0.0: return v, v, v
217 i = int(h * 6.0) # XXX assume int() truncates!
242 if s == 0.0:
243 return v, v, v
244 i = int(h * 6.0) # XXX assume int() truncates!
218 245 f = (h * 6.0) - i
219 246 p = v * (1.0 - s)
220 247 q = v * (1.0 - s * f)
221 248 t = v * (1.0 - s * (1.0 - f))
222 249 i = i % 6
223 if i == 0: return v, t, p
224 if i == 1: return q, v, p
225 if i == 2: return p, v, t
226 if i == 3: return p, q, v
227 if i == 4: return t, p, v
228 if i == 5: return v, p, q
250 if i == 0:
251 return v, t, p
252 if i == 1:
253 return q, v, p
254 if i == 2:
255 return p, v, t
256 if i == 3:
257 return p, q, v
258 if i == 4:
259 return t, p, v
260 if i == 5:
261 return v, p, q
229 262
230 263 golden_ratio = 0.618033988749895
231 264 h = 0.22717784590367374
@@ -235,12 +268,12 b' def pygmentize_annotation(repo_name, fil'
235 268 h %= 1
236 269 HSV_tuple = [h, 0.95, 0.95]
237 270 RGB_tuple = hsv_to_rgb(*HSV_tuple)
238 yield map(lambda x:str(int(x * 256)), RGB_tuple)
271 yield map(lambda x: str(int(x * 256)), RGB_tuple)
239 272
240 273 cgenerator = gen_color()
241 274
242 275 def get_color_string(cs):
243 if color_dict.has_key(cs):
276 if cs in color_dict:
244 277 col = color_dict[cs]
245 278 else:
246 279 col = color_dict[cs] = cgenerator.next()
@@ -275,6 +308,7 b' def pygmentize_annotation(repo_name, fil'
275 308
276 309 return literal(annotate_highlight(filenode, url_func(repo_name), **kwargs))
277 310
311
278 312 def is_following_repo(repo_name, user_id):
279 313 from rhodecode.model.scm import ScmModel
280 314 return ScmModel().is_following_repo(repo_name, user_id)
@@ -284,17 +318,75 b' flash = _Flash()'
284 318 #==============================================================================
285 319 # SCM FILTERS available via h.
286 320 #==============================================================================
287 from vcs.utils import author_name, author_email
321 from rhodecode.lib.vcs.utils import author_name, author_email
288 322 from rhodecode.lib import credentials_filter, age as _age
323 from rhodecode.model.db import User
289 324
290 age = lambda x:_age(x)
325 age = lambda x: _age(x)
291 326 capitalize = lambda x: x.capitalize()
292 327 email = author_email
293 email_or_none = lambda x: email(x) if email(x) != x else None
294 person = lambda x: author_name(x)
295 328 short_id = lambda x: x[:12]
296 329 hide_credentials = lambda x: ''.join(credentials_filter(x))
297 330
331
332 def is_git(repository):
333 if hasattr(repository, 'alias'):
334 _type = repository.alias
335 elif hasattr(repository, 'repo_type'):
336 _type = repository.repo_type
337 else:
338 _type = repository
339 return _type == 'git'
340
341
342 def is_hg(repository):
343 if hasattr(repository, 'alias'):
344 _type = repository.alias
345 elif hasattr(repository, 'repo_type'):
346 _type = repository.repo_type
347 else:
348 _type = repository
349 return _type == 'hg'
350
351
352 def email_or_none(author):
353 _email = email(author)
354 if _email != '':
355 return _email
356
357 # See if it contains a username we can get an email from
358 user = User.get_by_username(author_name(author), case_insensitive=True,
359 cache=True)
360 if user is not None:
361 return user.email
362
363 # No valid email, not a valid user in the system, none!
364 return None
365
366
367 def person(author):
368 # attr to return from fetched user
369 person_getter = lambda usr: usr.username
370
371 # Valid email in the attribute passed, see if they're in the system
372 _email = email(author)
373 if _email != '':
374 user = User.get_by_email(_email, case_insensitive=True, cache=True)
375 if user is not None:
376 return person_getter(user)
377 return _email
378
379 # Maybe it's a username?
380 _author = author_name(author)
381 user = User.get_by_username(_author, case_insensitive=True,
382 cache=True)
383 if user is not None:
384 return person_getter(user)
385
386 # Still nothing? Just pass back the author name then
387 return _author
388
389
298 390 def bool2icon(value):
299 391 """Returns True/False values represented as small html image of true/false
300 392 icons
@@ -314,7 +406,8 b' def bool2icon(value):'
314 406
315 407
316 408 def action_parser(user_log, feed=False):
317 """This helper will action_map the specified string action into translated
409 """
410 This helper will action_map the specified string action into translated
318 411 fancy names with icons and links
319 412
320 413 :param user_log: user log instance
@@ -330,52 +423,84 b' def action_parser(user_log, feed=False):'
330 423 action, action_params = x
331 424
332 425 def get_cs_links():
333 revs_limit = 3 #display this amount always
334 revs_top_limit = 50 #show upto this amount of changesets hidden
335 revs = action_params.split(',')
426 revs_limit = 3 # display this amount always
427 revs_top_limit = 50 # show upto this amount of changesets hidden
428 revs_ids = action_params.split(',')
429 deleted = user_log.repository is None
430 if deleted:
431 return ','.join(revs_ids)
432
336 433 repo_name = user_log.repository.repo_name
337 434
338 from rhodecode.model.scm import ScmModel
339 435 repo = user_log.repository.scm_instance
340 436
341 message = lambda rev: get_changeset_safe(repo, rev).message
437 message = lambda rev: rev.message
438 lnk = lambda rev, repo_name: (
439 link_to('r%s:%s' % (rev.revision, rev.short_id),
440 url('changeset_home', repo_name=repo_name,
441 revision=rev.raw_id),
442 title=tooltip(message(rev)), class_='tooltip')
443 )
444 # get only max revs_top_limit of changeset for performance/ui reasons
445 revs = [
446 x for x in repo.get_changesets(revs_ids[0],
447 revs_ids[:revs_top_limit][-1])
448 ]
449
342 450 cs_links = []
343 cs_links.append(" " + ', '.join ([link_to(rev,
344 url('changeset_home',
345 repo_name=repo_name,
346 revision=rev), title=tooltip(message(rev)),
347 class_='tooltip') for rev in revs[:revs_limit] ]))
451 cs_links.append(" " + ', '.join(
452 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
453 )
454 )
348 455
349 compare_view = (' <div class="compare_view tooltip" title="%s">'
350 '<a href="%s">%s</a> '
351 '</div>' % (_('Show all combined changesets %s->%s' \
352 % (revs[0], revs[-1])),
353 url('changeset_home', repo_name=repo_name,
354 revision='%s...%s' % (revs[0], revs[-1])
355 ),
356 _('compare view'))
357 )
456 compare_view = (
457 ' <div class="compare_view tooltip" title="%s">'
458 '<a href="%s">%s</a> </div>' % (
459 _('Show all combined changesets %s->%s') % (
460 revs_ids[0], revs_ids[-1]
461 ),
462 url('changeset_home', repo_name=repo_name,
463 revision='%s...%s' % (revs_ids[0], revs_ids[-1])
464 ),
465 _('compare view')
466 )
467 )
358 468
359 if len(revs) > revs_limit:
360 uniq_id = revs[0]
361 html_tmpl = ('<span> %s '
362 '<a class="show_more" id="_%s" href="#more">%s</a> '
363 '%s</span>')
469 # if we have exactly one more than normally displayed
470 # just display it, takes less space than displaying
471 # "and 1 more revisions"
472 if len(revs_ids) == revs_limit + 1:
473 rev = revs[revs_limit]
474 cs_links.append(", " + lnk(rev, repo_name))
475
476 # hidden-by-default ones
477 if len(revs_ids) > revs_limit + 1:
478 uniq_id = revs_ids[0]
479 html_tmpl = (
480 '<span> %s <a class="show_more" id="_%s" '
481 'href="#more">%s</a> %s</span>'
482 )
364 483 if not feed:
365 cs_links.append(html_tmpl % (_('and'), uniq_id, _('%s more') \
366 % (len(revs) - revs_limit),
367 _('revisions')))
484 cs_links.append(html_tmpl % (
485 _('and'),
486 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
487 _('revisions')
488 )
489 )
368 490
369 491 if not feed:
370 html_tmpl = '<span id="%s" style="display:none"> %s </span>'
492 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
371 493 else:
372 494 html_tmpl = '<span id="%s"> %s </span>'
373 495
374 cs_links.append(html_tmpl % (uniq_id, ', '.join([link_to(rev,
375 url('changeset_home',
376 repo_name=repo_name, revision=rev),
377 title=message(rev), class_='tooltip')
378 for rev in revs[revs_limit:revs_top_limit]])))
496 morelinks = ', '.join(
497 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
498 )
499
500 if len(revs_ids) > revs_top_limit:
501 morelinks += ', ...'
502
503 cs_links.append(html_tmpl % (uniq_id, morelinks))
379 504 if len(revs) > 1:
380 505 cs_links.append(compare_view)
381 506 return ''.join(cs_links)
@@ -385,36 +510,39 b' def action_parser(user_log, feed=False):'
385 510 return _('fork name ') + str(link_to(action_params, url('summary_home',
386 511 repo_name=repo_name,)))
387 512
388 action_map = {'user_deleted_repo':(_('[deleted] repository'), None),
389 'user_created_repo':(_('[created] repository'), None),
390 'user_forked_repo':(_('[forked] repository'), get_fork_name),
391 'user_updated_repo':(_('[updated] repository'), None),
392 'admin_deleted_repo':(_('[delete] repository'), None),
393 'admin_created_repo':(_('[created] repository'), None),
394 'admin_forked_repo':(_('[forked] repository'), None),
395 'admin_updated_repo':(_('[updated] repository'), None),
396 'push':(_('[pushed] into'), get_cs_links),
397 'push_local':(_('[committed via RhodeCode] into'), get_cs_links),
398 'push_remote':(_('[pulled from remote] into'), get_cs_links),
399 'pull':(_('[pulled] from'), None),
400 'started_following_repo':(_('[started following] repository'), None),
401 'stopped_following_repo':(_('[stopped following] repository'), None),
513 action_map = {'user_deleted_repo': (_('[deleted] repository'), None),
514 'user_created_repo': (_('[created] repository'), None),
515 'user_created_fork': (_('[created] repository as fork'), None),
516 'user_forked_repo': (_('[forked] repository'), get_fork_name),
517 'user_updated_repo': (_('[updated] repository'), None),
518 'admin_deleted_repo': (_('[delete] repository'), None),
519 'admin_created_repo': (_('[created] repository'), None),
520 'admin_forked_repo': (_('[forked] repository'), None),
521 'admin_updated_repo': (_('[updated] repository'), None),
522 'push': (_('[pushed] into'), get_cs_links),
523 'push_local': (_('[committed via RhodeCode] into'), get_cs_links),
524 'push_remote': (_('[pulled from remote] into'), get_cs_links),
525 'pull': (_('[pulled] from'), None),
526 'started_following_repo': (_('[started following] repository'), None),
527 'stopped_following_repo': (_('[stopped following] repository'), None),
402 528 }
403 529
404 530 action_str = action_map.get(action, action)
405 531 if feed:
406 532 action = action_str[0].replace('[', '').replace(']', '')
407 533 else:
408 action = action_str[0].replace('[', '<span class="journal_highlight">')\
409 .replace(']', '</span>')
534 action = action_str[0]\
535 .replace('[', '<span class="journal_highlight">')\
536 .replace(']', '</span>')
410 537
411 action_params_func = lambda :""
538 action_params_func = lambda: ""
412 539
413 540 if callable(action_str[1]):
414 541 action_params_func = action_str[1]
415 542
416 543 return [literal(action), action_params_func]
417 544
545
418 546 def action_parser_icon(user_log):
419 547 action = user_log.action
420 548 action_params = None
@@ -426,6 +554,7 b' def action_parser_icon(user_log):'
426 554 tmpl = """<img src="%s%s" alt="%s"/>"""
427 555 map = {'user_deleted_repo':'database_delete.png',
428 556 'user_created_repo':'database_add.png',
557 'user_created_fork':'arrow_divide.png',
429 558 'user_forked_repo':'arrow_divide.png',
430 559 'user_updated_repo':'database_edit.png',
431 560 'admin_deleted_repo':'database_delete.png',
@@ -449,6 +578,7 b' def action_parser_icon(user_log):'
449 578 from rhodecode.lib.auth import HasPermissionAny, HasPermissionAll, \
450 579 HasRepoPermissionAny, HasRepoPermissionAll
451 580
581
452 582 #==============================================================================
453 583 # GRAVATAR URL
454 584 #==============================================================================
@@ -456,7 +586,8 b' HasRepoPermissionAny, HasRepoPermissionA'
456 586 def gravatar_url(email_address, size=30):
457 587 if (not str2bool(config['app_conf'].get('use_gravatar')) or
458 588 not email_address or email_address == 'anonymous@rhodecode.org'):
459 return url("/images/user%s.png" % size)
589 f = lambda a, l: min(l, key=lambda x: abs(x - a))
590 return url("/images/user%s.png" % f(size, [14, 16, 20, 24, 30]))
460 591
461 592 ssl_enabled = 'https' == request.environ.get('wsgi.url_scheme')
462 593 default = 'identicon'
@@ -469,7 +600,7 b' def gravatar_url(email_address, size=30)'
469 600 email_address = safe_str(email_address)
470 601 # construct the url
471 602 gravatar_url = baseurl + hashlib.md5(email_address.lower()).hexdigest() + "?"
472 gravatar_url += urllib.urlencode({'d':default, 's':str(size)})
603 gravatar_url += urllib.urlencode({'d': default, 's': str(size)})
473 604
474 605 return gravatar_url
475 606
@@ -480,7 +611,7 b' def gravatar_url(email_address, size=30)'
480 611 class RepoPage(Page):
481 612
482 613 def __init__(self, collection, page=1, items_per_page=20,
483 item_count=None, url=None, branch_name=None, **kwargs):
614 item_count=None, url=None, **kwargs):
484 615
485 616 """Create a "RepoPage" instance. special pager for paging
486 617 repository
@@ -498,7 +629,7 b' class RepoPage(Page):'
498 629 # The self.page is the number of the current page.
499 630 # The first page has the number 1!
500 631 try:
501 self.page = int(page) # make it int() if we get it as a string
632 self.page = int(page) # make it int() if we get it as a string
502 633 except (ValueError, TypeError):
503 634 self.page = 1
504 635
@@ -518,7 +649,8 b' class RepoPage(Page):'
518 649 self.items_per_page))
519 650 self.last_page = self.first_page + self.page_count - 1
520 651
521 # Make sure that the requested page number is the range of valid pages
652 # Make sure that the requested page number is the range of
653 # valid pages
522 654 if self.page > self.last_page:
523 655 self.page = self.last_page
524 656 elif self.page < self.first_page:
@@ -531,11 +663,7 b' class RepoPage(Page):'
531 663 self.last_item = ((self.item_count - 1) - items_per_page *
532 664 (self.page - 1))
533 665
534 iterator = self.collection.get_changesets(start=self.first_item,
535 end=self.last_item,
536 reverse=True,
537 branch_name=branch_name)
538 self.items = list(iterator)
666 self.items = list(self.collection[self.first_item:self.last_item + 1])
539 667
540 668 # Links to previous and next page
541 669 if self.page > self.first_page:
@@ -560,14 +688,14 b' class RepoPage(Page):'
560 688 self.items = []
561 689
562 690 # This is a subclass of the 'list' type. Initialise the list now.
563 list.__init__(self, self.items)
691 list.__init__(self, reversed(self.items))
564 692
565 693
566 694 def changed_tooltip(nodes):
567 695 """
568 696 Generates a html string for changed nodes in changeset page.
569 697 It limits the output to 30 entries
570
698
571 699 :param nodes: LazyNodesGenerator
572 700 """
573 701 if nodes:
@@ -581,15 +709,14 b' def changed_tooltip(nodes):'
581 709 return ': ' + _('No Files')
582 710
583 711
584
585 712 def repo_link(groups_and_repos):
586 713 """
587 714 Makes a breadcrumbs link to repo within a group
588 715 joins &raquo; on each group to create a fancy link
589
716
590 717 ex::
591 718 group >> subgroup >> repo
592
719
593 720 :param groups_and_repos:
594 721 """
595 722 groups, repo_name = groups_and_repos
@@ -603,11 +730,12 b' def repo_link(groups_and_repos):'
603 730 return literal(' &raquo; '.join(map(make_link, groups)) + \
604 731 " &raquo; " + repo_name)
605 732
733
606 734 def fancy_file_stats(stats):
607 735 """
608 736 Displays a fancy two colored bar for number of added/deleted
609 737 lines of code on file
610
738
611 739 :param stats: two element list of added/deleted lines of code
612 740 """
613 741
@@ -630,13 +758,12 b' def fancy_file_stats(stats):'
630 758 a_v = a if a > 0 else ''
631 759 d_v = d if d > 0 else ''
632 760
633
634 761 def cgen(l_type):
635 mapping = {'tr':'top-right-rounded-corner',
636 'tl':'top-left-rounded-corner',
637 'br':'bottom-right-rounded-corner',
638 'bl':'bottom-left-rounded-corner'}
639 map_getter = lambda x:mapping[x]
762 mapping = {'tr': 'top-right-rounded-corner',
763 'tl': 'top-left-rounded-corner',
764 'br': 'bottom-right-rounded-corner',
765 'bl': 'bottom-left-rounded-corner'}
766 map_getter = lambda x: mapping[x]
640 767
641 768 if l_type == 'a' and d_v:
642 769 #case when added and deleted are present
@@ -651,22 +778,137 b' def fancy_file_stats(stats):'
651 778 if l_type == 'd' and not a_v:
652 779 return ' '.join(map(map_getter, ['tr', 'br', 'tl', 'bl']))
653 780
654
655
656 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (cgen('a'),
657 a_p, a_v)
658 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (cgen('d'),
659 d_p, d_v)
781 d_a = '<div class="added %s" style="width:%s%%">%s</div>' % (
782 cgen('a'), a_p, a_v
783 )
784 d_d = '<div class="deleted %s" style="width:%s%%">%s</div>' % (
785 cgen('d'), d_p, d_v
786 )
660 787 return literal('<div style="width:%spx">%s%s</div>' % (width, d_a, d_d))
661 788
662 789
663 def urlify_text(text):
790 def urlify_text(text_):
664 791 import re
665 792
666 url_pat = re.compile(r'(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)')
793 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]'''
794 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
667 795
668 796 def url_func(match_obj):
669 797 url_full = match_obj.groups()[0]
670 return '<a href="%(url)s">%(url)s</a>' % ({'url':url_full})
798 return '<a href="%(url)s">%(url)s</a>' % ({'url': url_full})
799
800 return literal(url_pat.sub(url_func, text_))
801
802
803 def urlify_changesets(text_, repository):
804 import re
805 URL_PAT = re.compile(r'([0-9a-fA-F]{12,})')
806
807 def url_func(match_obj):
808 rev = match_obj.groups()[0]
809 pref = ''
810 if match_obj.group().startswith(' '):
811 pref = ' '
812 tmpl = (
813 '%(pref)s<a class="%(cls)s" href="%(url)s">'
814 '%(rev)s'
815 '</a>'
816 )
817 return tmpl % {
818 'pref': pref,
819 'cls': 'revision-link',
820 'url': url('changeset_home', repo_name=repository, revision=rev),
821 'rev': rev,
822 }
823
824 newtext = URL_PAT.sub(url_func, text_)
825
826 return newtext
827
828
829 def urlify_commit(text_, repository=None, link_=None):
830 """
831 Parses given text message and makes proper links.
832 issues are linked to given issue-server, and rest is a changeset link
833 if link_ is given, in other case it's a plain text
834
835 :param text_:
836 :param repository:
837 :param link_: changeset link
838 """
839 import re
840 import traceback
841
842 # urlify changesets
843 text_ = urlify_changesets(text_, repository)
844
845 def linkify_others(t, l):
846 urls = re.compile(r'(\<a.*?\<\/a\>)',)
847 links = []
848 for e in urls.split(t):
849 if not urls.match(e):
850 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
851 else:
852 links.append(e)
671 853
672 return literal(url_pat.sub(url_func, text))
854 return ''.join(links)
855 try:
856 conf = config['app_conf']
857
858 URL_PAT = re.compile(r'%s' % conf.get('issue_pat'))
859
860 if URL_PAT:
861 ISSUE_SERVER_LNK = conf.get('issue_server_link')
862 ISSUE_PREFIX = conf.get('issue_prefix')
863
864 def url_func(match_obj):
865 pref = ''
866 if match_obj.group().startswith(' '):
867 pref = ' '
868
869 issue_id = ''.join(match_obj.groups())
870 tmpl = (
871 '%(pref)s<a class="%(cls)s" href="%(url)s">'
872 '%(issue-prefix)s%(id-repr)s'
873 '</a>'
874 )
875 url = ISSUE_SERVER_LNK.replace('{id}', issue_id)
876 if repository:
877 url = url.replace('{repo}', repository)
878
879 return tmpl % {
880 'pref': pref,
881 'cls': 'issue-tracker-link',
882 'url': url,
883 'id-repr': issue_id,
884 'issue-prefix': ISSUE_PREFIX,
885 'serv': ISSUE_SERVER_LNK,
886 }
887
888 newtext = URL_PAT.sub(url_func, text_)
889
890 if link_:
891 # wrap not links into final link => link_
892 newtext = linkify_others(newtext, link_)
893
894 return literal(newtext)
895 except:
896 log.error(traceback.format_exc())
897 pass
898
899 return text_
900
901
902 def rst(source):
903 return literal('<div class="rst-block">%s</div>' %
904 MarkupRenderer.rst(source))
905
906
907 def rst_w_mentions(source):
908 """
909 Wrapped rst renderer with @mention highlighting
910
911 :param source:
912 """
913 return literal('<div class="rst-block">%s</div>' %
914 MarkupRenderer.rst_with_mentions(source))
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Aug 6, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -33,15 +33,14 b' from rhodecode.lib.utils import action_l'
33 33
34 34
35 35 def repo_size(ui, repo, hooktype=None, **kwargs):
36 """Presents size of repository after push
36 """
37 Presents size of repository after push
37 38
38 39 :param ui:
39 40 :param repo:
40 41 :param hooktype:
41 42 """
42 43
43 if hooktype != 'changegroup':
44 return False
45 44 size_hg, size_root = 0, 0
46 45 for path, dirs, files in os.walk(repo.root):
47 46 if path.find('.hg') != -1:
@@ -60,12 +59,20 b' def repo_size(ui, repo, hooktype=None, *'
60 59 size_hg_f = h.format_byte_size(size_hg)
61 60 size_root_f = h.format_byte_size(size_root)
62 61 size_total_f = h.format_byte_size(size_root + size_hg)
63 sys.stdout.write('Repository size .hg:%s repo:%s total:%s\n' \
64 % (size_hg_f, size_root_f, size_total_f))
62
63 last_cs = repo[len(repo) - 1]
64
65 msg = ('Repository size .hg:%s repo:%s total:%s\n'
66 'Last revision is now r%s:%s\n') % (
67 size_hg_f, size_root_f, size_total_f, last_cs.rev(), last_cs.hex()[:12]
68 )
69
70 sys.stdout.write(msg)
65 71
66 72
67 73 def log_pull_action(ui, repo, **kwargs):
68 """Logs user last pull action
74 """
75 Logs user last pull action
69 76
70 77 :param ui:
71 78 :param repo:
@@ -76,13 +83,15 b' def log_pull_action(ui, repo, **kwargs):'
76 83 repository = extra_params['repository']
77 84 action = 'pull'
78 85
79 action_logger(username, action, repository, extra_params['ip'])
86 action_logger(username, action, repository, extra_params['ip'],
87 commit=True)
80 88
81 89 return 0
82 90
83 91
84 92 def log_push_action(ui, repo, **kwargs):
85 """Maps user last push action to new changeset id, from mercurial
93 """
94 Maps user last push action to new changeset id, from mercurial
86 95
87 96 :param ui:
88 97 :param repo:
@@ -110,6 +119,37 b' def log_push_action(ui, repo, **kwargs):'
110 119
111 120 action = action % ','.join(revs)
112 121
113 action_logger(username, action, repository, extra_params['ip'])
122 action_logger(username, action, repository, extra_params['ip'],
123 commit=True)
114 124
115 125 return 0
126
127
128 def log_create_repository(repository_dict, created_by, **kwargs):
129 """
130 Post create repository Hook. This is a dummy function for admins to re-use
131 if needed
132
133 :param repository: dict dump of repository object
134 :param created_by: username who created repository
135 :param created_date: date of creation
136
137 available keys of repository_dict:
138
139 'repo_type',
140 'description',
141 'private',
142 'created_on',
143 'enable_downloads',
144 'repo_id',
145 'user_id',
146 'enable_statistics',
147 'clone_uri',
148 'fork_id',
149 'group_id',
150 'repo_name'
151
152 """
153
154
155 return 0
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Aug 17, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -37,38 +37,39 b' from whoosh.analysis import RegexTokeniz'
37 37 from whoosh.fields import TEXT, ID, STORED, Schema, FieldType
38 38 from whoosh.index import create_in, open_dir
39 39 from whoosh.formats import Characters
40 from whoosh.highlight import highlight, SimpleFragmenter, HtmlFormatter
40 from whoosh.highlight import highlight, HtmlFormatter, ContextFragmenter
41 41
42 42 from webhelpers.html.builder import escape
43 43 from sqlalchemy import engine_from_config
44 from vcs.utils.lazy import LazyProperty
45 44
46 45 from rhodecode.model import init_model
47 46 from rhodecode.model.scm import ScmModel
48 47 from rhodecode.model.repo import RepoModel
49 48 from rhodecode.config.environment import load_environment
50 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP
49 from rhodecode.lib import LANGUAGES_EXTENSIONS_MAP, LazyProperty
51 50 from rhodecode.lib.utils import BasePasterCommand, Command, add_cache
52 51
53 #EXTENSIONS WE WANT TO INDEX CONTENT OFF
52 # EXTENSIONS WE WANT TO INDEX CONTENT OFF
54 53 INDEX_EXTENSIONS = LANGUAGES_EXTENSIONS_MAP.keys()
55 54
56 #CUSTOM ANALYZER wordsplit + lowercase filter
55 # CUSTOM ANALYZER wordsplit + lowercase filter
57 56 ANALYZER = RegexTokenizer(expression=r"\w+") | LowercaseFilter()
58 57
59 58
60 59 #INDEX SCHEMA DEFINITION
61 SCHEMA = Schema(owner=TEXT(),
62 repository=TEXT(stored=True),
63 path=TEXT(stored=True),
64 content=FieldType(format=Characters(ANALYZER),
65 scorable=True, stored=True),
66 modtime=STORED(), extension=TEXT(stored=True))
67
60 SCHEMA = Schema(
61 owner=TEXT(),
62 repository=TEXT(stored=True),
63 path=TEXT(stored=True),
64 content=FieldType(format=Characters(), analyzer=ANALYZER,
65 scorable=True, stored=True),
66 modtime=STORED(),
67 extension=TEXT(stored=True)
68 )
68 69
69 70 IDX_NAME = 'HG_INDEX'
70 71 FORMATTER = HtmlFormatter('span', between='\n<span class="break">...</span>\n')
71 FRAGMENTER = SimpleFragmenter(200)
72 FRAGMENTER = ContextFragmenter(200)
72 73
73 74
74 75 class MakeIndex(BasePasterCommand):
@@ -129,13 +130,14 b' class MakeIndex(BasePasterCommand):'
129 130 " destroy old and build from scratch",
130 131 default=False)
131 132
133
132 134 class ResultWrapper(object):
133 135 def __init__(self, search_type, searcher, matcher, highlight_items):
134 136 self.search_type = search_type
135 137 self.searcher = searcher
136 138 self.matcher = matcher
137 139 self.highlight_items = highlight_items
138 self.fragment_size = 200 / 2
140 self.fragment_size = 200
139 141
140 142 @LazyProperty
141 143 def doc_ids(self):
@@ -171,11 +173,10 b' class ResultWrapper(object):'
171 173 """
172 174 i, j = key.start, key.stop
173 175
174 slice = []
176 slices = []
175 177 for docid in self.doc_ids[i:j]:
176 slice.append(self.get_full_content(docid))
177 return slice
178
178 slices.append(self.get_full_content(docid))
179 return slices
179 180
180 181 def get_full_content(self, docid):
181 182 res = self.searcher.stored_fields(docid[0])
@@ -183,9 +184,9 b' class ResultWrapper(object):'
183 184 + len(res['repository']):].lstrip('/')
184 185
185 186 content_short = self.get_short_content(res, docid[1])
186 res.update({'content_short':content_short,
187 'content_short_hl':self.highlight(content_short),
188 'f_path':f_path})
187 res.update({'content_short': content_short,
188 'content_short_hl': self.highlight(content_short),
189 'f_path': f_path})
189 190
190 191 return res
191 192
@@ -198,7 +199,7 b' class ResultWrapper(object):'
198 199 Smart function that implements chunking the content
199 200 but not overlap chunks so it doesn't highlight the same
200 201 close occurrences twice.
201
202
202 203 :param matcher:
203 204 :param size:
204 205 """
@@ -217,10 +218,12 b' class ResultWrapper(object):'
217 218 def highlight(self, content, top=5):
218 219 if self.search_type != 'content':
219 220 return ''
220 hl = highlight(escape(content),
221 self.highlight_items,
222 analyzer=ANALYZER,
223 fragmenter=FRAGMENTER,
224 formatter=FORMATTER,
225 top=top)
221 hl = highlight(
222 text=escape(content),
223 terms=self.highlight_items,
224 analyzer=ANALYZER,
225 fragmenter=FRAGMENTER,
226 formatter=FORMATTER,
227 top=top
228 )
226 229 return hl
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: Jan 26, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -43,12 +43,12 b' from rhodecode.model.scm import ScmModel'
43 43 from rhodecode.lib import safe_unicode
44 44 from rhodecode.lib.indexers import INDEX_EXTENSIONS, SCHEMA, IDX_NAME
45 45
46 from vcs.exceptions import ChangesetError, RepositoryError
46 from rhodecode.lib.vcs.exceptions import ChangesetError, RepositoryError, \
47 NodeDoesNotExistError
47 48
48 49 from whoosh.index import create_in, open_dir
49 50
50 51
51
52 52 log = logging.getLogger('whooshIndexer')
53 53 # create logger
54 54 log.setLevel(logging.DEBUG)
@@ -67,12 +67,13 b' ch.setFormatter(formatter)'
67 67 # add ch to logger
68 68 log.addHandler(ch)
69 69
70
70 71 class WhooshIndexingDaemon(object):
71 72 """
72 73 Daemon for atomic jobs
73 74 """
74 75
75 def __init__(self, indexname='HG_INDEX', index_location=None,
76 def __init__(self, indexname=IDX_NAME, index_location=None,
76 77 repo_location=None, sa=None, repo_list=None):
77 78 self.indexname = indexname
78 79
@@ -94,7 +95,6 b' class WhooshIndexingDaemon(object):'
94 95
95 96 self.repo_paths = filtered_repo_paths
96 97
97
98 98 self.initial = False
99 99 if not os.path.isdir(self.index_location):
100 100 os.makedirs(self.index_location)
@@ -154,7 +154,6 b' class WhooshIndexingDaemon(object):'
154 154 modtime=self.get_node_mtime(node),
155 155 extension=node.extension)
156 156
157
158 157 def build_index(self):
159 158 if os.path.exists(self.index_location):
160 159 log.debug('removing previous index')
@@ -176,7 +175,6 b' class WhooshIndexingDaemon(object):'
176 175 writer.commit(merge=True)
177 176 log.debug('>>> FINISHED BUILDING INDEX <<<')
178 177
179
180 178 def update_index(self):
181 179 log.debug('STARTING INCREMENTAL INDEXING UPDATE')
182 180
@@ -198,7 +196,7 b' class WhooshIndexingDaemon(object):'
198 196
199 197 try:
200 198 node = self.get_node(repo, indexed_path)
201 except ChangesetError:
199 except (ChangesetError, NodeDoesNotExistError):
202 200 # This file was deleted since it was indexed
203 201 log.debug('removing from index %s' % indexed_path)
204 202 writer.delete_by_term('path', indexed_path)
@@ -7,7 +7,7 b''
7 7
8 8 :created_on: May 23, 2010
9 9 :author: marcink
10 :copyright: (C) 2009-2011 Marcin Kuzminski <marcin@python-works.com>
10 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
11 11 :license: GPLv3, see COPYING for more details.
12 12 """
13 13 # This program is free software: you can redistribute it and/or modify
@@ -8,7 +8,7 b''
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
@@ -27,7 +27,6 b''
27 27 import os
28 28 import logging
29 29 import traceback
30 import time
31 30
32 31 from dulwich import server as dulserver
33 32
@@ -67,13 +66,12 b' dulserver.DEFAULT_HANDLERS = {'
67 66 from dulwich.repo import Repo
68 67 from dulwich.web import HTTPGitApplication
69 68
70 from paste.auth.basic import AuthBasicAuthenticator
71 69 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
72 70
73 71 from rhodecode.lib import safe_str
74 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
75 from rhodecode.lib.utils import invalidate_cache, is_valid_repo
76 from rhodecode.model import meta
72 from rhodecode.lib.base import BaseVCSController
73 from rhodecode.lib.auth import get_container_username
74 from rhodecode.lib.utils import is_valid_repo
77 75 from rhodecode.model.db import User
78 76
79 77 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
@@ -93,24 +91,7 b' def is_git(environ):'
93 91 return False
94 92
95 93
96 class SimpleGit(object):
97
98 def __init__(self, application, config):
99 self.application = application
100 self.config = config
101 # base path of repo locations
102 self.basepath = self.config['base_path']
103 #authenticate this mercurial request using authfunc
104 self.authenticate = AuthBasicAuthenticator('', authfunc)
105
106 def __call__(self, environ, start_response):
107 start = time.time()
108 try:
109 return self._handle_request(environ, start_response)
110 finally:
111 log = logging.getLogger(self.__class__.__name__)
112 log.debug('Request time: %.3fs' % (time.time() - start))
113 meta.Session.remove()
94 class SimpleGit(BaseVCSController):
114 95
115 96 def _handle_request(self, environ, start_response):
116 97 if not is_git(environ):
@@ -143,9 +124,8 b' class SimpleGit(object):'
143 124 if action in ['pull', 'push']:
144 125 anonymous_user = self.__get_user('default')
145 126 username = anonymous_user.username
146 anonymous_perm = self.__check_permission(action,
147 anonymous_user,
148 repo_name)
127 anonymous_perm = self._check_permission(action, anonymous_user,
128 repo_name)
149 129
150 130 if anonymous_perm is not True or anonymous_user.active is False:
151 131 if anonymous_perm is not True:
@@ -159,27 +139,29 b' class SimpleGit(object):'
159 139 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
160 140 #==============================================================
161 141
162 if not REMOTE_USER(environ):
142 # Attempting to retrieve username from the container
143 username = get_container_username(environ, self.config)
144
145 # If not authenticated by the container, running basic auth
146 if not username:
163 147 self.authenticate.realm = \
164 148 safe_str(self.config['rhodecode_realm'])
165 149 result = self.authenticate(environ)
166 150 if isinstance(result, str):
167 151 AUTH_TYPE.update(environ, 'basic')
168 152 REMOTE_USER.update(environ, result)
153 username = result
169 154 else:
170 155 return result.wsgi_application(environ, start_response)
171 156
172 157 #==============================================================
173 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
174 # BASIC AUTH
158 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
175 159 #==============================================================
176
177 160 if action in ['pull', 'push']:
178 username = REMOTE_USER(environ)
179 161 try:
180 162 user = self.__get_user(username)
181 if user is None:
182 return HTTPForbidden()(environ, start_response)
163 if user is None or not user.active:
164 return HTTPForbidden()(environ, start_response)
183 165 username = user.username
184 166 except:
185 167 log.error(traceback.format_exc())
@@ -187,16 +169,11 b' class SimpleGit(object):'
187 169 start_response)
188 170
189 171 #check permissions for this repository
190 perm = self.__check_permission(action, user,
172 perm = self._check_permission(action, user,
191 173 repo_name)
192 174 if perm is not True:
193 175 return HTTPForbidden()(environ, start_response)
194 176
195 extras = {'ip': ipaddr,
196 'username': username,
197 'action': action,
198 'repository': repo_name}
199
200 177 #===================================================================
201 178 # GIT REQUEST HANDLING
202 179 #===================================================================
@@ -211,8 +188,8 b' class SimpleGit(object):'
211 188 try:
212 189 #invalidate cache on push
213 190 if action == 'push':
214 self.__invalidate_cache(repo_name)
215
191 self._invalidate_cache(repo_name)
192 log.info('%s action on GIT repo "%s"' % (action, repo_name))
216 193 app = self.__make_app(repo_name, repo_path)
217 194 return app(environ, start_response)
218 195 except Exception:
@@ -222,7 +199,7 b' class SimpleGit(object):'
222 199 def __make_app(self, repo_name, repo_path):
223 200 """
224 201 Make an wsgi application using dulserver
225
202
226 203 :param repo_name: name of the repository
227 204 :param repo_path: full path to the repository
228 205 """
@@ -233,31 +210,6 b' class SimpleGit(object):'
233 210
234 211 return gitserve
235 212
236 def __check_permission(self, action, user, repo_name):
237 """
238 Checks permissions using action (push/pull) user and repository
239 name
240
241 :param action: push or pull action
242 :param user: user instance
243 :param repo_name: repository name
244 """
245 if action == 'push':
246 if not HasPermissionAnyMiddleware('repository.write',
247 'repository.admin')(user,
248 repo_name):
249 return False
250
251 else:
252 #any other action need at least read permission
253 if not HasPermissionAnyMiddleware('repository.read',
254 'repository.write',
255 'repository.admin')(user,
256 repo_name):
257 return False
258
259 return True
260
261 213 def __get_repository(self, environ):
262 214 """
263 215 Get's repository name out of PATH_INFO header
@@ -265,6 +217,7 b' class SimpleGit(object):'
265 217 :param environ: environ where PATH_INFO is stored
266 218 """
267 219 try:
220 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
268 221 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
269 222 if repo_name.endswith('/'):
270 223 repo_name = repo_name.rstrip('/')
@@ -293,10 +246,3 b' class SimpleGit(object):'
293 246 service_cmd if service_cmd else 'other')
294 247 else:
295 248 return 'other'
296
297 def __invalidate_cache(self, repo_name):
298 """we know that some change was made to repositories and we should
299 invalidate the cache to see the changes right away but only for
300 push requests"""
301 invalidate_cache('get_repo_cached_%s' % repo_name)
302
@@ -8,7 +8,7 b''
8 8
9 9 :created_on: Apr 28, 2010
10 10 :author: marcink
11 :copyright: (C) 2009-2010 Marcin Kuzminski <marcin@python-works.com>
11 :copyright: (C) 2010-2012 Marcin Kuzminski <marcin@python-works.com>
12 12 :license: GPLv3, see COPYING for more details.
13 13 """
14 14 # This program is free software: you can redistribute it and/or modify
@@ -27,19 +27,16 b''
27 27 import os
28 28 import logging
29 29 import traceback
30 import time
31 30
32 31 from mercurial.error import RepoError
33 32 from mercurial.hgweb import hgweb_mod
34 33
35 from paste.auth.basic import AuthBasicAuthenticator
36 34 from paste.httpheaders import REMOTE_USER, AUTH_TYPE
37 35
38 36 from rhodecode.lib import safe_str
39 from rhodecode.lib.auth import authfunc, HasPermissionAnyMiddleware
40 from rhodecode.lib.utils import make_ui, invalidate_cache, \
41 is_valid_repo, ui_sections
42 from rhodecode.model import meta
37 from rhodecode.lib.base import BaseVCSController
38 from rhodecode.lib.auth import get_container_username
39 from rhodecode.lib.utils import make_ui, is_valid_repo, ui_sections
43 40 from rhodecode.model.db import User
44 41
45 42 from webob.exc import HTTPNotFound, HTTPForbidden, HTTPInternalServerError
@@ -57,25 +54,7 b' def is_mercurial(environ):'
57 54 return False
58 55
59 56
60 class SimpleHg(object):
61
62 def __init__(self, application, config):
63 self.application = application
64 self.config = config
65 # base path of repo locations
66 self.basepath = self.config['base_path']
67 #authenticate this mercurial request using authfunc
68 self.authenticate = AuthBasicAuthenticator('', authfunc)
69 self.ipaddr = '0.0.0.0'
70
71 def __call__(self, environ, start_response):
72 start = time.time()
73 try:
74 return self._handle_request(environ, start_response)
75 finally:
76 log = logging.getLogger(self.__class__.__name__)
77 log.debug('Request time: %.3fs' % (time.time() - start))
78 meta.Session.remove()
57 class SimpleHg(BaseVCSController):
79 58
80 59 def _handle_request(self, environ, start_response):
81 60 if not is_mercurial(environ):
@@ -101,7 +80,6 b' class SimpleHg(object):'
101 80 # GET ACTION PULL or PUSH
102 81 #======================================================================
103 82 action = self.__get_action(environ)
104
105 83 #======================================================================
106 84 # CHECK ANONYMOUS PERMISSION
107 85 #======================================================================
@@ -109,9 +87,8 b' class SimpleHg(object):'
109 87 anonymous_user = self.__get_user('default')
110 88
111 89 username = anonymous_user.username
112 anonymous_perm = self.__check_permission(action,
113 anonymous_user,
114 repo_name)
90 anonymous_perm = self._check_permission(action, anonymous_user,
91 repo_name)
115 92
116 93 if anonymous_perm is not True or anonymous_user.active is False:
117 94 if anonymous_perm is not True:
@@ -125,26 +102,28 b' class SimpleHg(object):'
125 102 # NEED TO AUTHENTICATE AND ASK FOR AUTH USER PERMISSIONS
126 103 #==============================================================
127 104
128 if not REMOTE_USER(environ):
105 # Attempting to retrieve username from the container
106 username = get_container_username(environ, self.config)
107
108 # If not authenticated by the container, running basic auth
109 if not username:
129 110 self.authenticate.realm = \
130 111 safe_str(self.config['rhodecode_realm'])
131 112 result = self.authenticate(environ)
132 113 if isinstance(result, str):
133 114 AUTH_TYPE.update(environ, 'basic')
134 115 REMOTE_USER.update(environ, result)
116 username = result
135 117 else:
136 118 return result.wsgi_application(environ, start_response)
137 119
138 120 #==============================================================
139 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME FROM
140 # BASIC AUTH
121 # CHECK PERMISSIONS FOR THIS REQUEST USING GIVEN USERNAME
141 122 #==============================================================
142
143 123 if action in ['pull', 'push']:
144 username = REMOTE_USER(environ)
145 124 try:
146 125 user = self.__get_user(username)
147 if user is None:
126 if user is None or not user.active:
148 127 return HTTPForbidden()(environ, start_response)
149 128 username = user.username
150 129 except:
@@ -153,7 +132,7 b' class SimpleHg(object):'
153 132 start_response)
154 133
155 134 #check permissions for this repository
156 perm = self.__check_permission(action, user,
135 perm = self._check_permission(action, user,
157 136 repo_name)
158 137 if perm is not True:
159 138 return HTTPForbidden()(environ, start_response)
@@ -173,16 +152,15 b' class SimpleHg(object):'
173 152 baseui = make_ui('db')
174 153 self.__inject_extras(repo_path, baseui, extras)
175 154
176
177 155 # quick check if that dir exists...
178 156 if is_valid_repo(repo_name, self.basepath) is False:
179 157 return HTTPNotFound()(environ, start_response)
180 158
181 159 try:
182 #invalidate cache on push
160 # invalidate cache on push
183 161 if action == 'push':
184 self.__invalidate_cache(repo_name)
185
162 self._invalidate_cache(repo_name)
163 log.info('%s action on HG repo "%s"' % (action, repo_name))
186 164 app = self.__make_app(repo_path, baseui, extras)
187 165 return app(environ, start_response)
188 166 except RepoError, e:
@@ -199,32 +177,6 b' class SimpleHg(object):'
199 177 """
200 178 return hgweb_mod.hgweb(repo_name, name=repo_name, baseui=baseui)
201 179
202
203 def __check_permission(self, action, user, repo_name):
204 """
205 Checks permissions using action (push/pull) user and repository
206 name
207
208 :param action: push or pull action
209 :param user: user instance
210 :param repo_name: repository name
211 """
212 if action == 'push':
213 if not HasPermissionAnyMiddleware('repository.write',
214 'repository.admin')(user,
215 repo_name):
216 return False
217
218 else:
219 #any other action need at least read permission
220 if not HasPermissionAnyMiddleware('repository.read',
221 'repository.write',
222 'repository.admin')(user,
223 repo_name):
224 return False
225
226 return True
227
228 180 def __get_repository(self, environ):
229 181 """
230 182 Get's repository name out of PATH_INFO header
@@ -232,6 +184,7 b' class SimpleHg(object):'
232 184 :param environ: environ where PATH_INFO is stored
233 185 """
234 186 try:
187 environ['PATH_INFO'] = self._get_by_id(environ['PATH_INFO'])
235 188 repo_name = '/'.join(environ['PATH_INFO'].split('/')[1:])
236 189 if repo_name.endswith('/'):
237 190 repo_name = repo_name.rstrip('/')
@@ -265,18 +218,12 b' class SimpleHg(object):'
265 218 else:
266 219 return 'pull'
267 220
268 def __invalidate_cache(self, repo_name):
269 """we know that some change was made to repositories and we should
270 invalidate the cache to see the changes right away but only for
271 push requests"""
272 invalidate_cache('get_repo_cached_%s' % repo_name)
273
274 221 def __inject_extras(self, repo_path, baseui, extras={}):
275 222 """
276 223 Injects some extra params into baseui instance
277
224
278 225 also overwrites global settings with those takes from local hgrc file
279
226
280 227 :param baseui: baseui instance
281 228 :param extras: dict with extra params to put into baseui
282 229 """
@@ -298,4 +245,3 b' class SimpleHg(object):'
298 245 for section in ui_sections:
299 246 for k, v in repoui.configitems(section):
300 247 baseui.setconfig(section, k, v)
301
1 NO CONTENT: file renamed from rhodecode/lib/smtp_mailer.py to rhodecode/lib/rcmail/smtp_mailer.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file chmod 100755 => 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/templates/settings/repo_fork.html to rhodecode/templates/forks/fork.html
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file chmod 100755 => 100644, file renamed from rhodecode/tests/test_concurency.py to rhodecode/tests/_test_concurency.py
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed, binary diff hidden
1 NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now