##// END OF EJS Templates
imports: try to use global imports unless it is a layering violation...
Mads Kiilerich -
r8495:f3fab7b1 default
parent child Browse files
Show More
@@ -1,229 +1,227 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.middleware.pygrack
15 kallithea.lib.middleware.pygrack
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Python implementation of git-http-backend's Smart HTTP protocol
18 Python implementation of git-http-backend's Smart HTTP protocol
19
19
20 Based on original code from git_http_backend.py project.
20 Based on original code from git_http_backend.py project.
21
21
22 Copyright (c) 2010 Daniel Dotsenko <dotsa@hotmail.com>
22 Copyright (c) 2010 Daniel Dotsenko <dotsa@hotmail.com>
23 Copyright (c) 2012 Marcin Kuzminski <marcin@python-works.com>
23 Copyright (c) 2012 Marcin Kuzminski <marcin@python-works.com>
24
24
25 This file was forked by the Kallithea project in July 2014.
25 This file was forked by the Kallithea project in July 2014.
26 """
26 """
27
27
28 import logging
28 import logging
29 import os
29 import os
30 import socket
30 import socket
31 import traceback
31 import traceback
32
32
33 from dulwich.server import update_server_info
33 from dulwich.server import update_server_info
34 from dulwich.web import GunzipFilter, LimitedInputFilter
34 from dulwich.web import GunzipFilter, LimitedInputFilter
35 from webob import Request, Response, exc
35 from webob import Request, Response, exc
36
36
37 import kallithea
37 import kallithea
38 from kallithea.lib.utils2 import ascii_bytes
38 from kallithea.lib.utils2 import ascii_bytes
39 from kallithea.lib.vcs import subprocessio
39 from kallithea.lib.vcs import get_repo, subprocessio
40
40
41
41
42 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
43
43
44
44
45 class FileWrapper(object):
45 class FileWrapper(object):
46
46
47 def __init__(self, fd, content_length):
47 def __init__(self, fd, content_length):
48 self.fd = fd
48 self.fd = fd
49 self.content_length = content_length
49 self.content_length = content_length
50 self.remain = content_length
50 self.remain = content_length
51
51
52 def read(self, size):
52 def read(self, size):
53 if size <= self.remain:
53 if size <= self.remain:
54 try:
54 try:
55 data = self.fd.read(size)
55 data = self.fd.read(size)
56 except socket.error:
56 except socket.error:
57 raise IOError(self)
57 raise IOError(self)
58 self.remain -= size
58 self.remain -= size
59 elif self.remain:
59 elif self.remain:
60 data = self.fd.read(self.remain)
60 data = self.fd.read(self.remain)
61 self.remain = 0
61 self.remain = 0
62 else:
62 else:
63 data = None
63 data = None
64 return data
64 return data
65
65
66 def __repr__(self):
66 def __repr__(self):
67 return '<FileWrapper %s len: %s, read: %s>' % (
67 return '<FileWrapper %s len: %s, read: %s>' % (
68 self.fd, self.content_length, self.content_length - self.remain
68 self.fd, self.content_length, self.content_length - self.remain
69 )
69 )
70
70
71
71
72 class GitRepository(object):
72 class GitRepository(object):
73 git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
73 git_folder_signature = set(['config', 'head', 'info', 'objects', 'refs'])
74 commands = ['git-upload-pack', 'git-receive-pack']
74 commands = ['git-upload-pack', 'git-receive-pack']
75
75
76 def __init__(self, repo_name, content_path):
76 def __init__(self, repo_name, content_path):
77 files = set([f.lower() for f in os.listdir(content_path)])
77 files = set([f.lower() for f in os.listdir(content_path)])
78 if not (self.git_folder_signature.intersection(files)
78 if not (self.git_folder_signature.intersection(files)
79 == self.git_folder_signature):
79 == self.git_folder_signature):
80 raise OSError('%s missing git signature' % content_path)
80 raise OSError('%s missing git signature' % content_path)
81 self.content_path = content_path
81 self.content_path = content_path
82 self.valid_accepts = ['application/x-%s-result' %
82 self.valid_accepts = ['application/x-%s-result' %
83 c for c in self.commands]
83 c for c in self.commands]
84 self.repo_name = repo_name
84 self.repo_name = repo_name
85
85
86 def _get_fixedpath(self, path):
86 def _get_fixedpath(self, path):
87 """
87 """
88 Small fix for repo_path
88 Small fix for repo_path
89
89
90 :param path:
90 :param path:
91 """
91 """
92 assert path.startswith('/' + self.repo_name + '/')
92 assert path.startswith('/' + self.repo_name + '/')
93 return path[len(self.repo_name) + 2:].strip('/')
93 return path[len(self.repo_name) + 2:].strip('/')
94
94
95 def inforefs(self, req, environ):
95 def inforefs(self, req, environ):
96 """
96 """
97 WSGI Response producer for HTTP GET Git Smart
97 WSGI Response producer for HTTP GET Git Smart
98 HTTP /info/refs request.
98 HTTP /info/refs request.
99 """
99 """
100
100
101 git_command = req.GET.get('service')
101 git_command = req.GET.get('service')
102 if git_command not in self.commands:
102 if git_command not in self.commands:
103 log.debug('command %s not allowed', git_command)
103 log.debug('command %s not allowed', git_command)
104 return exc.HTTPMethodNotAllowed()
104 return exc.HTTPMethodNotAllowed()
105
105
106 # From Documentation/technical/http-protocol.txt shipped with Git:
106 # From Documentation/technical/http-protocol.txt shipped with Git:
107 #
107 #
108 # Clients MUST verify the first pkt-line is `# service=$servicename`.
108 # Clients MUST verify the first pkt-line is `# service=$servicename`.
109 # Servers MUST set $servicename to be the request parameter value.
109 # Servers MUST set $servicename to be the request parameter value.
110 # Servers SHOULD include an LF at the end of this line.
110 # Servers SHOULD include an LF at the end of this line.
111 # Clients MUST ignore an LF at the end of the line.
111 # Clients MUST ignore an LF at the end of the line.
112 #
112 #
113 # smart_reply = PKT-LINE("# service=$servicename" LF)
113 # smart_reply = PKT-LINE("# service=$servicename" LF)
114 # ref_list
114 # ref_list
115 # "0000"
115 # "0000"
116 server_advert = '# service=%s\n' % git_command
116 server_advert = '# service=%s\n' % git_command
117 packet_len = hex(len(server_advert) + 4)[2:].rjust(4, '0').lower()
117 packet_len = hex(len(server_advert) + 4)[2:].rjust(4, '0').lower()
118 _git_path = kallithea.CONFIG.get('git_path', 'git')
118 _git_path = kallithea.CONFIG.get('git_path', 'git')
119 cmd = [_git_path, git_command[4:],
119 cmd = [_git_path, git_command[4:],
120 '--stateless-rpc', '--advertise-refs', self.content_path]
120 '--stateless-rpc', '--advertise-refs', self.content_path]
121 log.debug('handling cmd %s', cmd)
121 log.debug('handling cmd %s', cmd)
122 try:
122 try:
123 out = subprocessio.SubprocessIOChunker(cmd,
123 out = subprocessio.SubprocessIOChunker(cmd,
124 starting_values=[ascii_bytes(packet_len + server_advert + '0000')]
124 starting_values=[ascii_bytes(packet_len + server_advert + '0000')]
125 )
125 )
126 except EnvironmentError as e:
126 except EnvironmentError as e:
127 log.error(traceback.format_exc())
127 log.error(traceback.format_exc())
128 raise exc.HTTPExpectationFailed()
128 raise exc.HTTPExpectationFailed()
129 resp = Response()
129 resp = Response()
130 resp.content_type = 'application/x-%s-advertisement' % git_command
130 resp.content_type = 'application/x-%s-advertisement' % git_command
131 resp.charset = None
131 resp.charset = None
132 resp.app_iter = out
132 resp.app_iter = out
133 return resp
133 return resp
134
134
135 def backend(self, req, environ):
135 def backend(self, req, environ):
136 """
136 """
137 WSGI Response producer for HTTP POST Git Smart HTTP requests.
137 WSGI Response producer for HTTP POST Git Smart HTTP requests.
138 Reads commands and data from HTTP POST's body.
138 Reads commands and data from HTTP POST's body.
139 returns an iterator obj with contents of git command's
139 returns an iterator obj with contents of git command's
140 response to stdout
140 response to stdout
141 """
141 """
142 _git_path = kallithea.CONFIG.get('git_path', 'git')
142 _git_path = kallithea.CONFIG.get('git_path', 'git')
143 git_command = self._get_fixedpath(req.path_info)
143 git_command = self._get_fixedpath(req.path_info)
144 if git_command not in self.commands:
144 if git_command not in self.commands:
145 log.debug('command %s not allowed', git_command)
145 log.debug('command %s not allowed', git_command)
146 return exc.HTTPMethodNotAllowed()
146 return exc.HTTPMethodNotAllowed()
147
147
148 if 'CONTENT_LENGTH' in environ:
148 if 'CONTENT_LENGTH' in environ:
149 inputstream = FileWrapper(environ['wsgi.input'],
149 inputstream = FileWrapper(environ['wsgi.input'],
150 req.content_length)
150 req.content_length)
151 else:
151 else:
152 inputstream = environ['wsgi.input']
152 inputstream = environ['wsgi.input']
153
153
154 gitenv = dict(os.environ)
154 gitenv = dict(os.environ)
155 # forget all configs
155 # forget all configs
156 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
156 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
157 cmd = [_git_path, git_command[4:], '--stateless-rpc', self.content_path]
157 cmd = [_git_path, git_command[4:], '--stateless-rpc', self.content_path]
158 log.debug('handling cmd %s', cmd)
158 log.debug('handling cmd %s', cmd)
159 try:
159 try:
160 out = subprocessio.SubprocessIOChunker(
160 out = subprocessio.SubprocessIOChunker(
161 cmd,
161 cmd,
162 inputstream=inputstream,
162 inputstream=inputstream,
163 env=gitenv,
163 env=gitenv,
164 cwd=self.content_path,
164 cwd=self.content_path,
165 )
165 )
166 except EnvironmentError as e:
166 except EnvironmentError as e:
167 log.error(traceback.format_exc())
167 log.error(traceback.format_exc())
168 raise exc.HTTPExpectationFailed()
168 raise exc.HTTPExpectationFailed()
169
169
170 if git_command in ['git-receive-pack']:
170 if git_command in ['git-receive-pack']:
171 # updating refs manually after each push.
171 # updating refs manually after each push.
172 # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
172 # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
173
174 from kallithea.lib.vcs import get_repo
175 repo = get_repo(self.content_path)
173 repo = get_repo(self.content_path)
176 if repo:
174 if repo:
177 update_server_info(repo._repo)
175 update_server_info(repo._repo)
178
176
179 resp = Response()
177 resp = Response()
180 resp.content_type = 'application/x-%s-result' % git_command
178 resp.content_type = 'application/x-%s-result' % git_command
181 resp.charset = None
179 resp.charset = None
182 resp.app_iter = out
180 resp.app_iter = out
183 return resp
181 return resp
184
182
185 def __call__(self, environ, start_response):
183 def __call__(self, environ, start_response):
186 req = Request(environ)
184 req = Request(environ)
187 _path = self._get_fixedpath(req.path_info)
185 _path = self._get_fixedpath(req.path_info)
188 if _path.startswith('info/refs'):
186 if _path.startswith('info/refs'):
189 app = self.inforefs
187 app = self.inforefs
190 elif req.accept.acceptable_offers(self.valid_accepts):
188 elif req.accept.acceptable_offers(self.valid_accepts):
191 app = self.backend
189 app = self.backend
192 try:
190 try:
193 resp = app(req, environ)
191 resp = app(req, environ)
194 except exc.HTTPException as e:
192 except exc.HTTPException as e:
195 resp = e
193 resp = e
196 log.error(traceback.format_exc())
194 log.error(traceback.format_exc())
197 except Exception as e:
195 except Exception as e:
198 log.error(traceback.format_exc())
196 log.error(traceback.format_exc())
199 resp = exc.HTTPInternalServerError()
197 resp = exc.HTTPInternalServerError()
200 return resp(environ, start_response)
198 return resp(environ, start_response)
201
199
202
200
203 class GitDirectory(object):
201 class GitDirectory(object):
204
202
205 def __init__(self, repo_root, repo_name):
203 def __init__(self, repo_root, repo_name):
206 repo_location = os.path.join(repo_root, repo_name)
204 repo_location = os.path.join(repo_root, repo_name)
207 if not os.path.isdir(repo_location):
205 if not os.path.isdir(repo_location):
208 raise OSError(repo_location)
206 raise OSError(repo_location)
209
207
210 self.content_path = repo_location
208 self.content_path = repo_location
211 self.repo_name = repo_name
209 self.repo_name = repo_name
212 self.repo_location = repo_location
210 self.repo_location = repo_location
213
211
214 def __call__(self, environ, start_response):
212 def __call__(self, environ, start_response):
215 content_path = self.content_path
213 content_path = self.content_path
216 try:
214 try:
217 app = GitRepository(self.repo_name, content_path)
215 app = GitRepository(self.repo_name, content_path)
218 except (AssertionError, OSError):
216 except (AssertionError, OSError):
219 content_path = os.path.join(content_path, '.git')
217 content_path = os.path.join(content_path, '.git')
220 if os.path.isdir(content_path):
218 if os.path.isdir(content_path):
221 app = GitRepository(self.repo_name, content_path)
219 app = GitRepository(self.repo_name, content_path)
222 else:
220 else:
223 return exc.HTTPNotFound()(environ, start_response)
221 return exc.HTTPNotFound()(environ, start_response)
224 return app(environ, start_response)
222 return app(environ, start_response)
225
223
226
224
227 def make_wsgi_app(repo_name, repo_root):
225 def make_wsgi_app(repo_name, repo_root):
228 app = GitDirectory(repo_root, repo_name)
226 app = GitDirectory(repo_root, repo_name)
229 return GunzipFilter(LimitedInputFilter(app))
227 return GunzipFilter(LimitedInputFilter(app))
@@ -1,412 +1,411 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.admin.settings
15 kallithea.controllers.admin.settings
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 settings controller for Kallithea admin
18 settings controller for Kallithea admin
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Jul 14, 2010
22 :created_on: Jul 14, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import logging
28 import logging
29 import traceback
29 import traceback
30
30
31 import formencode
31 import formencode
32 from formencode import htmlfill
32 from formencode import htmlfill
33 from tg import config, request
33 from tg import config, request
34 from tg import tmpl_context as c
34 from tg import tmpl_context as c
35 from tg.i18n import ugettext as _
35 from tg.i18n import ugettext as _
36 from webob.exc import HTTPFound
36 from webob.exc import HTTPFound
37
37
38 import kallithea
38 from kallithea.lib import webutils
39 from kallithea.lib import webutils
39 from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired
40 from kallithea.lib.auth import HasPermissionAnyDecorator, LoginRequired
40 from kallithea.lib.base import BaseController, render
41 from kallithea.lib.base import BaseController, render
41 from kallithea.lib.celerylib import tasks
42 from kallithea.lib.celerylib import tasks
42 from kallithea.lib.utils import repo2db_mapper, set_app_settings
43 from kallithea.lib.utils import repo2db_mapper, set_app_settings
43 from kallithea.lib.utils2 import safe_str
44 from kallithea.lib.utils2 import safe_str
44 from kallithea.lib.vcs import VCSError
45 from kallithea.lib.vcs import VCSError
45 from kallithea.lib.webutils import url
46 from kallithea.lib.webutils import url
46 from kallithea.model import db, meta
47 from kallithea.model import db, meta
47 from kallithea.model.forms import ApplicationSettingsForm, ApplicationUiSettingsForm, ApplicationVisualisationForm
48 from kallithea.model.forms import ApplicationSettingsForm, ApplicationUiSettingsForm, ApplicationVisualisationForm
48 from kallithea.model.notification import EmailNotificationModel
49 from kallithea.model.notification import EmailNotificationModel
49 from kallithea.model.scm import ScmModel
50 from kallithea.model.scm import ScmModel
50
51
51
52
52 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
53
54
54
55
55 class SettingsController(BaseController):
56 class SettingsController(BaseController):
56
57
57 @LoginRequired(allow_default_user=True)
58 @LoginRequired(allow_default_user=True)
58 def _before(self, *args, **kwargs):
59 def _before(self, *args, **kwargs):
59 super(SettingsController, self)._before(*args, **kwargs)
60 super(SettingsController, self)._before(*args, **kwargs)
60
61
61 def _get_hg_ui_settings(self):
62 def _get_hg_ui_settings(self):
62 ret = db.Ui.query().all()
63 ret = db.Ui.query().all()
63
64
64 settings = {}
65 settings = {}
65 for each in ret:
66 for each in ret:
66 k = each.ui_section + '_' + each.ui_key
67 k = each.ui_section + '_' + each.ui_key
67 v = each.ui_value
68 v = each.ui_value
68 if k == 'paths_/':
69 if k == 'paths_/':
69 k = 'paths_root_path'
70 k = 'paths_root_path'
70
71
71 k = k.replace('.', '_')
72 k = k.replace('.', '_')
72
73
73 if each.ui_section in ['hooks', 'extensions']:
74 if each.ui_section in ['hooks', 'extensions']:
74 v = each.ui_active
75 v = each.ui_active
75
76
76 settings[k] = v
77 settings[k] = v
77 return settings
78 return settings
78
79
79 @HasPermissionAnyDecorator('hg.admin')
80 @HasPermissionAnyDecorator('hg.admin')
80 def settings_vcs(self):
81 def settings_vcs(self):
81 c.active = 'vcs'
82 c.active = 'vcs'
82 if request.POST:
83 if request.POST:
83 application_form = ApplicationUiSettingsForm()()
84 application_form = ApplicationUiSettingsForm()()
84 try:
85 try:
85 form_result = application_form.to_python(dict(request.POST))
86 form_result = application_form.to_python(dict(request.POST))
86 except formencode.Invalid as errors:
87 except formencode.Invalid as errors:
87 return htmlfill.render(
88 return htmlfill.render(
88 render('admin/settings/settings.html'),
89 render('admin/settings/settings.html'),
89 defaults=errors.value,
90 defaults=errors.value,
90 errors=errors.error_dict or {},
91 errors=errors.error_dict or {},
91 prefix_error=False,
92 prefix_error=False,
92 encoding="UTF-8",
93 encoding="UTF-8",
93 force_defaults=False)
94 force_defaults=False)
94
95
95 try:
96 try:
96 if c.visual.allow_repo_location_change:
97 if c.visual.allow_repo_location_change:
97 sett = db.Ui.get_by_key('paths', '/')
98 sett = db.Ui.get_by_key('paths', '/')
98 sett.ui_value = form_result['paths_root_path']
99 sett.ui_value = form_result['paths_root_path']
99
100
100 # HOOKS
101 # HOOKS
101 sett = db.Ui.get_by_key('hooks', db.Ui.HOOK_UPDATE)
102 sett = db.Ui.get_by_key('hooks', db.Ui.HOOK_UPDATE)
102 sett.ui_active = form_result['hooks_changegroup_update']
103 sett.ui_active = form_result['hooks_changegroup_update']
103
104
104 sett = db.Ui.get_by_key('hooks', db.Ui.HOOK_REPO_SIZE)
105 sett = db.Ui.get_by_key('hooks', db.Ui.HOOK_REPO_SIZE)
105 sett.ui_active = form_result['hooks_changegroup_repo_size']
106 sett.ui_active = form_result['hooks_changegroup_repo_size']
106
107
107 ## EXTENSIONS
108 ## EXTENSIONS
108 sett = db.Ui.get_or_create('extensions', 'largefiles')
109 sett = db.Ui.get_or_create('extensions', 'largefiles')
109 sett.ui_active = form_result['extensions_largefiles']
110 sett.ui_active = form_result['extensions_largefiles']
110
111
111 # sett = db.Ui.get_or_create('extensions', 'hggit')
112 # sett = db.Ui.get_or_create('extensions', 'hggit')
112 # sett.ui_active = form_result['extensions_hggit']
113 # sett.ui_active = form_result['extensions_hggit']
113
114
114 meta.Session().commit()
115 meta.Session().commit()
115
116
116 webutils.flash(_('Updated VCS settings'), category='success')
117 webutils.flash(_('Updated VCS settings'), category='success')
117
118
118 except Exception:
119 except Exception:
119 log.error(traceback.format_exc())
120 log.error(traceback.format_exc())
120 webutils.flash(_('Error occurred while updating '
121 webutils.flash(_('Error occurred while updating '
121 'application settings'), category='error')
122 'application settings'), category='error')
122
123
123 defaults = db.Setting.get_app_settings()
124 defaults = db.Setting.get_app_settings()
124 defaults.update(self._get_hg_ui_settings())
125 defaults.update(self._get_hg_ui_settings())
125
126
126 return htmlfill.render(
127 return htmlfill.render(
127 render('admin/settings/settings.html'),
128 render('admin/settings/settings.html'),
128 defaults=defaults,
129 defaults=defaults,
129 encoding="UTF-8",
130 encoding="UTF-8",
130 force_defaults=False)
131 force_defaults=False)
131
132
132 @HasPermissionAnyDecorator('hg.admin')
133 @HasPermissionAnyDecorator('hg.admin')
133 def settings_mapping(self):
134 def settings_mapping(self):
134 c.active = 'mapping'
135 c.active = 'mapping'
135 if request.POST:
136 if request.POST:
136 rm_obsolete = request.POST.get('destroy', False)
137 rm_obsolete = request.POST.get('destroy', False)
137 install_git_hooks = request.POST.get('hooks', False)
138 install_git_hooks = request.POST.get('hooks', False)
138 overwrite_git_hooks = request.POST.get('hooks_overwrite', False)
139 overwrite_git_hooks = request.POST.get('hooks_overwrite', False)
139 invalidate_cache = request.POST.get('invalidate', False)
140 invalidate_cache = request.POST.get('invalidate', False)
140 log.debug('rescanning repo location with destroy obsolete=%s, '
141 log.debug('rescanning repo location with destroy obsolete=%s, '
141 'install git hooks=%s and '
142 'install git hooks=%s and '
142 'overwrite git hooks=%s' % (rm_obsolete, install_git_hooks, overwrite_git_hooks))
143 'overwrite git hooks=%s' % (rm_obsolete, install_git_hooks, overwrite_git_hooks))
143
144
144 filesystem_repos = ScmModel().repo_scan()
145 filesystem_repos = ScmModel().repo_scan()
145 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete,
146 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete,
146 install_git_hooks=install_git_hooks,
147 install_git_hooks=install_git_hooks,
147 user=request.authuser.username,
148 user=request.authuser.username,
148 overwrite_git_hooks=overwrite_git_hooks)
149 overwrite_git_hooks=overwrite_git_hooks)
149 added_msg = webutils.HTML(', ').join(
150 added_msg = webutils.HTML(', ').join(
150 webutils.link_to(safe_str(repo_name), webutils.url('summary_home', repo_name=repo_name)) for repo_name in added
151 webutils.link_to(safe_str(repo_name), webutils.url('summary_home', repo_name=repo_name)) for repo_name in added
151 ) or '-'
152 ) or '-'
152 removed_msg = webutils.HTML(', ').join(
153 removed_msg = webutils.HTML(', ').join(
153 safe_str(repo_name) for repo_name in removed
154 safe_str(repo_name) for repo_name in removed
154 ) or '-'
155 ) or '-'
155 webutils.flash(webutils.HTML(_('Repositories successfully rescanned. Added: %s. Removed: %s.')) %
156 webutils.flash(webutils.HTML(_('Repositories successfully rescanned. Added: %s. Removed: %s.')) %
156 (added_msg, removed_msg), category='success')
157 (added_msg, removed_msg), category='success')
157
158
158 if invalidate_cache:
159 if invalidate_cache:
159 log.debug('invalidating all repositories cache')
160 log.debug('invalidating all repositories cache')
160 i = 0
161 i = 0
161 for repo in db.Repository.query():
162 for repo in db.Repository.query():
162 try:
163 try:
163 ScmModel().mark_for_invalidation(repo.repo_name)
164 ScmModel().mark_for_invalidation(repo.repo_name)
164 i += 1
165 i += 1
165 except VCSError as e:
166 except VCSError as e:
166 log.warning('VCS error invalidating %s: %s', repo.repo_name, e)
167 log.warning('VCS error invalidating %s: %s', repo.repo_name, e)
167 webutils.flash(_('Invalidated %s repositories') % i, category='success')
168 webutils.flash(_('Invalidated %s repositories') % i, category='success')
168
169
169 raise HTTPFound(location=url('admin_settings_mapping'))
170 raise HTTPFound(location=url('admin_settings_mapping'))
170
171
171 defaults = db.Setting.get_app_settings()
172 defaults = db.Setting.get_app_settings()
172 defaults.update(self._get_hg_ui_settings())
173 defaults.update(self._get_hg_ui_settings())
173
174
174 return htmlfill.render(
175 return htmlfill.render(
175 render('admin/settings/settings.html'),
176 render('admin/settings/settings.html'),
176 defaults=defaults,
177 defaults=defaults,
177 encoding="UTF-8",
178 encoding="UTF-8",
178 force_defaults=False)
179 force_defaults=False)
179
180
180 @HasPermissionAnyDecorator('hg.admin')
181 @HasPermissionAnyDecorator('hg.admin')
181 def settings_global(self):
182 def settings_global(self):
182 c.active = 'global'
183 c.active = 'global'
183 if request.POST:
184 if request.POST:
184 application_form = ApplicationSettingsForm()()
185 application_form = ApplicationSettingsForm()()
185 try:
186 try:
186 form_result = application_form.to_python(dict(request.POST))
187 form_result = application_form.to_python(dict(request.POST))
187 except formencode.Invalid as errors:
188 except formencode.Invalid as errors:
188 return htmlfill.render(
189 return htmlfill.render(
189 render('admin/settings/settings.html'),
190 render('admin/settings/settings.html'),
190 defaults=errors.value,
191 defaults=errors.value,
191 errors=errors.error_dict or {},
192 errors=errors.error_dict or {},
192 prefix_error=False,
193 prefix_error=False,
193 encoding="UTF-8",
194 encoding="UTF-8",
194 force_defaults=False)
195 force_defaults=False)
195
196
196 try:
197 try:
197 for setting in (
198 for setting in (
198 'title',
199 'title',
199 'realm',
200 'realm',
200 'ga_code',
201 'ga_code',
201 'captcha_public_key',
202 'captcha_public_key',
202 'captcha_private_key',
203 'captcha_private_key',
203 ):
204 ):
204 db.Setting.create_or_update(setting, form_result[setting])
205 db.Setting.create_or_update(setting, form_result[setting])
205
206
206 meta.Session().commit()
207 meta.Session().commit()
207 set_app_settings(config)
208 set_app_settings(config)
208 webutils.flash(_('Updated application settings'), category='success')
209 webutils.flash(_('Updated application settings'), category='success')
209
210
210 except Exception:
211 except Exception:
211 log.error(traceback.format_exc())
212 log.error(traceback.format_exc())
212 webutils.flash(_('Error occurred while updating '
213 webutils.flash(_('Error occurred while updating '
213 'application settings'),
214 'application settings'),
214 category='error')
215 category='error')
215
216
216 raise HTTPFound(location=url('admin_settings_global'))
217 raise HTTPFound(location=url('admin_settings_global'))
217
218
218 defaults = db.Setting.get_app_settings()
219 defaults = db.Setting.get_app_settings()
219 defaults.update(self._get_hg_ui_settings())
220 defaults.update(self._get_hg_ui_settings())
220
221
221 return htmlfill.render(
222 return htmlfill.render(
222 render('admin/settings/settings.html'),
223 render('admin/settings/settings.html'),
223 defaults=defaults,
224 defaults=defaults,
224 encoding="UTF-8",
225 encoding="UTF-8",
225 force_defaults=False)
226 force_defaults=False)
226
227
227 @HasPermissionAnyDecorator('hg.admin')
228 @HasPermissionAnyDecorator('hg.admin')
228 def settings_visual(self):
229 def settings_visual(self):
229 c.active = 'visual'
230 c.active = 'visual'
230 if request.POST:
231 if request.POST:
231 application_form = ApplicationVisualisationForm()()
232 application_form = ApplicationVisualisationForm()()
232 try:
233 try:
233 form_result = application_form.to_python(dict(request.POST))
234 form_result = application_form.to_python(dict(request.POST))
234 except formencode.Invalid as errors:
235 except formencode.Invalid as errors:
235 return htmlfill.render(
236 return htmlfill.render(
236 render('admin/settings/settings.html'),
237 render('admin/settings/settings.html'),
237 defaults=errors.value,
238 defaults=errors.value,
238 errors=errors.error_dict or {},
239 errors=errors.error_dict or {},
239 prefix_error=False,
240 prefix_error=False,
240 encoding="UTF-8",
241 encoding="UTF-8",
241 force_defaults=False)
242 force_defaults=False)
242
243
243 try:
244 try:
244 settings = [
245 settings = [
245 ('show_public_icon', 'show_public_icon', 'bool'),
246 ('show_public_icon', 'show_public_icon', 'bool'),
246 ('show_private_icon', 'show_private_icon', 'bool'),
247 ('show_private_icon', 'show_private_icon', 'bool'),
247 ('stylify_metalabels', 'stylify_metalabels', 'bool'),
248 ('stylify_metalabels', 'stylify_metalabels', 'bool'),
248 ('repository_fields', 'repository_fields', 'bool'),
249 ('repository_fields', 'repository_fields', 'bool'),
249 ('dashboard_items', 'dashboard_items', 'int'),
250 ('dashboard_items', 'dashboard_items', 'int'),
250 ('admin_grid_items', 'admin_grid_items', 'int'),
251 ('admin_grid_items', 'admin_grid_items', 'int'),
251 ('show_version', 'show_version', 'bool'),
252 ('show_version', 'show_version', 'bool'),
252 ('use_gravatar', 'use_gravatar', 'bool'),
253 ('use_gravatar', 'use_gravatar', 'bool'),
253 ('gravatar_url', 'gravatar_url', 'unicode'),
254 ('gravatar_url', 'gravatar_url', 'unicode'),
254 ('clone_uri_tmpl', 'clone_uri_tmpl', 'unicode'),
255 ('clone_uri_tmpl', 'clone_uri_tmpl', 'unicode'),
255 ('clone_ssh_tmpl', 'clone_ssh_tmpl', 'unicode'),
256 ('clone_ssh_tmpl', 'clone_ssh_tmpl', 'unicode'),
256 ]
257 ]
257 for setting, form_key, type_ in settings:
258 for setting, form_key, type_ in settings:
258 db.Setting.create_or_update(setting, form_result[form_key], type_)
259 db.Setting.create_or_update(setting, form_result[form_key], type_)
259
260
260 meta.Session().commit()
261 meta.Session().commit()
261 set_app_settings(config)
262 set_app_settings(config)
262 webutils.flash(_('Updated visualisation settings'),
263 webutils.flash(_('Updated visualisation settings'),
263 category='success')
264 category='success')
264
265
265 except Exception:
266 except Exception:
266 log.error(traceback.format_exc())
267 log.error(traceback.format_exc())
267 webutils.flash(_('Error occurred during updating '
268 webutils.flash(_('Error occurred during updating '
268 'visualisation settings'),
269 'visualisation settings'),
269 category='error')
270 category='error')
270
271
271 raise HTTPFound(location=url('admin_settings_visual'))
272 raise HTTPFound(location=url('admin_settings_visual'))
272
273
273 defaults = db.Setting.get_app_settings()
274 defaults = db.Setting.get_app_settings()
274 defaults.update(self._get_hg_ui_settings())
275 defaults.update(self._get_hg_ui_settings())
275
276
276 return htmlfill.render(
277 return htmlfill.render(
277 render('admin/settings/settings.html'),
278 render('admin/settings/settings.html'),
278 defaults=defaults,
279 defaults=defaults,
279 encoding="UTF-8",
280 encoding="UTF-8",
280 force_defaults=False)
281 force_defaults=False)
281
282
282 @HasPermissionAnyDecorator('hg.admin')
283 @HasPermissionAnyDecorator('hg.admin')
283 def settings_email(self):
284 def settings_email(self):
284 c.active = 'email'
285 c.active = 'email'
285 if request.POST:
286 if request.POST:
286 test_email = request.POST.get('test_email')
287 test_email = request.POST.get('test_email')
287 test_email_subj = 'Kallithea test email'
288 test_email_subj = 'Kallithea test email'
288 test_body = ('Kallithea Email test, '
289 test_body = ('Kallithea Email test, '
289 'Kallithea version: %s' % c.kallithea_version)
290 'Kallithea version: %s' % c.kallithea_version)
290 if not test_email:
291 if not test_email:
291 webutils.flash(_('Please enter email address'), category='error')
292 webutils.flash(_('Please enter email address'), category='error')
292 raise HTTPFound(location=url('admin_settings_email'))
293 raise HTTPFound(location=url('admin_settings_email'))
293
294
294 test_email_txt_body = EmailNotificationModel() \
295 test_email_txt_body = EmailNotificationModel() \
295 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
296 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
296 'txt', body=test_body)
297 'txt', body=test_body)
297 test_email_html_body = EmailNotificationModel() \
298 test_email_html_body = EmailNotificationModel() \
298 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
299 .get_email_tmpl(EmailNotificationModel.TYPE_DEFAULT,
299 'html', body=test_body)
300 'html', body=test_body)
300
301
301 recipients = [test_email] if test_email else None
302 recipients = [test_email] if test_email else None
302
303
303 tasks.send_email(recipients, test_email_subj,
304 tasks.send_email(recipients, test_email_subj,
304 test_email_txt_body, test_email_html_body)
305 test_email_txt_body, test_email_html_body)
305
306
306 webutils.flash(_('Send email task created'), category='success')
307 webutils.flash(_('Send email task created'), category='success')
307 raise HTTPFound(location=url('admin_settings_email'))
308 raise HTTPFound(location=url('admin_settings_email'))
308
309
309 defaults = db.Setting.get_app_settings()
310 defaults = db.Setting.get_app_settings()
310 defaults.update(self._get_hg_ui_settings())
311 defaults.update(self._get_hg_ui_settings())
311
312
312 import kallithea
313 c.ini = kallithea.CONFIG
313 c.ini = kallithea.CONFIG
314
314
315 return htmlfill.render(
315 return htmlfill.render(
316 render('admin/settings/settings.html'),
316 render('admin/settings/settings.html'),
317 defaults=defaults,
317 defaults=defaults,
318 encoding="UTF-8",
318 encoding="UTF-8",
319 force_defaults=False)
319 force_defaults=False)
320
320
321 @HasPermissionAnyDecorator('hg.admin')
321 @HasPermissionAnyDecorator('hg.admin')
322 def settings_hooks(self):
322 def settings_hooks(self):
323 c.active = 'hooks'
323 c.active = 'hooks'
324 if request.POST:
324 if request.POST:
325 if c.visual.allow_custom_hooks_settings:
325 if c.visual.allow_custom_hooks_settings:
326 ui_key = request.POST.get('new_hook_ui_key')
326 ui_key = request.POST.get('new_hook_ui_key')
327 ui_value = request.POST.get('new_hook_ui_value')
327 ui_value = request.POST.get('new_hook_ui_value')
328
328
329 hook_id = request.POST.get('hook_id')
329 hook_id = request.POST.get('hook_id')
330
330
331 try:
331 try:
332 ui_key = ui_key and ui_key.strip()
332 ui_key = ui_key and ui_key.strip()
333 if ui_key in (x.ui_key for x in db.Ui.get_custom_hooks()):
333 if ui_key in (x.ui_key for x in db.Ui.get_custom_hooks()):
334 webutils.flash(_('Hook already exists'), category='error')
334 webutils.flash(_('Hook already exists'), category='error')
335 elif ui_key in (x.ui_key for x in db.Ui.get_builtin_hooks()):
335 elif ui_key in (x.ui_key for x in db.Ui.get_builtin_hooks()):
336 webutils.flash(_('Builtin hooks are read-only. Please use another hook name.'), category='error')
336 webutils.flash(_('Builtin hooks are read-only. Please use another hook name.'), category='error')
337 elif ui_value and ui_key:
337 elif ui_value and ui_key:
338 db.Ui.create_or_update_hook(ui_key, ui_value)
338 db.Ui.create_or_update_hook(ui_key, ui_value)
339 webutils.flash(_('Added new hook'), category='success')
339 webutils.flash(_('Added new hook'), category='success')
340 elif hook_id:
340 elif hook_id:
341 db.Ui.delete(hook_id)
341 db.Ui.delete(hook_id)
342 meta.Session().commit()
342 meta.Session().commit()
343
343
344 # check for edits
344 # check for edits
345 update = False
345 update = False
346 _d = request.POST.dict_of_lists()
346 _d = request.POST.dict_of_lists()
347 for k, v, ov in zip(_d.get('hook_ui_key', []),
347 for k, v, ov in zip(_d.get('hook_ui_key', []),
348 _d.get('hook_ui_value_new', []),
348 _d.get('hook_ui_value_new', []),
349 _d.get('hook_ui_value', [])):
349 _d.get('hook_ui_value', [])):
350 if v != ov:
350 if v != ov:
351 db.Ui.create_or_update_hook(k, v)
351 db.Ui.create_or_update_hook(k, v)
352 update = True
352 update = True
353
353
354 if update:
354 if update:
355 webutils.flash(_('Updated hooks'), category='success')
355 webutils.flash(_('Updated hooks'), category='success')
356 meta.Session().commit()
356 meta.Session().commit()
357 except Exception:
357 except Exception:
358 log.error(traceback.format_exc())
358 log.error(traceback.format_exc())
359 webutils.flash(_('Error occurred during hook creation'),
359 webutils.flash(_('Error occurred during hook creation'),
360 category='error')
360 category='error')
361
361
362 raise HTTPFound(location=url('admin_settings_hooks'))
362 raise HTTPFound(location=url('admin_settings_hooks'))
363
363
364 defaults = db.Setting.get_app_settings()
364 defaults = db.Setting.get_app_settings()
365 defaults.update(self._get_hg_ui_settings())
365 defaults.update(self._get_hg_ui_settings())
366
366
367 c.hooks = db.Ui.get_builtin_hooks()
367 c.hooks = db.Ui.get_builtin_hooks()
368 c.custom_hooks = db.Ui.get_custom_hooks()
368 c.custom_hooks = db.Ui.get_custom_hooks()
369
369
370 return htmlfill.render(
370 return htmlfill.render(
371 render('admin/settings/settings.html'),
371 render('admin/settings/settings.html'),
372 defaults=defaults,
372 defaults=defaults,
373 encoding="UTF-8",
373 encoding="UTF-8",
374 force_defaults=False)
374 force_defaults=False)
375
375
376 @HasPermissionAnyDecorator('hg.admin')
376 @HasPermissionAnyDecorator('hg.admin')
377 def settings_search(self):
377 def settings_search(self):
378 c.active = 'search'
378 c.active = 'search'
379 if request.POST:
379 if request.POST:
380 repo_location = self._get_hg_ui_settings()['paths_root_path']
380 repo_location = self._get_hg_ui_settings()['paths_root_path']
381 full_index = request.POST.get('full_index', False)
381 full_index = request.POST.get('full_index', False)
382 tasks.whoosh_index(repo_location, full_index)
382 tasks.whoosh_index(repo_location, full_index)
383 webutils.flash(_('Whoosh reindex task scheduled'), category='success')
383 webutils.flash(_('Whoosh reindex task scheduled'), category='success')
384 raise HTTPFound(location=url('admin_settings_search'))
384 raise HTTPFound(location=url('admin_settings_search'))
385
385
386 defaults = db.Setting.get_app_settings()
386 defaults = db.Setting.get_app_settings()
387 defaults.update(self._get_hg_ui_settings())
387 defaults.update(self._get_hg_ui_settings())
388
388
389 return htmlfill.render(
389 return htmlfill.render(
390 render('admin/settings/settings.html'),
390 render('admin/settings/settings.html'),
391 defaults=defaults,
391 defaults=defaults,
392 encoding="UTF-8",
392 encoding="UTF-8",
393 force_defaults=False)
393 force_defaults=False)
394
394
395 @HasPermissionAnyDecorator('hg.admin')
395 @HasPermissionAnyDecorator('hg.admin')
396 def settings_system(self):
396 def settings_system(self):
397 c.active = 'system'
397 c.active = 'system'
398
398
399 defaults = db.Setting.get_app_settings()
399 defaults = db.Setting.get_app_settings()
400 defaults.update(self._get_hg_ui_settings())
400 defaults.update(self._get_hg_ui_settings())
401
401
402 import kallithea
403 c.ini = kallithea.CONFIG
402 c.ini = kallithea.CONFIG
404 server_info = db.Setting.get_server_info()
403 server_info = db.Setting.get_server_info()
405 for key, val in server_info.items():
404 for key, val in server_info.items():
406 setattr(c, key, val)
405 setattr(c, key, val)
407
406
408 return htmlfill.render(
407 return htmlfill.render(
409 render('admin/settings/settings.html'),
408 render('admin/settings/settings.html'),
410 defaults=defaults,
409 defaults=defaults,
411 encoding="UTF-8",
410 encoding="UTF-8",
412 force_defaults=False)
411 force_defaults=False)
@@ -1,257 +1,256 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.controllers.login
15 kallithea.controllers.login
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Login controller for Kallithea
18 Login controller for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 22, 2010
22 :created_on: Apr 22, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28
28
29 import logging
29 import logging
30 import re
30 import re
31
31
32 import formencode
32 import formencode
33 from formencode import htmlfill
33 from formencode import htmlfill
34 from tg import request, session
34 from tg import request, session
35 from tg import tmpl_context as c
35 from tg import tmpl_context as c
36 from tg.i18n import ugettext as _
36 from tg.i18n import ugettext as _
37 from webob.exc import HTTPBadRequest, HTTPFound
37 from webob.exc import HTTPBadRequest, HTTPFound
38
38
39 from kallithea.lib import webutils
39 from kallithea.lib import webutils
40 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
40 from kallithea.lib.auth import AuthUser, HasPermissionAnyDecorator
41 from kallithea.lib.base import BaseController, log_in_user, render
41 from kallithea.lib.base import BaseController, log_in_user, render
42 from kallithea.lib.exceptions import UserCreationError
42 from kallithea.lib.exceptions import UserCreationError
43 from kallithea.lib.recaptcha import submit
43 from kallithea.lib.webutils import url
44 from kallithea.lib.webutils import url
44 from kallithea.model import db, meta
45 from kallithea.model import db, meta
45 from kallithea.model.forms import LoginForm, PasswordResetConfirmationForm, PasswordResetRequestForm, RegisterForm
46 from kallithea.model.forms import LoginForm, PasswordResetConfirmationForm, PasswordResetRequestForm, RegisterForm
46 from kallithea.model.user import UserModel
47 from kallithea.model.user import UserModel
47
48
48
49
49 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
50
51
51
52
52 class LoginController(BaseController):
53 class LoginController(BaseController):
53
54
54 def _validate_came_from(self, came_from,
55 def _validate_came_from(self, came_from,
55 _re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")):
56 _re=re.compile(r"/(?!/)[-!#$%&'()*+,./:;=?@_~0-9A-Za-z]*$")):
56 """Return True if came_from is valid and can and should be used.
57 """Return True if came_from is valid and can and should be used.
57
58
58 Determines if a URI reference is valid and relative to the origin;
59 Determines if a URI reference is valid and relative to the origin;
59 or in RFC 3986 terms, whether it matches this production:
60 or in RFC 3986 terms, whether it matches this production:
60
61
61 origin-relative-ref = path-absolute [ "?" query ] [ "#" fragment ]
62 origin-relative-ref = path-absolute [ "?" query ] [ "#" fragment ]
62
63
63 with the exception that '%' escapes are not validated and '#' is
64 with the exception that '%' escapes are not validated and '#' is
64 allowed inside the fragment part.
65 allowed inside the fragment part.
65 """
66 """
66 return _re.match(came_from) is not None
67 return _re.match(came_from) is not None
67
68
68 def index(self):
69 def index(self):
69 c.came_from = request.GET.get('came_from', '')
70 c.came_from = request.GET.get('came_from', '')
70 if c.came_from:
71 if c.came_from:
71 if not self._validate_came_from(c.came_from):
72 if not self._validate_came_from(c.came_from):
72 log.error('Invalid came_from (not server-relative): %r', c.came_from)
73 log.error('Invalid came_from (not server-relative): %r', c.came_from)
73 raise HTTPBadRequest()
74 raise HTTPBadRequest()
74 else:
75 else:
75 c.came_from = url('home')
76 c.came_from = url('home')
76
77
77 if request.POST:
78 if request.POST:
78 # import Login Form validator class
79 # import Login Form validator class
79 login_form = LoginForm()()
80 login_form = LoginForm()()
80 try:
81 try:
81 # login_form will check username/password using ValidAuth and report failure to the user
82 # login_form will check username/password using ValidAuth and report failure to the user
82 c.form_result = login_form.to_python(dict(request.POST))
83 c.form_result = login_form.to_python(dict(request.POST))
83 username = c.form_result['username']
84 username = c.form_result['username']
84 user = db.User.get_by_username_or_email(username)
85 user = db.User.get_by_username_or_email(username)
85 assert user is not None # the same user get just passed in the form validation
86 assert user is not None # the same user get just passed in the form validation
86 except formencode.Invalid as errors:
87 except formencode.Invalid as errors:
87 defaults = errors.value
88 defaults = errors.value
88 # remove password from filling in form again
89 # remove password from filling in form again
89 defaults.pop('password', None)
90 defaults.pop('password', None)
90 return htmlfill.render(
91 return htmlfill.render(
91 render('/login.html'),
92 render('/login.html'),
92 defaults=errors.value,
93 defaults=errors.value,
93 errors=errors.error_dict or {},
94 errors=errors.error_dict or {},
94 prefix_error=False,
95 prefix_error=False,
95 encoding="UTF-8",
96 encoding="UTF-8",
96 force_defaults=False)
97 force_defaults=False)
97 except UserCreationError as e:
98 except UserCreationError as e:
98 # container auth or other auth functions that create users on
99 # container auth or other auth functions that create users on
99 # the fly can throw this exception signaling that there's issue
100 # the fly can throw this exception signaling that there's issue
100 # with user creation, explanation should be provided in
101 # with user creation, explanation should be provided in
101 # Exception itself
102 # Exception itself
102 webutils.flash(e, 'error')
103 webutils.flash(e, 'error')
103 else:
104 else:
104 # login_form already validated the password - now set the session cookie accordingly
105 # login_form already validated the password - now set the session cookie accordingly
105 auth_user = log_in_user(user, c.form_result['remember'], is_external_auth=False, ip_addr=request.ip_addr)
106 auth_user = log_in_user(user, c.form_result['remember'], is_external_auth=False, ip_addr=request.ip_addr)
106 if auth_user:
107 if auth_user:
107 raise HTTPFound(location=c.came_from)
108 raise HTTPFound(location=c.came_from)
108 webutils.flash(_('Authentication failed.'), 'error')
109 webutils.flash(_('Authentication failed.'), 'error')
109 else:
110 else:
110 # redirect if already logged in
111 # redirect if already logged in
111 if not request.authuser.is_anonymous:
112 if not request.authuser.is_anonymous:
112 raise HTTPFound(location=c.came_from)
113 raise HTTPFound(location=c.came_from)
113 # continue to show login to default user
114 # continue to show login to default user
114
115
115 return render('/login.html')
116 return render('/login.html')
116
117
117 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
118 @HasPermissionAnyDecorator('hg.admin', 'hg.register.auto_activate',
118 'hg.register.manual_activate')
119 'hg.register.manual_activate')
119 def register(self):
120 def register(self):
120 def_user_perms = AuthUser(dbuser=db.User.get_default_user()).global_permissions
121 def_user_perms = AuthUser(dbuser=db.User.get_default_user()).global_permissions
121 c.auto_active = 'hg.register.auto_activate' in def_user_perms
122 c.auto_active = 'hg.register.auto_activate' in def_user_perms
122
123
123 settings = db.Setting.get_app_settings()
124 settings = db.Setting.get_app_settings()
124 captcha_private_key = settings.get('captcha_private_key')
125 captcha_private_key = settings.get('captcha_private_key')
125 c.captcha_active = bool(captcha_private_key)
126 c.captcha_active = bool(captcha_private_key)
126 c.captcha_public_key = settings.get('captcha_public_key')
127 c.captcha_public_key = settings.get('captcha_public_key')
127
128
128 if request.POST:
129 if request.POST:
129 register_form = RegisterForm()()
130 register_form = RegisterForm()()
130 try:
131 try:
131 form_result = register_form.to_python(dict(request.POST))
132 form_result = register_form.to_python(dict(request.POST))
132 form_result['active'] = c.auto_active
133 form_result['active'] = c.auto_active
133
134
134 if c.captcha_active:
135 if c.captcha_active:
135 from kallithea.lib.recaptcha import submit
136 response = submit(request.POST.get('g-recaptcha-response'),
136 response = submit(request.POST.get('g-recaptcha-response'),
137 private_key=captcha_private_key,
137 private_key=captcha_private_key,
138 remoteip=request.ip_addr)
138 remoteip=request.ip_addr)
139 if not response.is_valid:
139 if not response.is_valid:
140 _value = form_result
140 _value = form_result
141 _msg = _('Bad captcha')
141 _msg = _('Bad captcha')
142 error_dict = {'recaptcha_field': _msg}
142 error_dict = {'recaptcha_field': _msg}
143 raise formencode.Invalid(_msg, _value, None,
143 raise formencode.Invalid(_msg, _value, None,
144 error_dict=error_dict)
144 error_dict=error_dict)
145
145
146 UserModel().create_registration(form_result)
146 UserModel().create_registration(form_result)
147 webutils.flash(_('You have successfully registered with %s') % (c.site_name or 'Kallithea'),
147 webutils.flash(_('You have successfully registered with %s') % (c.site_name or 'Kallithea'),
148 category='success')
148 category='success')
149 meta.Session().commit()
149 meta.Session().commit()
150 raise HTTPFound(location=url('login_home'))
150 raise HTTPFound(location=url('login_home'))
151
151
152 except formencode.Invalid as errors:
152 except formencode.Invalid as errors:
153 return htmlfill.render(
153 return htmlfill.render(
154 render('/register.html'),
154 render('/register.html'),
155 defaults=errors.value,
155 defaults=errors.value,
156 errors=errors.error_dict or {},
156 errors=errors.error_dict or {},
157 prefix_error=False,
157 prefix_error=False,
158 encoding="UTF-8",
158 encoding="UTF-8",
159 force_defaults=False)
159 force_defaults=False)
160 except UserCreationError as e:
160 except UserCreationError as e:
161 # container auth or other auth functions that create users on
161 # container auth or other auth functions that create users on
162 # the fly can throw this exception signaling that there's issue
162 # the fly can throw this exception signaling that there's issue
163 # with user creation, explanation should be provided in
163 # with user creation, explanation should be provided in
164 # Exception itself
164 # Exception itself
165 webutils.flash(e, 'error')
165 webutils.flash(e, 'error')
166
166
167 return render('/register.html')
167 return render('/register.html')
168
168
169 def password_reset(self):
169 def password_reset(self):
170 settings = db.Setting.get_app_settings()
170 settings = db.Setting.get_app_settings()
171 captcha_private_key = settings.get('captcha_private_key')
171 captcha_private_key = settings.get('captcha_private_key')
172 c.captcha_active = bool(captcha_private_key)
172 c.captcha_active = bool(captcha_private_key)
173 c.captcha_public_key = settings.get('captcha_public_key')
173 c.captcha_public_key = settings.get('captcha_public_key')
174
174
175 if request.POST:
175 if request.POST:
176 password_reset_form = PasswordResetRequestForm()()
176 password_reset_form = PasswordResetRequestForm()()
177 try:
177 try:
178 form_result = password_reset_form.to_python(dict(request.POST))
178 form_result = password_reset_form.to_python(dict(request.POST))
179 if c.captcha_active:
179 if c.captcha_active:
180 from kallithea.lib.recaptcha import submit
181 response = submit(request.POST.get('g-recaptcha-response'),
180 response = submit(request.POST.get('g-recaptcha-response'),
182 private_key=captcha_private_key,
181 private_key=captcha_private_key,
183 remoteip=request.ip_addr)
182 remoteip=request.ip_addr)
184 if not response.is_valid:
183 if not response.is_valid:
185 _value = form_result
184 _value = form_result
186 _msg = _('Bad captcha')
185 _msg = _('Bad captcha')
187 error_dict = {'recaptcha_field': _msg}
186 error_dict = {'recaptcha_field': _msg}
188 raise formencode.Invalid(_msg, _value, None,
187 raise formencode.Invalid(_msg, _value, None,
189 error_dict=error_dict)
188 error_dict=error_dict)
190 redirect_link = UserModel().send_reset_password_email(form_result)
189 redirect_link = UserModel().send_reset_password_email(form_result)
191 webutils.flash(_('A password reset confirmation code has been sent'),
190 webutils.flash(_('A password reset confirmation code has been sent'),
192 category='success')
191 category='success')
193 raise HTTPFound(location=redirect_link)
192 raise HTTPFound(location=redirect_link)
194
193
195 except formencode.Invalid as errors:
194 except formencode.Invalid as errors:
196 return htmlfill.render(
195 return htmlfill.render(
197 render('/password_reset.html'),
196 render('/password_reset.html'),
198 defaults=errors.value,
197 defaults=errors.value,
199 errors=errors.error_dict or {},
198 errors=errors.error_dict or {},
200 prefix_error=False,
199 prefix_error=False,
201 encoding="UTF-8",
200 encoding="UTF-8",
202 force_defaults=False)
201 force_defaults=False)
203
202
204 return render('/password_reset.html')
203 return render('/password_reset.html')
205
204
206 def password_reset_confirmation(self):
205 def password_reset_confirmation(self):
207 # This controller handles both GET and POST requests, though we
206 # This controller handles both GET and POST requests, though we
208 # only ever perform the actual password change on POST (since
207 # only ever perform the actual password change on POST (since
209 # GET requests are not allowed to have side effects, and do not
208 # GET requests are not allowed to have side effects, and do not
210 # receive automatic CSRF protection).
209 # receive automatic CSRF protection).
211
210
212 # The template needs the email address outside of the form.
211 # The template needs the email address outside of the form.
213 c.email = request.params.get('email')
212 c.email = request.params.get('email')
214 c.timestamp = request.params.get('timestamp') or ''
213 c.timestamp = request.params.get('timestamp') or ''
215 c.token = request.params.get('token') or ''
214 c.token = request.params.get('token') or ''
216 if not request.POST:
215 if not request.POST:
217 return render('/password_reset_confirmation.html')
216 return render('/password_reset_confirmation.html')
218
217
219 form = PasswordResetConfirmationForm()()
218 form = PasswordResetConfirmationForm()()
220 try:
219 try:
221 form_result = form.to_python(dict(request.POST))
220 form_result = form.to_python(dict(request.POST))
222 except formencode.Invalid as errors:
221 except formencode.Invalid as errors:
223 return htmlfill.render(
222 return htmlfill.render(
224 render('/password_reset_confirmation.html'),
223 render('/password_reset_confirmation.html'),
225 defaults=errors.value,
224 defaults=errors.value,
226 errors=errors.error_dict or {},
225 errors=errors.error_dict or {},
227 prefix_error=False,
226 prefix_error=False,
228 encoding='UTF-8')
227 encoding='UTF-8')
229
228
230 if not UserModel().verify_reset_password_token(
229 if not UserModel().verify_reset_password_token(
231 form_result['email'],
230 form_result['email'],
232 form_result['timestamp'],
231 form_result['timestamp'],
233 form_result['token'],
232 form_result['token'],
234 ):
233 ):
235 return htmlfill.render(
234 return htmlfill.render(
236 render('/password_reset_confirmation.html'),
235 render('/password_reset_confirmation.html'),
237 defaults=form_result,
236 defaults=form_result,
238 errors={'token': _('Invalid password reset token')},
237 errors={'token': _('Invalid password reset token')},
239 prefix_error=False,
238 prefix_error=False,
240 encoding='UTF-8')
239 encoding='UTF-8')
241
240
242 UserModel().reset_password(form_result['email'], form_result['password'])
241 UserModel().reset_password(form_result['email'], form_result['password'])
243 webutils.flash(_('Successfully updated password'), category='success')
242 webutils.flash(_('Successfully updated password'), category='success')
244 raise HTTPFound(location=url('login_home'))
243 raise HTTPFound(location=url('login_home'))
245
244
246 def logout(self):
245 def logout(self):
247 session.delete()
246 session.delete()
248 log.info('Logging out and deleting session for user')
247 log.info('Logging out and deleting session for user')
249 raise HTTPFound(location=url('home'))
248 raise HTTPFound(location=url('home'))
250
249
251 def session_csrf_secret_token(self):
250 def session_csrf_secret_token(self):
252 """Return the CSRF protection token for the session - just like it
251 """Return the CSRF protection token for the session - just like it
253 could have been screen scraped from a page with a form.
252 could have been screen scraped from a page with a form.
254 Only intended for testing but might also be useful for other kinds
253 Only intended for testing but might also be useful for other kinds
255 of automation.
254 of automation.
256 """
255 """
257 return webutils.session_csrf_secret_token()
256 return webutils.session_csrf_secret_token()
@@ -1,774 +1,773 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Routes configuration
15 Routes configuration
16
16
17 The more specific and detailed routes should be defined first so they
17 The more specific and detailed routes should be defined first so they
18 may take precedent over the more generic routes. For more information
18 may take precedent over the more generic routes. For more information
19 refer to the routes manual at http://routes.groovie.org/docs/
19 refer to the routes manual at http://routes.groovie.org/docs/
20 """
20 """
21
21
22 import routes
22 import routes
23
23
24 import kallithea
24 import kallithea
25 from kallithea.lib.utils import is_valid_repo, is_valid_repo_group
25 from kallithea.lib.utils2 import safe_str
26 from kallithea.lib.utils2 import safe_str
26
27
27
28
28 class Mapper(routes.Mapper):
29 class Mapper(routes.Mapper):
29 """
30 """
30 Subclassed Mapper with routematch patched to decode "unicode" str url to
31 Subclassed Mapper with routematch patched to decode "unicode" str url to
31 *real* unicode str before applying matches and invoking controller methods.
32 *real* unicode str before applying matches and invoking controller methods.
32 """
33 """
33
34
34 def routematch(self, url=None, environ=None):
35 def routematch(self, url=None, environ=None):
35 """
36 """
36 routematch that also decode url from "fake bytes" to real unicode
37 routematch that also decode url from "fake bytes" to real unicode
37 string before matching and invoking controllers.
38 string before matching and invoking controllers.
38 """
39 """
39 # Process url like get_path_info does ... but PATH_INFO has already
40 # Process url like get_path_info does ... but PATH_INFO has already
40 # been retrieved from environ and is passed, so - let's just use that
41 # been retrieved from environ and is passed, so - let's just use that
41 # instead.
42 # instead.
42 url = safe_str(url.encode('latin1'))
43 url = safe_str(url.encode('latin1'))
43 return super().routematch(url=url, environ=environ)
44 return super().routematch(url=url, environ=environ)
44
45
45
46
46 def make_map(config):
47 def make_map(config):
47 """Create, configure and return the routes Mapper"""
48 """Create, configure and return the routes Mapper"""
48 rmap = Mapper(directory=config['paths']['controllers'],
49 rmap = Mapper(directory=config['paths']['controllers'],
49 always_scan=config['debug'])
50 always_scan=config['debug'])
50 rmap.minimization = False
51 rmap.minimization = False
51 rmap.explicit = False
52 rmap.explicit = False
52
53
53 from kallithea.lib.utils import is_valid_repo, is_valid_repo_group
54
55 def check_repo(environ, match_dict):
54 def check_repo(environ, match_dict):
56 """
55 """
57 Check for valid repository for proper 404 handling.
56 Check for valid repository for proper 404 handling.
58 Also, a bit of side effect modifying match_dict ...
57 Also, a bit of side effect modifying match_dict ...
59 """
58 """
60 if match_dict.get('f_path'):
59 if match_dict.get('f_path'):
61 # fix for multiple initial slashes that causes errors
60 # fix for multiple initial slashes that causes errors
62 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
61 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
63
62
64 return is_valid_repo(match_dict['repo_name'], config['base_path'])
63 return is_valid_repo(match_dict['repo_name'], config['base_path'])
65
64
66 def check_group(environ, match_dict):
65 def check_group(environ, match_dict):
67 """
66 """
68 check for valid repository group for proper 404 handling
67 check for valid repository group for proper 404 handling
69
68
70 :param environ:
69 :param environ:
71 :param match_dict:
70 :param match_dict:
72 """
71 """
73 repo_group_name = match_dict.get('group_name')
72 repo_group_name = match_dict.get('group_name')
74 return is_valid_repo_group(repo_group_name, config['base_path'])
73 return is_valid_repo_group(repo_group_name, config['base_path'])
75
74
76 def check_group_skip_path(environ, match_dict):
75 def check_group_skip_path(environ, match_dict):
77 """
76 """
78 check for valid repository group for proper 404 handling, but skips
77 check for valid repository group for proper 404 handling, but skips
79 verification of existing path
78 verification of existing path
80
79
81 :param environ:
80 :param environ:
82 :param match_dict:
81 :param match_dict:
83 """
82 """
84 repo_group_name = match_dict.get('group_name')
83 repo_group_name = match_dict.get('group_name')
85 return is_valid_repo_group(repo_group_name, config['base_path'],
84 return is_valid_repo_group(repo_group_name, config['base_path'],
86 skip_path_check=True)
85 skip_path_check=True)
87
86
88 def check_user_group(environ, match_dict):
87 def check_user_group(environ, match_dict):
89 """
88 """
90 check for valid user group for proper 404 handling
89 check for valid user group for proper 404 handling
91
90
92 :param environ:
91 :param environ:
93 :param match_dict:
92 :param match_dict:
94 """
93 """
95 return True
94 return True
96
95
97 def check_int(environ, match_dict):
96 def check_int(environ, match_dict):
98 return match_dict.get('id').isdigit()
97 return match_dict.get('id').isdigit()
99
98
100 #==========================================================================
99 #==========================================================================
101 # CUSTOM ROUTES HERE
100 # CUSTOM ROUTES HERE
102 #==========================================================================
101 #==========================================================================
103
102
104 # MAIN PAGE
103 # MAIN PAGE
105 rmap.connect('home', '/', controller='home')
104 rmap.connect('home', '/', controller='home')
106 rmap.connect('about', '/about', controller='home', action='about')
105 rmap.connect('about', '/about', controller='home', action='about')
107 rmap.redirect('/favicon.ico', '/images/favicon.ico')
106 rmap.redirect('/favicon.ico', '/images/favicon.ico')
108 rmap.connect('repo_switcher_data', '/_repos', controller='home',
107 rmap.connect('repo_switcher_data', '/_repos', controller='home',
109 action='repo_switcher_data')
108 action='repo_switcher_data')
110 rmap.connect('users_and_groups_data', '/_users_and_groups', controller='home',
109 rmap.connect('users_and_groups_data', '/_users_and_groups', controller='home',
111 action='users_and_groups_data')
110 action='users_and_groups_data')
112
111
113 rmap.connect('rst_help',
112 rmap.connect('rst_help',
114 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
113 "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
115 _static=True)
114 _static=True)
116 rmap.connect('kallithea_project_url', "https://kallithea-scm.org/", _static=True)
115 rmap.connect('kallithea_project_url', "https://kallithea-scm.org/", _static=True)
117 rmap.connect('issues_url', 'https://bitbucket.org/conservancy/kallithea/issues', _static=True)
116 rmap.connect('issues_url', 'https://bitbucket.org/conservancy/kallithea/issues', _static=True)
118
117
119 # ADMIN REPOSITORY ROUTES
118 # ADMIN REPOSITORY ROUTES
120 ADMIN_PREFIX = kallithea.ADMIN_PREFIX
119 ADMIN_PREFIX = kallithea.ADMIN_PREFIX
121 with rmap.submapper(path_prefix=ADMIN_PREFIX,
120 with rmap.submapper(path_prefix=ADMIN_PREFIX,
122 controller='admin/repos') as m:
121 controller='admin/repos') as m:
123 m.connect("repos", "/repos",
122 m.connect("repos", "/repos",
124 action="create", conditions=dict(method=["POST"]))
123 action="create", conditions=dict(method=["POST"]))
125 m.connect("repos", "/repos",
124 m.connect("repos", "/repos",
126 conditions=dict(method=["GET"]))
125 conditions=dict(method=["GET"]))
127 m.connect("new_repo", "/create_repository",
126 m.connect("new_repo", "/create_repository",
128 action="create_repository", conditions=dict(method=["GET"]))
127 action="create_repository", conditions=dict(method=["GET"]))
129 m.connect("update_repo", "/repos/{repo_name:.*?}",
128 m.connect("update_repo", "/repos/{repo_name:.*?}",
130 action="update", conditions=dict(method=["POST"],
129 action="update", conditions=dict(method=["POST"],
131 function=check_repo))
130 function=check_repo))
132 m.connect("delete_repo", "/repos/{repo_name:.*?}/delete",
131 m.connect("delete_repo", "/repos/{repo_name:.*?}/delete",
133 action="delete", conditions=dict(method=["POST"]))
132 action="delete", conditions=dict(method=["POST"]))
134
133
135 # ADMIN REPOSITORY GROUPS ROUTES
134 # ADMIN REPOSITORY GROUPS ROUTES
136 with rmap.submapper(path_prefix=ADMIN_PREFIX,
135 with rmap.submapper(path_prefix=ADMIN_PREFIX,
137 controller='admin/repo_groups') as m:
136 controller='admin/repo_groups') as m:
138 m.connect("repos_groups", "/repo_groups",
137 m.connect("repos_groups", "/repo_groups",
139 action="create", conditions=dict(method=["POST"]))
138 action="create", conditions=dict(method=["POST"]))
140 m.connect("repos_groups", "/repo_groups",
139 m.connect("repos_groups", "/repo_groups",
141 conditions=dict(method=["GET"]))
140 conditions=dict(method=["GET"]))
142 m.connect("new_repos_group", "/repo_groups/new",
141 m.connect("new_repos_group", "/repo_groups/new",
143 action="new", conditions=dict(method=["GET"]))
142 action="new", conditions=dict(method=["GET"]))
144 m.connect("update_repos_group", "/repo_groups/{group_name:.*?}",
143 m.connect("update_repos_group", "/repo_groups/{group_name:.*?}",
145 action="update", conditions=dict(method=["POST"],
144 action="update", conditions=dict(method=["POST"],
146 function=check_group))
145 function=check_group))
147
146
148 m.connect("repos_group", "/repo_groups/{group_name:.*?}",
147 m.connect("repos_group", "/repo_groups/{group_name:.*?}",
149 action="show", conditions=dict(method=["GET"],
148 action="show", conditions=dict(method=["GET"],
150 function=check_group))
149 function=check_group))
151
150
152 # EXTRAS REPO GROUP ROUTES
151 # EXTRAS REPO GROUP ROUTES
153 m.connect("edit_repo_group", "/repo_groups/{group_name:.*?}/edit",
152 m.connect("edit_repo_group", "/repo_groups/{group_name:.*?}/edit",
154 action="edit",
153 action="edit",
155 conditions=dict(method=["GET"], function=check_group))
154 conditions=dict(method=["GET"], function=check_group))
156
155
157 m.connect("edit_repo_group_advanced", "/repo_groups/{group_name:.*?}/edit/advanced",
156 m.connect("edit_repo_group_advanced", "/repo_groups/{group_name:.*?}/edit/advanced",
158 action="edit_repo_group_advanced",
157 action="edit_repo_group_advanced",
159 conditions=dict(method=["GET"], function=check_group))
158 conditions=dict(method=["GET"], function=check_group))
160
159
161 m.connect("edit_repo_group_perms", "/repo_groups/{group_name:.*?}/edit/permissions",
160 m.connect("edit_repo_group_perms", "/repo_groups/{group_name:.*?}/edit/permissions",
162 action="edit_repo_group_perms",
161 action="edit_repo_group_perms",
163 conditions=dict(method=["GET"], function=check_group))
162 conditions=dict(method=["GET"], function=check_group))
164 m.connect("edit_repo_group_perms_update", "/repo_groups/{group_name:.*?}/edit/permissions",
163 m.connect("edit_repo_group_perms_update", "/repo_groups/{group_name:.*?}/edit/permissions",
165 action="update_perms",
164 action="update_perms",
166 conditions=dict(method=["POST"], function=check_group))
165 conditions=dict(method=["POST"], function=check_group))
167 m.connect("edit_repo_group_perms_delete", "/repo_groups/{group_name:.*?}/edit/permissions/delete",
166 m.connect("edit_repo_group_perms_delete", "/repo_groups/{group_name:.*?}/edit/permissions/delete",
168 action="delete_perms",
167 action="delete_perms",
169 conditions=dict(method=["POST"], function=check_group))
168 conditions=dict(method=["POST"], function=check_group))
170
169
171 m.connect("delete_repo_group", "/repo_groups/{group_name:.*?}/delete",
170 m.connect("delete_repo_group", "/repo_groups/{group_name:.*?}/delete",
172 action="delete", conditions=dict(method=["POST"],
171 action="delete", conditions=dict(method=["POST"],
173 function=check_group_skip_path))
172 function=check_group_skip_path))
174
173
175 # ADMIN USER ROUTES
174 # ADMIN USER ROUTES
176 with rmap.submapper(path_prefix=ADMIN_PREFIX,
175 with rmap.submapper(path_prefix=ADMIN_PREFIX,
177 controller='admin/users') as m:
176 controller='admin/users') as m:
178 m.connect("new_user", "/users/new",
177 m.connect("new_user", "/users/new",
179 action="create", conditions=dict(method=["POST"]))
178 action="create", conditions=dict(method=["POST"]))
180 m.connect("users", "/users",
179 m.connect("users", "/users",
181 conditions=dict(method=["GET"]))
180 conditions=dict(method=["GET"]))
182 m.connect("formatted_users", "/users.{format}",
181 m.connect("formatted_users", "/users.{format}",
183 conditions=dict(method=["GET"]))
182 conditions=dict(method=["GET"]))
184 m.connect("new_user", "/users/new",
183 m.connect("new_user", "/users/new",
185 action="new", conditions=dict(method=["GET"]))
184 action="new", conditions=dict(method=["GET"]))
186 m.connect("update_user", "/users/{id}",
185 m.connect("update_user", "/users/{id}",
187 action="update", conditions=dict(method=["POST"]))
186 action="update", conditions=dict(method=["POST"]))
188 m.connect("delete_user", "/users/{id}/delete",
187 m.connect("delete_user", "/users/{id}/delete",
189 action="delete", conditions=dict(method=["POST"]))
188 action="delete", conditions=dict(method=["POST"]))
190 m.connect("edit_user", "/users/{id}/edit",
189 m.connect("edit_user", "/users/{id}/edit",
191 action="edit", conditions=dict(method=["GET"]))
190 action="edit", conditions=dict(method=["GET"]))
192
191
193 # EXTRAS USER ROUTES
192 # EXTRAS USER ROUTES
194 m.connect("edit_user_advanced", "/users/{id}/edit/advanced",
193 m.connect("edit_user_advanced", "/users/{id}/edit/advanced",
195 action="edit_advanced", conditions=dict(method=["GET"]))
194 action="edit_advanced", conditions=dict(method=["GET"]))
196
195
197 m.connect("edit_user_api_keys", "/users/{id}/edit/api_keys",
196 m.connect("edit_user_api_keys", "/users/{id}/edit/api_keys",
198 action="edit_api_keys", conditions=dict(method=["GET"]))
197 action="edit_api_keys", conditions=dict(method=["GET"]))
199 m.connect("edit_user_api_keys_update", "/users/{id}/edit/api_keys",
198 m.connect("edit_user_api_keys_update", "/users/{id}/edit/api_keys",
200 action="add_api_key", conditions=dict(method=["POST"]))
199 action="add_api_key", conditions=dict(method=["POST"]))
201 m.connect("edit_user_api_keys_delete", "/users/{id}/edit/api_keys/delete",
200 m.connect("edit_user_api_keys_delete", "/users/{id}/edit/api_keys/delete",
202 action="delete_api_key", conditions=dict(method=["POST"]))
201 action="delete_api_key", conditions=dict(method=["POST"]))
203
202
204 m.connect("edit_user_ssh_keys", "/users/{id}/edit/ssh_keys",
203 m.connect("edit_user_ssh_keys", "/users/{id}/edit/ssh_keys",
205 action="edit_ssh_keys", conditions=dict(method=["GET"]))
204 action="edit_ssh_keys", conditions=dict(method=["GET"]))
206 m.connect("edit_user_ssh_keys", "/users/{id}/edit/ssh_keys",
205 m.connect("edit_user_ssh_keys", "/users/{id}/edit/ssh_keys",
207 action="ssh_keys_add", conditions=dict(method=["POST"]))
206 action="ssh_keys_add", conditions=dict(method=["POST"]))
208 m.connect("edit_user_ssh_keys_delete", "/users/{id}/edit/ssh_keys/delete",
207 m.connect("edit_user_ssh_keys_delete", "/users/{id}/edit/ssh_keys/delete",
209 action="ssh_keys_delete", conditions=dict(method=["POST"]))
208 action="ssh_keys_delete", conditions=dict(method=["POST"]))
210
209
211 m.connect("edit_user_perms", "/users/{id}/edit/permissions",
210 m.connect("edit_user_perms", "/users/{id}/edit/permissions",
212 action="edit_perms", conditions=dict(method=["GET"]))
211 action="edit_perms", conditions=dict(method=["GET"]))
213 m.connect("edit_user_perms_update", "/users/{id}/edit/permissions",
212 m.connect("edit_user_perms_update", "/users/{id}/edit/permissions",
214 action="update_perms", conditions=dict(method=["POST"]))
213 action="update_perms", conditions=dict(method=["POST"]))
215
214
216 m.connect("edit_user_emails", "/users/{id}/edit/emails",
215 m.connect("edit_user_emails", "/users/{id}/edit/emails",
217 action="edit_emails", conditions=dict(method=["GET"]))
216 action="edit_emails", conditions=dict(method=["GET"]))
218 m.connect("edit_user_emails_update", "/users/{id}/edit/emails",
217 m.connect("edit_user_emails_update", "/users/{id}/edit/emails",
219 action="add_email", conditions=dict(method=["POST"]))
218 action="add_email", conditions=dict(method=["POST"]))
220 m.connect("edit_user_emails_delete", "/users/{id}/edit/emails/delete",
219 m.connect("edit_user_emails_delete", "/users/{id}/edit/emails/delete",
221 action="delete_email", conditions=dict(method=["POST"]))
220 action="delete_email", conditions=dict(method=["POST"]))
222
221
223 m.connect("edit_user_ips", "/users/{id}/edit/ips",
222 m.connect("edit_user_ips", "/users/{id}/edit/ips",
224 action="edit_ips", conditions=dict(method=["GET"]))
223 action="edit_ips", conditions=dict(method=["GET"]))
225 m.connect("edit_user_ips_update", "/users/{id}/edit/ips",
224 m.connect("edit_user_ips_update", "/users/{id}/edit/ips",
226 action="add_ip", conditions=dict(method=["POST"]))
225 action="add_ip", conditions=dict(method=["POST"]))
227 m.connect("edit_user_ips_delete", "/users/{id}/edit/ips/delete",
226 m.connect("edit_user_ips_delete", "/users/{id}/edit/ips/delete",
228 action="delete_ip", conditions=dict(method=["POST"]))
227 action="delete_ip", conditions=dict(method=["POST"]))
229
228
230 # ADMIN USER GROUPS REST ROUTES
229 # ADMIN USER GROUPS REST ROUTES
231 with rmap.submapper(path_prefix=ADMIN_PREFIX,
230 with rmap.submapper(path_prefix=ADMIN_PREFIX,
232 controller='admin/user_groups') as m:
231 controller='admin/user_groups') as m:
233 m.connect("users_groups", "/user_groups",
232 m.connect("users_groups", "/user_groups",
234 action="create", conditions=dict(method=["POST"]))
233 action="create", conditions=dict(method=["POST"]))
235 m.connect("users_groups", "/user_groups",
234 m.connect("users_groups", "/user_groups",
236 conditions=dict(method=["GET"]))
235 conditions=dict(method=["GET"]))
237 m.connect("new_users_group", "/user_groups/new",
236 m.connect("new_users_group", "/user_groups/new",
238 action="new", conditions=dict(method=["GET"]))
237 action="new", conditions=dict(method=["GET"]))
239 m.connect("update_users_group", "/user_groups/{id}",
238 m.connect("update_users_group", "/user_groups/{id}",
240 action="update", conditions=dict(method=["POST"]))
239 action="update", conditions=dict(method=["POST"]))
241 m.connect("delete_users_group", "/user_groups/{id}/delete",
240 m.connect("delete_users_group", "/user_groups/{id}/delete",
242 action="delete", conditions=dict(method=["POST"]))
241 action="delete", conditions=dict(method=["POST"]))
243 m.connect("edit_users_group", "/user_groups/{id}/edit",
242 m.connect("edit_users_group", "/user_groups/{id}/edit",
244 action="edit", conditions=dict(method=["GET"]),
243 action="edit", conditions=dict(method=["GET"]),
245 function=check_user_group)
244 function=check_user_group)
246
245
247 # EXTRAS USER GROUP ROUTES
246 # EXTRAS USER GROUP ROUTES
248 m.connect("edit_user_group_default_perms", "/user_groups/{id}/edit/default_perms",
247 m.connect("edit_user_group_default_perms", "/user_groups/{id}/edit/default_perms",
249 action="edit_default_perms", conditions=dict(method=["GET"]))
248 action="edit_default_perms", conditions=dict(method=["GET"]))
250 m.connect("edit_user_group_default_perms_update", "/user_groups/{id}/edit/default_perms",
249 m.connect("edit_user_group_default_perms_update", "/user_groups/{id}/edit/default_perms",
251 action="update_default_perms", conditions=dict(method=["POST"]))
250 action="update_default_perms", conditions=dict(method=["POST"]))
252
251
253 m.connect("edit_user_group_perms", "/user_groups/{id}/edit/perms",
252 m.connect("edit_user_group_perms", "/user_groups/{id}/edit/perms",
254 action="edit_perms", conditions=dict(method=["GET"]))
253 action="edit_perms", conditions=dict(method=["GET"]))
255 m.connect("edit_user_group_perms_update", "/user_groups/{id}/edit/perms",
254 m.connect("edit_user_group_perms_update", "/user_groups/{id}/edit/perms",
256 action="update_perms", conditions=dict(method=["POST"]))
255 action="update_perms", conditions=dict(method=["POST"]))
257 m.connect("edit_user_group_perms_delete", "/user_groups/{id}/edit/perms/delete",
256 m.connect("edit_user_group_perms_delete", "/user_groups/{id}/edit/perms/delete",
258 action="delete_perms", conditions=dict(method=["POST"]))
257 action="delete_perms", conditions=dict(method=["POST"]))
259
258
260 m.connect("edit_user_group_advanced", "/user_groups/{id}/edit/advanced",
259 m.connect("edit_user_group_advanced", "/user_groups/{id}/edit/advanced",
261 action="edit_advanced", conditions=dict(method=["GET"]))
260 action="edit_advanced", conditions=dict(method=["GET"]))
262
261
263 m.connect("edit_user_group_members", "/user_groups/{id}/edit/members",
262 m.connect("edit_user_group_members", "/user_groups/{id}/edit/members",
264 action="edit_members", conditions=dict(method=["GET"]))
263 action="edit_members", conditions=dict(method=["GET"]))
265
264
266 # ADMIN PERMISSIONS ROUTES
265 # ADMIN PERMISSIONS ROUTES
267 with rmap.submapper(path_prefix=ADMIN_PREFIX,
266 with rmap.submapper(path_prefix=ADMIN_PREFIX,
268 controller='admin/permissions') as m:
267 controller='admin/permissions') as m:
269 m.connect("admin_permissions", "/permissions",
268 m.connect("admin_permissions", "/permissions",
270 action="permission_globals", conditions=dict(method=["POST"]))
269 action="permission_globals", conditions=dict(method=["POST"]))
271 m.connect("admin_permissions", "/permissions",
270 m.connect("admin_permissions", "/permissions",
272 action="permission_globals", conditions=dict(method=["GET"]))
271 action="permission_globals", conditions=dict(method=["GET"]))
273
272
274 m.connect("admin_permissions_ips", "/permissions/ips",
273 m.connect("admin_permissions_ips", "/permissions/ips",
275 action="permission_ips", conditions=dict(method=["GET"]))
274 action="permission_ips", conditions=dict(method=["GET"]))
276
275
277 m.connect("admin_permissions_perms", "/permissions/perms",
276 m.connect("admin_permissions_perms", "/permissions/perms",
278 action="permission_perms", conditions=dict(method=["GET"]))
277 action="permission_perms", conditions=dict(method=["GET"]))
279
278
280 # ADMIN DEFAULTS ROUTES
279 # ADMIN DEFAULTS ROUTES
281 with rmap.submapper(path_prefix=ADMIN_PREFIX,
280 with rmap.submapper(path_prefix=ADMIN_PREFIX,
282 controller='admin/defaults') as m:
281 controller='admin/defaults') as m:
283 m.connect('defaults', '/defaults')
282 m.connect('defaults', '/defaults')
284 m.connect('defaults_update', 'defaults/{id}/update',
283 m.connect('defaults_update', 'defaults/{id}/update',
285 action="update", conditions=dict(method=["POST"]))
284 action="update", conditions=dict(method=["POST"]))
286
285
287 # ADMIN AUTH SETTINGS
286 # ADMIN AUTH SETTINGS
288 rmap.connect('auth_settings', '%s/auth' % ADMIN_PREFIX,
287 rmap.connect('auth_settings', '%s/auth' % ADMIN_PREFIX,
289 controller='admin/auth_settings', action='auth_settings',
288 controller='admin/auth_settings', action='auth_settings',
290 conditions=dict(method=["POST"]))
289 conditions=dict(method=["POST"]))
291 rmap.connect('auth_home', '%s/auth' % ADMIN_PREFIX,
290 rmap.connect('auth_home', '%s/auth' % ADMIN_PREFIX,
292 controller='admin/auth_settings')
291 controller='admin/auth_settings')
293
292
294 # ADMIN SETTINGS ROUTES
293 # ADMIN SETTINGS ROUTES
295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
294 with rmap.submapper(path_prefix=ADMIN_PREFIX,
296 controller='admin/settings') as m:
295 controller='admin/settings') as m:
297 m.connect("admin_settings", "/settings",
296 m.connect("admin_settings", "/settings",
298 action="settings_vcs", conditions=dict(method=["POST"]))
297 action="settings_vcs", conditions=dict(method=["POST"]))
299 m.connect("admin_settings", "/settings",
298 m.connect("admin_settings", "/settings",
300 action="settings_vcs", conditions=dict(method=["GET"]))
299 action="settings_vcs", conditions=dict(method=["GET"]))
301
300
302 m.connect("admin_settings_mapping", "/settings/mapping",
301 m.connect("admin_settings_mapping", "/settings/mapping",
303 action="settings_mapping", conditions=dict(method=["POST"]))
302 action="settings_mapping", conditions=dict(method=["POST"]))
304 m.connect("admin_settings_mapping", "/settings/mapping",
303 m.connect("admin_settings_mapping", "/settings/mapping",
305 action="settings_mapping", conditions=dict(method=["GET"]))
304 action="settings_mapping", conditions=dict(method=["GET"]))
306
305
307 m.connect("admin_settings_global", "/settings/global",
306 m.connect("admin_settings_global", "/settings/global",
308 action="settings_global", conditions=dict(method=["POST"]))
307 action="settings_global", conditions=dict(method=["POST"]))
309 m.connect("admin_settings_global", "/settings/global",
308 m.connect("admin_settings_global", "/settings/global",
310 action="settings_global", conditions=dict(method=["GET"]))
309 action="settings_global", conditions=dict(method=["GET"]))
311
310
312 m.connect("admin_settings_visual", "/settings/visual",
311 m.connect("admin_settings_visual", "/settings/visual",
313 action="settings_visual", conditions=dict(method=["POST"]))
312 action="settings_visual", conditions=dict(method=["POST"]))
314 m.connect("admin_settings_visual", "/settings/visual",
313 m.connect("admin_settings_visual", "/settings/visual",
315 action="settings_visual", conditions=dict(method=["GET"]))
314 action="settings_visual", conditions=dict(method=["GET"]))
316
315
317 m.connect("admin_settings_email", "/settings/email",
316 m.connect("admin_settings_email", "/settings/email",
318 action="settings_email", conditions=dict(method=["POST"]))
317 action="settings_email", conditions=dict(method=["POST"]))
319 m.connect("admin_settings_email", "/settings/email",
318 m.connect("admin_settings_email", "/settings/email",
320 action="settings_email", conditions=dict(method=["GET"]))
319 action="settings_email", conditions=dict(method=["GET"]))
321
320
322 m.connect("admin_settings_hooks", "/settings/hooks",
321 m.connect("admin_settings_hooks", "/settings/hooks",
323 action="settings_hooks", conditions=dict(method=["POST"]))
322 action="settings_hooks", conditions=dict(method=["POST"]))
324 m.connect("admin_settings_hooks_delete", "/settings/hooks/delete",
323 m.connect("admin_settings_hooks_delete", "/settings/hooks/delete",
325 action="settings_hooks", conditions=dict(method=["POST"]))
324 action="settings_hooks", conditions=dict(method=["POST"]))
326 m.connect("admin_settings_hooks", "/settings/hooks",
325 m.connect("admin_settings_hooks", "/settings/hooks",
327 action="settings_hooks", conditions=dict(method=["GET"]))
326 action="settings_hooks", conditions=dict(method=["GET"]))
328
327
329 m.connect("admin_settings_search", "/settings/search",
328 m.connect("admin_settings_search", "/settings/search",
330 action="settings_search", conditions=dict(method=["POST"]))
329 action="settings_search", conditions=dict(method=["POST"]))
331 m.connect("admin_settings_search", "/settings/search",
330 m.connect("admin_settings_search", "/settings/search",
332 action="settings_search", conditions=dict(method=["GET"]))
331 action="settings_search", conditions=dict(method=["GET"]))
333
332
334 m.connect("admin_settings_system", "/settings/system",
333 m.connect("admin_settings_system", "/settings/system",
335 action="settings_system", conditions=dict(method=["POST"]))
334 action="settings_system", conditions=dict(method=["POST"]))
336 m.connect("admin_settings_system", "/settings/system",
335 m.connect("admin_settings_system", "/settings/system",
337 action="settings_system", conditions=dict(method=["GET"]))
336 action="settings_system", conditions=dict(method=["GET"]))
338
337
339 # ADMIN MY ACCOUNT
338 # ADMIN MY ACCOUNT
340 with rmap.submapper(path_prefix=ADMIN_PREFIX,
339 with rmap.submapper(path_prefix=ADMIN_PREFIX,
341 controller='admin/my_account') as m:
340 controller='admin/my_account') as m:
342
341
343 m.connect("my_account", "/my_account",
342 m.connect("my_account", "/my_account",
344 action="my_account", conditions=dict(method=["GET"]))
343 action="my_account", conditions=dict(method=["GET"]))
345 m.connect("my_account", "/my_account",
344 m.connect("my_account", "/my_account",
346 action="my_account", conditions=dict(method=["POST"]))
345 action="my_account", conditions=dict(method=["POST"]))
347
346
348 m.connect("my_account_password", "/my_account/password",
347 m.connect("my_account_password", "/my_account/password",
349 action="my_account_password", conditions=dict(method=["GET"]))
348 action="my_account_password", conditions=dict(method=["GET"]))
350 m.connect("my_account_password", "/my_account/password",
349 m.connect("my_account_password", "/my_account/password",
351 action="my_account_password", conditions=dict(method=["POST"]))
350 action="my_account_password", conditions=dict(method=["POST"]))
352
351
353 m.connect("my_account_repos", "/my_account/repos",
352 m.connect("my_account_repos", "/my_account/repos",
354 action="my_account_repos", conditions=dict(method=["GET"]))
353 action="my_account_repos", conditions=dict(method=["GET"]))
355
354
356 m.connect("my_account_watched", "/my_account/watched",
355 m.connect("my_account_watched", "/my_account/watched",
357 action="my_account_watched", conditions=dict(method=["GET"]))
356 action="my_account_watched", conditions=dict(method=["GET"]))
358
357
359 m.connect("my_account_perms", "/my_account/perms",
358 m.connect("my_account_perms", "/my_account/perms",
360 action="my_account_perms", conditions=dict(method=["GET"]))
359 action="my_account_perms", conditions=dict(method=["GET"]))
361
360
362 m.connect("my_account_emails", "/my_account/emails",
361 m.connect("my_account_emails", "/my_account/emails",
363 action="my_account_emails", conditions=dict(method=["GET"]))
362 action="my_account_emails", conditions=dict(method=["GET"]))
364 m.connect("my_account_emails", "/my_account/emails",
363 m.connect("my_account_emails", "/my_account/emails",
365 action="my_account_emails_add", conditions=dict(method=["POST"]))
364 action="my_account_emails_add", conditions=dict(method=["POST"]))
366 m.connect("my_account_emails_delete", "/my_account/emails/delete",
365 m.connect("my_account_emails_delete", "/my_account/emails/delete",
367 action="my_account_emails_delete", conditions=dict(method=["POST"]))
366 action="my_account_emails_delete", conditions=dict(method=["POST"]))
368
367
369 m.connect("my_account_api_keys", "/my_account/api_keys",
368 m.connect("my_account_api_keys", "/my_account/api_keys",
370 action="my_account_api_keys", conditions=dict(method=["GET"]))
369 action="my_account_api_keys", conditions=dict(method=["GET"]))
371 m.connect("my_account_api_keys", "/my_account/api_keys",
370 m.connect("my_account_api_keys", "/my_account/api_keys",
372 action="my_account_api_keys_add", conditions=dict(method=["POST"]))
371 action="my_account_api_keys_add", conditions=dict(method=["POST"]))
373 m.connect("my_account_api_keys_delete", "/my_account/api_keys/delete",
372 m.connect("my_account_api_keys_delete", "/my_account/api_keys/delete",
374 action="my_account_api_keys_delete", conditions=dict(method=["POST"]))
373 action="my_account_api_keys_delete", conditions=dict(method=["POST"]))
375
374
376 m.connect("my_account_ssh_keys", "/my_account/ssh_keys",
375 m.connect("my_account_ssh_keys", "/my_account/ssh_keys",
377 action="my_account_ssh_keys", conditions=dict(method=["GET"]))
376 action="my_account_ssh_keys", conditions=dict(method=["GET"]))
378 m.connect("my_account_ssh_keys", "/my_account/ssh_keys",
377 m.connect("my_account_ssh_keys", "/my_account/ssh_keys",
379 action="my_account_ssh_keys_add", conditions=dict(method=["POST"]))
378 action="my_account_ssh_keys_add", conditions=dict(method=["POST"]))
380 m.connect("my_account_ssh_keys_delete", "/my_account/ssh_keys/delete",
379 m.connect("my_account_ssh_keys_delete", "/my_account/ssh_keys/delete",
381 action="my_account_ssh_keys_delete", conditions=dict(method=["POST"]))
380 action="my_account_ssh_keys_delete", conditions=dict(method=["POST"]))
382
381
383 # ADMIN GIST
382 # ADMIN GIST
384 with rmap.submapper(path_prefix=ADMIN_PREFIX,
383 with rmap.submapper(path_prefix=ADMIN_PREFIX,
385 controller='admin/gists') as m:
384 controller='admin/gists') as m:
386 m.connect("gists", "/gists",
385 m.connect("gists", "/gists",
387 action="create", conditions=dict(method=["POST"]))
386 action="create", conditions=dict(method=["POST"]))
388 m.connect("gists", "/gists",
387 m.connect("gists", "/gists",
389 conditions=dict(method=["GET"]))
388 conditions=dict(method=["GET"]))
390 m.connect("new_gist", "/gists/new",
389 m.connect("new_gist", "/gists/new",
391 action="new", conditions=dict(method=["GET"]))
390 action="new", conditions=dict(method=["GET"]))
392
391
393 m.connect("gist_delete", "/gists/{gist_id}/delete",
392 m.connect("gist_delete", "/gists/{gist_id}/delete",
394 action="delete", conditions=dict(method=["POST"]))
393 action="delete", conditions=dict(method=["POST"]))
395 m.connect("edit_gist", "/gists/{gist_id}/edit",
394 m.connect("edit_gist", "/gists/{gist_id}/edit",
396 action="edit", conditions=dict(method=["GET", "POST"]))
395 action="edit", conditions=dict(method=["GET", "POST"]))
397 m.connect("edit_gist_check_revision", "/gists/{gist_id}/edit/check_revision",
396 m.connect("edit_gist_check_revision", "/gists/{gist_id}/edit/check_revision",
398 action="check_revision", conditions=dict(method=["POST"]))
397 action="check_revision", conditions=dict(method=["POST"]))
399
398
400 m.connect("gist", "/gists/{gist_id}",
399 m.connect("gist", "/gists/{gist_id}",
401 action="show", conditions=dict(method=["GET"]))
400 action="show", conditions=dict(method=["GET"]))
402 m.connect("gist_rev", "/gists/{gist_id}/{revision}",
401 m.connect("gist_rev", "/gists/{gist_id}/{revision}",
403 revision="tip",
402 revision="tip",
404 action="show", conditions=dict(method=["GET"]))
403 action="show", conditions=dict(method=["GET"]))
405 m.connect("formatted_gist", "/gists/{gist_id}/{revision}/{format}",
404 m.connect("formatted_gist", "/gists/{gist_id}/{revision}/{format}",
406 revision="tip",
405 revision="tip",
407 action="show", conditions=dict(method=["GET"]))
406 action="show", conditions=dict(method=["GET"]))
408 m.connect("formatted_gist_file", "/gists/{gist_id}/{revision}/{format}/{f_path:.*}",
407 m.connect("formatted_gist_file", "/gists/{gist_id}/{revision}/{format}/{f_path:.*}",
409 revision='tip',
408 revision='tip',
410 action="show", conditions=dict(method=["GET"]))
409 action="show", conditions=dict(method=["GET"]))
411
410
412 # ADMIN MAIN PAGES
411 # ADMIN MAIN PAGES
413 with rmap.submapper(path_prefix=ADMIN_PREFIX,
412 with rmap.submapper(path_prefix=ADMIN_PREFIX,
414 controller='admin/admin') as m:
413 controller='admin/admin') as m:
415 m.connect('admin_home', '')
414 m.connect('admin_home', '')
416 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9. _-]*}',
415 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9. _-]*}',
417 action='add_repo')
416 action='add_repo')
418 #==========================================================================
417 #==========================================================================
419 # API V2
418 # API V2
420 #==========================================================================
419 #==========================================================================
421 with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='api/api',
420 with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='api/api',
422 action='_dispatch') as m:
421 action='_dispatch') as m:
423 m.connect('api', '/api')
422 m.connect('api', '/api')
424
423
425 # USER JOURNAL
424 # USER JOURNAL
426 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
425 rmap.connect('journal', '%s/journal' % ADMIN_PREFIX,
427 controller='journal')
426 controller='journal')
428 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
427 rmap.connect('journal_rss', '%s/journal/rss' % ADMIN_PREFIX,
429 controller='journal', action='journal_rss')
428 controller='journal', action='journal_rss')
430 rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
429 rmap.connect('journal_atom', '%s/journal/atom' % ADMIN_PREFIX,
431 controller='journal', action='journal_atom')
430 controller='journal', action='journal_atom')
432
431
433 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
432 rmap.connect('public_journal', '%s/public_journal' % ADMIN_PREFIX,
434 controller='journal', action="public_journal")
433 controller='journal', action="public_journal")
435
434
436 rmap.connect('public_journal_rss', '%s/public_journal/rss' % ADMIN_PREFIX,
435 rmap.connect('public_journal_rss', '%s/public_journal/rss' % ADMIN_PREFIX,
437 controller='journal', action="public_journal_rss")
436 controller='journal', action="public_journal_rss")
438
437
439 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
438 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % ADMIN_PREFIX,
440 controller='journal', action="public_journal_rss")
439 controller='journal', action="public_journal_rss")
441
440
442 rmap.connect('public_journal_atom',
441 rmap.connect('public_journal_atom',
443 '%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
442 '%s/public_journal/atom' % ADMIN_PREFIX, controller='journal',
444 action="public_journal_atom")
443 action="public_journal_atom")
445
444
446 rmap.connect('public_journal_atom_old',
445 rmap.connect('public_journal_atom_old',
447 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
446 '%s/public_journal_atom' % ADMIN_PREFIX, controller='journal',
448 action="public_journal_atom")
447 action="public_journal_atom")
449
448
450 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
449 rmap.connect('toggle_following', '%s/toggle_following' % ADMIN_PREFIX,
451 controller='journal', action='toggle_following',
450 controller='journal', action='toggle_following',
452 conditions=dict(method=["POST"]))
451 conditions=dict(method=["POST"]))
453
452
454 # SEARCH
453 # SEARCH
455 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
454 rmap.connect('search', '%s/search' % ADMIN_PREFIX, controller='search',)
456 rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
455 rmap.connect('search_repo_admin', '%s/search/{repo_name:.*}' % ADMIN_PREFIX,
457 controller='search',
456 controller='search',
458 conditions=dict(function=check_repo))
457 conditions=dict(function=check_repo))
459 rmap.connect('search_repo', '/{repo_name:.*?}/search',
458 rmap.connect('search_repo', '/{repo_name:.*?}/search',
460 controller='search',
459 controller='search',
461 conditions=dict(function=check_repo),
460 conditions=dict(function=check_repo),
462 )
461 )
463
462
464 # LOGIN/LOGOUT/REGISTER/SIGN IN
463 # LOGIN/LOGOUT/REGISTER/SIGN IN
465 rmap.connect('session_csrf_secret_token', '%s/session_csrf_secret_token' % ADMIN_PREFIX, controller='login', action='session_csrf_secret_token')
464 rmap.connect('session_csrf_secret_token', '%s/session_csrf_secret_token' % ADMIN_PREFIX, controller='login', action='session_csrf_secret_token')
466 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
465 rmap.connect('login_home', '%s/login' % ADMIN_PREFIX, controller='login')
467 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
466 rmap.connect('logout_home', '%s/logout' % ADMIN_PREFIX, controller='login',
468 action='logout')
467 action='logout')
469
468
470 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
469 rmap.connect('register', '%s/register' % ADMIN_PREFIX, controller='login',
471 action='register')
470 action='register')
472
471
473 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
472 rmap.connect('reset_password', '%s/password_reset' % ADMIN_PREFIX,
474 controller='login', action='password_reset')
473 controller='login', action='password_reset')
475
474
476 rmap.connect('reset_password_confirmation',
475 rmap.connect('reset_password_confirmation',
477 '%s/password_reset_confirmation' % ADMIN_PREFIX,
476 '%s/password_reset_confirmation' % ADMIN_PREFIX,
478 controller='login', action='password_reset_confirmation')
477 controller='login', action='password_reset_confirmation')
479
478
480 # FEEDS
479 # FEEDS
481 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
480 rmap.connect('rss_feed_home', '/{repo_name:.*?}/feed/rss',
482 controller='feed', action='rss',
481 controller='feed', action='rss',
483 conditions=dict(function=check_repo))
482 conditions=dict(function=check_repo))
484
483
485 rmap.connect('atom_feed_home', '/{repo_name:.*?}/feed/atom',
484 rmap.connect('atom_feed_home', '/{repo_name:.*?}/feed/atom',
486 controller='feed', action='atom',
485 controller='feed', action='atom',
487 conditions=dict(function=check_repo))
486 conditions=dict(function=check_repo))
488
487
489 #==========================================================================
488 #==========================================================================
490 # REPOSITORY ROUTES
489 # REPOSITORY ROUTES
491 #==========================================================================
490 #==========================================================================
492 rmap.connect('repo_creating_home', '/{repo_name:.*?}/repo_creating',
491 rmap.connect('repo_creating_home', '/{repo_name:.*?}/repo_creating',
493 controller='admin/repos', action='repo_creating')
492 controller='admin/repos', action='repo_creating')
494 rmap.connect('repo_check_home', '/{repo_name:.*?}/repo_check_creating',
493 rmap.connect('repo_check_home', '/{repo_name:.*?}/repo_check_creating',
495 controller='admin/repos', action='repo_check')
494 controller='admin/repos', action='repo_check')
496
495
497 rmap.connect('summary_home', '/{repo_name:.*?}',
496 rmap.connect('summary_home', '/{repo_name:.*?}',
498 controller='summary',
497 controller='summary',
499 conditions=dict(function=check_repo))
498 conditions=dict(function=check_repo))
500
499
501 # must be here for proper group/repo catching
500 # must be here for proper group/repo catching
502 rmap.connect('repos_group_home', '/{group_name:.*}',
501 rmap.connect('repos_group_home', '/{group_name:.*}',
503 controller='admin/repo_groups', action="show_by_name",
502 controller='admin/repo_groups', action="show_by_name",
504 conditions=dict(function=check_group))
503 conditions=dict(function=check_group))
505 rmap.connect('repo_stats_home', '/{repo_name:.*?}/statistics',
504 rmap.connect('repo_stats_home', '/{repo_name:.*?}/statistics',
506 controller='summary', action='statistics',
505 controller='summary', action='statistics',
507 conditions=dict(function=check_repo))
506 conditions=dict(function=check_repo))
508
507
509 rmap.connect('repo_size', '/{repo_name:.*?}/repo_size',
508 rmap.connect('repo_size', '/{repo_name:.*?}/repo_size',
510 controller='summary', action='repo_size',
509 controller='summary', action='repo_size',
511 conditions=dict(function=check_repo))
510 conditions=dict(function=check_repo))
512
511
513 rmap.connect('repo_refs_data', '/{repo_name:.*?}/refs-data',
512 rmap.connect('repo_refs_data', '/{repo_name:.*?}/refs-data',
514 controller='home', action='repo_refs_data')
513 controller='home', action='repo_refs_data')
515
514
516 rmap.connect('changeset_home', '/{repo_name:.*?}/changeset/{revision:.*}',
515 rmap.connect('changeset_home', '/{repo_name:.*?}/changeset/{revision:.*}',
517 controller='changeset', revision='tip',
516 controller='changeset', revision='tip',
518 conditions=dict(function=check_repo))
517 conditions=dict(function=check_repo))
519 rmap.connect('changeset_children', '/{repo_name:.*?}/changeset_children/{revision}',
518 rmap.connect('changeset_children', '/{repo_name:.*?}/changeset_children/{revision}',
520 controller='changeset', revision='tip', action="changeset_children",
519 controller='changeset', revision='tip', action="changeset_children",
521 conditions=dict(function=check_repo))
520 conditions=dict(function=check_repo))
522 rmap.connect('changeset_parents', '/{repo_name:.*?}/changeset_parents/{revision}',
521 rmap.connect('changeset_parents', '/{repo_name:.*?}/changeset_parents/{revision}',
523 controller='changeset', revision='tip', action="changeset_parents",
522 controller='changeset', revision='tip', action="changeset_parents",
524 conditions=dict(function=check_repo))
523 conditions=dict(function=check_repo))
525
524
526 # repo edit options
525 # repo edit options
527 rmap.connect("edit_repo", "/{repo_name:.*?}/settings",
526 rmap.connect("edit_repo", "/{repo_name:.*?}/settings",
528 controller='admin/repos', action="edit",
527 controller='admin/repos', action="edit",
529 conditions=dict(method=["GET"], function=check_repo))
528 conditions=dict(method=["GET"], function=check_repo))
530
529
531 rmap.connect("edit_repo_perms", "/{repo_name:.*?}/settings/permissions",
530 rmap.connect("edit_repo_perms", "/{repo_name:.*?}/settings/permissions",
532 controller='admin/repos', action="edit_permissions",
531 controller='admin/repos', action="edit_permissions",
533 conditions=dict(method=["GET"], function=check_repo))
532 conditions=dict(method=["GET"], function=check_repo))
534 rmap.connect("edit_repo_perms_update", "/{repo_name:.*?}/settings/permissions",
533 rmap.connect("edit_repo_perms_update", "/{repo_name:.*?}/settings/permissions",
535 controller='admin/repos', action="edit_permissions_update",
534 controller='admin/repos', action="edit_permissions_update",
536 conditions=dict(method=["POST"], function=check_repo))
535 conditions=dict(method=["POST"], function=check_repo))
537 rmap.connect("edit_repo_perms_revoke", "/{repo_name:.*?}/settings/permissions/delete",
536 rmap.connect("edit_repo_perms_revoke", "/{repo_name:.*?}/settings/permissions/delete",
538 controller='admin/repos', action="edit_permissions_revoke",
537 controller='admin/repos', action="edit_permissions_revoke",
539 conditions=dict(method=["POST"], function=check_repo))
538 conditions=dict(method=["POST"], function=check_repo))
540
539
541 rmap.connect("edit_repo_fields", "/{repo_name:.*?}/settings/fields",
540 rmap.connect("edit_repo_fields", "/{repo_name:.*?}/settings/fields",
542 controller='admin/repos', action="edit_fields",
541 controller='admin/repos', action="edit_fields",
543 conditions=dict(method=["GET"], function=check_repo))
542 conditions=dict(method=["GET"], function=check_repo))
544 rmap.connect('create_repo_fields', "/{repo_name:.*?}/settings/fields/new",
543 rmap.connect('create_repo_fields', "/{repo_name:.*?}/settings/fields/new",
545 controller='admin/repos', action="create_repo_field",
544 controller='admin/repos', action="create_repo_field",
546 conditions=dict(method=["POST"], function=check_repo))
545 conditions=dict(method=["POST"], function=check_repo))
547 rmap.connect('delete_repo_fields', "/{repo_name:.*?}/settings/fields/{field_id}/delete",
546 rmap.connect('delete_repo_fields', "/{repo_name:.*?}/settings/fields/{field_id}/delete",
548 controller='admin/repos', action="delete_repo_field",
547 controller='admin/repos', action="delete_repo_field",
549 conditions=dict(method=["POST"], function=check_repo))
548 conditions=dict(method=["POST"], function=check_repo))
550
549
551 rmap.connect("edit_repo_advanced", "/{repo_name:.*?}/settings/advanced",
550 rmap.connect("edit_repo_advanced", "/{repo_name:.*?}/settings/advanced",
552 controller='admin/repos', action="edit_advanced",
551 controller='admin/repos', action="edit_advanced",
553 conditions=dict(method=["GET"], function=check_repo))
552 conditions=dict(method=["GET"], function=check_repo))
554
553
555 rmap.connect("edit_repo_advanced_journal", "/{repo_name:.*?}/settings/advanced/journal",
554 rmap.connect("edit_repo_advanced_journal", "/{repo_name:.*?}/settings/advanced/journal",
556 controller='admin/repos', action="edit_advanced_journal",
555 controller='admin/repos', action="edit_advanced_journal",
557 conditions=dict(method=["POST"], function=check_repo))
556 conditions=dict(method=["POST"], function=check_repo))
558
557
559 rmap.connect("edit_repo_advanced_fork", "/{repo_name:.*?}/settings/advanced/fork",
558 rmap.connect("edit_repo_advanced_fork", "/{repo_name:.*?}/settings/advanced/fork",
560 controller='admin/repos', action="edit_advanced_fork",
559 controller='admin/repos', action="edit_advanced_fork",
561 conditions=dict(method=["POST"], function=check_repo))
560 conditions=dict(method=["POST"], function=check_repo))
562
561
563 rmap.connect("edit_repo_remote", "/{repo_name:.*?}/settings/remote",
562 rmap.connect("edit_repo_remote", "/{repo_name:.*?}/settings/remote",
564 controller='admin/repos', action="edit_remote",
563 controller='admin/repos', action="edit_remote",
565 conditions=dict(method=["GET"], function=check_repo))
564 conditions=dict(method=["GET"], function=check_repo))
566 rmap.connect("edit_repo_remote_update", "/{repo_name:.*?}/settings/remote",
565 rmap.connect("edit_repo_remote_update", "/{repo_name:.*?}/settings/remote",
567 controller='admin/repos', action="edit_remote",
566 controller='admin/repos', action="edit_remote",
568 conditions=dict(method=["POST"], function=check_repo))
567 conditions=dict(method=["POST"], function=check_repo))
569
568
570 rmap.connect("edit_repo_statistics", "/{repo_name:.*?}/settings/statistics",
569 rmap.connect("edit_repo_statistics", "/{repo_name:.*?}/settings/statistics",
571 controller='admin/repos', action="edit_statistics",
570 controller='admin/repos', action="edit_statistics",
572 conditions=dict(method=["GET"], function=check_repo))
571 conditions=dict(method=["GET"], function=check_repo))
573 rmap.connect("edit_repo_statistics_update", "/{repo_name:.*?}/settings/statistics",
572 rmap.connect("edit_repo_statistics_update", "/{repo_name:.*?}/settings/statistics",
574 controller='admin/repos', action="edit_statistics",
573 controller='admin/repos', action="edit_statistics",
575 conditions=dict(method=["POST"], function=check_repo))
574 conditions=dict(method=["POST"], function=check_repo))
576
575
577 # still working url for backward compat.
576 # still working url for backward compat.
578 rmap.connect('raw_changeset_home_depraced',
577 rmap.connect('raw_changeset_home_depraced',
579 '/{repo_name:.*?}/raw-changeset/{revision}',
578 '/{repo_name:.*?}/raw-changeset/{revision}',
580 controller='changeset', action='changeset_raw',
579 controller='changeset', action='changeset_raw',
581 revision='tip', conditions=dict(function=check_repo))
580 revision='tip', conditions=dict(function=check_repo))
582
581
583 ## new URLs
582 ## new URLs
584 rmap.connect('changeset_raw_home',
583 rmap.connect('changeset_raw_home',
585 '/{repo_name:.*?}/changeset-diff/{revision}',
584 '/{repo_name:.*?}/changeset-diff/{revision}',
586 controller='changeset', action='changeset_raw',
585 controller='changeset', action='changeset_raw',
587 revision='tip', conditions=dict(function=check_repo))
586 revision='tip', conditions=dict(function=check_repo))
588
587
589 rmap.connect('changeset_patch_home',
588 rmap.connect('changeset_patch_home',
590 '/{repo_name:.*?}/changeset-patch/{revision}',
589 '/{repo_name:.*?}/changeset-patch/{revision}',
591 controller='changeset', action='changeset_patch',
590 controller='changeset', action='changeset_patch',
592 revision='tip', conditions=dict(function=check_repo))
591 revision='tip', conditions=dict(function=check_repo))
593
592
594 rmap.connect('changeset_download_home',
593 rmap.connect('changeset_download_home',
595 '/{repo_name:.*?}/changeset-download/{revision}',
594 '/{repo_name:.*?}/changeset-download/{revision}',
596 controller='changeset', action='changeset_download',
595 controller='changeset', action='changeset_download',
597 revision='tip', conditions=dict(function=check_repo))
596 revision='tip', conditions=dict(function=check_repo))
598
597
599 rmap.connect('changeset_comment',
598 rmap.connect('changeset_comment',
600 '/{repo_name:.*?}/changeset-comment/{revision}',
599 '/{repo_name:.*?}/changeset-comment/{revision}',
601 controller='changeset', revision='tip', action='comment',
600 controller='changeset', revision='tip', action='comment',
602 conditions=dict(function=check_repo))
601 conditions=dict(function=check_repo))
603
602
604 rmap.connect('changeset_comment_delete',
603 rmap.connect('changeset_comment_delete',
605 '/{repo_name:.*?}/changeset-comment/{comment_id}/delete',
604 '/{repo_name:.*?}/changeset-comment/{comment_id}/delete',
606 controller='changeset', action='delete_comment',
605 controller='changeset', action='delete_comment',
607 conditions=dict(function=check_repo, method=["POST"]))
606 conditions=dict(function=check_repo, method=["POST"]))
608
607
609 rmap.connect('changeset_info', '/changeset_info/{repo_name:.*?}/{revision}',
608 rmap.connect('changeset_info', '/changeset_info/{repo_name:.*?}/{revision}',
610 controller='changeset', action='changeset_info')
609 controller='changeset', action='changeset_info')
611
610
612 rmap.connect('compare_home',
611 rmap.connect('compare_home',
613 '/{repo_name:.*?}/compare',
612 '/{repo_name:.*?}/compare',
614 controller='compare',
613 controller='compare',
615 conditions=dict(function=check_repo))
614 conditions=dict(function=check_repo))
616
615
617 rmap.connect('compare_url',
616 rmap.connect('compare_url',
618 '/{repo_name:.*?}/compare/{org_ref_type}@{org_ref_name:.*?}...{other_ref_type}@{other_ref_name:.*?}',
617 '/{repo_name:.*?}/compare/{org_ref_type}@{org_ref_name:.*?}...{other_ref_type}@{other_ref_name:.*?}',
619 controller='compare', action='compare',
618 controller='compare', action='compare',
620 conditions=dict(function=check_repo),
619 conditions=dict(function=check_repo),
621 requirements=dict(
620 requirements=dict(
622 org_ref_type='(branch|book|tag|rev|__other_ref_type__)',
621 org_ref_type='(branch|book|tag|rev|__other_ref_type__)',
623 other_ref_type='(branch|book|tag|rev|__org_ref_type__)')
622 other_ref_type='(branch|book|tag|rev|__org_ref_type__)')
624 )
623 )
625
624
626 rmap.connect('pullrequest_home',
625 rmap.connect('pullrequest_home',
627 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
626 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
628 conditions=dict(function=check_repo,
627 conditions=dict(function=check_repo,
629 method=["GET"]))
628 method=["GET"]))
630
629
631 rmap.connect('pullrequest_repo_info',
630 rmap.connect('pullrequest_repo_info',
632 '/{repo_name:.*?}/pull-request-repo-info',
631 '/{repo_name:.*?}/pull-request-repo-info',
633 controller='pullrequests', action='repo_info',
632 controller='pullrequests', action='repo_info',
634 conditions=dict(function=check_repo, method=["GET"]))
633 conditions=dict(function=check_repo, method=["GET"]))
635
634
636 rmap.connect('pullrequest',
635 rmap.connect('pullrequest',
637 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
636 '/{repo_name:.*?}/pull-request/new', controller='pullrequests',
638 action='create', conditions=dict(function=check_repo,
637 action='create', conditions=dict(function=check_repo,
639 method=["POST"]))
638 method=["POST"]))
640
639
641 rmap.connect('pullrequest_show',
640 rmap.connect('pullrequest_show',
642 '/{repo_name:.*?}/pull-request/{pull_request_id:\\d+}{extra:(/.*)?}', extra='',
641 '/{repo_name:.*?}/pull-request/{pull_request_id:\\d+}{extra:(/.*)?}', extra='',
643 controller='pullrequests',
642 controller='pullrequests',
644 action='show', conditions=dict(function=check_repo,
643 action='show', conditions=dict(function=check_repo,
645 method=["GET"]))
644 method=["GET"]))
646 rmap.connect('pullrequest_post',
645 rmap.connect('pullrequest_post',
647 '/{repo_name:.*?}/pull-request/{pull_request_id}',
646 '/{repo_name:.*?}/pull-request/{pull_request_id}',
648 controller='pullrequests',
647 controller='pullrequests',
649 action='post', conditions=dict(function=check_repo,
648 action='post', conditions=dict(function=check_repo,
650 method=["POST"]))
649 method=["POST"]))
651 rmap.connect('pullrequest_delete',
650 rmap.connect('pullrequest_delete',
652 '/{repo_name:.*?}/pull-request/{pull_request_id}/delete',
651 '/{repo_name:.*?}/pull-request/{pull_request_id}/delete',
653 controller='pullrequests',
652 controller='pullrequests',
654 action='delete', conditions=dict(function=check_repo,
653 action='delete', conditions=dict(function=check_repo,
655 method=["POST"]))
654 method=["POST"]))
656
655
657 rmap.connect('pullrequest_show_all',
656 rmap.connect('pullrequest_show_all',
658 '/{repo_name:.*?}/pull-request',
657 '/{repo_name:.*?}/pull-request',
659 controller='pullrequests',
658 controller='pullrequests',
660 action='show_all', conditions=dict(function=check_repo,
659 action='show_all', conditions=dict(function=check_repo,
661 method=["GET"]))
660 method=["GET"]))
662
661
663 rmap.connect('my_pullrequests',
662 rmap.connect('my_pullrequests',
664 '/my_pullrequests',
663 '/my_pullrequests',
665 controller='pullrequests',
664 controller='pullrequests',
666 action='show_my', conditions=dict(method=["GET"]))
665 action='show_my', conditions=dict(method=["GET"]))
667
666
668 rmap.connect('pullrequest_comment',
667 rmap.connect('pullrequest_comment',
669 '/{repo_name:.*?}/pull-request-comment/{pull_request_id}',
668 '/{repo_name:.*?}/pull-request-comment/{pull_request_id}',
670 controller='pullrequests',
669 controller='pullrequests',
671 action='comment', conditions=dict(function=check_repo,
670 action='comment', conditions=dict(function=check_repo,
672 method=["POST"]))
671 method=["POST"]))
673
672
674 rmap.connect('pullrequest_comment_delete',
673 rmap.connect('pullrequest_comment_delete',
675 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
674 '/{repo_name:.*?}/pull-request-comment/{comment_id}/delete',
676 controller='pullrequests', action='delete_comment',
675 controller='pullrequests', action='delete_comment',
677 conditions=dict(function=check_repo, method=["POST"]))
676 conditions=dict(function=check_repo, method=["POST"]))
678
677
679 rmap.connect('summary_home_summary', '/{repo_name:.*?}/summary',
678 rmap.connect('summary_home_summary', '/{repo_name:.*?}/summary',
680 controller='summary', conditions=dict(function=check_repo))
679 controller='summary', conditions=dict(function=check_repo))
681
680
682 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
681 rmap.connect('changelog_home', '/{repo_name:.*?}/changelog',
683 controller='changelog', conditions=dict(function=check_repo))
682 controller='changelog', conditions=dict(function=check_repo))
684
683
685 rmap.connect('changelog_file_home', '/{repo_name:.*?}/changelog/{revision}/{f_path:.*}',
684 rmap.connect('changelog_file_home', '/{repo_name:.*?}/changelog/{revision}/{f_path:.*}',
686 controller='changelog',
685 controller='changelog',
687 conditions=dict(function=check_repo))
686 conditions=dict(function=check_repo))
688
687
689 rmap.connect('changelog_details', '/{repo_name:.*?}/changelog_details/{cs}',
688 rmap.connect('changelog_details', '/{repo_name:.*?}/changelog_details/{cs}',
690 controller='changelog', action='changelog_details',
689 controller='changelog', action='changelog_details',
691 conditions=dict(function=check_repo))
690 conditions=dict(function=check_repo))
692
691
693 rmap.connect('files_home', '/{repo_name:.*?}/files/{revision}/{f_path:.*}',
692 rmap.connect('files_home', '/{repo_name:.*?}/files/{revision}/{f_path:.*}',
694 controller='files', revision='tip', f_path='',
693 controller='files', revision='tip', f_path='',
695 conditions=dict(function=check_repo))
694 conditions=dict(function=check_repo))
696
695
697 rmap.connect('files_home_nopath', '/{repo_name:.*?}/files/{revision}',
696 rmap.connect('files_home_nopath', '/{repo_name:.*?}/files/{revision}',
698 controller='files', revision='tip', f_path='',
697 controller='files', revision='tip', f_path='',
699 conditions=dict(function=check_repo))
698 conditions=dict(function=check_repo))
700
699
701 rmap.connect('files_history_home',
700 rmap.connect('files_history_home',
702 '/{repo_name:.*?}/history/{revision}/{f_path:.*}',
701 '/{repo_name:.*?}/history/{revision}/{f_path:.*}',
703 controller='files', action='history', revision='tip', f_path='',
702 controller='files', action='history', revision='tip', f_path='',
704 conditions=dict(function=check_repo))
703 conditions=dict(function=check_repo))
705
704
706 rmap.connect('files_authors_home',
705 rmap.connect('files_authors_home',
707 '/{repo_name:.*?}/authors/{revision}/{f_path:.*}',
706 '/{repo_name:.*?}/authors/{revision}/{f_path:.*}',
708 controller='files', action='authors', revision='tip', f_path='',
707 controller='files', action='authors', revision='tip', f_path='',
709 conditions=dict(function=check_repo))
708 conditions=dict(function=check_repo))
710
709
711 rmap.connect('files_diff_home', '/{repo_name:.*?}/diff/{f_path:.*}',
710 rmap.connect('files_diff_home', '/{repo_name:.*?}/diff/{f_path:.*}',
712 controller='files', action='diff', revision='tip', f_path='',
711 controller='files', action='diff', revision='tip', f_path='',
713 conditions=dict(function=check_repo))
712 conditions=dict(function=check_repo))
714
713
715 rmap.connect('files_diff_2way_home', '/{repo_name:.*?}/diff-2way/{f_path:.+}',
714 rmap.connect('files_diff_2way_home', '/{repo_name:.*?}/diff-2way/{f_path:.+}',
716 controller='files', action='diff_2way', revision='tip', f_path='',
715 controller='files', action='diff_2way', revision='tip', f_path='',
717 conditions=dict(function=check_repo))
716 conditions=dict(function=check_repo))
718
717
719 rmap.connect('files_rawfile_home',
718 rmap.connect('files_rawfile_home',
720 '/{repo_name:.*?}/rawfile/{revision}/{f_path:.*}',
719 '/{repo_name:.*?}/rawfile/{revision}/{f_path:.*}',
721 controller='files', action='rawfile', revision='tip',
720 controller='files', action='rawfile', revision='tip',
722 f_path='', conditions=dict(function=check_repo))
721 f_path='', conditions=dict(function=check_repo))
723
722
724 rmap.connect('files_raw_home',
723 rmap.connect('files_raw_home',
725 '/{repo_name:.*?}/raw/{revision}/{f_path:.*}',
724 '/{repo_name:.*?}/raw/{revision}/{f_path:.*}',
726 controller='files', action='raw', revision='tip', f_path='',
725 controller='files', action='raw', revision='tip', f_path='',
727 conditions=dict(function=check_repo))
726 conditions=dict(function=check_repo))
728
727
729 rmap.connect('files_annotate_home',
728 rmap.connect('files_annotate_home',
730 '/{repo_name:.*?}/annotate/{revision}/{f_path:.*}',
729 '/{repo_name:.*?}/annotate/{revision}/{f_path:.*}',
731 controller='files', revision='tip',
730 controller='files', revision='tip',
732 f_path='', annotate='1', conditions=dict(function=check_repo))
731 f_path='', annotate='1', conditions=dict(function=check_repo))
733
732
734 rmap.connect('files_edit_home',
733 rmap.connect('files_edit_home',
735 '/{repo_name:.*?}/edit/{revision}/{f_path:.*}',
734 '/{repo_name:.*?}/edit/{revision}/{f_path:.*}',
736 controller='files', action='edit', revision='tip',
735 controller='files', action='edit', revision='tip',
737 f_path='', conditions=dict(function=check_repo))
736 f_path='', conditions=dict(function=check_repo))
738
737
739 rmap.connect('files_add_home',
738 rmap.connect('files_add_home',
740 '/{repo_name:.*?}/add/{revision}/{f_path:.*}',
739 '/{repo_name:.*?}/add/{revision}/{f_path:.*}',
741 controller='files', action='add', revision='tip',
740 controller='files', action='add', revision='tip',
742 f_path='', conditions=dict(function=check_repo))
741 f_path='', conditions=dict(function=check_repo))
743
742
744 rmap.connect('files_delete_home',
743 rmap.connect('files_delete_home',
745 '/{repo_name:.*?}/delete/{revision}/{f_path:.*}',
744 '/{repo_name:.*?}/delete/{revision}/{f_path:.*}',
746 controller='files', action='delete', revision='tip',
745 controller='files', action='delete', revision='tip',
747 f_path='', conditions=dict(function=check_repo))
746 f_path='', conditions=dict(function=check_repo))
748
747
749 rmap.connect('files_archive_home', '/{repo_name:.*?}/archive/{fname}',
748 rmap.connect('files_archive_home', '/{repo_name:.*?}/archive/{fname}',
750 controller='files', action='archivefile',
749 controller='files', action='archivefile',
751 conditions=dict(function=check_repo))
750 conditions=dict(function=check_repo))
752
751
753 rmap.connect('files_nodelist_home',
752 rmap.connect('files_nodelist_home',
754 '/{repo_name:.*?}/nodelist/{revision}/{f_path:.*}',
753 '/{repo_name:.*?}/nodelist/{revision}/{f_path:.*}',
755 controller='files', action='nodelist',
754 controller='files', action='nodelist',
756 conditions=dict(function=check_repo))
755 conditions=dict(function=check_repo))
757
756
758 rmap.connect('repo_fork_create_home', '/{repo_name:.*?}/fork',
757 rmap.connect('repo_fork_create_home', '/{repo_name:.*?}/fork',
759 controller='forks', action='fork_create',
758 controller='forks', action='fork_create',
760 conditions=dict(function=check_repo, method=["POST"]))
759 conditions=dict(function=check_repo, method=["POST"]))
761
760
762 rmap.connect('repo_fork_home', '/{repo_name:.*?}/fork',
761 rmap.connect('repo_fork_home', '/{repo_name:.*?}/fork',
763 controller='forks', action='fork',
762 controller='forks', action='fork',
764 conditions=dict(function=check_repo))
763 conditions=dict(function=check_repo))
765
764
766 rmap.connect('repo_forks_home', '/{repo_name:.*?}/forks',
765 rmap.connect('repo_forks_home', '/{repo_name:.*?}/forks',
767 controller='forks', action='forks',
766 controller='forks', action='forks',
768 conditions=dict(function=check_repo))
767 conditions=dict(function=check_repo))
769
768
770 rmap.connect('repo_followers_home', '/{repo_name:.*?}/followers',
769 rmap.connect('repo_followers_home', '/{repo_name:.*?}/followers',
771 controller='followers', action='followers',
770 controller='followers', action='followers',
772 conditions=dict(function=check_repo))
771 conditions=dict(function=check_repo))
773
772
774 return rmap
773 return rmap
@@ -1,1166 +1,1163 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Helper functions
15 Helper functions
16
16
17 Consists of functions to typically be used within templates, but also
17 Consists of functions to typically be used within templates, but also
18 available to Controllers. This module is available to both as 'h'.
18 available to Controllers. This module is available to both as 'h'.
19 """
19 """
20 import hashlib
20 import hashlib
21 import json
21 import json
22 import logging
22 import logging
23 import re
23 import re
24 import textwrap
24 import textwrap
25 import urllib.parse
25 import urllib.parse
26
26
27 from beaker.cache import cache_region
27 from beaker.cache import cache_region
28 from pygments import highlight as code_highlight
28 from pygments import highlight as code_highlight
29 from pygments.formatters.html import HtmlFormatter
29 from pygments.formatters.html import HtmlFormatter
30 from tg import tmpl_context as c
30 from tg.i18n import ugettext as _
31 from tg.i18n import ugettext as _
31
32
32 import kallithea
33 import kallithea
33 from kallithea.lib.annotate import annotate_highlight
34 from kallithea.lib.annotate import annotate_highlight
34 #==============================================================================
35 #==============================================================================
35 # PERMS
36 # PERMS
36 #==============================================================================
37 #==============================================================================
37 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoPermissionLevel
38 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel, HasRepoPermissionLevel
38 from kallithea.lib.diffs import BIN_FILENODE, CHMOD_FILENODE, DEL_FILENODE, MOD_FILENODE, NEW_FILENODE, RENAMED_FILENODE
39 from kallithea.lib.diffs import BIN_FILENODE, CHMOD_FILENODE, DEL_FILENODE, MOD_FILENODE, NEW_FILENODE, RENAMED_FILENODE
39 from kallithea.lib.markup_renderer import url_re
40 from kallithea.lib.markup_renderer import url_re
40 from kallithea.lib.pygmentsutils import get_custom_lexer
41 from kallithea.lib.pygmentsutils import get_custom_lexer
41 from kallithea.lib.utils2 import (MENTIONS_REGEX, AttributeDict, age, asbool, credentials_filter, fmt_date, link_to_ref, safe_bytes, safe_int, safe_str,
42 from kallithea.lib.utils2 import (MENTIONS_REGEX, AttributeDict, age, asbool, credentials_filter, fmt_date, link_to_ref, safe_bytes, safe_int, safe_str,
42 shorter, time_to_datetime)
43 shorter, time_to_datetime)
43 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
44 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
44 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
45 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
45 #==============================================================================
46 #==============================================================================
46 # SCM FILTERS available via h.
47 # SCM FILTERS available via h.
47 #==============================================================================
48 #==============================================================================
48 from kallithea.lib.vcs.utils import author_email, author_name
49 from kallithea.lib.vcs.utils import author_email, author_name
49 from kallithea.lib.webutils import (HTML, Option, canonical_url, checkbox, chop_at, end_form, escape, form, format_byte_size, hidden, html_escape, link_to,
50 from kallithea.lib.webutils import (HTML, Option, canonical_url, checkbox, chop_at, end_form, escape, form, format_byte_size, hidden, html_escape, link_to,
50 literal, password, pop_flash_messages, radio, reset, safeid, select, session_csrf_secret_name, session_csrf_secret_token,
51 literal, password, pop_flash_messages, radio, reset, safeid, select, session_csrf_secret_name, session_csrf_secret_token,
51 submit, text, textarea, truncate, url, wrap_paragraphs)
52 submit, text, textarea, truncate, url, wrap_paragraphs)
52 from kallithea.model import db
53 from kallithea.model import db
53 from kallithea.model.changeset_status import ChangesetStatusModel
54 from kallithea.model.changeset_status import ChangesetStatusModel
54
55
55
56
56 # mute pyflakes "imported but unused"
57 # mute pyflakes "imported but unused"
57 # from webutils
58 # from webutils
58 assert Option
59 assert Option
59 assert canonical_url
60 assert canonical_url
60 assert checkbox
61 assert checkbox
61 assert chop_at
62 assert chop_at
62 assert end_form
63 assert end_form
63 assert form
64 assert form
64 assert format_byte_size
65 assert format_byte_size
65 assert hidden
66 assert hidden
66 assert password
67 assert password
67 assert pop_flash_messages
68 assert pop_flash_messages
68 assert radio
69 assert radio
69 assert reset
70 assert reset
70 assert safeid
71 assert safeid
71 assert select
72 assert select
72 assert session_csrf_secret_name
73 assert session_csrf_secret_name
73 assert session_csrf_secret_token
74 assert session_csrf_secret_token
74 assert submit
75 assert submit
75 assert text
76 assert text
76 assert textarea
77 assert textarea
77 assert wrap_paragraphs
78 assert wrap_paragraphs
78 # from kallithea.lib.auth
79 # from kallithea.lib.auth
79 assert HasPermissionAny
80 assert HasPermissionAny
80 assert HasRepoGroupPermissionLevel
81 assert HasRepoGroupPermissionLevel
81 assert HasRepoPermissionLevel
82 assert HasRepoPermissionLevel
82 # from utils2
83 # from utils2
83 assert age
84 assert age
84 assert fmt_date
85 assert fmt_date
85 assert link_to_ref
86 assert link_to_ref
86 assert shorter
87 assert shorter
87 assert time_to_datetime
88 assert time_to_datetime
88 # from vcs
89 # from vcs
89 assert EmptyChangeset
90 assert EmptyChangeset
90
91
91
92
92 log = logging.getLogger(__name__)
93 log = logging.getLogger(__name__)
93
94
94
95
95 def js(value):
96 def js(value):
96 """Convert Python value to the corresponding JavaScript representation.
97 """Convert Python value to the corresponding JavaScript representation.
97
98
98 This is necessary to safely insert arbitrary values into HTML <script>
99 This is necessary to safely insert arbitrary values into HTML <script>
99 sections e.g. using Mako template expression substitution.
100 sections e.g. using Mako template expression substitution.
100
101
101 Note: Rather than using this function, it's preferable to avoid the
102 Note: Rather than using this function, it's preferable to avoid the
102 insertion of values into HTML <script> sections altogether. Instead,
103 insertion of values into HTML <script> sections altogether. Instead,
103 data should (to the extent possible) be passed to JavaScript using
104 data should (to the extent possible) be passed to JavaScript using
104 data attributes or AJAX calls, eliminating the need for JS specific
105 data attributes or AJAX calls, eliminating the need for JS specific
105 escaping.
106 escaping.
106
107
107 Note: This is not safe for use in attributes (e.g. onclick), because
108 Note: This is not safe for use in attributes (e.g. onclick), because
108 quotes are not escaped.
109 quotes are not escaped.
109
110
110 Because the rules for parsing <script> varies between XHTML (where
111 Because the rules for parsing <script> varies between XHTML (where
111 normal rules apply for any special characters) and HTML (where
112 normal rules apply for any special characters) and HTML (where
112 entities are not interpreted, but the literal string "</script>"
113 entities are not interpreted, but the literal string "</script>"
113 is forbidden), the function ensures that the result never contains
114 is forbidden), the function ensures that the result never contains
114 '&', '<' and '>', thus making it safe in both those contexts (but
115 '&', '<' and '>', thus making it safe in both those contexts (but
115 not in attributes).
116 not in attributes).
116 """
117 """
117 return literal(
118 return literal(
118 ('(' + json.dumps(value) + ')')
119 ('(' + json.dumps(value) + ')')
119 # In JSON, the following can only appear in string literals.
120 # In JSON, the following can only appear in string literals.
120 .replace('&', r'\x26')
121 .replace('&', r'\x26')
121 .replace('<', r'\x3c')
122 .replace('<', r'\x3c')
122 .replace('>', r'\x3e')
123 .replace('>', r'\x3e')
123 )
124 )
124
125
125
126
126 def jshtml(val):
127 def jshtml(val):
127 """HTML escapes a string value, then converts the resulting string
128 """HTML escapes a string value, then converts the resulting string
128 to its corresponding JavaScript representation (see `js`).
129 to its corresponding JavaScript representation (see `js`).
129
130
130 This is used when a plain-text string (possibly containing special
131 This is used when a plain-text string (possibly containing special
131 HTML characters) will be used by a script in an HTML context (e.g.
132 HTML characters) will be used by a script in an HTML context (e.g.
132 element.innerHTML or jQuery's 'html' method).
133 element.innerHTML or jQuery's 'html' method).
133
134
134 If in doubt, err on the side of using `jshtml` over `js`, since it's
135 If in doubt, err on the side of using `jshtml` over `js`, since it's
135 better to escape too much than too little.
136 better to escape too much than too little.
136 """
137 """
137 return js(escape(val))
138 return js(escape(val))
138
139
139
140
140 def FID(raw_id, path):
141 def FID(raw_id, path):
141 """
142 """
142 Creates a unique ID for filenode based on it's hash of path and revision
143 Creates a unique ID for filenode based on it's hash of path and revision
143 it's safe to use in urls
144 it's safe to use in urls
144 """
145 """
145 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_bytes(path)).hexdigest()[:12])
146 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_bytes(path)).hexdigest()[:12])
146
147
147
148
148 def get_ignore_whitespace_diff(GET):
149 def get_ignore_whitespace_diff(GET):
149 """Return true if URL requested whitespace to be ignored"""
150 """Return true if URL requested whitespace to be ignored"""
150 return bool(GET.get('ignorews'))
151 return bool(GET.get('ignorews'))
151
152
152
153
153 def ignore_whitespace_link(GET, anchor=None):
154 def ignore_whitespace_link(GET, anchor=None):
154 """Return snippet with link to current URL with whitespace ignoring toggled"""
155 """Return snippet with link to current URL with whitespace ignoring toggled"""
155 params = dict(GET) # ignoring duplicates
156 params = dict(GET) # ignoring duplicates
156 if get_ignore_whitespace_diff(GET):
157 if get_ignore_whitespace_diff(GET):
157 params.pop('ignorews')
158 params.pop('ignorews')
158 title = _("Show whitespace changes")
159 title = _("Show whitespace changes")
159 else:
160 else:
160 params['ignorews'] = '1'
161 params['ignorews'] = '1'
161 title = _("Ignore whitespace changes")
162 title = _("Ignore whitespace changes")
162 params['anchor'] = anchor
163 params['anchor'] = anchor
163 return link_to(
164 return link_to(
164 literal('<i class="icon-strike"></i>'),
165 literal('<i class="icon-strike"></i>'),
165 url.current(**params),
166 url.current(**params),
166 title=title,
167 title=title,
167 **{'data-toggle': 'tooltip'})
168 **{'data-toggle': 'tooltip'})
168
169
169
170
170 def get_diff_context_size(GET):
171 def get_diff_context_size(GET):
171 """Return effective context size requested in URL"""
172 """Return effective context size requested in URL"""
172 return safe_int(GET.get('context'), default=3)
173 return safe_int(GET.get('context'), default=3)
173
174
174
175
175 def increase_context_link(GET, anchor=None):
176 def increase_context_link(GET, anchor=None):
176 """Return snippet with link to current URL with double context size"""
177 """Return snippet with link to current URL with double context size"""
177 context = get_diff_context_size(GET) * 2
178 context = get_diff_context_size(GET) * 2
178 params = dict(GET) # ignoring duplicates
179 params = dict(GET) # ignoring duplicates
179 params['context'] = str(context)
180 params['context'] = str(context)
180 params['anchor'] = anchor
181 params['anchor'] = anchor
181 return link_to(
182 return link_to(
182 literal('<i class="icon-sort"></i>'),
183 literal('<i class="icon-sort"></i>'),
183 url.current(**params),
184 url.current(**params),
184 title=_('Increase diff context to %(num)s lines') % {'num': context},
185 title=_('Increase diff context to %(num)s lines') % {'num': context},
185 **{'data-toggle': 'tooltip'})
186 **{'data-toggle': 'tooltip'})
186
187
187
188
188 class _FilesBreadCrumbs(object):
189 class _FilesBreadCrumbs(object):
189
190
190 def __call__(self, repo_name, rev, paths):
191 def __call__(self, repo_name, rev, paths):
191 url_l = [link_to(repo_name, url('files_home',
192 url_l = [link_to(repo_name, url('files_home',
192 repo_name=repo_name,
193 repo_name=repo_name,
193 revision=rev, f_path=''),
194 revision=rev, f_path=''),
194 class_='ypjax-link')]
195 class_='ypjax-link')]
195 paths_l = paths.split('/')
196 paths_l = paths.split('/')
196 for cnt, p in enumerate(paths_l):
197 for cnt, p in enumerate(paths_l):
197 if p != '':
198 if p != '':
198 url_l.append(link_to(p,
199 url_l.append(link_to(p,
199 url('files_home',
200 url('files_home',
200 repo_name=repo_name,
201 repo_name=repo_name,
201 revision=rev,
202 revision=rev,
202 f_path='/'.join(paths_l[:cnt + 1])
203 f_path='/'.join(paths_l[:cnt + 1])
203 ),
204 ),
204 class_='ypjax-link'
205 class_='ypjax-link'
205 )
206 )
206 )
207 )
207
208
208 return literal('/'.join(url_l))
209 return literal('/'.join(url_l))
209
210
210
211
211 files_breadcrumbs = _FilesBreadCrumbs()
212 files_breadcrumbs = _FilesBreadCrumbs()
212
213
213
214
214 class CodeHtmlFormatter(HtmlFormatter):
215 class CodeHtmlFormatter(HtmlFormatter):
215 """
216 """
216 My code Html Formatter for source codes
217 My code Html Formatter for source codes
217 """
218 """
218
219
219 def wrap(self, source, outfile):
220 def wrap(self, source, outfile):
220 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
221 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
221
222
222 def _wrap_code(self, source):
223 def _wrap_code(self, source):
223 for cnt, it in enumerate(source):
224 for cnt, it in enumerate(source):
224 i, t = it
225 i, t = it
225 t = '<span id="L%s">%s</span>' % (cnt + 1, t)
226 t = '<span id="L%s">%s</span>' % (cnt + 1, t)
226 yield i, t
227 yield i, t
227
228
228 def _wrap_tablelinenos(self, inner):
229 def _wrap_tablelinenos(self, inner):
229 inner_lines = []
230 inner_lines = []
230 lncount = 0
231 lncount = 0
231 for t, line in inner:
232 for t, line in inner:
232 if t:
233 if t:
233 lncount += 1
234 lncount += 1
234 inner_lines.append(line)
235 inner_lines.append(line)
235
236
236 fl = self.linenostart
237 fl = self.linenostart
237 mw = len(str(lncount + fl - 1))
238 mw = len(str(lncount + fl - 1))
238 sp = self.linenospecial
239 sp = self.linenospecial
239 st = self.linenostep
240 st = self.linenostep
240 la = self.lineanchors
241 la = self.lineanchors
241 aln = self.anchorlinenos
242 aln = self.anchorlinenos
242 nocls = self.noclasses
243 nocls = self.noclasses
243 if sp:
244 if sp:
244 lines = []
245 lines = []
245
246
246 for i in range(fl, fl + lncount):
247 for i in range(fl, fl + lncount):
247 if i % st == 0:
248 if i % st == 0:
248 if i % sp == 0:
249 if i % sp == 0:
249 if aln:
250 if aln:
250 lines.append('<a href="#%s%d" class="special">%*d</a>' %
251 lines.append('<a href="#%s%d" class="special">%*d</a>' %
251 (la, i, mw, i))
252 (la, i, mw, i))
252 else:
253 else:
253 lines.append('<span class="special">%*d</span>' % (mw, i))
254 lines.append('<span class="special">%*d</span>' % (mw, i))
254 else:
255 else:
255 if aln:
256 if aln:
256 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
257 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
257 else:
258 else:
258 lines.append('%*d' % (mw, i))
259 lines.append('%*d' % (mw, i))
259 else:
260 else:
260 lines.append('')
261 lines.append('')
261 ls = '\n'.join(lines)
262 ls = '\n'.join(lines)
262 else:
263 else:
263 lines = []
264 lines = []
264 for i in range(fl, fl + lncount):
265 for i in range(fl, fl + lncount):
265 if i % st == 0:
266 if i % st == 0:
266 if aln:
267 if aln:
267 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
268 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
268 else:
269 else:
269 lines.append('%*d' % (mw, i))
270 lines.append('%*d' % (mw, i))
270 else:
271 else:
271 lines.append('')
272 lines.append('')
272 ls = '\n'.join(lines)
273 ls = '\n'.join(lines)
273
274
274 # in case you wonder about the seemingly redundant <div> here: since the
275 # in case you wonder about the seemingly redundant <div> here: since the
275 # content in the other cell also is wrapped in a div, some browsers in
276 # content in the other cell also is wrapped in a div, some browsers in
276 # some configurations seem to mess up the formatting...
277 # some configurations seem to mess up the formatting...
277 if nocls:
278 if nocls:
278 yield 0, ('<table class="%stable">' % self.cssclass +
279 yield 0, ('<table class="%stable">' % self.cssclass +
279 '<tr><td><div class="linenodiv">'
280 '<tr><td><div class="linenodiv">'
280 '<pre>' + ls + '</pre></div></td>'
281 '<pre>' + ls + '</pre></div></td>'
281 '<td id="hlcode" class="code">')
282 '<td id="hlcode" class="code">')
282 else:
283 else:
283 yield 0, ('<table class="%stable">' % self.cssclass +
284 yield 0, ('<table class="%stable">' % self.cssclass +
284 '<tr><td class="linenos"><div class="linenodiv">'
285 '<tr><td class="linenos"><div class="linenodiv">'
285 '<pre>' + ls + '</pre></div></td>'
286 '<pre>' + ls + '</pre></div></td>'
286 '<td id="hlcode" class="code">')
287 '<td id="hlcode" class="code">')
287 yield 0, ''.join(inner_lines)
288 yield 0, ''.join(inner_lines)
288 yield 0, '</td></tr></table>'
289 yield 0, '</td></tr></table>'
289
290
290
291
291 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
292 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
292
293
293
294
294 def _markup_whitespace(m):
295 def _markup_whitespace(m):
295 groups = m.groups()
296 groups = m.groups()
296 if groups[0]:
297 if groups[0]:
297 return '<u>\t</u>'
298 return '<u>\t</u>'
298 if groups[1]:
299 if groups[1]:
299 return ' <i></i>'
300 return ' <i></i>'
300
301
301
302
302 def markup_whitespace(s):
303 def markup_whitespace(s):
303 return _whitespace_re.sub(_markup_whitespace, s)
304 return _whitespace_re.sub(_markup_whitespace, s)
304
305
305
306
306 def pygmentize(filenode, **kwargs):
307 def pygmentize(filenode, **kwargs):
307 """
308 """
308 pygmentize function using pygments
309 pygmentize function using pygments
309
310
310 :param filenode:
311 :param filenode:
311 """
312 """
312 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
313 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
313 return literal(markup_whitespace(
314 return literal(markup_whitespace(
314 code_highlight(safe_str(filenode.content), lexer, CodeHtmlFormatter(**kwargs))))
315 code_highlight(safe_str(filenode.content), lexer, CodeHtmlFormatter(**kwargs))))
315
316
316
317
317 def hsv_to_rgb(h, s, v):
318 def hsv_to_rgb(h, s, v):
318 if s == 0.0:
319 if s == 0.0:
319 return v, v, v
320 return v, v, v
320 i = int(h * 6.0) # XXX assume int() truncates!
321 i = int(h * 6.0) # XXX assume int() truncates!
321 f = (h * 6.0) - i
322 f = (h * 6.0) - i
322 p = v * (1.0 - s)
323 p = v * (1.0 - s)
323 q = v * (1.0 - s * f)
324 q = v * (1.0 - s * f)
324 t = v * (1.0 - s * (1.0 - f))
325 t = v * (1.0 - s * (1.0 - f))
325 i = i % 6
326 i = i % 6
326 if i == 0:
327 if i == 0:
327 return v, t, p
328 return v, t, p
328 if i == 1:
329 if i == 1:
329 return q, v, p
330 return q, v, p
330 if i == 2:
331 if i == 2:
331 return p, v, t
332 return p, v, t
332 if i == 3:
333 if i == 3:
333 return p, q, v
334 return p, q, v
334 if i == 4:
335 if i == 4:
335 return t, p, v
336 return t, p, v
336 if i == 5:
337 if i == 5:
337 return v, p, q
338 return v, p, q
338
339
339
340
340 def gen_color(n=10000):
341 def gen_color(n=10000):
341 """generator for getting n of evenly distributed colors using
342 """generator for getting n of evenly distributed colors using
342 hsv color and golden ratio. It always return same order of colors
343 hsv color and golden ratio. It always return same order of colors
343
344
344 :returns: RGB tuple
345 :returns: RGB tuple
345 """
346 """
346
347
347 golden_ratio = 0.618033988749895
348 golden_ratio = 0.618033988749895
348 h = 0.22717784590367374
349 h = 0.22717784590367374
349
350
350 for _unused in range(n):
351 for _unused in range(n):
351 h += golden_ratio
352 h += golden_ratio
352 h %= 1
353 h %= 1
353 HSV_tuple = [h, 0.95, 0.95]
354 HSV_tuple = [h, 0.95, 0.95]
354 RGB_tuple = hsv_to_rgb(*HSV_tuple)
355 RGB_tuple = hsv_to_rgb(*HSV_tuple)
355 yield [str(int(x * 256)) for x in RGB_tuple]
356 yield [str(int(x * 256)) for x in RGB_tuple]
356
357
357
358
358 def pygmentize_annotation(repo_name, filenode, **kwargs):
359 def pygmentize_annotation(repo_name, filenode, **kwargs):
359 """
360 """
360 pygmentize function for annotation
361 pygmentize function for annotation
361
362
362 :param filenode:
363 :param filenode:
363 """
364 """
364 cgenerator = gen_color()
365 cgenerator = gen_color()
365 color_dict = {}
366 color_dict = {}
366
367
367 def get_color_string(cs):
368 def get_color_string(cs):
368 if cs in color_dict:
369 if cs in color_dict:
369 col = color_dict[cs]
370 col = color_dict[cs]
370 else:
371 else:
371 col = color_dict[cs] = next(cgenerator)
372 col = color_dict[cs] = next(cgenerator)
372 return "color: rgb(%s)! important;" % (', '.join(col))
373 return "color: rgb(%s)! important;" % (', '.join(col))
373
374
374 def url_func(changeset):
375 def url_func(changeset):
375 author = escape(changeset.author)
376 author = escape(changeset.author)
376 date = changeset.date
377 date = changeset.date
377 message = escape(changeset.message)
378 message = escape(changeset.message)
378 tooltip_html = ("<b>Author:</b> %s<br/>"
379 tooltip_html = ("<b>Author:</b> %s<br/>"
379 "<b>Date:</b> %s</b><br/>"
380 "<b>Date:</b> %s</b><br/>"
380 "<b>Message:</b> %s") % (author, date, message)
381 "<b>Message:</b> %s") % (author, date, message)
381
382
382 lnk_format = show_id(changeset)
383 lnk_format = show_id(changeset)
383 uri = link_to(
384 uri = link_to(
384 lnk_format,
385 lnk_format,
385 url('changeset_home', repo_name=repo_name,
386 url('changeset_home', repo_name=repo_name,
386 revision=changeset.raw_id),
387 revision=changeset.raw_id),
387 style=get_color_string(changeset.raw_id),
388 style=get_color_string(changeset.raw_id),
388 **{'data-toggle': 'popover',
389 **{'data-toggle': 'popover',
389 'data-content': tooltip_html}
390 'data-content': tooltip_html}
390 )
391 )
391
392
392 uri += '\n'
393 uri += '\n'
393 return uri
394 return uri
394
395
395 return literal(markup_whitespace(annotate_highlight(filenode, url_func, **kwargs)))
396 return literal(markup_whitespace(annotate_highlight(filenode, url_func, **kwargs)))
396
397
397
398
398 def capitalize(x):
399 def capitalize(x):
399 return x.capitalize()
400 return x.capitalize()
400
401
401 def short_id(x):
402 def short_id(x):
402 return x[:12]
403 return x[:12]
403
404
404 def hide_credentials(x):
405 def hide_credentials(x):
405 return ''.join(credentials_filter(x))
406 return ''.join(credentials_filter(x))
406
407
407
408
408 def show_id(cs):
409 def show_id(cs):
409 """
410 """
410 Configurable function that shows ID
411 Configurable function that shows ID
411 by default it's r123:fffeeefffeee
412 by default it's r123:fffeeefffeee
412
413
413 :param cs: changeset instance
414 :param cs: changeset instance
414 """
415 """
415 def_len = safe_int(kallithea.CONFIG.get('show_sha_length', 12))
416 def_len = safe_int(kallithea.CONFIG.get('show_sha_length', 12))
416 show_rev = asbool(kallithea.CONFIG.get('show_revision_number', False))
417 show_rev = asbool(kallithea.CONFIG.get('show_revision_number', False))
417
418
418 raw_id = cs.raw_id[:def_len]
419 raw_id = cs.raw_id[:def_len]
419 if show_rev:
420 if show_rev:
420 return 'r%s:%s' % (cs.revision, raw_id)
421 return 'r%s:%s' % (cs.revision, raw_id)
421 else:
422 else:
422 return raw_id
423 return raw_id
423
424
424
425
425 def is_git(repository):
426 def is_git(repository):
426 if hasattr(repository, 'alias'):
427 if hasattr(repository, 'alias'):
427 _type = repository.alias
428 _type = repository.alias
428 elif hasattr(repository, 'repo_type'):
429 elif hasattr(repository, 'repo_type'):
429 _type = repository.repo_type
430 _type = repository.repo_type
430 else:
431 else:
431 _type = repository
432 _type = repository
432 return _type == 'git'
433 return _type == 'git'
433
434
434
435
435 def is_hg(repository):
436 def is_hg(repository):
436 if hasattr(repository, 'alias'):
437 if hasattr(repository, 'alias'):
437 _type = repository.alias
438 _type = repository.alias
438 elif hasattr(repository, 'repo_type'):
439 elif hasattr(repository, 'repo_type'):
439 _type = repository.repo_type
440 _type = repository.repo_type
440 else:
441 else:
441 _type = repository
442 _type = repository
442 return _type == 'hg'
443 return _type == 'hg'
443
444
444
445
445 @cache_region('long_term', 'user_attr_or_none')
446 @cache_region('long_term', 'user_attr_or_none')
446 def user_attr_or_none(author, show_attr):
447 def user_attr_or_none(author, show_attr):
447 """Try to match email part of VCS committer string with a local user and return show_attr
448 """Try to match email part of VCS committer string with a local user and return show_attr
448 - or return None if user not found"""
449 - or return None if user not found"""
449 email = author_email(author)
450 email = author_email(author)
450 if email:
451 if email:
451 user = db.User.get_by_email(email)
452 user = db.User.get_by_email(email)
452 if user is not None:
453 if user is not None:
453 return getattr(user, show_attr)
454 return getattr(user, show_attr)
454 return None
455 return None
455
456
456
457
457 def email_or_none(author):
458 def email_or_none(author):
458 """Try to match email part of VCS committer string with a local user.
459 """Try to match email part of VCS committer string with a local user.
459 Return primary email of user, email part of the specified author name, or None."""
460 Return primary email of user, email part of the specified author name, or None."""
460 if not author:
461 if not author:
461 return None
462 return None
462 email = user_attr_or_none(author, 'email')
463 email = user_attr_or_none(author, 'email')
463 if email is not None:
464 if email is not None:
464 return email # always use user's main email address - not necessarily the one used to find user
465 return email # always use user's main email address - not necessarily the one used to find user
465
466
466 # extract email from the commit string
467 # extract email from the commit string
467 email = author_email(author)
468 email = author_email(author)
468 if email:
469 if email:
469 return email
470 return email
470
471
471 # No valid email, not a valid user in the system, none!
472 # No valid email, not a valid user in the system, none!
472 return None
473 return None
473
474
474
475
475 def person(author, show_attr="username"):
476 def person(author, show_attr="username"):
476 """Find the user identified by 'author', return one of the users attributes,
477 """Find the user identified by 'author', return one of the users attributes,
477 default to the username attribute, None if there is no user"""
478 default to the username attribute, None if there is no user"""
478 # if author is already an instance use it for extraction
479 # if author is already an instance use it for extraction
479 if isinstance(author, db.User):
480 if isinstance(author, db.User):
480 return getattr(author, show_attr)
481 return getattr(author, show_attr)
481
482
482 value = user_attr_or_none(author, show_attr)
483 value = user_attr_or_none(author, show_attr)
483 if value is not None:
484 if value is not None:
484 return value
485 return value
485
486
486 # Still nothing? Just pass back the author name if any, else the email
487 # Still nothing? Just pass back the author name if any, else the email
487 return author_name(author) or author_email(author)
488 return author_name(author) or author_email(author)
488
489
489
490
490 def person_by_id(id_, show_attr="username"):
491 def person_by_id(id_, show_attr="username"):
491 # maybe it's an ID ?
492 # maybe it's an ID ?
492 if str(id_).isdigit() or isinstance(id_, int):
493 if str(id_).isdigit() or isinstance(id_, int):
493 id_ = int(id_)
494 id_ = int(id_)
494 user = db.User.get(id_)
495 user = db.User.get(id_)
495 if user is not None:
496 if user is not None:
496 return getattr(user, show_attr)
497 return getattr(user, show_attr)
497 return id_
498 return id_
498
499
499
500
500 def boolicon(value):
501 def boolicon(value):
501 """Returns boolean value of a value, represented as small html image of true/false
502 """Returns boolean value of a value, represented as small html image of true/false
502 icons
503 icons
503
504
504 :param value: value
505 :param value: value
505 """
506 """
506
507
507 if value:
508 if value:
508 return HTML.tag('i', class_="icon-ok")
509 return HTML.tag('i', class_="icon-ok")
509 else:
510 else:
510 return HTML.tag('i', class_="icon-minus-circled")
511 return HTML.tag('i', class_="icon-minus-circled")
511
512
512
513
513 def action_parser(user_log, feed=False, parse_cs=False):
514 def action_parser(user_log, feed=False, parse_cs=False):
514 """
515 """
515 This helper will action_map the specified string action into translated
516 This helper will action_map the specified string action into translated
516 fancy names with icons and links
517 fancy names with icons and links
517
518
518 :param user_log: user log instance
519 :param user_log: user log instance
519 :param feed: use output for feeds (no html and fancy icons)
520 :param feed: use output for feeds (no html and fancy icons)
520 :param parse_cs: parse Changesets into VCS instances
521 :param parse_cs: parse Changesets into VCS instances
521 """
522 """
522
523
523 action = user_log.action
524 action = user_log.action
524 action_params = ' '
525 action_params = ' '
525
526
526 x = action.split(':')
527 x = action.split(':')
527
528
528 if len(x) > 1:
529 if len(x) > 1:
529 action, action_params = x
530 action, action_params = x
530
531
531 def get_cs_links():
532 def get_cs_links():
532 revs_limit = 3 # display this amount always
533 revs_limit = 3 # display this amount always
533 revs_top_limit = 50 # show upto this amount of changesets hidden
534 revs_top_limit = 50 # show upto this amount of changesets hidden
534 revs_ids = action_params.split(',')
535 revs_ids = action_params.split(',')
535 deleted = user_log.repository is None
536 deleted = user_log.repository is None
536 if deleted:
537 if deleted:
537 return ','.join(revs_ids)
538 return ','.join(revs_ids)
538
539
539 repo_name = user_log.repository.repo_name
540 repo_name = user_log.repository.repo_name
540
541
541 def lnk(rev, repo_name):
542 def lnk(rev, repo_name):
542 lazy_cs = False
543 lazy_cs = False
543 title_ = None
544 title_ = None
544 url_ = '#'
545 url_ = '#'
545 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
546 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
546 if rev.op and rev.ref_name:
547 if rev.op and rev.ref_name:
547 if rev.op == 'delete_branch':
548 if rev.op == 'delete_branch':
548 lbl = _('Deleted branch: %s') % rev.ref_name
549 lbl = _('Deleted branch: %s') % rev.ref_name
549 elif rev.op == 'tag':
550 elif rev.op == 'tag':
550 lbl = _('Created tag: %s') % rev.ref_name
551 lbl = _('Created tag: %s') % rev.ref_name
551 else:
552 else:
552 lbl = 'Unknown operation %s' % rev.op
553 lbl = 'Unknown operation %s' % rev.op
553 else:
554 else:
554 lazy_cs = True
555 lazy_cs = True
555 lbl = rev.short_id[:8]
556 lbl = rev.short_id[:8]
556 url_ = url('changeset_home', repo_name=repo_name,
557 url_ = url('changeset_home', repo_name=repo_name,
557 revision=rev.raw_id)
558 revision=rev.raw_id)
558 else:
559 else:
559 # changeset cannot be found - it might have been stripped or removed
560 # changeset cannot be found - it might have been stripped or removed
560 lbl = rev[:12]
561 lbl = rev[:12]
561 title_ = _('Changeset %s not found') % lbl
562 title_ = _('Changeset %s not found') % lbl
562 if parse_cs:
563 if parse_cs:
563 return link_to(lbl, url_, title=title_, **{'data-toggle': 'tooltip'})
564 return link_to(lbl, url_, title=title_, **{'data-toggle': 'tooltip'})
564 return link_to(lbl, url_, class_='lazy-cs' if lazy_cs else '',
565 return link_to(lbl, url_, class_='lazy-cs' if lazy_cs else '',
565 **{'data-raw_id': rev.raw_id, 'data-repo_name': repo_name})
566 **{'data-raw_id': rev.raw_id, 'data-repo_name': repo_name})
566
567
567 def _get_op(rev_txt):
568 def _get_op(rev_txt):
568 _op = None
569 _op = None
569 _name = rev_txt
570 _name = rev_txt
570 if len(rev_txt.split('=>')) == 2:
571 if len(rev_txt.split('=>')) == 2:
571 _op, _name = rev_txt.split('=>')
572 _op, _name = rev_txt.split('=>')
572 return _op, _name
573 return _op, _name
573
574
574 revs = []
575 revs = []
575 if len([v for v in revs_ids if v != '']) > 0:
576 if len([v for v in revs_ids if v != '']) > 0:
576 repo = None
577 repo = None
577 for rev in revs_ids[:revs_top_limit]:
578 for rev in revs_ids[:revs_top_limit]:
578 _op, _name = _get_op(rev)
579 _op, _name = _get_op(rev)
579
580
580 # we want parsed changesets, or new log store format is bad
581 # we want parsed changesets, or new log store format is bad
581 if parse_cs:
582 if parse_cs:
582 try:
583 try:
583 if repo is None:
584 if repo is None:
584 repo = user_log.repository.scm_instance
585 repo = user_log.repository.scm_instance
585 _rev = repo.get_changeset(rev)
586 _rev = repo.get_changeset(rev)
586 revs.append(_rev)
587 revs.append(_rev)
587 except ChangesetDoesNotExistError:
588 except ChangesetDoesNotExistError:
588 log.error('cannot find revision %s in this repo', rev)
589 log.error('cannot find revision %s in this repo', rev)
589 revs.append(rev)
590 revs.append(rev)
590 else:
591 else:
591 _rev = AttributeDict({
592 _rev = AttributeDict({
592 'short_id': rev[:12],
593 'short_id': rev[:12],
593 'raw_id': rev,
594 'raw_id': rev,
594 'message': '',
595 'message': '',
595 'op': _op,
596 'op': _op,
596 'ref_name': _name
597 'ref_name': _name
597 })
598 })
598 revs.append(_rev)
599 revs.append(_rev)
599 cs_links = [" " + ', '.join(
600 cs_links = [" " + ', '.join(
600 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
601 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
601 )]
602 )]
602 _op1, _name1 = _get_op(revs_ids[0])
603 _op1, _name1 = _get_op(revs_ids[0])
603 _op2, _name2 = _get_op(revs_ids[-1])
604 _op2, _name2 = _get_op(revs_ids[-1])
604
605
605 _rev = '%s...%s' % (_name1, _name2)
606 _rev = '%s...%s' % (_name1, _name2)
606
607
607 compare_view = (
608 compare_view = (
608 ' <div class="compare_view" data-toggle="tooltip" title="%s">'
609 ' <div class="compare_view" data-toggle="tooltip" title="%s">'
609 '<a href="%s">%s</a> </div>' % (
610 '<a href="%s">%s</a> </div>' % (
610 _('Show all combined changesets %s->%s') % (
611 _('Show all combined changesets %s->%s') % (
611 revs_ids[0][:12], revs_ids[-1][:12]
612 revs_ids[0][:12], revs_ids[-1][:12]
612 ),
613 ),
613 url('changeset_home', repo_name=repo_name,
614 url('changeset_home', repo_name=repo_name,
614 revision=_rev
615 revision=_rev
615 ),
616 ),
616 _('Compare view')
617 _('Compare view')
617 )
618 )
618 )
619 )
619
620
620 # if we have exactly one more than normally displayed
621 # if we have exactly one more than normally displayed
621 # just display it, takes less space than displaying
622 # just display it, takes less space than displaying
622 # "and 1 more revisions"
623 # "and 1 more revisions"
623 if len(revs_ids) == revs_limit + 1:
624 if len(revs_ids) == revs_limit + 1:
624 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
625 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
625
626
626 # hidden-by-default ones
627 # hidden-by-default ones
627 if len(revs_ids) > revs_limit + 1:
628 if len(revs_ids) > revs_limit + 1:
628 uniq_id = revs_ids[0]
629 uniq_id = revs_ids[0]
629 html_tmpl = (
630 html_tmpl = (
630 '<span> %s <a class="show_more" id="_%s" '
631 '<span> %s <a class="show_more" id="_%s" '
631 'href="#more">%s</a> %s</span>'
632 'href="#more">%s</a> %s</span>'
632 )
633 )
633 if not feed:
634 if not feed:
634 cs_links.append(html_tmpl % (
635 cs_links.append(html_tmpl % (
635 _('and'),
636 _('and'),
636 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
637 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
637 _('revisions')
638 _('revisions')
638 )
639 )
639 )
640 )
640
641
641 if not feed:
642 if not feed:
642 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
643 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
643 else:
644 else:
644 html_tmpl = '<span id="%s"> %s </span>'
645 html_tmpl = '<span id="%s"> %s </span>'
645
646
646 morelinks = ', '.join(
647 morelinks = ', '.join(
647 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
648 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
648 )
649 )
649
650
650 if len(revs_ids) > revs_top_limit:
651 if len(revs_ids) > revs_top_limit:
651 morelinks += ', ...'
652 morelinks += ', ...'
652
653
653 cs_links.append(html_tmpl % (uniq_id, morelinks))
654 cs_links.append(html_tmpl % (uniq_id, morelinks))
654 if len(revs) > 1:
655 if len(revs) > 1:
655 cs_links.append(compare_view)
656 cs_links.append(compare_view)
656 return ''.join(cs_links)
657 return ''.join(cs_links)
657
658
658 def get_fork_name():
659 def get_fork_name():
659 repo_name = action_params
660 repo_name = action_params
660 url_ = url('summary_home', repo_name=repo_name)
661 url_ = url('summary_home', repo_name=repo_name)
661 return _('Fork name %s') % link_to(action_params, url_)
662 return _('Fork name %s') % link_to(action_params, url_)
662
663
663 def get_user_name():
664 def get_user_name():
664 user_name = action_params
665 user_name = action_params
665 return user_name
666 return user_name
666
667
667 def get_users_group():
668 def get_users_group():
668 group_name = action_params
669 group_name = action_params
669 return group_name
670 return group_name
670
671
671 def get_pull_request():
672 def get_pull_request():
672 pull_request_id = action_params
673 pull_request_id = action_params
673 nice_id = db.PullRequest.make_nice_id(pull_request_id)
674 nice_id = db.PullRequest.make_nice_id(pull_request_id)
674
675
675 deleted = user_log.repository is None
676 deleted = user_log.repository is None
676 if deleted:
677 if deleted:
677 repo_name = user_log.repository_name
678 repo_name = user_log.repository_name
678 else:
679 else:
679 repo_name = user_log.repository.repo_name
680 repo_name = user_log.repository.repo_name
680
681
681 return link_to(_('Pull request %s') % nice_id,
682 return link_to(_('Pull request %s') % nice_id,
682 url('pullrequest_show', repo_name=repo_name,
683 url('pullrequest_show', repo_name=repo_name,
683 pull_request_id=pull_request_id))
684 pull_request_id=pull_request_id))
684
685
685 def get_archive_name():
686 def get_archive_name():
686 archive_name = action_params
687 archive_name = action_params
687 return archive_name
688 return archive_name
688
689
689 # action : translated str, callback(extractor), icon
690 # action : translated str, callback(extractor), icon
690 action_map = {
691 action_map = {
691 'user_deleted_repo': (_('[deleted] repository'),
692 'user_deleted_repo': (_('[deleted] repository'),
692 None, 'icon-trashcan'),
693 None, 'icon-trashcan'),
693 'user_created_repo': (_('[created] repository'),
694 'user_created_repo': (_('[created] repository'),
694 None, 'icon-plus'),
695 None, 'icon-plus'),
695 'user_created_fork': (_('[created] repository as fork'),
696 'user_created_fork': (_('[created] repository as fork'),
696 None, 'icon-fork'),
697 None, 'icon-fork'),
697 'user_forked_repo': (_('[forked] repository'),
698 'user_forked_repo': (_('[forked] repository'),
698 get_fork_name, 'icon-fork'),
699 get_fork_name, 'icon-fork'),
699 'user_updated_repo': (_('[updated] repository'),
700 'user_updated_repo': (_('[updated] repository'),
700 None, 'icon-pencil'),
701 None, 'icon-pencil'),
701 'user_downloaded_archive': (_('[downloaded] archive from repository'),
702 'user_downloaded_archive': (_('[downloaded] archive from repository'),
702 get_archive_name, 'icon-download-cloud'),
703 get_archive_name, 'icon-download-cloud'),
703 'admin_deleted_repo': (_('[delete] repository'),
704 'admin_deleted_repo': (_('[delete] repository'),
704 None, 'icon-trashcan'),
705 None, 'icon-trashcan'),
705 'admin_created_repo': (_('[created] repository'),
706 'admin_created_repo': (_('[created] repository'),
706 None, 'icon-plus'),
707 None, 'icon-plus'),
707 'admin_forked_repo': (_('[forked] repository'),
708 'admin_forked_repo': (_('[forked] repository'),
708 None, 'icon-fork'),
709 None, 'icon-fork'),
709 'admin_updated_repo': (_('[updated] repository'),
710 'admin_updated_repo': (_('[updated] repository'),
710 None, 'icon-pencil'),
711 None, 'icon-pencil'),
711 'admin_created_user': (_('[created] user'),
712 'admin_created_user': (_('[created] user'),
712 get_user_name, 'icon-user'),
713 get_user_name, 'icon-user'),
713 'admin_updated_user': (_('[updated] user'),
714 'admin_updated_user': (_('[updated] user'),
714 get_user_name, 'icon-user'),
715 get_user_name, 'icon-user'),
715 'admin_created_users_group': (_('[created] user group'),
716 'admin_created_users_group': (_('[created] user group'),
716 get_users_group, 'icon-pencil'),
717 get_users_group, 'icon-pencil'),
717 'admin_updated_users_group': (_('[updated] user group'),
718 'admin_updated_users_group': (_('[updated] user group'),
718 get_users_group, 'icon-pencil'),
719 get_users_group, 'icon-pencil'),
719 'user_commented_revision': (_('[commented] on revision in repository'),
720 'user_commented_revision': (_('[commented] on revision in repository'),
720 get_cs_links, 'icon-comment'),
721 get_cs_links, 'icon-comment'),
721 'user_commented_pull_request': (_('[commented] on pull request for'),
722 'user_commented_pull_request': (_('[commented] on pull request for'),
722 get_pull_request, 'icon-comment'),
723 get_pull_request, 'icon-comment'),
723 'user_closed_pull_request': (_('[closed] pull request for'),
724 'user_closed_pull_request': (_('[closed] pull request for'),
724 get_pull_request, 'icon-ok'),
725 get_pull_request, 'icon-ok'),
725 'push': (_('[pushed] into'),
726 'push': (_('[pushed] into'),
726 get_cs_links, 'icon-move-up'),
727 get_cs_links, 'icon-move-up'),
727 'push_local': (_('[committed via Kallithea] into repository'),
728 'push_local': (_('[committed via Kallithea] into repository'),
728 get_cs_links, 'icon-pencil'),
729 get_cs_links, 'icon-pencil'),
729 'push_remote': (_('[pulled from remote] into repository'),
730 'push_remote': (_('[pulled from remote] into repository'),
730 get_cs_links, 'icon-move-up'),
731 get_cs_links, 'icon-move-up'),
731 'pull': (_('[pulled] from'),
732 'pull': (_('[pulled] from'),
732 None, 'icon-move-down'),
733 None, 'icon-move-down'),
733 'started_following_repo': (_('[started following] repository'),
734 'started_following_repo': (_('[started following] repository'),
734 None, 'icon-heart'),
735 None, 'icon-heart'),
735 'stopped_following_repo': (_('[stopped following] repository'),
736 'stopped_following_repo': (_('[stopped following] repository'),
736 None, 'icon-heart-empty'),
737 None, 'icon-heart-empty'),
737 }
738 }
738
739
739 action_str = action_map.get(action, action)
740 action_str = action_map.get(action, action)
740 if feed:
741 if feed:
741 action = action_str[0].replace('[', '').replace(']', '')
742 action = action_str[0].replace('[', '').replace(']', '')
742 else:
743 else:
743 action = action_str[0] \
744 action = action_str[0] \
744 .replace('[', '<b>') \
745 .replace('[', '<b>') \
745 .replace(']', '</b>')
746 .replace(']', '</b>')
746
747
747 action_params_func = action_str[1] if callable(action_str[1]) else (lambda: "")
748 action_params_func = action_str[1] if callable(action_str[1]) else (lambda: "")
748
749
749 def action_parser_icon():
750 def action_parser_icon():
750 action = user_log.action
751 action = user_log.action
751 action_params = None
752 action_params = None
752 x = action.split(':')
753 x = action.split(':')
753
754
754 if len(x) > 1:
755 if len(x) > 1:
755 action, action_params = x
756 action, action_params = x
756
757
757 ico = action_map.get(action, ['', '', ''])[2]
758 ico = action_map.get(action, ['', '', ''])[2]
758 html = """<i class="%s"></i>""" % ico
759 html = """<i class="%s"></i>""" % ico
759 return literal(html)
760 return literal(html)
760
761
761 # returned callbacks we need to call to get
762 # returned callbacks we need to call to get
762 return [lambda: literal(action), action_params_func, action_parser_icon]
763 return [lambda: literal(action), action_params_func, action_parser_icon]
763
764
764
765
765 #==============================================================================
766 #==============================================================================
766 # GRAVATAR URL
767 # GRAVATAR URL
767 #==============================================================================
768 #==============================================================================
768 def gravatar_div(email_address, cls='', size=30, **div_attributes):
769 def gravatar_div(email_address, cls='', size=30, **div_attributes):
769 """Return an html literal with a span around a gravatar if they are enabled.
770 """Return an html literal with a span around a gravatar if they are enabled.
770 Extra keyword parameters starting with 'div_' will get the prefix removed
771 Extra keyword parameters starting with 'div_' will get the prefix removed
771 and '_' changed to '-' and be used as attributes on the div. The default
772 and '_' changed to '-' and be used as attributes on the div. The default
772 class is 'gravatar'.
773 class is 'gravatar'.
773 """
774 """
774 from tg import tmpl_context as c
775 if not c.visual.use_gravatar:
775 if not c.visual.use_gravatar:
776 return ''
776 return ''
777 if 'div_class' not in div_attributes:
777 if 'div_class' not in div_attributes:
778 div_attributes['div_class'] = "gravatar"
778 div_attributes['div_class'] = "gravatar"
779 attributes = []
779 attributes = []
780 for k, v in sorted(div_attributes.items()):
780 for k, v in sorted(div_attributes.items()):
781 assert k.startswith('div_'), k
781 assert k.startswith('div_'), k
782 attributes.append(' %s="%s"' % (k[4:].replace('_', '-'), escape(v)))
782 attributes.append(' %s="%s"' % (k[4:].replace('_', '-'), escape(v)))
783 return literal("""<span%s>%s</span>""" %
783 return literal("""<span%s>%s</span>""" %
784 (''.join(attributes),
784 (''.join(attributes),
785 gravatar(email_address, cls=cls, size=size)))
785 gravatar(email_address, cls=cls, size=size)))
786
786
787
787
788 def gravatar(email_address, cls='', size=30):
788 def gravatar(email_address, cls='', size=30):
789 """return html element of the gravatar
789 """return html element of the gravatar
790
790
791 This method will return an <img> with the resolution double the size (for
791 This method will return an <img> with the resolution double the size (for
792 retina screens) of the image. If the url returned from gravatar_url is
792 retina screens) of the image. If the url returned from gravatar_url is
793 empty then we fallback to using an icon.
793 empty then we fallback to using an icon.
794
794
795 """
795 """
796 from tg import tmpl_context as c
797 if not c.visual.use_gravatar:
796 if not c.visual.use_gravatar:
798 return ''
797 return ''
799
798
800 src = gravatar_url(email_address, size * 2)
799 src = gravatar_url(email_address, size * 2)
801
800
802 if src:
801 if src:
803 # here it makes sense to use style="width: ..." (instead of, say, a
802 # here it makes sense to use style="width: ..." (instead of, say, a
804 # stylesheet) because we using this to generate a high-res (retina) size
803 # stylesheet) because we using this to generate a high-res (retina) size
805 html = ('<i class="icon-gravatar {cls}"'
804 html = ('<i class="icon-gravatar {cls}"'
806 ' style="font-size: {size}px;background-size: {size}px;background-image: url(\'{src}\')"'
805 ' style="font-size: {size}px;background-size: {size}px;background-image: url(\'{src}\')"'
807 '></i>').format(cls=cls, size=size, src=src)
806 '></i>').format(cls=cls, size=size, src=src)
808
807
809 else:
808 else:
810 # if src is empty then there was no gravatar, so we use a font icon
809 # if src is empty then there was no gravatar, so we use a font icon
811 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
810 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
812 .format(cls=cls, size=size))
811 .format(cls=cls, size=size))
813
812
814 return literal(html)
813 return literal(html)
815
814
816
815
817 def gravatar_url(email_address, size=30, default=''):
816 def gravatar_url(email_address, size=30, default=''):
818 from tg import tmpl_context as c
819
820 if not c.visual.use_gravatar:
817 if not c.visual.use_gravatar:
821 return ""
818 return ""
822
819
823 _def = 'anonymous@kallithea-scm.org' # default gravatar
820 _def = 'anonymous@kallithea-scm.org' # default gravatar
824 email_address = email_address or _def
821 email_address = email_address or _def
825
822
826 if email_address == _def:
823 if email_address == _def:
827 return default
824 return default
828
825
829 parsed_url = urllib.parse.urlparse(url.current(qualified=True))
826 parsed_url = urllib.parse.urlparse(url.current(qualified=True))
830 return (c.visual.gravatar_url or db.User.DEFAULT_GRAVATAR_URL) \
827 return (c.visual.gravatar_url or db.User.DEFAULT_GRAVATAR_URL) \
831 .replace('{email}', email_address) \
828 .replace('{email}', email_address) \
832 .replace('{md5email}', hashlib.md5(safe_bytes(email_address).lower()).hexdigest()) \
829 .replace('{md5email}', hashlib.md5(safe_bytes(email_address).lower()).hexdigest()) \
833 .replace('{netloc}', parsed_url.netloc) \
830 .replace('{netloc}', parsed_url.netloc) \
834 .replace('{scheme}', parsed_url.scheme) \
831 .replace('{scheme}', parsed_url.scheme) \
835 .replace('{size}', str(size))
832 .replace('{size}', str(size))
836
833
837
834
838 def changed_tooltip(nodes):
835 def changed_tooltip(nodes):
839 """
836 """
840 Generates a html string for changed nodes in changeset page.
837 Generates a html string for changed nodes in changeset page.
841 It limits the output to 30 entries
838 It limits the output to 30 entries
842
839
843 :param nodes: LazyNodesGenerator
840 :param nodes: LazyNodesGenerator
844 """
841 """
845 if nodes:
842 if nodes:
846 pref = ': <br/> '
843 pref = ': <br/> '
847 suf = ''
844 suf = ''
848 if len(nodes) > 30:
845 if len(nodes) > 30:
849 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
846 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
850 return literal(pref + '<br/> '.join([x.path
847 return literal(pref + '<br/> '.join([x.path
851 for x in nodes[:30]]) + suf)
848 for x in nodes[:30]]) + suf)
852 else:
849 else:
853 return ': ' + _('No files')
850 return ': ' + _('No files')
854
851
855
852
856 def fancy_file_stats(stats):
853 def fancy_file_stats(stats):
857 """
854 """
858 Displays a fancy two colored bar for number of added/deleted
855 Displays a fancy two colored bar for number of added/deleted
859 lines of code on file
856 lines of code on file
860
857
861 :param stats: two element list of added/deleted lines of code
858 :param stats: two element list of added/deleted lines of code
862 """
859 """
863
860
864 a, d = stats['added'], stats['deleted']
861 a, d = stats['added'], stats['deleted']
865 width = 100
862 width = 100
866
863
867 if stats['binary']:
864 if stats['binary']:
868 # binary mode
865 # binary mode
869 lbl = ''
866 lbl = ''
870 bin_op = 1
867 bin_op = 1
871
868
872 if BIN_FILENODE in stats['ops']:
869 if BIN_FILENODE in stats['ops']:
873 lbl = 'bin+'
870 lbl = 'bin+'
874
871
875 if NEW_FILENODE in stats['ops']:
872 if NEW_FILENODE in stats['ops']:
876 lbl += _('new file')
873 lbl += _('new file')
877 bin_op = NEW_FILENODE
874 bin_op = NEW_FILENODE
878 elif MOD_FILENODE in stats['ops']:
875 elif MOD_FILENODE in stats['ops']:
879 lbl += _('mod')
876 lbl += _('mod')
880 bin_op = MOD_FILENODE
877 bin_op = MOD_FILENODE
881 elif DEL_FILENODE in stats['ops']:
878 elif DEL_FILENODE in stats['ops']:
882 lbl += _('del')
879 lbl += _('del')
883 bin_op = DEL_FILENODE
880 bin_op = DEL_FILENODE
884 elif RENAMED_FILENODE in stats['ops']:
881 elif RENAMED_FILENODE in stats['ops']:
885 lbl += _('rename')
882 lbl += _('rename')
886 bin_op = RENAMED_FILENODE
883 bin_op = RENAMED_FILENODE
887
884
888 # chmod can go with other operations
885 # chmod can go with other operations
889 if CHMOD_FILENODE in stats['ops']:
886 if CHMOD_FILENODE in stats['ops']:
890 _org_lbl = _('chmod')
887 _org_lbl = _('chmod')
891 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
888 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
892
889
893 #import ipdb;ipdb.set_trace()
890 #import ipdb;ipdb.set_trace()
894 b_d = '<div class="bin bin%s progress-bar" style="width:100%%">%s</div>' % (bin_op, lbl)
891 b_d = '<div class="bin bin%s progress-bar" style="width:100%%">%s</div>' % (bin_op, lbl)
895 b_a = '<div class="bin bin1" style="width:0%"></div>'
892 b_a = '<div class="bin bin1" style="width:0%"></div>'
896 return literal('<div style="width:%spx" class="progress">%s%s</div>' % (width, b_a, b_d))
893 return literal('<div style="width:%spx" class="progress">%s%s</div>' % (width, b_a, b_d))
897
894
898 t = stats['added'] + stats['deleted']
895 t = stats['added'] + stats['deleted']
899 unit = float(width) / (t or 1)
896 unit = float(width) / (t or 1)
900
897
901 # needs > 9% of width to be visible or 0 to be hidden
898 # needs > 9% of width to be visible or 0 to be hidden
902 a_p = max(9, unit * a) if a > 0 else 0
899 a_p = max(9, unit * a) if a > 0 else 0
903 d_p = max(9, unit * d) if d > 0 else 0
900 d_p = max(9, unit * d) if d > 0 else 0
904 p_sum = a_p + d_p
901 p_sum = a_p + d_p
905
902
906 if p_sum > width:
903 if p_sum > width:
907 # adjust the percentage to be == 100% since we adjusted to 9
904 # adjust the percentage to be == 100% since we adjusted to 9
908 if a_p > d_p:
905 if a_p > d_p:
909 a_p = a_p - (p_sum - width)
906 a_p = a_p - (p_sum - width)
910 else:
907 else:
911 d_p = d_p - (p_sum - width)
908 d_p = d_p - (p_sum - width)
912
909
913 a_v = a if a > 0 else ''
910 a_v = a if a > 0 else ''
914 d_v = d if d > 0 else ''
911 d_v = d if d > 0 else ''
915
912
916 d_a = '<div class="added progress-bar" style="width:%s%%">%s</div>' % (
913 d_a = '<div class="added progress-bar" style="width:%s%%">%s</div>' % (
917 a_p, a_v
914 a_p, a_v
918 )
915 )
919 d_d = '<div class="deleted progress-bar" style="width:%s%%">%s</div>' % (
916 d_d = '<div class="deleted progress-bar" style="width:%s%%">%s</div>' % (
920 d_p, d_v
917 d_p, d_v
921 )
918 )
922 return literal('<div class="progress" style="width:%spx">%s%s</div>' % (width, d_a, d_d))
919 return literal('<div class="progress" style="width:%spx">%s%s</div>' % (width, d_a, d_d))
923
920
924
921
925 _URLIFY_RE = re.compile(r'''
922 _URLIFY_RE = re.compile(r'''
926 # URL markup
923 # URL markup
927 (?P<url>%s) |
924 (?P<url>%s) |
928 # @mention markup
925 # @mention markup
929 (?P<mention>%s) |
926 (?P<mention>%s) |
930 # Changeset hash markup
927 # Changeset hash markup
931 (?<!\w|[-_])
928 (?<!\w|[-_])
932 (?P<hash>[0-9a-f]{12,40})
929 (?P<hash>[0-9a-f]{12,40})
933 (?!\w|[-_]) |
930 (?!\w|[-_]) |
934 # Markup of *bold text*
931 # Markup of *bold text*
935 (?:
932 (?:
936 (?:^|(?<=\s))
933 (?:^|(?<=\s))
937 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
934 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
938 (?![*\w])
935 (?![*\w])
939 ) |
936 ) |
940 # "Stylize" markup
937 # "Stylize" markup
941 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
938 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
942 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
939 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
943 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
940 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
944 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
941 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
945 \[(?P<tag>[a-z]+)\]
942 \[(?P<tag>[a-z]+)\]
946 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
943 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
947 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
944 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
948
945
949
946
950 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
947 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
951 """
948 """
952 Parses given text message and make literal html with markup.
949 Parses given text message and make literal html with markup.
953 The text will be truncated to the specified length.
950 The text will be truncated to the specified length.
954 Hashes are turned into changeset links to specified repository.
951 Hashes are turned into changeset links to specified repository.
955 URLs links to what they say.
952 URLs links to what they say.
956 Issues are linked to given issue-server.
953 Issues are linked to given issue-server.
957 If link_ is provided, all text not already linking somewhere will link there.
954 If link_ is provided, all text not already linking somewhere will link there.
958 >>> urlify_text("Urlify http://example.com/ and 'https://example.com' *and* <b>markup/b>")
955 >>> urlify_text("Urlify http://example.com/ and 'https://example.com' *and* <b>markup/b>")
959 literal('Urlify <a href="http://example.com/">http://example.com/</a> and &#39;<a href="https://example.com&apos">https://example.com&apos</a>; <b>*and*</b> &lt;b&gt;markup/b&gt;')
956 literal('Urlify <a href="http://example.com/">http://example.com/</a> and &#39;<a href="https://example.com&apos">https://example.com&apos</a>; <b>*and*</b> &lt;b&gt;markup/b&gt;')
960 """
957 """
961
958
962 def _replace(match_obj):
959 def _replace(match_obj):
963 match_url = match_obj.group('url')
960 match_url = match_obj.group('url')
964 if match_url is not None:
961 if match_url is not None:
965 return '<a href="%(url)s">%(url)s</a>' % {'url': match_url}
962 return '<a href="%(url)s">%(url)s</a>' % {'url': match_url}
966 mention = match_obj.group('mention')
963 mention = match_obj.group('mention')
967 if mention is not None:
964 if mention is not None:
968 return '<b>%s</b>' % mention
965 return '<b>%s</b>' % mention
969 hash_ = match_obj.group('hash')
966 hash_ = match_obj.group('hash')
970 if hash_ is not None and repo_name is not None:
967 if hash_ is not None and repo_name is not None:
971 return '<a class="changeset_hash" href="%(url)s">%(hash)s</a>' % {
968 return '<a class="changeset_hash" href="%(url)s">%(hash)s</a>' % {
972 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
969 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
973 'hash': hash_,
970 'hash': hash_,
974 }
971 }
975 bold = match_obj.group('bold')
972 bold = match_obj.group('bold')
976 if bold is not None:
973 if bold is not None:
977 return '<b>*%s*</b>' % _urlify(bold[1:-1])
974 return '<b>*%s*</b>' % _urlify(bold[1:-1])
978 if stylize:
975 if stylize:
979 seen = match_obj.group('seen')
976 seen = match_obj.group('seen')
980 if seen:
977 if seen:
981 return '<div class="label label-meta" data-tag="see">see =&gt; %s</div>' % seen
978 return '<div class="label label-meta" data-tag="see">see =&gt; %s</div>' % seen
982 license = match_obj.group('license')
979 license = match_obj.group('license')
983 if license:
980 if license:
984 return '<div class="label label-meta" data-tag="license"><a href="http://www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
981 return '<div class="label label-meta" data-tag="license"><a href="http://www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
985 tagtype = match_obj.group('tagtype')
982 tagtype = match_obj.group('tagtype')
986 if tagtype:
983 if tagtype:
987 tagvalue = match_obj.group('tagvalue')
984 tagvalue = match_obj.group('tagvalue')
988 return '<div class="label label-meta" data-tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
985 return '<div class="label label-meta" data-tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
989 lang = match_obj.group('lang')
986 lang = match_obj.group('lang')
990 if lang:
987 if lang:
991 return '<div class="label label-meta" data-tag="lang">%s</div>' % lang
988 return '<div class="label label-meta" data-tag="lang">%s</div>' % lang
992 tag = match_obj.group('tag')
989 tag = match_obj.group('tag')
993 if tag:
990 if tag:
994 return '<div class="label label-meta" data-tag="%s">%s</div>' % (tag, tag)
991 return '<div class="label label-meta" data-tag="%s">%s</div>' % (tag, tag)
995 return match_obj.group(0)
992 return match_obj.group(0)
996
993
997 def _urlify(s):
994 def _urlify(s):
998 """
995 """
999 Extract urls from text and make html links out of them
996 Extract urls from text and make html links out of them
1000 """
997 """
1001 return _URLIFY_RE.sub(_replace, s)
998 return _URLIFY_RE.sub(_replace, s)
1002
999
1003 if truncate is None:
1000 if truncate is None:
1004 s = s.rstrip()
1001 s = s.rstrip()
1005 else:
1002 else:
1006 s = truncatef(s, truncate, whole_word=True)
1003 s = truncatef(s, truncate, whole_word=True)
1007 s = html_escape(s)
1004 s = html_escape(s)
1008 s = _urlify(s)
1005 s = _urlify(s)
1009 if repo_name is not None:
1006 if repo_name is not None:
1010 s = urlify_issues(s, repo_name)
1007 s = urlify_issues(s, repo_name)
1011 if link_ is not None:
1008 if link_ is not None:
1012 # make href around everything that isn't a href already
1009 # make href around everything that isn't a href already
1013 s = linkify_others(s, link_)
1010 s = linkify_others(s, link_)
1014 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1011 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1015 # Turn HTML5 into more valid HTML4 as required by some mail readers.
1012 # Turn HTML5 into more valid HTML4 as required by some mail readers.
1016 # (This is not done in one step in html_escape, because character codes like
1013 # (This is not done in one step in html_escape, because character codes like
1017 # &#123; risk to be seen as an issue reference due to the presence of '#'.)
1014 # &#123; risk to be seen as an issue reference due to the presence of '#'.)
1018 s = s.replace("&apos;", "&#39;")
1015 s = s.replace("&apos;", "&#39;")
1019 return literal(s)
1016 return literal(s)
1020
1017
1021
1018
1022 def linkify_others(t, l):
1019 def linkify_others(t, l):
1023 """Add a default link to html with links.
1020 """Add a default link to html with links.
1024 HTML doesn't allow nesting of links, so the outer link must be broken up
1021 HTML doesn't allow nesting of links, so the outer link must be broken up
1025 in pieces and give space for other links.
1022 in pieces and give space for other links.
1026 """
1023 """
1027 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1024 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1028 links = []
1025 links = []
1029 for e in urls.split(t):
1026 for e in urls.split(t):
1030 if e.strip() and not urls.match(e):
1027 if e.strip() and not urls.match(e):
1031 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1028 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1032 else:
1029 else:
1033 links.append(e)
1030 links.append(e)
1034
1031
1035 return ''.join(links)
1032 return ''.join(links)
1036
1033
1037
1034
1038 # Global variable that will hold the actual urlify_issues function body.
1035 # Global variable that will hold the actual urlify_issues function body.
1039 # Will be set on first use when the global configuration has been read.
1036 # Will be set on first use when the global configuration has been read.
1040 _urlify_issues_f = None
1037 _urlify_issues_f = None
1041
1038
1042
1039
1043 def urlify_issues(newtext, repo_name):
1040 def urlify_issues(newtext, repo_name):
1044 """Urlify issue references according to .ini configuration"""
1041 """Urlify issue references according to .ini configuration"""
1045 global _urlify_issues_f
1042 global _urlify_issues_f
1046 if _urlify_issues_f is None:
1043 if _urlify_issues_f is None:
1047 assert kallithea.CONFIG['sqlalchemy.url'] # make sure config has been loaded
1044 assert kallithea.CONFIG['sqlalchemy.url'] # make sure config has been loaded
1048
1045
1049 # Build chain of urlify functions, starting with not doing any transformation
1046 # Build chain of urlify functions, starting with not doing any transformation
1050 def tmp_urlify_issues_f(s):
1047 def tmp_urlify_issues_f(s):
1051 return s
1048 return s
1052
1049
1053 issue_pat_re = re.compile(r'issue_pat(.*)')
1050 issue_pat_re = re.compile(r'issue_pat(.*)')
1054 for k in kallithea.CONFIG:
1051 for k in kallithea.CONFIG:
1055 # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
1052 # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
1056 m = issue_pat_re.match(k)
1053 m = issue_pat_re.match(k)
1057 if m is None:
1054 if m is None:
1058 continue
1055 continue
1059 suffix = m.group(1)
1056 suffix = m.group(1)
1060 issue_pat = kallithea.CONFIG.get(k)
1057 issue_pat = kallithea.CONFIG.get(k)
1061 issue_server_link = kallithea.CONFIG.get('issue_server_link%s' % suffix)
1058 issue_server_link = kallithea.CONFIG.get('issue_server_link%s' % suffix)
1062 issue_sub = kallithea.CONFIG.get('issue_sub%s' % suffix)
1059 issue_sub = kallithea.CONFIG.get('issue_sub%s' % suffix)
1063 issue_prefix = kallithea.CONFIG.get('issue_prefix%s' % suffix)
1060 issue_prefix = kallithea.CONFIG.get('issue_prefix%s' % suffix)
1064 if issue_prefix:
1061 if issue_prefix:
1065 log.error('found unsupported issue_prefix%s = %r - use issue_sub%s instead', suffix, issue_prefix, suffix)
1062 log.error('found unsupported issue_prefix%s = %r - use issue_sub%s instead', suffix, issue_prefix, suffix)
1066 if not issue_pat:
1063 if not issue_pat:
1067 log.error('skipping incomplete issue pattern %r: it needs a regexp', k)
1064 log.error('skipping incomplete issue pattern %r: it needs a regexp', k)
1068 continue
1065 continue
1069 if not issue_server_link:
1066 if not issue_server_link:
1070 log.error('skipping incomplete issue pattern %r: it needs issue_server_link%s', k, suffix)
1067 log.error('skipping incomplete issue pattern %r: it needs issue_server_link%s', k, suffix)
1071 continue
1068 continue
1072 if issue_sub is None: # issue_sub can be empty but should be present
1069 if issue_sub is None: # issue_sub can be empty but should be present
1073 log.error('skipping incomplete issue pattern %r: it needs (a potentially empty) issue_sub%s', k, suffix)
1070 log.error('skipping incomplete issue pattern %r: it needs (a potentially empty) issue_sub%s', k, suffix)
1074 continue
1071 continue
1075
1072
1076 # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
1073 # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
1077 try:
1074 try:
1078 issue_re = re.compile(issue_pat)
1075 issue_re = re.compile(issue_pat)
1079 except re.error as e:
1076 except re.error as e:
1080 log.error('skipping invalid issue pattern %r: %r -> %r %r. Error: %s', k, issue_pat, issue_server_link, issue_sub, str(e))
1077 log.error('skipping invalid issue pattern %r: %r -> %r %r. Error: %s', k, issue_pat, issue_server_link, issue_sub, str(e))
1081 continue
1078 continue
1082
1079
1083 log.debug('issue pattern %r: %r -> %r %r', k, issue_pat, issue_server_link, issue_sub)
1080 log.debug('issue pattern %r: %r -> %r %r', k, issue_pat, issue_server_link, issue_sub)
1084
1081
1085 def issues_replace(match_obj,
1082 def issues_replace(match_obj,
1086 issue_server_link=issue_server_link, issue_sub=issue_sub):
1083 issue_server_link=issue_server_link, issue_sub=issue_sub):
1087 try:
1084 try:
1088 issue_url = match_obj.expand(issue_server_link)
1085 issue_url = match_obj.expand(issue_server_link)
1089 except (IndexError, re.error) as e:
1086 except (IndexError, re.error) as e:
1090 log.error('invalid issue_url setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1087 log.error('invalid issue_url setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1091 issue_url = issue_server_link
1088 issue_url = issue_server_link
1092 issue_url = issue_url.replace('{repo}', repo_name)
1089 issue_url = issue_url.replace('{repo}', repo_name)
1093 issue_url = issue_url.replace('{repo_name}', repo_name.split(kallithea.URL_SEP)[-1])
1090 issue_url = issue_url.replace('{repo_name}', repo_name.split(kallithea.URL_SEP)[-1])
1094 # if issue_sub is empty use the matched issue reference verbatim
1091 # if issue_sub is empty use the matched issue reference verbatim
1095 if not issue_sub:
1092 if not issue_sub:
1096 issue_text = match_obj.group()
1093 issue_text = match_obj.group()
1097 else:
1094 else:
1098 try:
1095 try:
1099 issue_text = match_obj.expand(issue_sub)
1096 issue_text = match_obj.expand(issue_sub)
1100 except (IndexError, re.error) as e:
1097 except (IndexError, re.error) as e:
1101 log.error('invalid issue_sub setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1098 log.error('invalid issue_sub setting %r -> %r %r. Error: %s', issue_pat, issue_server_link, issue_sub, str(e))
1102 issue_text = match_obj.group()
1099 issue_text = match_obj.group()
1103
1100
1104 return (
1101 return (
1105 '<a class="issue-tracker-link" href="%(url)s">'
1102 '<a class="issue-tracker-link" href="%(url)s">'
1106 '%(text)s'
1103 '%(text)s'
1107 '</a>'
1104 '</a>'
1108 ) % {
1105 ) % {
1109 'url': issue_url,
1106 'url': issue_url,
1110 'text': issue_text,
1107 'text': issue_text,
1111 }
1108 }
1112
1109
1113 def tmp_urlify_issues_f(s, issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f):
1110 def tmp_urlify_issues_f(s, issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f):
1114 return issue_re.sub(issues_replace, chain_f(s))
1111 return issue_re.sub(issues_replace, chain_f(s))
1115
1112
1116 # Set tmp function globally - atomically
1113 # Set tmp function globally - atomically
1117 _urlify_issues_f = tmp_urlify_issues_f
1114 _urlify_issues_f = tmp_urlify_issues_f
1118
1115
1119 return _urlify_issues_f(newtext)
1116 return _urlify_issues_f(newtext)
1120
1117
1121
1118
1122 def render_w_mentions(source, repo_name=None):
1119 def render_w_mentions(source, repo_name=None):
1123 """
1120 """
1124 Render plain text with revision hashes and issue references urlified
1121 Render plain text with revision hashes and issue references urlified
1125 and with @mention highlighting.
1122 and with @mention highlighting.
1126 """
1123 """
1127 s = safe_str(source)
1124 s = safe_str(source)
1128 s = urlify_text(s, repo_name=repo_name)
1125 s = urlify_text(s, repo_name=repo_name)
1129 return literal('<div class="formatted-fixed">%s</div>' % s)
1126 return literal('<div class="formatted-fixed">%s</div>' % s)
1130
1127
1131
1128
1132 def changeset_status(repo, revision):
1129 def changeset_status(repo, revision):
1133 return ChangesetStatusModel().get_status(repo, revision)
1130 return ChangesetStatusModel().get_status(repo, revision)
1134
1131
1135
1132
1136 def changeset_status_lbl(changeset_status):
1133 def changeset_status_lbl(changeset_status):
1137 return db.ChangesetStatus.get_status_lbl(changeset_status)
1134 return db.ChangesetStatus.get_status_lbl(changeset_status)
1138
1135
1139
1136
1140 def get_permission_name(key):
1137 def get_permission_name(key):
1141 return dict(db.Permission.PERMS).get(key)
1138 return dict(db.Permission.PERMS).get(key)
1142
1139
1143
1140
1144 def journal_filter_help():
1141 def journal_filter_help():
1145 return _(textwrap.dedent('''
1142 return _(textwrap.dedent('''
1146 Example filter terms:
1143 Example filter terms:
1147 repository:vcs
1144 repository:vcs
1148 username:developer
1145 username:developer
1149 action:*push*
1146 action:*push*
1150 ip:127.0.0.1
1147 ip:127.0.0.1
1151 date:20120101
1148 date:20120101
1152 date:[20120101100000 TO 20120102]
1149 date:[20120101100000 TO 20120102]
1153
1150
1154 Generate wildcards using '*' character:
1151 Generate wildcards using '*' character:
1155 "repository:vcs*" - search everything starting with 'vcs'
1152 "repository:vcs*" - search everything starting with 'vcs'
1156 "repository:*vcs*" - search for repository containing 'vcs'
1153 "repository:*vcs*" - search for repository containing 'vcs'
1157
1154
1158 Optional AND / OR operators in queries
1155 Optional AND / OR operators in queries
1159 "repository:vcs OR repository:test"
1156 "repository:vcs OR repository:test"
1160 "username:test AND repository:test*"
1157 "username:test AND repository:test*"
1161 '''))
1158 '''))
1162
1159
1163
1160
1164 def ip_range(ip_addr):
1161 def ip_range(ip_addr):
1165 s, e = db.UserIpMap._get_ip_range(ip_addr)
1162 s, e = db.UserIpMap._get_ip_range(ip_addr)
1166 return '%s - %s' % (s, e)
1163 return '%s - %s' % (s, e)
@@ -1,406 +1,405 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.hooks
15 kallithea.lib.hooks
16 ~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~
17
17
18 Hooks run by Kallithea
18 Hooks run by Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Aug 6, 2010
22 :created_on: Aug 6, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import os
28 import os
29 import sys
29 import sys
30 import time
30 import time
31
31
32 import mercurial.scmutil
32 import mercurial.scmutil
33 import paste.deploy
33
34
34 import kallithea
35 import kallithea
35 from kallithea.lib import webutils
36 from kallithea.lib import webutils
36 from kallithea.lib.exceptions import UserCreationError
37 from kallithea.lib.exceptions import UserCreationError
37 from kallithea.lib.utils import action_logger, make_ui
38 from kallithea.lib.utils import action_logger, make_ui
38 from kallithea.lib.utils2 import HookEnvironmentError, ascii_str, get_hook_environment, safe_bytes, safe_str
39 from kallithea.lib.utils2 import HookEnvironmentError, ascii_str, get_hook_environment, safe_bytes, safe_str
39 from kallithea.lib.vcs.backends.base import EmptyChangeset
40 from kallithea.lib.vcs.backends.base import EmptyChangeset
40 from kallithea.model import db
41 from kallithea.model import db
41
42
42
43
43 def _get_scm_size(alias, root_path):
44 def _get_scm_size(alias, root_path):
44 if not alias.startswith('.'):
45 if not alias.startswith('.'):
45 alias += '.'
46 alias += '.'
46
47
47 size_scm, size_root = 0, 0
48 size_scm, size_root = 0, 0
48 for path, dirs, files in os.walk(root_path):
49 for path, dirs, files in os.walk(root_path):
49 if path.find(alias) != -1:
50 if path.find(alias) != -1:
50 for f in files:
51 for f in files:
51 try:
52 try:
52 size_scm += os.path.getsize(os.path.join(path, f))
53 size_scm += os.path.getsize(os.path.join(path, f))
53 except OSError:
54 except OSError:
54 pass
55 pass
55 else:
56 else:
56 for f in files:
57 for f in files:
57 try:
58 try:
58 size_root += os.path.getsize(os.path.join(path, f))
59 size_root += os.path.getsize(os.path.join(path, f))
59 except OSError:
60 except OSError:
60 pass
61 pass
61
62
62 size_scm_f = webutils.format_byte_size(size_scm)
63 size_scm_f = webutils.format_byte_size(size_scm)
63 size_root_f = webutils.format_byte_size(size_root)
64 size_root_f = webutils.format_byte_size(size_root)
64 size_total_f = webutils.format_byte_size(size_root + size_scm)
65 size_total_f = webutils.format_byte_size(size_root + size_scm)
65
66
66 return size_scm_f, size_root_f, size_total_f
67 return size_scm_f, size_root_f, size_total_f
67
68
68
69
69 def repo_size(ui, repo, hooktype=None, **kwargs):
70 def repo_size(ui, repo, hooktype=None, **kwargs):
70 """Show size of Mercurial repository.
71 """Show size of Mercurial repository.
71
72
72 Called as Mercurial hook changegroup.repo_size after push.
73 Called as Mercurial hook changegroup.repo_size after push.
73 """
74 """
74 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', safe_str(repo.root))
75 size_hg_f, size_root_f, size_total_f = _get_scm_size('.hg', safe_str(repo.root))
75
76
76 last_cs = repo[len(repo) - 1]
77 last_cs = repo[len(repo) - 1]
77
78
78 msg = ('Repository size .hg: %s Checkout: %s Total: %s\n'
79 msg = ('Repository size .hg: %s Checkout: %s Total: %s\n'
79 'Last revision is now r%s:%s\n') % (
80 'Last revision is now r%s:%s\n') % (
80 size_hg_f, size_root_f, size_total_f, last_cs.rev(), ascii_str(last_cs.hex())[:12]
81 size_hg_f, size_root_f, size_total_f, last_cs.rev(), ascii_str(last_cs.hex())[:12]
81 )
82 )
82 ui.status(safe_bytes(msg))
83 ui.status(safe_bytes(msg))
83
84
84
85
85 def log_pull_action(ui, repo, **kwargs):
86 def log_pull_action(ui, repo, **kwargs):
86 """Logs user last pull action
87 """Logs user last pull action
87
88
88 Called as Mercurial hook outgoing.pull_logger or from Kallithea before invoking Git.
89 Called as Mercurial hook outgoing.pull_logger or from Kallithea before invoking Git.
89
90
90 Does *not* use the action from the hook environment but is always 'pull'.
91 Does *not* use the action from the hook environment but is always 'pull'.
91 """
92 """
92 ex = get_hook_environment()
93 ex = get_hook_environment()
93
94
94 user = db.User.get_by_username(ex.username)
95 user = db.User.get_by_username(ex.username)
95 action = 'pull'
96 action = 'pull'
96 action_logger(user, action, ex.repository, ex.ip, commit=True)
97 action_logger(user, action, ex.repository, ex.ip, commit=True)
97 # extension hook call
98 # extension hook call
98 callback = getattr(kallithea.EXTENSIONS, 'PULL_HOOK', None)
99 callback = getattr(kallithea.EXTENSIONS, 'PULL_HOOK', None)
99 if callable(callback):
100 if callable(callback):
100 kw = {}
101 kw = {}
101 kw.update(ex)
102 kw.update(ex)
102 callback(**kw)
103 callback(**kw)
103
104
104
105
105 def log_push_action(ui, repo, node, node_last, **kwargs):
106 def log_push_action(ui, repo, node, node_last, **kwargs):
106 """
107 """
107 Register that changes have been added to the repo - log the action *and* invalidate caches.
108 Register that changes have been added to the repo - log the action *and* invalidate caches.
108 Note: This hook is not only logging, but also the side effect invalidating
109 Note: This hook is not only logging, but also the side effect invalidating
109 caches! The function should perhaps be renamed.
110 caches! The function should perhaps be renamed.
110
111
111 Called as Mercurial hook changegroup.kallithea_log_push_action .
112 Called as Mercurial hook changegroup.kallithea_log_push_action .
112
113
113 The pushed changesets is given by the revset 'node:node_last'.
114 The pushed changesets is given by the revset 'node:node_last'.
114 """
115 """
115 revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])]
116 revs = [ascii_str(repo[r].hex()) for r in mercurial.scmutil.revrange(repo, [b'%s:%s' % (node, node_last)])]
116 process_pushed_raw_ids(revs)
117 process_pushed_raw_ids(revs)
117
118
118
119
119 def process_pushed_raw_ids(revs):
120 def process_pushed_raw_ids(revs):
120 """
121 """
121 Register that changes have been added to the repo - log the action *and* invalidate caches.
122 Register that changes have been added to the repo - log the action *and* invalidate caches.
122
123
123 Called from Mercurial changegroup.kallithea_log_push_action calling hook log_push_action,
124 Called from Mercurial changegroup.kallithea_log_push_action calling hook log_push_action,
124 or from the Git post-receive hook calling handle_git_post_receive ...
125 or from the Git post-receive hook calling handle_git_post_receive ...
125 or from scm _handle_push.
126 or from scm _handle_push.
126 """
127 """
127 ex = get_hook_environment()
128 ex = get_hook_environment()
128
129
129 action = '%s:%s' % (ex.action, ','.join(revs))
130 action = '%s:%s' % (ex.action, ','.join(revs))
130 action_logger(ex.username, action, ex.repository, ex.ip, commit=True)
131 action_logger(ex.username, action, ex.repository, ex.ip, commit=True)
131
132
132 from kallithea.model.scm import ScmModel
133 from kallithea.model.scm import ScmModel
133 ScmModel().mark_for_invalidation(ex.repository)
134 ScmModel().mark_for_invalidation(ex.repository)
134
135
135 # extension hook call
136 # extension hook call
136 callback = getattr(kallithea.EXTENSIONS, 'PUSH_HOOK', None)
137 callback = getattr(kallithea.EXTENSIONS, 'PUSH_HOOK', None)
137 if callable(callback):
138 if callable(callback):
138 kw = {'pushed_revs': revs}
139 kw = {'pushed_revs': revs}
139 kw.update(ex)
140 kw.update(ex)
140 callback(**kw)
141 callback(**kw)
141
142
142
143
143 def log_create_repository(repository_dict, created_by, **kwargs):
144 def log_create_repository(repository_dict, created_by, **kwargs):
144 """
145 """
145 Post create repository Hook.
146 Post create repository Hook.
146
147
147 :param repository: dict dump of repository object
148 :param repository: dict dump of repository object
148 :param created_by: username who created repository
149 :param created_by: username who created repository
149
150
150 available keys of repository_dict:
151 available keys of repository_dict:
151
152
152 'repo_type',
153 'repo_type',
153 'description',
154 'description',
154 'private',
155 'private',
155 'created_on',
156 'created_on',
156 'enable_downloads',
157 'enable_downloads',
157 'repo_id',
158 'repo_id',
158 'owner_id',
159 'owner_id',
159 'enable_statistics',
160 'enable_statistics',
160 'clone_uri',
161 'clone_uri',
161 'fork_id',
162 'fork_id',
162 'group_id',
163 'group_id',
163 'repo_name'
164 'repo_name'
164
165
165 """
166 """
166 callback = getattr(kallithea.EXTENSIONS, 'CREATE_REPO_HOOK', None)
167 callback = getattr(kallithea.EXTENSIONS, 'CREATE_REPO_HOOK', None)
167 if callable(callback):
168 if callable(callback):
168 kw = {}
169 kw = {}
169 kw.update(repository_dict)
170 kw.update(repository_dict)
170 kw.update({'created_by': created_by})
171 kw.update({'created_by': created_by})
171 kw.update(kwargs)
172 kw.update(kwargs)
172 callback(**kw)
173 callback(**kw)
173
174
174
175
175 def check_allowed_create_user(user_dict, created_by, **kwargs):
176 def check_allowed_create_user(user_dict, created_by, **kwargs):
176 # pre create hooks
177 # pre create hooks
177 callback = getattr(kallithea.EXTENSIONS, 'PRE_CREATE_USER_HOOK', None)
178 callback = getattr(kallithea.EXTENSIONS, 'PRE_CREATE_USER_HOOK', None)
178 if callable(callback):
179 if callable(callback):
179 allowed, reason = callback(created_by=created_by, **user_dict)
180 allowed, reason = callback(created_by=created_by, **user_dict)
180 if not allowed:
181 if not allowed:
181 raise UserCreationError(reason)
182 raise UserCreationError(reason)
182
183
183
184
184 def log_create_user(user_dict, created_by, **kwargs):
185 def log_create_user(user_dict, created_by, **kwargs):
185 """
186 """
186 Post create user Hook.
187 Post create user Hook.
187
188
188 :param user_dict: dict dump of user object
189 :param user_dict: dict dump of user object
189
190
190 available keys for user_dict:
191 available keys for user_dict:
191
192
192 'username',
193 'username',
193 'full_name_or_username',
194 'full_name_or_username',
194 'full_contact',
195 'full_contact',
195 'user_id',
196 'user_id',
196 'name',
197 'name',
197 'firstname',
198 'firstname',
198 'short_contact',
199 'short_contact',
199 'admin',
200 'admin',
200 'lastname',
201 'lastname',
201 'ip_addresses',
202 'ip_addresses',
202 'ldap_dn',
203 'ldap_dn',
203 'email',
204 'email',
204 'api_key',
205 'api_key',
205 'last_login',
206 'last_login',
206 'full_name',
207 'full_name',
207 'active',
208 'active',
208 'password',
209 'password',
209 'emails',
210 'emails',
210
211
211 """
212 """
212 callback = getattr(kallithea.EXTENSIONS, 'CREATE_USER_HOOK', None)
213 callback = getattr(kallithea.EXTENSIONS, 'CREATE_USER_HOOK', None)
213 if callable(callback):
214 if callable(callback):
214 callback(created_by=created_by, **user_dict)
215 callback(created_by=created_by, **user_dict)
215
216
216
217
217 def log_create_pullrequest(pullrequest_dict, created_by, **kwargs):
218 def log_create_pullrequest(pullrequest_dict, created_by, **kwargs):
218 """
219 """
219 Post create pull request hook.
220 Post create pull request hook.
220
221
221 :param pullrequest_dict: dict dump of pull request object
222 :param pullrequest_dict: dict dump of pull request object
222 """
223 """
223 callback = getattr(kallithea.EXTENSIONS, 'CREATE_PULLREQUEST_HOOK', None)
224 callback = getattr(kallithea.EXTENSIONS, 'CREATE_PULLREQUEST_HOOK', None)
224 if callable(callback):
225 if callable(callback):
225 return callback(created_by=created_by, **pullrequest_dict)
226 return callback(created_by=created_by, **pullrequest_dict)
226
227
227 return 0
228 return 0
228
229
229 def log_delete_repository(repository_dict, deleted_by, **kwargs):
230 def log_delete_repository(repository_dict, deleted_by, **kwargs):
230 """
231 """
231 Post delete repository Hook.
232 Post delete repository Hook.
232
233
233 :param repository: dict dump of repository object
234 :param repository: dict dump of repository object
234 :param deleted_by: username who deleted the repository
235 :param deleted_by: username who deleted the repository
235
236
236 available keys of repository_dict:
237 available keys of repository_dict:
237
238
238 'repo_type',
239 'repo_type',
239 'description',
240 'description',
240 'private',
241 'private',
241 'created_on',
242 'created_on',
242 'enable_downloads',
243 'enable_downloads',
243 'repo_id',
244 'repo_id',
244 'owner_id',
245 'owner_id',
245 'enable_statistics',
246 'enable_statistics',
246 'clone_uri',
247 'clone_uri',
247 'fork_id',
248 'fork_id',
248 'group_id',
249 'group_id',
249 'repo_name'
250 'repo_name'
250
251
251 """
252 """
252 callback = getattr(kallithea.EXTENSIONS, 'DELETE_REPO_HOOK', None)
253 callback = getattr(kallithea.EXTENSIONS, 'DELETE_REPO_HOOK', None)
253 if callable(callback):
254 if callable(callback):
254 kw = {}
255 kw = {}
255 kw.update(repository_dict)
256 kw.update(repository_dict)
256 kw.update({'deleted_by': deleted_by,
257 kw.update({'deleted_by': deleted_by,
257 'deleted_on': time.time()})
258 'deleted_on': time.time()})
258 kw.update(kwargs)
259 kw.update(kwargs)
259 callback(**kw)
260 callback(**kw)
260
261
261
262
262 def log_delete_user(user_dict, deleted_by, **kwargs):
263 def log_delete_user(user_dict, deleted_by, **kwargs):
263 """
264 """
264 Post delete user Hook.
265 Post delete user Hook.
265
266
266 :param user_dict: dict dump of user object
267 :param user_dict: dict dump of user object
267
268
268 available keys for user_dict:
269 available keys for user_dict:
269
270
270 'username',
271 'username',
271 'full_name_or_username',
272 'full_name_or_username',
272 'full_contact',
273 'full_contact',
273 'user_id',
274 'user_id',
274 'name',
275 'name',
275 'firstname',
276 'firstname',
276 'short_contact',
277 'short_contact',
277 'admin',
278 'admin',
278 'lastname',
279 'lastname',
279 'ip_addresses',
280 'ip_addresses',
280 'ldap_dn',
281 'ldap_dn',
281 'email',
282 'email',
282 'api_key',
283 'api_key',
283 'last_login',
284 'last_login',
284 'full_name',
285 'full_name',
285 'active',
286 'active',
286 'password',
287 'password',
287 'emails',
288 'emails',
288
289
289 """
290 """
290 callback = getattr(kallithea.EXTENSIONS, 'DELETE_USER_HOOK', None)
291 callback = getattr(kallithea.EXTENSIONS, 'DELETE_USER_HOOK', None)
291 if callable(callback):
292 if callable(callback):
292 callback(deleted_by=deleted_by, **user_dict)
293 callback(deleted_by=deleted_by, **user_dict)
293
294
294
295
295 def _hook_environment(repo_path):
296 def _hook_environment(repo_path):
296 """
297 """
297 Create a light-weight environment for stand-alone scripts and return an UI and the
298 Create a light-weight environment for stand-alone scripts and return an UI and the
298 db repository.
299 db repository.
299
300
300 Git hooks are executed as subprocess of Git while Kallithea is waiting, and
301 Git hooks are executed as subprocess of Git while Kallithea is waiting, and
301 they thus need enough info to be able to create an app environment and
302 they thus need enough info to be able to create an app environment and
302 connect to the database.
303 connect to the database.
303 """
304 """
304 import paste.deploy
305
306 import kallithea.config.application
305 import kallithea.config.application
307
306
308 extras = get_hook_environment()
307 extras = get_hook_environment()
309
308
310 path_to_ini_file = extras['config']
309 path_to_ini_file = extras['config']
311 config = paste.deploy.appconfig('config:' + path_to_ini_file)
310 config = paste.deploy.appconfig('config:' + path_to_ini_file)
312 #logging.config.fileConfig(ini_file_path) # Note: we are in a different process - don't use configured logging
311 #logging.config.fileConfig(ini_file_path) # Note: we are in a different process - don't use configured logging
313 kallithea.config.application.make_app(config.global_conf, **config.local_conf)
312 kallithea.config.application.make_app(config.global_conf, **config.local_conf)
314
313
315 # fix if it's not a bare repo
314 # fix if it's not a bare repo
316 if repo_path.endswith(os.sep + '.git'):
315 if repo_path.endswith(os.sep + '.git'):
317 repo_path = repo_path[:-5]
316 repo_path = repo_path[:-5]
318
317
319 repo = db.Repository.get_by_full_path(repo_path)
318 repo = db.Repository.get_by_full_path(repo_path)
320 if not repo:
319 if not repo:
321 raise OSError('Repository %s not found in database' % repo_path)
320 raise OSError('Repository %s not found in database' % repo_path)
322
321
323 baseui = make_ui()
322 baseui = make_ui()
324 return baseui, repo
323 return baseui, repo
325
324
326
325
327 def handle_git_pre_receive(repo_path, git_stdin_lines):
326 def handle_git_pre_receive(repo_path, git_stdin_lines):
328 """Called from Git pre-receive hook.
327 """Called from Git pre-receive hook.
329 The returned value is used as hook exit code and must be 0.
328 The returned value is used as hook exit code and must be 0.
330 """
329 """
331 # Currently unused. TODO: remove?
330 # Currently unused. TODO: remove?
332 return 0
331 return 0
333
332
334
333
335 def handle_git_post_receive(repo_path, git_stdin_lines):
334 def handle_git_post_receive(repo_path, git_stdin_lines):
336 """Called from Git post-receive hook.
335 """Called from Git post-receive hook.
337 The returned value is used as hook exit code and must be 0.
336 The returned value is used as hook exit code and must be 0.
338 """
337 """
339 try:
338 try:
340 baseui, repo = _hook_environment(repo_path)
339 baseui, repo = _hook_environment(repo_path)
341 except HookEnvironmentError as e:
340 except HookEnvironmentError as e:
342 sys.stderr.write("Skipping Kallithea Git post-recieve hook %r.\nGit was apparently not invoked by Kallithea: %s\n" % (sys.argv[0], e))
341 sys.stderr.write("Skipping Kallithea Git post-recieve hook %r.\nGit was apparently not invoked by Kallithea: %s\n" % (sys.argv[0], e))
343 return 0
342 return 0
344
343
345 # the post push hook should never use the cached instance
344 # the post push hook should never use the cached instance
346 scm_repo = repo.scm_instance_no_cache()
345 scm_repo = repo.scm_instance_no_cache()
347
346
348 rev_data = []
347 rev_data = []
349 for l in git_stdin_lines:
348 for l in git_stdin_lines:
350 old_rev, new_rev, ref = l.strip().split(' ')
349 old_rev, new_rev, ref = l.strip().split(' ')
351 _ref_data = ref.split('/')
350 _ref_data = ref.split('/')
352 if _ref_data[1] in ['tags', 'heads']:
351 if _ref_data[1] in ['tags', 'heads']:
353 rev_data.append({'old_rev': old_rev,
352 rev_data.append({'old_rev': old_rev,
354 'new_rev': new_rev,
353 'new_rev': new_rev,
355 'ref': ref,
354 'ref': ref,
356 'type': _ref_data[1],
355 'type': _ref_data[1],
357 'name': '/'.join(_ref_data[2:])})
356 'name': '/'.join(_ref_data[2:])})
358
357
359 git_revs = []
358 git_revs = []
360 for push_ref in rev_data:
359 for push_ref in rev_data:
361 _type = push_ref['type']
360 _type = push_ref['type']
362 if _type == 'heads':
361 if _type == 'heads':
363 if push_ref['old_rev'] == EmptyChangeset().raw_id:
362 if push_ref['old_rev'] == EmptyChangeset().raw_id:
364 # update the symbolic ref if we push new repo
363 # update the symbolic ref if we push new repo
365 if scm_repo.is_empty():
364 if scm_repo.is_empty():
366 scm_repo._repo.refs.set_symbolic_ref(
365 scm_repo._repo.refs.set_symbolic_ref(
367 b'HEAD',
366 b'HEAD',
368 b'refs/heads/%s' % safe_bytes(push_ref['name']))
367 b'refs/heads/%s' % safe_bytes(push_ref['name']))
369
368
370 # build exclude list without the ref
369 # build exclude list without the ref
371 cmd = ['for-each-ref', '--format=%(refname)', 'refs/heads/*']
370 cmd = ['for-each-ref', '--format=%(refname)', 'refs/heads/*']
372 stdout = scm_repo.run_git_command(cmd)
371 stdout = scm_repo.run_git_command(cmd)
373 ref = push_ref['ref']
372 ref = push_ref['ref']
374 heads = [head for head in stdout.splitlines() if head != ref]
373 heads = [head for head in stdout.splitlines() if head != ref]
375 # now list the git revs while excluding from the list
374 # now list the git revs while excluding from the list
376 cmd = ['log', push_ref['new_rev'], '--reverse', '--pretty=format:%H']
375 cmd = ['log', push_ref['new_rev'], '--reverse', '--pretty=format:%H']
377 cmd.append('--not')
376 cmd.append('--not')
378 cmd.extend(heads) # empty list is ok
377 cmd.extend(heads) # empty list is ok
379 stdout = scm_repo.run_git_command(cmd)
378 stdout = scm_repo.run_git_command(cmd)
380 git_revs += stdout.splitlines()
379 git_revs += stdout.splitlines()
381
380
382 elif push_ref['new_rev'] == EmptyChangeset().raw_id:
381 elif push_ref['new_rev'] == EmptyChangeset().raw_id:
383 # delete branch case
382 # delete branch case
384 git_revs += ['delete_branch=>%s' % push_ref['name']]
383 git_revs += ['delete_branch=>%s' % push_ref['name']]
385 else:
384 else:
386 cmd = ['log', '%(old_rev)s..%(new_rev)s' % push_ref,
385 cmd = ['log', '%(old_rev)s..%(new_rev)s' % push_ref,
387 '--reverse', '--pretty=format:%H']
386 '--reverse', '--pretty=format:%H']
388 stdout = scm_repo.run_git_command(cmd)
387 stdout = scm_repo.run_git_command(cmd)
389 git_revs += stdout.splitlines()
388 git_revs += stdout.splitlines()
390
389
391 elif _type == 'tags':
390 elif _type == 'tags':
392 git_revs += ['tag=>%s' % push_ref['name']]
391 git_revs += ['tag=>%s' % push_ref['name']]
393
392
394 process_pushed_raw_ids(git_revs)
393 process_pushed_raw_ids(git_revs)
395
394
396 return 0
395 return 0
397
396
398
397
399 # Almost exactly like Mercurial contrib/hg-ssh:
398 # Almost exactly like Mercurial contrib/hg-ssh:
400 def rejectpush(ui, **kwargs):
399 def rejectpush(ui, **kwargs):
401 """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos.
400 """Mercurial hook to be installed as pretxnopen and prepushkey for read-only repos.
402 Return value 1 will make the hook fail and reject the push.
401 Return value 1 will make the hook fail and reject the push.
403 """
402 """
404 ex = get_hook_environment()
403 ex = get_hook_environment()
405 ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository))
404 ui.warn(safe_bytes("Push access to %r denied\n" % ex.repository))
406 return 1
405 return 1
@@ -1,551 +1,551 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.lib.utils2
15 kallithea.lib.utils2
16 ~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~
17
17
18 Some simple helper functions.
18 Some simple helper functions.
19 Note: all these functions should be independent of Kallithea classes, i.e.
19 Note: all these functions should be independent of Kallithea classes, i.e.
20 models, controllers, etc. to prevent import cycles.
20 models, controllers, etc. to prevent import cycles.
21
21
22 This file was forked by the Kallithea project in July 2014.
22 This file was forked by the Kallithea project in July 2014.
23 Original author and date, and relevant copyright and licensing information is below:
23 Original author and date, and relevant copyright and licensing information is below:
24 :created_on: Jan 5, 2011
24 :created_on: Jan 5, 2011
25 :author: marcink
25 :author: marcink
26 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :copyright: (c) 2013 RhodeCode GmbH, and others.
27 :license: GPLv3, see LICENSE.md for more details.
27 :license: GPLv3, see LICENSE.md for more details.
28 """
28 """
29
29
30 import binascii
30 import binascii
31 import datetime
31 import datetime
32 import json
32 import json
33 import os
33 import os
34 import re
34 import re
35 import time
35 import time
36 import urllib.parse
36 import urllib.parse
37
37
38 import urlobject
38 import urlobject
39 from dateutil import relativedelta
39 from dateutil import relativedelta
40 from sqlalchemy.engine import url as sa_url
40 from sqlalchemy.engine import url as sa_url
41 from sqlalchemy.exc import ArgumentError
41 from sqlalchemy.exc import ArgumentError
42 from tg import tmpl_context
42 from tg.i18n import ugettext as _
43 from tg.i18n import ugettext as _
43 from tg.i18n import ungettext
44 from tg.i18n import ungettext
44 from tg.support.converters import asbool, aslist
45 from tg.support.converters import asbool, aslist
45 from webhelpers2.text import collapse, remove_formatting, strip_tags
46 from webhelpers2.text import collapse, remove_formatting, strip_tags
46
47
47 import kallithea
48 import kallithea
48 from kallithea.lib import webutils
49 from kallithea.lib import webutils
49 from kallithea.lib.vcs.backends.base import BaseRepository, EmptyChangeset
50 from kallithea.lib.vcs.backends.base import BaseRepository, EmptyChangeset
50 from kallithea.lib.vcs.exceptions import RepositoryError
51 from kallithea.lib.vcs.exceptions import RepositoryError
51 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, safe_bytes, safe_str # re-export
52 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, safe_bytes, safe_str # re-export
52 from kallithea.lib.vcs.utils.lazy import LazyProperty
53 from kallithea.lib.vcs.utils.lazy import LazyProperty
53
54
54
55
55 try:
56 try:
56 import pwd
57 import pwd
57 except ImportError:
58 except ImportError:
58 pass
59 pass
59
60
60
61
61 # mute pyflakes "imported but unused"
62 # mute pyflakes "imported but unused"
62 assert asbool
63 assert asbool
63 assert aslist
64 assert aslist
64 assert ascii_bytes
65 assert ascii_bytes
65 assert ascii_str
66 assert ascii_str
66 assert safe_bytes
67 assert safe_bytes
67 assert safe_str
68 assert safe_str
68 assert LazyProperty
69 assert LazyProperty
69
70
70
71
71 def convert_line_endings(line, mode):
72 def convert_line_endings(line, mode):
72 """
73 """
73 Converts a given line "line end" according to given mode
74 Converts a given line "line end" according to given mode
74
75
75 Available modes are::
76 Available modes are::
76 0 - Unix
77 0 - Unix
77 1 - Mac
78 1 - Mac
78 2 - DOS
79 2 - DOS
79
80
80 :param line: given line to convert
81 :param line: given line to convert
81 :param mode: mode to convert to
82 :param mode: mode to convert to
82 :rtype: str
83 :rtype: str
83 :return: converted line according to mode
84 :return: converted line according to mode
84 """
85 """
85 if mode == 0:
86 if mode == 0:
86 line = line.replace('\r\n', '\n')
87 line = line.replace('\r\n', '\n')
87 line = line.replace('\r', '\n')
88 line = line.replace('\r', '\n')
88 elif mode == 1:
89 elif mode == 1:
89 line = line.replace('\r\n', '\r')
90 line = line.replace('\r\n', '\r')
90 line = line.replace('\n', '\r')
91 line = line.replace('\n', '\r')
91 elif mode == 2:
92 elif mode == 2:
92 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
93 line = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", line)
93 return line
94 return line
94
95
95
96
96 def detect_mode(line, default):
97 def detect_mode(line, default):
97 """
98 """
98 Detects line break for given line, if line break couldn't be found
99 Detects line break for given line, if line break couldn't be found
99 given default value is returned
100 given default value is returned
100
101
101 :param line: str line
102 :param line: str line
102 :param default: default
103 :param default: default
103 :rtype: int
104 :rtype: int
104 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
105 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
105 """
106 """
106 if line.endswith('\r\n'):
107 if line.endswith('\r\n'):
107 return 2
108 return 2
108 elif line.endswith('\n'):
109 elif line.endswith('\n'):
109 return 0
110 return 0
110 elif line.endswith('\r'):
111 elif line.endswith('\r'):
111 return 1
112 return 1
112 else:
113 else:
113 return default
114 return default
114
115
115
116
116 def generate_api_key():
117 def generate_api_key():
117 """
118 """
118 Generates a random (presumably unique) API key.
119 Generates a random (presumably unique) API key.
119
120
120 This value is used in URLs and "Bearer" HTTP Authorization headers,
121 This value is used in URLs and "Bearer" HTTP Authorization headers,
121 which in practice means it should only contain URL-safe characters
122 which in practice means it should only contain URL-safe characters
122 (RFC 3986):
123 (RFC 3986):
123
124
124 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
125 unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
125 """
126 """
126 # Hexadecimal certainly qualifies as URL-safe.
127 # Hexadecimal certainly qualifies as URL-safe.
127 return ascii_str(binascii.hexlify(os.urandom(20)))
128 return ascii_str(binascii.hexlify(os.urandom(20)))
128
129
129
130
130 def safe_int(val, default=None):
131 def safe_int(val, default=None):
131 """
132 """
132 Returns int() of val if val is not convertable to int use default
133 Returns int() of val if val is not convertable to int use default
133 instead
134 instead
134
135
135 :param val:
136 :param val:
136 :param default:
137 :param default:
137 """
138 """
138 try:
139 try:
139 val = int(val)
140 val = int(val)
140 except (ValueError, TypeError):
141 except (ValueError, TypeError):
141 val = default
142 val = default
142 return val
143 return val
143
144
144
145
145 def remove_suffix(s, suffix):
146 def remove_suffix(s, suffix):
146 if s.endswith(suffix):
147 if s.endswith(suffix):
147 s = s[:-1 * len(suffix)]
148 s = s[:-1 * len(suffix)]
148 return s
149 return s
149
150
150
151
151 def remove_prefix(s, prefix):
152 def remove_prefix(s, prefix):
152 if s.startswith(prefix):
153 if s.startswith(prefix):
153 s = s[len(prefix):]
154 s = s[len(prefix):]
154 return s
155 return s
155
156
156
157
157 def shorter(s, size=20, firstline=False, postfix='...'):
158 def shorter(s, size=20, firstline=False, postfix='...'):
158 """Truncate s to size, including the postfix string if truncating.
159 """Truncate s to size, including the postfix string if truncating.
159 If firstline, truncate at newline.
160 If firstline, truncate at newline.
160 """
161 """
161 if firstline:
162 if firstline:
162 s = s.split('\n', 1)[0].rstrip()
163 s = s.split('\n', 1)[0].rstrip()
163 if len(s) > size:
164 if len(s) > size:
164 return s[:size - len(postfix)] + postfix
165 return s[:size - len(postfix)] + postfix
165 return s
166 return s
166
167
167
168
168 def age(prevdate, show_short_version=False, now=None):
169 def age(prevdate, show_short_version=False, now=None):
169 """
170 """
170 turns a datetime into an age string.
171 turns a datetime into an age string.
171 If show_short_version is True, then it will generate a not so accurate but shorter string,
172 If show_short_version is True, then it will generate a not so accurate but shorter string,
172 example: 2days ago, instead of 2 days and 23 hours ago.
173 example: 2days ago, instead of 2 days and 23 hours ago.
173
174
174 :param prevdate: datetime object
175 :param prevdate: datetime object
175 :param show_short_version: if it should approximate the date and return a shorter string
176 :param show_short_version: if it should approximate the date and return a shorter string
176 :rtype: str
177 :rtype: str
177 :returns: str words describing age
178 :returns: str words describing age
178 """
179 """
179 now = now or datetime.datetime.now()
180 now = now or datetime.datetime.now()
180 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
181 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
181 deltas = {}
182 deltas = {}
182 future = False
183 future = False
183
184
184 if prevdate > now:
185 if prevdate > now:
185 now, prevdate = prevdate, now
186 now, prevdate = prevdate, now
186 future = True
187 future = True
187 if future:
188 if future:
188 prevdate = prevdate.replace(microsecond=0)
189 prevdate = prevdate.replace(microsecond=0)
189 # Get date parts deltas
190 # Get date parts deltas
190 for part in order:
191 for part in order:
191 d = relativedelta.relativedelta(now, prevdate)
192 d = relativedelta.relativedelta(now, prevdate)
192 deltas[part] = getattr(d, part + 's')
193 deltas[part] = getattr(d, part + 's')
193
194
194 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
195 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
195 # not 1 hour, -59 minutes and -59 seconds)
196 # not 1 hour, -59 minutes and -59 seconds)
196 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
197 for num, length in [(5, 60), (4, 60), (3, 24)]: # seconds, minutes, hours
197 part = order[num]
198 part = order[num]
198 carry_part = order[num - 1]
199 carry_part = order[num - 1]
199
200
200 if deltas[part] < 0:
201 if deltas[part] < 0:
201 deltas[part] += length
202 deltas[part] += length
202 deltas[carry_part] -= 1
203 deltas[carry_part] -= 1
203
204
204 # Same thing for days except that the increment depends on the (variable)
205 # Same thing for days except that the increment depends on the (variable)
205 # number of days in the month
206 # number of days in the month
206 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
207 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
207 if deltas['day'] < 0:
208 if deltas['day'] < 0:
208 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
209 if prevdate.month == 2 and (prevdate.year % 4 == 0 and
209 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)
210 (prevdate.year % 100 != 0 or prevdate.year % 400 == 0)
210 ):
211 ):
211 deltas['day'] += 29
212 deltas['day'] += 29
212 else:
213 else:
213 deltas['day'] += month_lengths[prevdate.month - 1]
214 deltas['day'] += month_lengths[prevdate.month - 1]
214
215
215 deltas['month'] -= 1
216 deltas['month'] -= 1
216
217
217 if deltas['month'] < 0:
218 if deltas['month'] < 0:
218 deltas['month'] += 12
219 deltas['month'] += 12
219 deltas['year'] -= 1
220 deltas['year'] -= 1
220
221
221 # In short version, we want nicer handling of ages of more than a year
222 # In short version, we want nicer handling of ages of more than a year
222 if show_short_version:
223 if show_short_version:
223 if deltas['year'] == 1:
224 if deltas['year'] == 1:
224 # ages between 1 and 2 years: show as months
225 # ages between 1 and 2 years: show as months
225 deltas['month'] += 12
226 deltas['month'] += 12
226 deltas['year'] = 0
227 deltas['year'] = 0
227 if deltas['year'] >= 2:
228 if deltas['year'] >= 2:
228 # ages 2+ years: round
229 # ages 2+ years: round
229 if deltas['month'] > 6:
230 if deltas['month'] > 6:
230 deltas['year'] += 1
231 deltas['year'] += 1
231 deltas['month'] = 0
232 deltas['month'] = 0
232
233
233 # Format the result
234 # Format the result
234 fmt_funcs = {
235 fmt_funcs = {
235 'year': lambda d: ungettext('%d year', '%d years', d) % d,
236 'year': lambda d: ungettext('%d year', '%d years', d) % d,
236 'month': lambda d: ungettext('%d month', '%d months', d) % d,
237 'month': lambda d: ungettext('%d month', '%d months', d) % d,
237 'day': lambda d: ungettext('%d day', '%d days', d) % d,
238 'day': lambda d: ungettext('%d day', '%d days', d) % d,
238 'hour': lambda d: ungettext('%d hour', '%d hours', d) % d,
239 'hour': lambda d: ungettext('%d hour', '%d hours', d) % d,
239 'minute': lambda d: ungettext('%d minute', '%d minutes', d) % d,
240 'minute': lambda d: ungettext('%d minute', '%d minutes', d) % d,
240 'second': lambda d: ungettext('%d second', '%d seconds', d) % d,
241 'second': lambda d: ungettext('%d second', '%d seconds', d) % d,
241 }
242 }
242
243
243 for i, part in enumerate(order):
244 for i, part in enumerate(order):
244 value = deltas[part]
245 value = deltas[part]
245 if value == 0:
246 if value == 0:
246 continue
247 continue
247
248
248 if i < 5:
249 if i < 5:
249 sub_part = order[i + 1]
250 sub_part = order[i + 1]
250 sub_value = deltas[sub_part]
251 sub_value = deltas[sub_part]
251 else:
252 else:
252 sub_value = 0
253 sub_value = 0
253
254
254 if sub_value == 0 or show_short_version:
255 if sub_value == 0 or show_short_version:
255 if future:
256 if future:
256 return _('in %s') % fmt_funcs[part](value)
257 return _('in %s') % fmt_funcs[part](value)
257 else:
258 else:
258 return _('%s ago') % fmt_funcs[part](value)
259 return _('%s ago') % fmt_funcs[part](value)
259 if future:
260 if future:
260 return _('in %s and %s') % (fmt_funcs[part](value),
261 return _('in %s and %s') % (fmt_funcs[part](value),
261 fmt_funcs[sub_part](sub_value))
262 fmt_funcs[sub_part](sub_value))
262 else:
263 else:
263 return _('%s and %s ago') % (fmt_funcs[part](value),
264 return _('%s and %s ago') % (fmt_funcs[part](value),
264 fmt_funcs[sub_part](sub_value))
265 fmt_funcs[sub_part](sub_value))
265
266
266 return _('just now')
267 return _('just now')
267
268
268
269
269 def fmt_date(date):
270 def fmt_date(date):
270 if date:
271 if date:
271 return date.strftime("%Y-%m-%d %H:%M:%S")
272 return date.strftime("%Y-%m-%d %H:%M:%S")
272 return ""
273 return ""
273
274
274
275
275 def uri_filter(uri):
276 def uri_filter(uri):
276 """
277 """
277 Removes user:password from given url string
278 Removes user:password from given url string
278
279
279 :param uri:
280 :param uri:
280 :rtype: str
281 :rtype: str
281 :returns: filtered list of strings
282 :returns: filtered list of strings
282 """
283 """
283 if not uri:
284 if not uri:
284 return []
285 return []
285
286
286 proto = ''
287 proto = ''
287
288
288 for pat in ('https://', 'http://', 'git://'):
289 for pat in ('https://', 'http://', 'git://'):
289 if uri.startswith(pat):
290 if uri.startswith(pat):
290 uri = uri[len(pat):]
291 uri = uri[len(pat):]
291 proto = pat
292 proto = pat
292 break
293 break
293
294
294 # remove passwords and username
295 # remove passwords and username
295 uri = uri[uri.find('@') + 1:]
296 uri = uri[uri.find('@') + 1:]
296
297
297 # get the port
298 # get the port
298 cred_pos = uri.find(':')
299 cred_pos = uri.find(':')
299 if cred_pos == -1:
300 if cred_pos == -1:
300 host, port = uri, None
301 host, port = uri, None
301 else:
302 else:
302 host, port = uri[:cred_pos], uri[cred_pos + 1:]
303 host, port = uri[:cred_pos], uri[cred_pos + 1:]
303
304
304 return [_f for _f in [proto, host, port] if _f]
305 return [_f for _f in [proto, host, port] if _f]
305
306
306
307
307 def credentials_filter(uri):
308 def credentials_filter(uri):
308 """
309 """
309 Returns a url with removed credentials
310 Returns a url with removed credentials
310
311
311 :param uri:
312 :param uri:
312 """
313 """
313
314
314 uri = uri_filter(uri)
315 uri = uri_filter(uri)
315 # check if we have port
316 # check if we have port
316 if len(uri) > 2 and uri[2]:
317 if len(uri) > 2 and uri[2]:
317 uri[2] = ':' + uri[2]
318 uri[2] = ':' + uri[2]
318
319
319 return ''.join(uri)
320 return ''.join(uri)
320
321
321
322
322 def get_clone_url(clone_uri_tmpl, prefix_url, repo_name, repo_id, username=None):
323 def get_clone_url(clone_uri_tmpl, prefix_url, repo_name, repo_id, username=None):
323 parsed_url = urlobject.URLObject(prefix_url)
324 parsed_url = urlobject.URLObject(prefix_url)
324 prefix = urllib.parse.unquote(parsed_url.path.rstrip('/'))
325 prefix = urllib.parse.unquote(parsed_url.path.rstrip('/'))
325 try:
326 try:
326 system_user = pwd.getpwuid(os.getuid()).pw_name
327 system_user = pwd.getpwuid(os.getuid()).pw_name
327 except NameError: # TODO: support all systems - especially Windows
328 except NameError: # TODO: support all systems - especially Windows
328 system_user = 'kallithea' # hardcoded default value ...
329 system_user = 'kallithea' # hardcoded default value ...
329 args = {
330 args = {
330 'scheme': parsed_url.scheme,
331 'scheme': parsed_url.scheme,
331 'user': urllib.parse.quote(username or ''),
332 'user': urllib.parse.quote(username or ''),
332 'netloc': parsed_url.netloc + prefix, # like "hostname:port/prefix" (with optional ":port" and "/prefix")
333 'netloc': parsed_url.netloc + prefix, # like "hostname:port/prefix" (with optional ":port" and "/prefix")
333 'prefix': prefix, # undocumented, empty or starting with /
334 'prefix': prefix, # undocumented, empty or starting with /
334 'repo': repo_name,
335 'repo': repo_name,
335 'repoid': str(repo_id),
336 'repoid': str(repo_id),
336 'system_user': system_user,
337 'system_user': system_user,
337 'hostname': parsed_url.hostname,
338 'hostname': parsed_url.hostname,
338 }
339 }
339 url = re.sub('{([^{}]+)}', lambda m: args.get(m.group(1), m.group(0)), clone_uri_tmpl)
340 url = re.sub('{([^{}]+)}', lambda m: args.get(m.group(1), m.group(0)), clone_uri_tmpl)
340
341
341 # remove leading @ sign if it's present. Case of empty user
342 # remove leading @ sign if it's present. Case of empty user
342 url_obj = urlobject.URLObject(url)
343 url_obj = urlobject.URLObject(url)
343 if not url_obj.username:
344 if not url_obj.username:
344 url_obj = url_obj.with_username(None)
345 url_obj = url_obj.with_username(None)
345
346
346 return str(url_obj)
347 return str(url_obj)
347
348
348
349
349 def short_ref_name(ref_type, ref_name):
350 def short_ref_name(ref_type, ref_name):
350 """Return short description of PR ref - revs will be truncated"""
351 """Return short description of PR ref - revs will be truncated"""
351 if ref_type == 'rev':
352 if ref_type == 'rev':
352 return ref_name[:12]
353 return ref_name[:12]
353 return ref_name
354 return ref_name
354
355
355
356
356 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
357 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
357 """
358 """
358 Return full markup for a PR ref to changeset_home for a changeset.
359 Return full markup for a PR ref to changeset_home for a changeset.
359 If ref_type is 'branch', it will link to changelog.
360 If ref_type is 'branch', it will link to changelog.
360 ref_name is shortened if ref_type is 'rev'.
361 ref_name is shortened if ref_type is 'rev'.
361 if rev is specified, show it too, explicitly linking to that revision.
362 if rev is specified, show it too, explicitly linking to that revision.
362 """
363 """
363 txt = short_ref_name(ref_type, ref_name)
364 txt = short_ref_name(ref_type, ref_name)
364 if ref_type == 'branch':
365 if ref_type == 'branch':
365 u = webutils.url('changelog_home', repo_name=repo_name, branch=ref_name)
366 u = webutils.url('changelog_home', repo_name=repo_name, branch=ref_name)
366 else:
367 else:
367 u = webutils.url('changeset_home', repo_name=repo_name, revision=ref_name)
368 u = webutils.url('changeset_home', repo_name=repo_name, revision=ref_name)
368 l = webutils.link_to(repo_name + '#' + txt, u)
369 l = webutils.link_to(repo_name + '#' + txt, u)
369 if rev and ref_type != 'rev':
370 if rev and ref_type != 'rev':
370 l = webutils.literal('%s (%s)' % (l, webutils.link_to(rev[:12], webutils.url('changeset_home', repo_name=repo_name, revision=rev))))
371 l = webutils.literal('%s (%s)' % (l, webutils.link_to(rev[:12], webutils.url('changeset_home', repo_name=repo_name, revision=rev))))
371 return l
372 return l
372
373
373
374
374 def get_changeset_safe(repo, rev):
375 def get_changeset_safe(repo, rev):
375 """
376 """
376 Safe version of get_changeset if this changeset doesn't exists for a
377 Safe version of get_changeset if this changeset doesn't exists for a
377 repo it returns a Dummy one instead
378 repo it returns a Dummy one instead
378
379
379 :param repo:
380 :param repo:
380 :param rev:
381 :param rev:
381 """
382 """
382 if not isinstance(repo, BaseRepository):
383 if not isinstance(repo, BaseRepository):
383 raise Exception('You must pass an Repository '
384 raise Exception('You must pass an Repository '
384 'object as first argument got %s' % type(repo))
385 'object as first argument got %s' % type(repo))
385
386
386 try:
387 try:
387 cs = repo.get_changeset(rev)
388 cs = repo.get_changeset(rev)
388 except (RepositoryError, LookupError):
389 except (RepositoryError, LookupError):
389 cs = EmptyChangeset(requested_revision=rev)
390 cs = EmptyChangeset(requested_revision=rev)
390 return cs
391 return cs
391
392
392
393
393 def datetime_to_time(dt):
394 def datetime_to_time(dt):
394 if dt:
395 if dt:
395 return time.mktime(dt.timetuple())
396 return time.mktime(dt.timetuple())
396
397
397
398
398 def time_to_datetime(tm):
399 def time_to_datetime(tm):
399 if tm:
400 if tm:
400 if isinstance(tm, str):
401 if isinstance(tm, str):
401 try:
402 try:
402 tm = float(tm)
403 tm = float(tm)
403 except ValueError:
404 except ValueError:
404 return
405 return
405 return datetime.datetime.fromtimestamp(tm)
406 return datetime.datetime.fromtimestamp(tm)
406
407
407
408
408 # Must match regexp in kallithea/public/js/base.js MentionsAutoComplete()
409 # Must match regexp in kallithea/public/js/base.js MentionsAutoComplete()
409 # Check char before @ - it must not look like we are in an email addresses.
410 # Check char before @ - it must not look like we are in an email addresses.
410 # Matching is greedy so we don't have to look beyond the end.
411 # Matching is greedy so we don't have to look beyond the end.
411 MENTIONS_REGEX = re.compile(r'(?:^|(?<=[^a-zA-Z0-9]))@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])')
412 MENTIONS_REGEX = re.compile(r'(?:^|(?<=[^a-zA-Z0-9]))@([a-zA-Z0-9][-_.a-zA-Z0-9]*[a-zA-Z0-9])')
412
413
413
414
414 def extract_mentioned_usernames(text):
415 def extract_mentioned_usernames(text):
415 r"""
416 r"""
416 Returns list of (possible) usernames @mentioned in given text.
417 Returns list of (possible) usernames @mentioned in given text.
417
418
418 >>> extract_mentioned_usernames('@1-2.a_X,@1234 not@not @ddd@not @n @ee @ff @gg, @gg;@hh @n\n@zz,')
419 >>> extract_mentioned_usernames('@1-2.a_X,@1234 not@not @ddd@not @n @ee @ff @gg, @gg;@hh @n\n@zz,')
419 ['1-2.a_X', '1234', 'ddd', 'ee', 'ff', 'gg', 'gg', 'hh', 'zz']
420 ['1-2.a_X', '1234', 'ddd', 'ee', 'ff', 'gg', 'gg', 'hh', 'zz']
420 """
421 """
421 return MENTIONS_REGEX.findall(text)
422 return MENTIONS_REGEX.findall(text)
422
423
423
424
424 class AttributeDict(dict):
425 class AttributeDict(dict):
425 def __getattr__(self, attr):
426 def __getattr__(self, attr):
426 return self.get(attr, None)
427 return self.get(attr, None)
427 __setattr__ = dict.__setitem__
428 __setattr__ = dict.__setitem__
428 __delattr__ = dict.__delitem__
429 __delattr__ = dict.__delitem__
429
430
430
431
431 def obfuscate_url_pw(engine):
432 def obfuscate_url_pw(engine):
432 try:
433 try:
433 _url = sa_url.make_url(engine or '')
434 _url = sa_url.make_url(engine or '')
434 except ArgumentError:
435 except ArgumentError:
435 return engine
436 return engine
436 if _url.password:
437 if _url.password:
437 _url.password = 'XXXXX'
438 _url.password = 'XXXXX'
438 return str(_url)
439 return str(_url)
439
440
440
441
441 class HookEnvironmentError(Exception): pass
442 class HookEnvironmentError(Exception): pass
442
443
443
444
444 def get_hook_environment():
445 def get_hook_environment():
445 """
446 """
446 Get hook context by deserializing the global KALLITHEA_EXTRAS environment
447 Get hook context by deserializing the global KALLITHEA_EXTRAS environment
447 variable.
448 variable.
448
449
449 Called early in Git out-of-process hooks to get .ini config path so the
450 Called early in Git out-of-process hooks to get .ini config path so the
450 basic environment can be configured properly. Also used in all hooks to get
451 basic environment can be configured properly. Also used in all hooks to get
451 information about the action that triggered it.
452 information about the action that triggered it.
452 """
453 """
453
454
454 try:
455 try:
455 kallithea_extras = os.environ['KALLITHEA_EXTRAS']
456 kallithea_extras = os.environ['KALLITHEA_EXTRAS']
456 except KeyError:
457 except KeyError:
457 raise HookEnvironmentError("Environment variable KALLITHEA_EXTRAS not found")
458 raise HookEnvironmentError("Environment variable KALLITHEA_EXTRAS not found")
458
459
459 extras = json.loads(kallithea_extras)
460 extras = json.loads(kallithea_extras)
460 for k in ['username', 'repository', 'scm', 'action', 'ip', 'config']:
461 for k in ['username', 'repository', 'scm', 'action', 'ip', 'config']:
461 try:
462 try:
462 extras[k]
463 extras[k]
463 except KeyError:
464 except KeyError:
464 raise HookEnvironmentError('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras))
465 raise HookEnvironmentError('Missing key %s in KALLITHEA_EXTRAS %s' % (k, extras))
465
466
466 return AttributeDict(extras)
467 return AttributeDict(extras)
467
468
468
469
469 def set_hook_environment(username, ip_addr, repo_name, repo_alias, action=None):
470 def set_hook_environment(username, ip_addr, repo_name, repo_alias, action=None):
470 """Prepare global context for running hooks by serializing data in the
471 """Prepare global context for running hooks by serializing data in the
471 global KALLITHEA_EXTRAS environment variable.
472 global KALLITHEA_EXTRAS environment variable.
472
473
473 Most importantly, this allow Git hooks to do proper logging and updating of
474 Most importantly, this allow Git hooks to do proper logging and updating of
474 caches after pushes.
475 caches after pushes.
475
476
476 Must always be called before anything with hooks are invoked.
477 Must always be called before anything with hooks are invoked.
477 """
478 """
478 extras = {
479 extras = {
479 'ip': ip_addr, # used in log_push/pull_action action_logger
480 'ip': ip_addr, # used in log_push/pull_action action_logger
480 'username': username,
481 'username': username,
481 'action': action or 'push_local', # used in log_push_action_raw_ids action_logger
482 'action': action or 'push_local', # used in log_push_action_raw_ids action_logger
482 'repository': repo_name,
483 'repository': repo_name,
483 'scm': repo_alias, # used to pick hack in log_push_action_raw_ids
484 'scm': repo_alias, # used to pick hack in log_push_action_raw_ids
484 'config': kallithea.CONFIG['__file__'], # used by git hook to read config
485 'config': kallithea.CONFIG['__file__'], # used by git hook to read config
485 }
486 }
486 os.environ['KALLITHEA_EXTRAS'] = json.dumps(extras)
487 os.environ['KALLITHEA_EXTRAS'] = json.dumps(extras)
487
488
488
489
489 def get_current_authuser():
490 def get_current_authuser():
490 """
491 """
491 Gets kallithea user from threadlocal tmpl_context variable if it's
492 Gets kallithea user from threadlocal tmpl_context variable if it's
492 defined, else returns None.
493 defined, else returns None.
493 """
494 """
494 from tg import tmpl_context
495 try:
495 try:
496 return getattr(tmpl_context, 'authuser', None)
496 return getattr(tmpl_context, 'authuser', None)
497 except TypeError: # No object (name: context) has been registered for this thread
497 except TypeError: # No object (name: context) has been registered for this thread
498 return None
498 return None
499
499
500
500
501 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
501 def urlreadable(s, _cleanstringsub=re.compile('[^-a-zA-Z0-9./]+').sub):
502 return _cleanstringsub('_', s).rstrip('_')
502 return _cleanstringsub('_', s).rstrip('_')
503
503
504
504
505 def recursive_replace(str_, replace=' '):
505 def recursive_replace(str_, replace=' '):
506 """
506 """
507 Recursive replace of given sign to just one instance
507 Recursive replace of given sign to just one instance
508
508
509 :param str_: given string
509 :param str_: given string
510 :param replace: char to find and replace multiple instances
510 :param replace: char to find and replace multiple instances
511
511
512 Examples::
512 Examples::
513 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
513 >>> recursive_replace("Mighty---Mighty-Bo--sstones",'-')
514 'Mighty-Mighty-Bo-sstones'
514 'Mighty-Mighty-Bo-sstones'
515 """
515 """
516
516
517 if str_.find(replace * 2) == -1:
517 if str_.find(replace * 2) == -1:
518 return str_
518 return str_
519 else:
519 else:
520 str_ = str_.replace(replace * 2, replace)
520 str_ = str_.replace(replace * 2, replace)
521 return recursive_replace(str_, replace)
521 return recursive_replace(str_, replace)
522
522
523
523
524 def repo_name_slug(value):
524 def repo_name_slug(value):
525 """
525 """
526 Return slug of name of repository
526 Return slug of name of repository
527 This function is called on each creation/modification
527 This function is called on each creation/modification
528 of repository to prevent bad names in repo
528 of repository to prevent bad names in repo
529 """
529 """
530
530
531 slug = remove_formatting(value)
531 slug = remove_formatting(value)
532 slug = strip_tags(slug)
532 slug = strip_tags(slug)
533
533
534 for c in r"""`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
534 for c in r"""`?=[]\;'"<>,/~!@#$%^&*()+{}|: """:
535 slug = slug.replace(c, '-')
535 slug = slug.replace(c, '-')
536 slug = recursive_replace(slug, '-')
536 slug = recursive_replace(slug, '-')
537 slug = collapse(slug, '-')
537 slug = collapse(slug, '-')
538 return slug
538 return slug
539
539
540
540
541 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
541 def ask_ok(prompt, retries=4, complaint='Yes or no please!'):
542 while True:
542 while True:
543 ok = input(prompt)
543 ok = input(prompt)
544 if ok in ('y', 'ye', 'yes'):
544 if ok in ('y', 'ye', 'yes'):
545 return True
545 return True
546 if ok in ('n', 'no', 'nop', 'nope'):
546 if ok in ('n', 'no', 'nop', 'nope'):
547 return False
547 return False
548 retries = retries - 1
548 retries = retries - 1
549 if retries < 0:
549 if retries < 0:
550 raise IOError
550 raise IOError
551 print(complaint)
551 print(complaint)
@@ -1,196 +1,197 b''
1 import datetime
1 import datetime
2 import posixpath
2 import posixpath
3 import stat
3 import stat
4 import time
4 import time
5
5
6 from dulwich import objects
6 from dulwich import objects
7
7
8 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
8 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
9 from kallithea.lib.vcs.exceptions import RepositoryError
9 from kallithea.lib.vcs.exceptions import RepositoryError
10 from kallithea.lib.vcs.utils import ascii_str, safe_bytes
10 from kallithea.lib.vcs.utils import ascii_str, safe_bytes
11
11
12 from . import repository
13
12
14
13 class GitInMemoryChangeset(BaseInMemoryChangeset):
15 class GitInMemoryChangeset(BaseInMemoryChangeset):
14
16
15 def commit(self, message, author, parents=None, branch=None, date=None,
17 def commit(self, message, author, parents=None, branch=None, date=None,
16 **kwargs):
18 **kwargs):
17 """
19 """
18 Performs in-memory commit (doesn't check workdir in any way) and
20 Performs in-memory commit (doesn't check workdir in any way) and
19 returns newly created ``Changeset``. Updates repository's
21 returns newly created ``Changeset``. Updates repository's
20 ``revisions``.
22 ``revisions``.
21
23
22 :param message: message of the commit
24 :param message: message of the commit
23 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
25 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
24 :param parents: single parent or sequence of parents from which commit
26 :param parents: single parent or sequence of parents from which commit
25 would be derived
27 would be derived
26 :param date: ``datetime.datetime`` instance. Defaults to
28 :param date: ``datetime.datetime`` instance. Defaults to
27 ``datetime.datetime.now()``.
29 ``datetime.datetime.now()``.
28 :param branch: branch name, as string. If none given, default backend's
30 :param branch: branch name, as string. If none given, default backend's
29 branch would be used.
31 branch would be used.
30
32
31 :raises ``CommitError``: if any error occurs while committing
33 :raises ``CommitError``: if any error occurs while committing
32 """
34 """
33 self.check_integrity(parents)
35 self.check_integrity(parents)
34
36
35 from .repository import GitRepository
36 if branch is None:
37 if branch is None:
37 branch = GitRepository.DEFAULT_BRANCH_NAME
38 branch = repository.GitRepository.DEFAULT_BRANCH_NAME
38
39
39 repo = self.repository._repo
40 repo = self.repository._repo
40 object_store = repo.object_store
41 object_store = repo.object_store
41
42
42 ENCODING = b"UTF-8" # TODO: should probably be kept in sync with safe_str/safe_bytes and vcs/conf/settings.py DEFAULT_ENCODINGS
43 ENCODING = b"UTF-8" # TODO: should probably be kept in sync with safe_str/safe_bytes and vcs/conf/settings.py DEFAULT_ENCODINGS
43
44
44 # Create tree and populates it with blobs
45 # Create tree and populates it with blobs
45 commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or \
46 commit_tree = self.parents[0] and repo[self.parents[0]._commit.tree] or \
46 objects.Tree()
47 objects.Tree()
47 for node in self.added + self.changed:
48 for node in self.added + self.changed:
48 # Compute subdirs if needed
49 # Compute subdirs if needed
49 dirpath, nodename = posixpath.split(node.path)
50 dirpath, nodename = posixpath.split(node.path)
50 dirnames = safe_bytes(dirpath).split(b'/') if dirpath else []
51 dirnames = safe_bytes(dirpath).split(b'/') if dirpath else []
51 parent = commit_tree
52 parent = commit_tree
52 ancestors = [('', parent)]
53 ancestors = [('', parent)]
53
54
54 # Tries to dig for the deepest existing tree
55 # Tries to dig for the deepest existing tree
55 while dirnames:
56 while dirnames:
56 curdir = dirnames.pop(0)
57 curdir = dirnames.pop(0)
57 try:
58 try:
58 dir_id = parent[curdir][1]
59 dir_id = parent[curdir][1]
59 except KeyError:
60 except KeyError:
60 # put curdir back into dirnames and stops
61 # put curdir back into dirnames and stops
61 dirnames.insert(0, curdir)
62 dirnames.insert(0, curdir)
62 break
63 break
63 else:
64 else:
64 # If found, updates parent
65 # If found, updates parent
65 parent = self.repository._repo[dir_id]
66 parent = self.repository._repo[dir_id]
66 ancestors.append((curdir, parent))
67 ancestors.append((curdir, parent))
67 # Now parent is deepest existing tree and we need to create subtrees
68 # Now parent is deepest existing tree and we need to create subtrees
68 # for dirnames (in reverse order) [this only applies for nodes from added]
69 # for dirnames (in reverse order) [this only applies for nodes from added]
69 new_trees = []
70 new_trees = []
70
71
71 blob = objects.Blob.from_string(node.content)
72 blob = objects.Blob.from_string(node.content)
72
73
73 node_path = safe_bytes(node.name)
74 node_path = safe_bytes(node.name)
74 if dirnames:
75 if dirnames:
75 # If there are trees which should be created we need to build
76 # If there are trees which should be created we need to build
76 # them now (in reverse order)
77 # them now (in reverse order)
77 reversed_dirnames = list(reversed(dirnames))
78 reversed_dirnames = list(reversed(dirnames))
78 curtree = objects.Tree()
79 curtree = objects.Tree()
79 curtree[node_path] = node.mode, blob.id
80 curtree[node_path] = node.mode, blob.id
80 new_trees.append(curtree)
81 new_trees.append(curtree)
81 for dirname in reversed_dirnames[:-1]:
82 for dirname in reversed_dirnames[:-1]:
82 newtree = objects.Tree()
83 newtree = objects.Tree()
83 #newtree.add(stat.S_IFDIR, dirname, curtree.id)
84 #newtree.add(stat.S_IFDIR, dirname, curtree.id)
84 newtree[dirname] = stat.S_IFDIR, curtree.id
85 newtree[dirname] = stat.S_IFDIR, curtree.id
85 new_trees.append(newtree)
86 new_trees.append(newtree)
86 curtree = newtree
87 curtree = newtree
87 parent[reversed_dirnames[-1]] = stat.S_IFDIR, curtree.id
88 parent[reversed_dirnames[-1]] = stat.S_IFDIR, curtree.id
88 else:
89 else:
89 parent.add(name=node_path, mode=node.mode, hexsha=blob.id)
90 parent.add(name=node_path, mode=node.mode, hexsha=blob.id)
90
91
91 new_trees.append(parent)
92 new_trees.append(parent)
92 # Update ancestors
93 # Update ancestors
93 for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in
94 for parent, tree, path in reversed([(a[1], b[1], b[0]) for a, b in
94 zip(ancestors, ancestors[1:])]
95 zip(ancestors, ancestors[1:])]
95 ):
96 ):
96 parent[path] = stat.S_IFDIR, tree.id
97 parent[path] = stat.S_IFDIR, tree.id
97 object_store.add_object(tree)
98 object_store.add_object(tree)
98
99
99 object_store.add_object(blob)
100 object_store.add_object(blob)
100 for tree in new_trees:
101 for tree in new_trees:
101 object_store.add_object(tree)
102 object_store.add_object(tree)
102 for node in self.removed:
103 for node in self.removed:
103 paths = safe_bytes(node.path).split(b'/')
104 paths = safe_bytes(node.path).split(b'/')
104 tree = commit_tree
105 tree = commit_tree
105 trees = [tree]
106 trees = [tree]
106 # Traverse deep into the forest...
107 # Traverse deep into the forest...
107 for path in paths:
108 for path in paths:
108 try:
109 try:
109 obj = self.repository._repo[tree[path][1]]
110 obj = self.repository._repo[tree[path][1]]
110 if isinstance(obj, objects.Tree):
111 if isinstance(obj, objects.Tree):
111 trees.append(obj)
112 trees.append(obj)
112 tree = obj
113 tree = obj
113 except KeyError:
114 except KeyError:
114 break
115 break
115 # Cut down the blob and all rotten trees on the way back...
116 # Cut down the blob and all rotten trees on the way back...
116 for path, tree in reversed(list(zip(paths, trees))):
117 for path, tree in reversed(list(zip(paths, trees))):
117 del tree[path]
118 del tree[path]
118 if tree:
119 if tree:
119 # This tree still has elements - don't remove it or any
120 # This tree still has elements - don't remove it or any
120 # of it's parents
121 # of it's parents
121 break
122 break
122
123
123 object_store.add_object(commit_tree)
124 object_store.add_object(commit_tree)
124
125
125 # Create commit
126 # Create commit
126 commit = objects.Commit()
127 commit = objects.Commit()
127 commit.tree = commit_tree.id
128 commit.tree = commit_tree.id
128 commit.parents = [p._commit.id for p in self.parents if p]
129 commit.parents = [p._commit.id for p in self.parents if p]
129 commit.author = commit.committer = safe_bytes(author)
130 commit.author = commit.committer = safe_bytes(author)
130 commit.encoding = ENCODING
131 commit.encoding = ENCODING
131 commit.message = safe_bytes(message)
132 commit.message = safe_bytes(message)
132
133
133 # Compute date
134 # Compute date
134 if date is None:
135 if date is None:
135 date = time.time()
136 date = time.time()
136 elif isinstance(date, datetime.datetime):
137 elif isinstance(date, datetime.datetime):
137 date = time.mktime(date.timetuple())
138 date = time.mktime(date.timetuple())
138
139
139 author_time = kwargs.pop('author_time', date)
140 author_time = kwargs.pop('author_time', date)
140 commit.commit_time = int(date)
141 commit.commit_time = int(date)
141 commit.author_time = int(author_time)
142 commit.author_time = int(author_time)
142 tz = time.timezone
143 tz = time.timezone
143 author_tz = kwargs.pop('author_timezone', tz)
144 author_tz = kwargs.pop('author_timezone', tz)
144 commit.commit_timezone = tz
145 commit.commit_timezone = tz
145 commit.author_timezone = author_tz
146 commit.author_timezone = author_tz
146
147
147 object_store.add_object(commit)
148 object_store.add_object(commit)
148
149
149 # Update vcs repository object & recreate dulwich repo
150 # Update vcs repository object & recreate dulwich repo
150 ref = b'refs/heads/%s' % safe_bytes(branch)
151 ref = b'refs/heads/%s' % safe_bytes(branch)
151 repo.refs[ref] = commit.id
152 repo.refs[ref] = commit.id
152 self.repository.revisions.append(ascii_str(commit.id))
153 self.repository.revisions.append(ascii_str(commit.id))
153 # invalidate parsed refs after commit
154 # invalidate parsed refs after commit
154 self.repository._parsed_refs = self.repository._get_parsed_refs()
155 self.repository._parsed_refs = self.repository._get_parsed_refs()
155 tip = self.repository.get_changeset()
156 tip = self.repository.get_changeset()
156 self.reset()
157 self.reset()
157 return tip
158 return tip
158
159
159 def _get_missing_trees(self, path, root_tree):
160 def _get_missing_trees(self, path, root_tree):
160 """
161 """
161 Creates missing ``Tree`` objects for the given path.
162 Creates missing ``Tree`` objects for the given path.
162
163
163 :param path: path given as a string. It may be a path to a file node
164 :param path: path given as a string. It may be a path to a file node
164 (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must
165 (i.e. ``foo/bar/baz.txt``) or directory path - in that case it must
165 end with slash (i.e. ``foo/bar/``).
166 end with slash (i.e. ``foo/bar/``).
166 :param root_tree: ``dulwich.objects.Tree`` object from which we start
167 :param root_tree: ``dulwich.objects.Tree`` object from which we start
167 traversing (should be commit's root tree)
168 traversing (should be commit's root tree)
168 """
169 """
169 dirpath = posixpath.split(path)[0]
170 dirpath = posixpath.split(path)[0]
170 dirs = dirpath.split('/')
171 dirs = dirpath.split('/')
171 if not dirs or dirs == ['']:
172 if not dirs or dirs == ['']:
172 return []
173 return []
173
174
174 def get_tree_for_dir(tree, dirname):
175 def get_tree_for_dir(tree, dirname):
175 for name, mode, id in tree.items():
176 for name, mode, id in tree.items():
176 if name == dirname:
177 if name == dirname:
177 obj = self.repository._repo[id]
178 obj = self.repository._repo[id]
178 if isinstance(obj, objects.Tree):
179 if isinstance(obj, objects.Tree):
179 return obj
180 return obj
180 else:
181 else:
181 raise RepositoryError("Cannot create directory %s "
182 raise RepositoryError("Cannot create directory %s "
182 "at tree %s as path is occupied and is not a "
183 "at tree %s as path is occupied and is not a "
183 "Tree" % (dirname, tree))
184 "Tree" % (dirname, tree))
184 return None
185 return None
185
186
186 trees = []
187 trees = []
187 parent = root_tree
188 parent = root_tree
188 for dirname in dirs:
189 for dirname in dirs:
189 tree = get_tree_for_dir(parent, dirname)
190 tree = get_tree_for_dir(parent, dirname)
190 if tree is None:
191 if tree is None:
191 tree = objects.Tree()
192 tree = objects.Tree()
192 parent.add(stat.S_IFDIR, dirname, tree.id)
193 parent.add(stat.S_IFDIR, dirname, tree.id)
193 parent = tree
194 parent = tree
194 # Always append tree
195 # Always append tree
195 trees.append(tree)
196 trees.append(tree)
196 return trees
197 return trees
@@ -1,778 +1,776 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.git.repository
3 vcs.backends.git.repository
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Git repository implementation.
6 Git repository implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import errno
12 import errno
13 import logging
13 import logging
14 import os
14 import os
15 import re
15 import re
16 import time
16 import time
17 import urllib.error
17 import urllib.error
18 import urllib.parse
18 import urllib.parse
19 import urllib.request
19 import urllib.request
20 from collections import OrderedDict
20 from collections import OrderedDict
21
21
22 import mercurial.util # import url as hg_url
22 import mercurial.util # import url as hg_url
23 from dulwich.client import SubprocessGitClient
23 from dulwich.client import SubprocessGitClient
24 from dulwich.config import ConfigFile
24 from dulwich.config import ConfigFile
25 from dulwich.objects import Tag
25 from dulwich.objects import Tag
26 from dulwich.repo import NotGitRepository, Repo
26 from dulwich.repo import NotGitRepository, Repo
27 from dulwich.server import update_server_info
27 from dulwich.server import update_server_info
28
28
29 from kallithea.lib.vcs import subprocessio
29 from kallithea.lib.vcs import subprocessio
30 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
30 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
31 from kallithea.lib.vcs.conf import settings
31 from kallithea.lib.vcs.conf import settings
32 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
32 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
33 TagDoesNotExistError)
33 TagDoesNotExistError)
34 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, makedate, safe_bytes, safe_str
34 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, date_fromtimestamp, makedate, safe_bytes, safe_str
35 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
35 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
36 from kallithea.lib.vcs.utils.lazy import LazyProperty
36 from kallithea.lib.vcs.utils.lazy import LazyProperty
37 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
37 from kallithea.lib.vcs.utils.paths import abspath, get_user_home
38
38
39 from .changeset import GitChangeset
39 from . import changeset, inmemory, workdir
40 from .inmemory import GitInMemoryChangeset
41 from .workdir import GitWorkdir
42
40
43
41
44 SHA_PATTERN = re.compile(r'^([0-9a-fA-F]{12}|[0-9a-fA-F]{40})$')
42 SHA_PATTERN = re.compile(r'^([0-9a-fA-F]{12}|[0-9a-fA-F]{40})$')
45
43
46 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
47
45
48
46
49 class GitRepository(BaseRepository):
47 class GitRepository(BaseRepository):
50 """
48 """
51 Git repository backend.
49 Git repository backend.
52 """
50 """
53 DEFAULT_BRANCH_NAME = 'master'
51 DEFAULT_BRANCH_NAME = 'master'
54 scm = 'git'
52 scm = 'git'
55
53
56 def __init__(self, repo_path, create=False, src_url=None,
54 def __init__(self, repo_path, create=False, src_url=None,
57 update_after_clone=False, bare=False):
55 update_after_clone=False, bare=False):
58
56
59 self.path = abspath(repo_path)
57 self.path = abspath(repo_path)
60 self.repo = self._get_repo(create, src_url, update_after_clone, bare)
58 self.repo = self._get_repo(create, src_url, update_after_clone, bare)
61 self.bare = self.repo.bare
59 self.bare = self.repo.bare
62
60
63 @property
61 @property
64 def _config_files(self):
62 def _config_files(self):
65 return [
63 return [
66 self.bare and abspath(self.path, 'config')
64 self.bare and abspath(self.path, 'config')
67 or abspath(self.path, '.git', 'config'),
65 or abspath(self.path, '.git', 'config'),
68 abspath(get_user_home(), '.gitconfig'),
66 abspath(get_user_home(), '.gitconfig'),
69 ]
67 ]
70
68
71 @property
69 @property
72 def _repo(self):
70 def _repo(self):
73 return self.repo
71 return self.repo
74
72
75 @property
73 @property
76 def head(self):
74 def head(self):
77 try:
75 try:
78 return self._repo.head()
76 return self._repo.head()
79 except KeyError:
77 except KeyError:
80 return None
78 return None
81
79
82 @property
80 @property
83 def _empty(self):
81 def _empty(self):
84 """
82 """
85 Checks if repository is empty ie. without any changesets
83 Checks if repository is empty ie. without any changesets
86 """
84 """
87
85
88 try:
86 try:
89 self.revisions[0]
87 self.revisions[0]
90 except (KeyError, IndexError):
88 except (KeyError, IndexError):
91 return True
89 return True
92 return False
90 return False
93
91
94 @LazyProperty
92 @LazyProperty
95 def revisions(self):
93 def revisions(self):
96 """
94 """
97 Returns list of revisions' ids, in ascending order. Being lazy
95 Returns list of revisions' ids, in ascending order. Being lazy
98 attribute allows external tools to inject shas from cache.
96 attribute allows external tools to inject shas from cache.
99 """
97 """
100 return self._get_all_revisions()
98 return self._get_all_revisions()
101
99
102 @classmethod
100 @classmethod
103 def _run_git_command(cls, cmd, cwd=None):
101 def _run_git_command(cls, cmd, cwd=None):
104 """
102 """
105 Runs given ``cmd`` as git command and returns output bytes in a tuple
103 Runs given ``cmd`` as git command and returns output bytes in a tuple
106 (stdout, stderr) ... or raise RepositoryError.
104 (stdout, stderr) ... or raise RepositoryError.
107
105
108 :param cmd: git command to be executed
106 :param cmd: git command to be executed
109 :param cwd: passed directly to subprocess
107 :param cwd: passed directly to subprocess
110 """
108 """
111 # need to clean fix GIT_DIR !
109 # need to clean fix GIT_DIR !
112 gitenv = dict(os.environ)
110 gitenv = dict(os.environ)
113 gitenv.pop('GIT_DIR', None)
111 gitenv.pop('GIT_DIR', None)
114 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
112 gitenv['GIT_CONFIG_NOGLOBAL'] = '1'
115
113
116 assert isinstance(cmd, list), cmd
114 assert isinstance(cmd, list), cmd
117 cmd = [settings.GIT_EXECUTABLE_PATH, '-c', 'core.quotepath=false'] + cmd
115 cmd = [settings.GIT_EXECUTABLE_PATH, '-c', 'core.quotepath=false'] + cmd
118 try:
116 try:
119 p = subprocessio.SubprocessIOChunker(cmd, cwd=cwd, env=gitenv, shell=False)
117 p = subprocessio.SubprocessIOChunker(cmd, cwd=cwd, env=gitenv, shell=False)
120 except (EnvironmentError, OSError) as err:
118 except (EnvironmentError, OSError) as err:
121 # output from the failing process is in str(EnvironmentError)
119 # output from the failing process is in str(EnvironmentError)
122 msg = ("Couldn't run git command %s.\n"
120 msg = ("Couldn't run git command %s.\n"
123 "Subprocess failed with '%s': %s\n" %
121 "Subprocess failed with '%s': %s\n" %
124 (cmd, type(err).__name__, err)
122 (cmd, type(err).__name__, err)
125 ).strip()
123 ).strip()
126 log.error(msg)
124 log.error(msg)
127 raise RepositoryError(msg)
125 raise RepositoryError(msg)
128
126
129 try:
127 try:
130 stdout = b''.join(p.output)
128 stdout = b''.join(p.output)
131 stderr = b''.join(p.error)
129 stderr = b''.join(p.error)
132 finally:
130 finally:
133 p.close()
131 p.close()
134 # TODO: introduce option to make commands fail if they have any stderr output?
132 # TODO: introduce option to make commands fail if they have any stderr output?
135 if stderr:
133 if stderr:
136 log.debug('stderr from %s:\n%s', cmd, stderr)
134 log.debug('stderr from %s:\n%s', cmd, stderr)
137 else:
135 else:
138 log.debug('stderr from %s: None', cmd)
136 log.debug('stderr from %s: None', cmd)
139 return stdout, stderr
137 return stdout, stderr
140
138
141 def run_git_command(self, cmd):
139 def run_git_command(self, cmd):
142 """
140 """
143 Runs given ``cmd`` as git command with cwd set to current repo.
141 Runs given ``cmd`` as git command with cwd set to current repo.
144 Returns stdout as unicode str ... or raise RepositoryError.
142 Returns stdout as unicode str ... or raise RepositoryError.
145 """
143 """
146 cwd = None
144 cwd = None
147 if os.path.isdir(self.path):
145 if os.path.isdir(self.path):
148 cwd = self.path
146 cwd = self.path
149 stdout, _stderr = self._run_git_command(cmd, cwd=cwd)
147 stdout, _stderr = self._run_git_command(cmd, cwd=cwd)
150 return safe_str(stdout)
148 return safe_str(stdout)
151
149
152 @classmethod
150 @classmethod
153 def _check_url(cls, url):
151 def _check_url(cls, url):
154 """
152 """
155 Function will check given url and try to verify if it's a valid
153 Function will check given url and try to verify if it's a valid
156 link. Sometimes it may happened that git will issue basic
154 link. Sometimes it may happened that git will issue basic
157 auth request that can cause whole API to hang when used from python
155 auth request that can cause whole API to hang when used from python
158 or other external calls.
156 or other external calls.
159
157
160 On failures it'll raise urllib2.HTTPError, exception is also thrown
158 On failures it'll raise urllib2.HTTPError, exception is also thrown
161 when the return code is non 200
159 when the return code is non 200
162 """
160 """
163 # check first if it's not an local url
161 # check first if it's not an local url
164 if os.path.isdir(url) or url.startswith('file:'):
162 if os.path.isdir(url) or url.startswith('file:'):
165 return True
163 return True
166
164
167 if url.startswith('git://'):
165 if url.startswith('git://'):
168 return True
166 return True
169
167
170 if '+' in url[:url.find('://')]:
168 if '+' in url[:url.find('://')]:
171 url = url[url.find('+') + 1:]
169 url = url[url.find('+') + 1:]
172
170
173 url_obj = mercurial.util.url(safe_bytes(url))
171 url_obj = mercurial.util.url(safe_bytes(url))
174 test_uri, handlers = get_urllib_request_handlers(url_obj)
172 test_uri, handlers = get_urllib_request_handlers(url_obj)
175 if not test_uri.endswith(b'info/refs'):
173 if not test_uri.endswith(b'info/refs'):
176 test_uri = test_uri.rstrip(b'/') + b'/info/refs'
174 test_uri = test_uri.rstrip(b'/') + b'/info/refs'
177
175
178 url_obj.passwd = b'*****'
176 url_obj.passwd = b'*****'
179 cleaned_uri = str(url_obj)
177 cleaned_uri = str(url_obj)
180
178
181 o = urllib.request.build_opener(*handlers)
179 o = urllib.request.build_opener(*handlers)
182 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
180 o.addheaders = [('User-Agent', 'git/1.7.8.0')] # fake some git
183
181
184 req = urllib.request.Request(
182 req = urllib.request.Request(
185 "%s?%s" % (
183 "%s?%s" % (
186 safe_str(test_uri),
184 safe_str(test_uri),
187 urllib.parse.urlencode({"service": 'git-upload-pack'})
185 urllib.parse.urlencode({"service": 'git-upload-pack'})
188 ))
186 ))
189
187
190 try:
188 try:
191 resp = o.open(req)
189 resp = o.open(req)
192 if resp.code != 200:
190 if resp.code != 200:
193 raise Exception('Return Code is not 200')
191 raise Exception('Return Code is not 200')
194 except Exception as e:
192 except Exception as e:
195 # means it cannot be cloned
193 # means it cannot be cloned
196 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
194 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
197
195
198 # now detect if it's proper git repo
196 # now detect if it's proper git repo
199 gitdata = resp.read()
197 gitdata = resp.read()
200 if b'service=git-upload-pack' not in gitdata:
198 if b'service=git-upload-pack' not in gitdata:
201 raise urllib.error.URLError(
199 raise urllib.error.URLError(
202 "url [%s] does not look like an git" % cleaned_uri)
200 "url [%s] does not look like an git" % cleaned_uri)
203
201
204 return True
202 return True
205
203
206 def _get_repo(self, create, src_url=None, update_after_clone=False,
204 def _get_repo(self, create, src_url=None, update_after_clone=False,
207 bare=False):
205 bare=False):
208 if create and os.path.exists(self.path):
206 if create and os.path.exists(self.path):
209 raise RepositoryError("Location already exist")
207 raise RepositoryError("Location already exist")
210 if src_url and not create:
208 if src_url and not create:
211 raise RepositoryError("Create should be set to True if src_url is "
209 raise RepositoryError("Create should be set to True if src_url is "
212 "given (clone operation creates repository)")
210 "given (clone operation creates repository)")
213 try:
211 try:
214 if create and src_url:
212 if create and src_url:
215 GitRepository._check_url(src_url)
213 GitRepository._check_url(src_url)
216 self.clone(src_url, update_after_clone, bare)
214 self.clone(src_url, update_after_clone, bare)
217 return Repo(self.path)
215 return Repo(self.path)
218 elif create:
216 elif create:
219 os.makedirs(self.path)
217 os.makedirs(self.path)
220 if bare:
218 if bare:
221 return Repo.init_bare(self.path)
219 return Repo.init_bare(self.path)
222 else:
220 else:
223 return Repo.init(self.path)
221 return Repo.init(self.path)
224 else:
222 else:
225 return Repo(self.path)
223 return Repo(self.path)
226 except (NotGitRepository, OSError) as err:
224 except (NotGitRepository, OSError) as err:
227 raise RepositoryError(err)
225 raise RepositoryError(err)
228
226
229 def _get_all_revisions(self):
227 def _get_all_revisions(self):
230 # we must check if this repo is not empty, since later command
228 # we must check if this repo is not empty, since later command
231 # fails if it is. And it's cheaper to ask than throw the subprocess
229 # fails if it is. And it's cheaper to ask than throw the subprocess
232 # errors
230 # errors
233 try:
231 try:
234 self._repo.head()
232 self._repo.head()
235 except KeyError:
233 except KeyError:
236 return []
234 return []
237
235
238 rev_filter = settings.GIT_REV_FILTER
236 rev_filter = settings.GIT_REV_FILTER
239 cmd = ['rev-list', rev_filter, '--reverse', '--date-order']
237 cmd = ['rev-list', rev_filter, '--reverse', '--date-order']
240 try:
238 try:
241 so = self.run_git_command(cmd)
239 so = self.run_git_command(cmd)
242 except RepositoryError:
240 except RepositoryError:
243 # Can be raised for empty repositories
241 # Can be raised for empty repositories
244 return []
242 return []
245 return so.splitlines()
243 return so.splitlines()
246
244
247 def _get_all_revisions2(self):
245 def _get_all_revisions2(self):
248 # alternate implementation using dulwich
246 # alternate implementation using dulwich
249 includes = [ascii_str(sha) for key, (sha, type_) in self._parsed_refs.items()
247 includes = [ascii_str(sha) for key, (sha, type_) in self._parsed_refs.items()
250 if type_ != b'T']
248 if type_ != b'T']
251 return [c.commit.id for c in self._repo.get_walker(include=includes)]
249 return [c.commit.id for c in self._repo.get_walker(include=includes)]
252
250
253 def _get_revision(self, revision):
251 def _get_revision(self, revision):
254 """
252 """
255 Given any revision identifier, returns a 40 char string with revision hash.
253 Given any revision identifier, returns a 40 char string with revision hash.
256 """
254 """
257 if self._empty:
255 if self._empty:
258 raise EmptyRepositoryError("There are no changesets yet")
256 raise EmptyRepositoryError("There are no changesets yet")
259
257
260 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
258 if revision in (None, '', 'tip', 'HEAD', 'head', -1):
261 revision = -1
259 revision = -1
262
260
263 if isinstance(revision, int):
261 if isinstance(revision, int):
264 try:
262 try:
265 return self.revisions[revision]
263 return self.revisions[revision]
266 except IndexError:
264 except IndexError:
267 msg = "Revision %r does not exist for %s" % (revision, self.name)
265 msg = "Revision %r does not exist for %s" % (revision, self.name)
268 raise ChangesetDoesNotExistError(msg)
266 raise ChangesetDoesNotExistError(msg)
269
267
270 if isinstance(revision, str):
268 if isinstance(revision, str):
271 if revision.isdigit() and (len(revision) < 12 or len(revision) == revision.count('0')):
269 if revision.isdigit() and (len(revision) < 12 or len(revision) == revision.count('0')):
272 try:
270 try:
273 return self.revisions[int(revision)]
271 return self.revisions[int(revision)]
274 except IndexError:
272 except IndexError:
275 msg = "Revision %r does not exist for %s" % (revision, self)
273 msg = "Revision %r does not exist for %s" % (revision, self)
276 raise ChangesetDoesNotExistError(msg)
274 raise ChangesetDoesNotExistError(msg)
277
275
278 # get by branch/tag name
276 # get by branch/tag name
279 _ref_revision = self._parsed_refs.get(safe_bytes(revision))
277 _ref_revision = self._parsed_refs.get(safe_bytes(revision))
280 if _ref_revision: # and _ref_revision[1] in [b'H', b'RH', b'T']:
278 if _ref_revision: # and _ref_revision[1] in [b'H', b'RH', b'T']:
281 return ascii_str(_ref_revision[0])
279 return ascii_str(_ref_revision[0])
282
280
283 if revision in self.revisions:
281 if revision in self.revisions:
284 return revision
282 return revision
285
283
286 # maybe it's a tag ? we don't have them in self.revisions
284 # maybe it's a tag ? we don't have them in self.revisions
287 if revision in self.tags.values():
285 if revision in self.tags.values():
288 return revision
286 return revision
289
287
290 if SHA_PATTERN.match(revision):
288 if SHA_PATTERN.match(revision):
291 msg = "Revision %r does not exist for %s" % (revision, self.name)
289 msg = "Revision %r does not exist for %s" % (revision, self.name)
292 raise ChangesetDoesNotExistError(msg)
290 raise ChangesetDoesNotExistError(msg)
293
291
294 raise ChangesetDoesNotExistError("Given revision %r not recognized" % revision)
292 raise ChangesetDoesNotExistError("Given revision %r not recognized" % revision)
295
293
296 def get_ref_revision(self, ref_type, ref_name):
294 def get_ref_revision(self, ref_type, ref_name):
297 """
295 """
298 Returns ``GitChangeset`` object representing repository's
296 Returns ``GitChangeset`` object representing repository's
299 changeset at the given ``revision``.
297 changeset at the given ``revision``.
300 """
298 """
301 return self._get_revision(ref_name)
299 return self._get_revision(ref_name)
302
300
303 def _get_archives(self, archive_name='tip'):
301 def _get_archives(self, archive_name='tip'):
304
302
305 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
303 for i in [('zip', '.zip'), ('gz', '.tar.gz'), ('bz2', '.tar.bz2')]:
306 yield {"type": i[0], "extension": i[1], "node": archive_name}
304 yield {"type": i[0], "extension": i[1], "node": archive_name}
307
305
308 def _get_url(self, url):
306 def _get_url(self, url):
309 """
307 """
310 Returns normalized url. If schema is not given, would fall to
308 Returns normalized url. If schema is not given, would fall to
311 filesystem (``file:///``) schema.
309 filesystem (``file:///``) schema.
312 """
310 """
313 if url != 'default' and '://' not in url:
311 if url != 'default' and '://' not in url:
314 url = ':///'.join(('file', url))
312 url = ':///'.join(('file', url))
315 return url
313 return url
316
314
317 @LazyProperty
315 @LazyProperty
318 def name(self):
316 def name(self):
319 return os.path.basename(self.path)
317 return os.path.basename(self.path)
320
318
321 @LazyProperty
319 @LazyProperty
322 def last_change(self):
320 def last_change(self):
323 """
321 """
324 Returns last change made on this repository as datetime object
322 Returns last change made on this repository as datetime object
325 """
323 """
326 return date_fromtimestamp(self._get_mtime(), makedate()[1])
324 return date_fromtimestamp(self._get_mtime(), makedate()[1])
327
325
328 def _get_mtime(self):
326 def _get_mtime(self):
329 try:
327 try:
330 return time.mktime(self.get_changeset().date.timetuple())
328 return time.mktime(self.get_changeset().date.timetuple())
331 except RepositoryError:
329 except RepositoryError:
332 idx_loc = '' if self.bare else '.git'
330 idx_loc = '' if self.bare else '.git'
333 # fallback to filesystem
331 # fallback to filesystem
334 in_path = os.path.join(self.path, idx_loc, "index")
332 in_path = os.path.join(self.path, idx_loc, "index")
335 he_path = os.path.join(self.path, idx_loc, "HEAD")
333 he_path = os.path.join(self.path, idx_loc, "HEAD")
336 if os.path.exists(in_path):
334 if os.path.exists(in_path):
337 return os.stat(in_path).st_mtime
335 return os.stat(in_path).st_mtime
338 else:
336 else:
339 return os.stat(he_path).st_mtime
337 return os.stat(he_path).st_mtime
340
338
341 @LazyProperty
339 @LazyProperty
342 def description(self):
340 def description(self):
343 return safe_str(self._repo.get_description() or b'unknown')
341 return safe_str(self._repo.get_description() or b'unknown')
344
342
345 @LazyProperty
343 @LazyProperty
346 def contact(self):
344 def contact(self):
347 undefined_contact = 'Unknown'
345 undefined_contact = 'Unknown'
348 return undefined_contact
346 return undefined_contact
349
347
350 @property
348 @property
351 def branches(self):
349 def branches(self):
352 if not self.revisions:
350 if not self.revisions:
353 return {}
351 return {}
354 _branches = [(safe_str(key), ascii_str(sha))
352 _branches = [(safe_str(key), ascii_str(sha))
355 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'H']
353 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'H']
356 return OrderedDict(sorted(_branches, key=(lambda ctx: ctx[0]), reverse=False))
354 return OrderedDict(sorted(_branches, key=(lambda ctx: ctx[0]), reverse=False))
357
355
358 @LazyProperty
356 @LazyProperty
359 def closed_branches(self):
357 def closed_branches(self):
360 return {}
358 return {}
361
359
362 @LazyProperty
360 @LazyProperty
363 def tags(self):
361 def tags(self):
364 return self._get_tags()
362 return self._get_tags()
365
363
366 def _get_tags(self):
364 def _get_tags(self):
367 if not self.revisions:
365 if not self.revisions:
368 return {}
366 return {}
369 _tags = [(safe_str(key), ascii_str(sha))
367 _tags = [(safe_str(key), ascii_str(sha))
370 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'T']
368 for key, (sha, type_) in self._parsed_refs.items() if type_ == b'T']
371 return OrderedDict(sorted(_tags, key=(lambda ctx: ctx[0]), reverse=True))
369 return OrderedDict(sorted(_tags, key=(lambda ctx: ctx[0]), reverse=True))
372
370
373 def tag(self, name, user, revision=None, message=None, date=None,
371 def tag(self, name, user, revision=None, message=None, date=None,
374 **kwargs):
372 **kwargs):
375 """
373 """
376 Creates and returns a tag for the given ``revision``.
374 Creates and returns a tag for the given ``revision``.
377
375
378 :param name: name for new tag
376 :param name: name for new tag
379 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
377 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
380 :param revision: changeset id for which new tag would be created
378 :param revision: changeset id for which new tag would be created
381 :param message: message of the tag's commit
379 :param message: message of the tag's commit
382 :param date: date of tag's commit
380 :param date: date of tag's commit
383
381
384 :raises TagAlreadyExistError: if tag with same name already exists
382 :raises TagAlreadyExistError: if tag with same name already exists
385 """
383 """
386 if name in self.tags:
384 if name in self.tags:
387 raise TagAlreadyExistError("Tag %s already exists" % name)
385 raise TagAlreadyExistError("Tag %s already exists" % name)
388 changeset = self.get_changeset(revision)
386 changeset = self.get_changeset(revision)
389 message = message or "Added tag %s for commit %s" % (name,
387 message = message or "Added tag %s for commit %s" % (name,
390 changeset.raw_id)
388 changeset.raw_id)
391 self._repo.refs[b"refs/tags/%s" % safe_bytes(name)] = changeset._commit.id
389 self._repo.refs[b"refs/tags/%s" % safe_bytes(name)] = changeset._commit.id
392
390
393 self._parsed_refs = self._get_parsed_refs()
391 self._parsed_refs = self._get_parsed_refs()
394 self.tags = self._get_tags()
392 self.tags = self._get_tags()
395 return changeset
393 return changeset
396
394
397 def remove_tag(self, name, user, message=None, date=None):
395 def remove_tag(self, name, user, message=None, date=None):
398 """
396 """
399 Removes tag with the given ``name``.
397 Removes tag with the given ``name``.
400
398
401 :param name: name of the tag to be removed
399 :param name: name of the tag to be removed
402 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
400 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
403 :param message: message of the tag's removal commit
401 :param message: message of the tag's removal commit
404 :param date: date of tag's removal commit
402 :param date: date of tag's removal commit
405
403
406 :raises TagDoesNotExistError: if tag with given name does not exists
404 :raises TagDoesNotExistError: if tag with given name does not exists
407 """
405 """
408 if name not in self.tags:
406 if name not in self.tags:
409 raise TagDoesNotExistError("Tag %s does not exist" % name)
407 raise TagDoesNotExistError("Tag %s does not exist" % name)
410 # self._repo.refs is a DiskRefsContainer, and .path gives the full absolute path of '.git'
408 # self._repo.refs is a DiskRefsContainer, and .path gives the full absolute path of '.git'
411 tagpath = os.path.join(safe_str(self._repo.refs.path), 'refs', 'tags', name)
409 tagpath = os.path.join(safe_str(self._repo.refs.path), 'refs', 'tags', name)
412 try:
410 try:
413 os.remove(tagpath)
411 os.remove(tagpath)
414 self._parsed_refs = self._get_parsed_refs()
412 self._parsed_refs = self._get_parsed_refs()
415 self.tags = self._get_tags()
413 self.tags = self._get_tags()
416 except OSError as e:
414 except OSError as e:
417 raise RepositoryError(e.strerror)
415 raise RepositoryError(e.strerror)
418
416
419 @LazyProperty
417 @LazyProperty
420 def bookmarks(self):
418 def bookmarks(self):
421 """
419 """
422 Gets bookmarks for this repository
420 Gets bookmarks for this repository
423 """
421 """
424 return {}
422 return {}
425
423
426 @LazyProperty
424 @LazyProperty
427 def _parsed_refs(self):
425 def _parsed_refs(self):
428 return self._get_parsed_refs()
426 return self._get_parsed_refs()
429
427
430 def _get_parsed_refs(self):
428 def _get_parsed_refs(self):
431 """Return refs as a dict, like:
429 """Return refs as a dict, like:
432 { b'v0.2.0': [b'599ba911aa24d2981225f3966eb659dfae9e9f30', b'T'] }
430 { b'v0.2.0': [b'599ba911aa24d2981225f3966eb659dfae9e9f30', b'T'] }
433 """
431 """
434 _repo = self._repo
432 _repo = self._repo
435 refs = _repo.get_refs()
433 refs = _repo.get_refs()
436 keys = [(b'refs/heads/', b'H'),
434 keys = [(b'refs/heads/', b'H'),
437 (b'refs/remotes/origin/', b'RH'),
435 (b'refs/remotes/origin/', b'RH'),
438 (b'refs/tags/', b'T')]
436 (b'refs/tags/', b'T')]
439 _refs = {}
437 _refs = {}
440 for ref, sha in refs.items():
438 for ref, sha in refs.items():
441 for k, type_ in keys:
439 for k, type_ in keys:
442 if ref.startswith(k):
440 if ref.startswith(k):
443 _key = ref[len(k):]
441 _key = ref[len(k):]
444 if type_ == b'T':
442 if type_ == b'T':
445 obj = _repo.get_object(sha)
443 obj = _repo.get_object(sha)
446 if isinstance(obj, Tag):
444 if isinstance(obj, Tag):
447 sha = _repo.get_object(sha).object[1]
445 sha = _repo.get_object(sha).object[1]
448 _refs[_key] = [sha, type_]
446 _refs[_key] = [sha, type_]
449 break
447 break
450 return _refs
448 return _refs
451
449
452 def _heads(self, reverse=False):
450 def _heads(self, reverse=False):
453 refs = self._repo.get_refs()
451 refs = self._repo.get_refs()
454 heads = {}
452 heads = {}
455
453
456 for key, val in refs.items():
454 for key, val in refs.items():
457 for ref_key in [b'refs/heads/', b'refs/remotes/origin/']:
455 for ref_key in [b'refs/heads/', b'refs/remotes/origin/']:
458 if key.startswith(ref_key):
456 if key.startswith(ref_key):
459 n = key[len(ref_key):]
457 n = key[len(ref_key):]
460 if n not in [b'HEAD']:
458 if n not in [b'HEAD']:
461 heads[n] = val
459 heads[n] = val
462
460
463 return heads if reverse else dict((y, x) for x, y in heads.items())
461 return heads if reverse else dict((y, x) for x, y in heads.items())
464
462
465 def get_changeset(self, revision=None):
463 def get_changeset(self, revision=None):
466 """
464 """
467 Returns ``GitChangeset`` object representing commit from git repository
465 Returns ``GitChangeset`` object representing commit from git repository
468 at the given revision or head (most recent commit) if None given.
466 at the given revision or head (most recent commit) if None given.
469 """
467 """
470 if isinstance(revision, GitChangeset):
468 if isinstance(revision, changeset.GitChangeset):
471 return revision
469 return revision
472 return GitChangeset(repository=self, revision=self._get_revision(revision))
470 return changeset.GitChangeset(repository=self, revision=self._get_revision(revision))
473
471
474 def get_changesets(self, start=None, end=None, start_date=None,
472 def get_changesets(self, start=None, end=None, start_date=None,
475 end_date=None, branch_name=None, reverse=False, max_revisions=None):
473 end_date=None, branch_name=None, reverse=False, max_revisions=None):
476 """
474 """
477 Returns iterator of ``GitChangeset`` objects from start to end (both
475 Returns iterator of ``GitChangeset`` objects from start to end (both
478 are inclusive), in ascending date order (unless ``reverse`` is set).
476 are inclusive), in ascending date order (unless ``reverse`` is set).
479
477
480 :param start: changeset ID, as str; first returned changeset
478 :param start: changeset ID, as str; first returned changeset
481 :param end: changeset ID, as str; last returned changeset
479 :param end: changeset ID, as str; last returned changeset
482 :param start_date: if specified, changesets with commit date less than
480 :param start_date: if specified, changesets with commit date less than
483 ``start_date`` would be filtered out from returned set
481 ``start_date`` would be filtered out from returned set
484 :param end_date: if specified, changesets with commit date greater than
482 :param end_date: if specified, changesets with commit date greater than
485 ``end_date`` would be filtered out from returned set
483 ``end_date`` would be filtered out from returned set
486 :param branch_name: if specified, changesets not reachable from given
484 :param branch_name: if specified, changesets not reachable from given
487 branch would be filtered out from returned set
485 branch would be filtered out from returned set
488 :param reverse: if ``True``, returned generator would be reversed
486 :param reverse: if ``True``, returned generator would be reversed
489 (meaning that returned changesets would have descending date order)
487 (meaning that returned changesets would have descending date order)
490
488
491 :raise BranchDoesNotExistError: If given ``branch_name`` does not
489 :raise BranchDoesNotExistError: If given ``branch_name`` does not
492 exist.
490 exist.
493 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
491 :raise ChangesetDoesNotExistError: If changeset for given ``start`` or
494 ``end`` could not be found.
492 ``end`` could not be found.
495
493
496 """
494 """
497 if branch_name and branch_name not in self.branches:
495 if branch_name and branch_name not in self.branches:
498 raise BranchDoesNotExistError("Branch '%s' not found"
496 raise BranchDoesNotExistError("Branch '%s' not found"
499 % branch_name)
497 % branch_name)
500 # actually we should check now if it's not an empty repo to not spaw
498 # actually we should check now if it's not an empty repo to not spaw
501 # subprocess commands
499 # subprocess commands
502 if self._empty:
500 if self._empty:
503 raise EmptyRepositoryError("There are no changesets yet")
501 raise EmptyRepositoryError("There are no changesets yet")
504
502
505 # %H at format means (full) commit hash, initial hashes are retrieved
503 # %H at format means (full) commit hash, initial hashes are retrieved
506 # in ascending date order
504 # in ascending date order
507 cmd = ['log', '--date-order', '--reverse', '--pretty=format:%H']
505 cmd = ['log', '--date-order', '--reverse', '--pretty=format:%H']
508 if max_revisions:
506 if max_revisions:
509 cmd += ['--max-count=%s' % max_revisions]
507 cmd += ['--max-count=%s' % max_revisions]
510 if start_date:
508 if start_date:
511 cmd += ['--since', start_date.strftime('%m/%d/%y %H:%M:%S')]
509 cmd += ['--since', start_date.strftime('%m/%d/%y %H:%M:%S')]
512 if end_date:
510 if end_date:
513 cmd += ['--until', end_date.strftime('%m/%d/%y %H:%M:%S')]
511 cmd += ['--until', end_date.strftime('%m/%d/%y %H:%M:%S')]
514 if branch_name:
512 if branch_name:
515 cmd.append(branch_name)
513 cmd.append(branch_name)
516 else:
514 else:
517 cmd.append(settings.GIT_REV_FILTER)
515 cmd.append(settings.GIT_REV_FILTER)
518
516
519 revs = self.run_git_command(cmd).splitlines()
517 revs = self.run_git_command(cmd).splitlines()
520 start_pos = 0
518 start_pos = 0
521 end_pos = len(revs)
519 end_pos = len(revs)
522 if start:
520 if start:
523 _start = self._get_revision(start)
521 _start = self._get_revision(start)
524 try:
522 try:
525 start_pos = revs.index(_start)
523 start_pos = revs.index(_start)
526 except ValueError:
524 except ValueError:
527 pass
525 pass
528
526
529 if end is not None:
527 if end is not None:
530 _end = self._get_revision(end)
528 _end = self._get_revision(end)
531 try:
529 try:
532 end_pos = revs.index(_end)
530 end_pos = revs.index(_end)
533 except ValueError:
531 except ValueError:
534 pass
532 pass
535
533
536 if None not in [start, end] and start_pos > end_pos:
534 if None not in [start, end] and start_pos > end_pos:
537 raise RepositoryError('start cannot be after end')
535 raise RepositoryError('start cannot be after end')
538
536
539 if end_pos is not None:
537 if end_pos is not None:
540 end_pos += 1
538 end_pos += 1
541
539
542 revs = revs[start_pos:end_pos]
540 revs = revs[start_pos:end_pos]
543 if reverse:
541 if reverse:
544 revs.reverse()
542 revs.reverse()
545
543
546 return CollectionGenerator(self, revs)
544 return CollectionGenerator(self, revs)
547
545
548 def get_diff_changesets(self, org_rev, other_repo, other_rev):
546 def get_diff_changesets(self, org_rev, other_repo, other_rev):
549 """
547 """
550 Returns lists of changesets that can be merged from this repo @org_rev
548 Returns lists of changesets that can be merged from this repo @org_rev
551 to other_repo @other_rev
549 to other_repo @other_rev
552 ... and the other way
550 ... and the other way
553 ... and the ancestors that would be used for merge
551 ... and the ancestors that would be used for merge
554
552
555 :param org_rev: the revision we want our compare to be made
553 :param org_rev: the revision we want our compare to be made
556 :param other_repo: repo object, most likely the fork of org_repo. It has
554 :param other_repo: repo object, most likely the fork of org_repo. It has
557 all changesets that we need to obtain
555 all changesets that we need to obtain
558 :param other_rev: revision we want out compare to be made on other_repo
556 :param other_rev: revision we want out compare to be made on other_repo
559 """
557 """
560 org_changesets = []
558 org_changesets = []
561 ancestors = None
559 ancestors = None
562 if org_rev == other_rev:
560 if org_rev == other_rev:
563 other_changesets = []
561 other_changesets = []
564 elif self != other_repo:
562 elif self != other_repo:
565 gitrepo = Repo(self.path)
563 gitrepo = Repo(self.path)
566 SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo)
564 SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo)
567
565
568 gitrepo_remote = Repo(other_repo.path)
566 gitrepo_remote = Repo(other_repo.path)
569 SubprocessGitClient(thin_packs=False).fetch(self.path, gitrepo_remote)
567 SubprocessGitClient(thin_packs=False).fetch(self.path, gitrepo_remote)
570
568
571 revs = [
569 revs = [
572 ascii_str(x.commit.id)
570 ascii_str(x.commit.id)
573 for x in gitrepo_remote.get_walker(include=[ascii_bytes(other_rev)],
571 for x in gitrepo_remote.get_walker(include=[ascii_bytes(other_rev)],
574 exclude=[ascii_bytes(org_rev)])
572 exclude=[ascii_bytes(org_rev)])
575 ]
573 ]
576 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
574 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
577 if other_changesets:
575 if other_changesets:
578 ancestors = [other_changesets[0].parents[0].raw_id]
576 ancestors = [other_changesets[0].parents[0].raw_id]
579 else:
577 else:
580 # no changesets from other repo, ancestor is the other_rev
578 # no changesets from other repo, ancestor is the other_rev
581 ancestors = [other_rev]
579 ancestors = [other_rev]
582
580
583 gitrepo.close()
581 gitrepo.close()
584 gitrepo_remote.close()
582 gitrepo_remote.close()
585
583
586 else:
584 else:
587 so = self.run_git_command(
585 so = self.run_git_command(
588 ['log', '--reverse', '--pretty=format:%H',
586 ['log', '--reverse', '--pretty=format:%H',
589 '-s', '%s..%s' % (org_rev, other_rev)]
587 '-s', '%s..%s' % (org_rev, other_rev)]
590 )
588 )
591 other_changesets = [self.get_changeset(cs)
589 other_changesets = [self.get_changeset(cs)
592 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
590 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
593 so = self.run_git_command(
591 so = self.run_git_command(
594 ['merge-base', org_rev, other_rev]
592 ['merge-base', org_rev, other_rev]
595 )
593 )
596 ancestors = [re.findall(r'[0-9a-fA-F]{40}', so)[0]]
594 ancestors = [re.findall(r'[0-9a-fA-F]{40}', so)[0]]
597
595
598 return other_changesets, org_changesets, ancestors
596 return other_changesets, org_changesets, ancestors
599
597
600 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
598 def get_diff(self, rev1, rev2, path=None, ignore_whitespace=False,
601 context=3):
599 context=3):
602 """
600 """
603 Returns (git like) *diff*, as plain bytes text. Shows changes
601 Returns (git like) *diff*, as plain bytes text. Shows changes
604 introduced by ``rev2`` since ``rev1``.
602 introduced by ``rev2`` since ``rev1``.
605
603
606 :param rev1: Entry point from which diff is shown. Can be
604 :param rev1: Entry point from which diff is shown. Can be
607 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
605 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
608 the changes since empty state of the repository until ``rev2``
606 the changes since empty state of the repository until ``rev2``
609 :param rev2: Until which revision changes should be shown.
607 :param rev2: Until which revision changes should be shown.
610 :param ignore_whitespace: If set to ``True``, would not show whitespace
608 :param ignore_whitespace: If set to ``True``, would not show whitespace
611 changes. Defaults to ``False``.
609 changes. Defaults to ``False``.
612 :param context: How many lines before/after changed lines should be
610 :param context: How many lines before/after changed lines should be
613 shown. Defaults to ``3``. Due to limitations in Git, if
611 shown. Defaults to ``3``. Due to limitations in Git, if
614 value passed-in is greater than ``2**31-1``
612 value passed-in is greater than ``2**31-1``
615 (``2147483647``), it will be set to ``2147483647``
613 (``2147483647``), it will be set to ``2147483647``
616 instead. If negative value is passed-in, it will be set to
614 instead. If negative value is passed-in, it will be set to
617 ``0`` instead.
615 ``0`` instead.
618 """
616 """
619
617
620 # Git internally uses a signed long int for storing context
618 # Git internally uses a signed long int for storing context
621 # size (number of lines to show before and after the
619 # size (number of lines to show before and after the
622 # differences). This can result in integer overflow, so we
620 # differences). This can result in integer overflow, so we
623 # ensure the requested context is smaller by one than the
621 # ensure the requested context is smaller by one than the
624 # number that would cause the overflow. It is highly unlikely
622 # number that would cause the overflow. It is highly unlikely
625 # that a single file will contain that many lines, so this
623 # that a single file will contain that many lines, so this
626 # kind of change should not cause any realistic consequences.
624 # kind of change should not cause any realistic consequences.
627 overflowed_long_int = 2**31
625 overflowed_long_int = 2**31
628
626
629 if context >= overflowed_long_int:
627 if context >= overflowed_long_int:
630 context = overflowed_long_int - 1
628 context = overflowed_long_int - 1
631
629
632 # Negative context values make no sense, and will result in
630 # Negative context values make no sense, and will result in
633 # errors. Ensure this does not happen.
631 # errors. Ensure this does not happen.
634 if context < 0:
632 if context < 0:
635 context = 0
633 context = 0
636
634
637 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
635 flags = ['-U%s' % context, '--full-index', '--binary', '-p', '-M', '--abbrev=40']
638 if ignore_whitespace:
636 if ignore_whitespace:
639 flags.append('-w')
637 flags.append('-w')
640
638
641 if hasattr(rev1, 'raw_id'):
639 if hasattr(rev1, 'raw_id'):
642 rev1 = getattr(rev1, 'raw_id')
640 rev1 = getattr(rev1, 'raw_id')
643
641
644 if hasattr(rev2, 'raw_id'):
642 if hasattr(rev2, 'raw_id'):
645 rev2 = getattr(rev2, 'raw_id')
643 rev2 = getattr(rev2, 'raw_id')
646
644
647 if rev1 == self.EMPTY_CHANGESET:
645 if rev1 == self.EMPTY_CHANGESET:
648 rev2 = self.get_changeset(rev2).raw_id
646 rev2 = self.get_changeset(rev2).raw_id
649 cmd = ['show'] + flags + [rev2]
647 cmd = ['show'] + flags + [rev2]
650 else:
648 else:
651 rev1 = self.get_changeset(rev1).raw_id
649 rev1 = self.get_changeset(rev1).raw_id
652 rev2 = self.get_changeset(rev2).raw_id
650 rev2 = self.get_changeset(rev2).raw_id
653 cmd = ['diff'] + flags + [rev1, rev2]
651 cmd = ['diff'] + flags + [rev1, rev2]
654
652
655 if path:
653 if path:
656 cmd += ['--', path]
654 cmd += ['--', path]
657
655
658 stdout, stderr = self._run_git_command(cmd, cwd=self.path)
656 stdout, stderr = self._run_git_command(cmd, cwd=self.path)
659 # If we used 'show' command, strip first few lines (until actual diff
657 # If we used 'show' command, strip first few lines (until actual diff
660 # starts)
658 # starts)
661 if rev1 == self.EMPTY_CHANGESET:
659 if rev1 == self.EMPTY_CHANGESET:
662 parts = stdout.split(b'\ndiff ', 1)
660 parts = stdout.split(b'\ndiff ', 1)
663 if len(parts) > 1:
661 if len(parts) > 1:
664 stdout = b'diff ' + parts[1]
662 stdout = b'diff ' + parts[1]
665 return stdout
663 return stdout
666
664
667 @LazyProperty
665 @LazyProperty
668 def in_memory_changeset(self):
666 def in_memory_changeset(self):
669 """
667 """
670 Returns ``GitInMemoryChangeset`` object for this repository.
668 Returns ``GitInMemoryChangeset`` object for this repository.
671 """
669 """
672 return GitInMemoryChangeset(self)
670 return inmemory.GitInMemoryChangeset(self)
673
671
674 def clone(self, url, update_after_clone=True, bare=False):
672 def clone(self, url, update_after_clone=True, bare=False):
675 """
673 """
676 Tries to clone changes from external location.
674 Tries to clone changes from external location.
677
675
678 :param update_after_clone: If set to ``False``, git won't checkout
676 :param update_after_clone: If set to ``False``, git won't checkout
679 working directory
677 working directory
680 :param bare: If set to ``True``, repository would be cloned into
678 :param bare: If set to ``True``, repository would be cloned into
681 *bare* git repository (no working directory at all).
679 *bare* git repository (no working directory at all).
682 """
680 """
683 url = self._get_url(url)
681 url = self._get_url(url)
684 cmd = ['clone', '-q']
682 cmd = ['clone', '-q']
685 if bare:
683 if bare:
686 cmd.append('--bare')
684 cmd.append('--bare')
687 elif not update_after_clone:
685 elif not update_after_clone:
688 cmd.append('--no-checkout')
686 cmd.append('--no-checkout')
689 cmd += ['--', url, self.path]
687 cmd += ['--', url, self.path]
690 # If error occurs run_git_command raises RepositoryError already
688 # If error occurs run_git_command raises RepositoryError already
691 self.run_git_command(cmd)
689 self.run_git_command(cmd)
692
690
693 def pull(self, url):
691 def pull(self, url):
694 """
692 """
695 Tries to pull changes from external location.
693 Tries to pull changes from external location.
696 """
694 """
697 url = self._get_url(url)
695 url = self._get_url(url)
698 cmd = ['pull', '--ff-only', url]
696 cmd = ['pull', '--ff-only', url]
699 # If error occurs run_git_command raises RepositoryError already
697 # If error occurs run_git_command raises RepositoryError already
700 self.run_git_command(cmd)
698 self.run_git_command(cmd)
701
699
702 def fetch(self, url):
700 def fetch(self, url):
703 """
701 """
704 Tries to pull changes from external location.
702 Tries to pull changes from external location.
705 """
703 """
706 url = self._get_url(url)
704 url = self._get_url(url)
707 so = self.run_git_command(['ls-remote', '-h', url])
705 so = self.run_git_command(['ls-remote', '-h', url])
708 cmd = ['fetch', url, '--']
706 cmd = ['fetch', url, '--']
709 for line in (x for x in so.splitlines()):
707 for line in (x for x in so.splitlines()):
710 sha, ref = line.split('\t')
708 sha, ref = line.split('\t')
711 cmd.append('+%s:%s' % (ref, ref))
709 cmd.append('+%s:%s' % (ref, ref))
712 self.run_git_command(cmd)
710 self.run_git_command(cmd)
713
711
714 def _update_server_info(self):
712 def _update_server_info(self):
715 """
713 """
716 runs gits update-server-info command in this repo instance
714 runs gits update-server-info command in this repo instance
717 """
715 """
718 try:
716 try:
719 update_server_info(self._repo)
717 update_server_info(self._repo)
720 except OSError as e:
718 except OSError as e:
721 if e.errno not in [errno.ENOENT, errno.EROFS]:
719 if e.errno not in [errno.ENOENT, errno.EROFS]:
722 raise
720 raise
723 # Workaround for dulwich crashing on for example its own dulwich/tests/data/repos/simple_merge.git/info/refs.lock
721 # Workaround for dulwich crashing on for example its own dulwich/tests/data/repos/simple_merge.git/info/refs.lock
724 log.error('Ignoring %s running update-server-info: %s', type(e).__name__, e)
722 log.error('Ignoring %s running update-server-info: %s', type(e).__name__, e)
725
723
726 @LazyProperty
724 @LazyProperty
727 def workdir(self):
725 def workdir(self):
728 """
726 """
729 Returns ``Workdir`` instance for this repository.
727 Returns ``Workdir`` instance for this repository.
730 """
728 """
731 return GitWorkdir(self)
729 return workdir.GitWorkdir(self)
732
730
733 def get_config_value(self, section, name, config_file=None):
731 def get_config_value(self, section, name, config_file=None):
734 """
732 """
735 Returns configuration value for a given [``section``] and ``name``.
733 Returns configuration value for a given [``section``] and ``name``.
736
734
737 :param section: Section we want to retrieve value from
735 :param section: Section we want to retrieve value from
738 :param name: Name of configuration we want to retrieve
736 :param name: Name of configuration we want to retrieve
739 :param config_file: A path to file which should be used to retrieve
737 :param config_file: A path to file which should be used to retrieve
740 configuration from (might also be a list of file paths)
738 configuration from (might also be a list of file paths)
741 """
739 """
742 if config_file is None:
740 if config_file is None:
743 config_file = []
741 config_file = []
744 elif isinstance(config_file, str):
742 elif isinstance(config_file, str):
745 config_file = [config_file]
743 config_file = [config_file]
746
744
747 def gen_configs():
745 def gen_configs():
748 for path in config_file + self._config_files:
746 for path in config_file + self._config_files:
749 try:
747 try:
750 yield ConfigFile.from_path(path)
748 yield ConfigFile.from_path(path)
751 except (IOError, OSError, ValueError):
749 except (IOError, OSError, ValueError):
752 continue
750 continue
753
751
754 for config in gen_configs():
752 for config in gen_configs():
755 try:
753 try:
756 value = config.get(section, name)
754 value = config.get(section, name)
757 except KeyError:
755 except KeyError:
758 continue
756 continue
759 return None if value is None else safe_str(value)
757 return None if value is None else safe_str(value)
760 return None
758 return None
761
759
762 def get_user_name(self, config_file=None):
760 def get_user_name(self, config_file=None):
763 """
761 """
764 Returns user's name from global configuration file.
762 Returns user's name from global configuration file.
765
763
766 :param config_file: A path to file which should be used to retrieve
764 :param config_file: A path to file which should be used to retrieve
767 configuration from (might also be a list of file paths)
765 configuration from (might also be a list of file paths)
768 """
766 """
769 return self.get_config_value('user', 'name', config_file)
767 return self.get_config_value('user', 'name', config_file)
770
768
771 def get_user_email(self, config_file=None):
769 def get_user_email(self, config_file=None):
772 """
770 """
773 Returns user's email from global configuration file.
771 Returns user's email from global configuration file.
774
772
775 :param config_file: A path to file which should be used to retrieve
773 :param config_file: A path to file which should be used to retrieve
776 configuration from (might also be a list of file paths)
774 configuration from (might also be a list of file paths)
777 """
775 """
778 return self.get_config_value('user', 'email', config_file)
776 return self.get_config_value('user', 'email', config_file)
@@ -1,110 +1,111 b''
1 import datetime
1 import datetime
2
2
3 import mercurial.context
3 import mercurial.context
4 import mercurial.node
4 import mercurial.node
5
5
6 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
6 from kallithea.lib.vcs.backends.base import BaseInMemoryChangeset
7 from kallithea.lib.vcs.exceptions import RepositoryError
7 from kallithea.lib.vcs.exceptions import RepositoryError
8 from kallithea.lib.vcs.utils import ascii_str, safe_bytes, safe_str
8 from kallithea.lib.vcs.utils import ascii_str, safe_bytes, safe_str
9
9
10 from . import repository
11
10
12
11 class MercurialInMemoryChangeset(BaseInMemoryChangeset):
13 class MercurialInMemoryChangeset(BaseInMemoryChangeset):
12
14
13 def commit(self, message, author, parents=None, branch=None, date=None,
15 def commit(self, message, author, parents=None, branch=None, date=None,
14 **kwargs):
16 **kwargs):
15 """
17 """
16 Performs in-memory commit (doesn't check workdir in any way) and
18 Performs in-memory commit (doesn't check workdir in any way) and
17 returns newly created ``Changeset``. Updates repository's
19 returns newly created ``Changeset``. Updates repository's
18 ``revisions``.
20 ``revisions``.
19
21
20 :param message: message of the commit
22 :param message: message of the commit
21 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
23 :param author: full username, i.e. "Joe Doe <joe.doe@example.com>"
22 :param parents: single parent or sequence of parents from which commit
24 :param parents: single parent or sequence of parents from which commit
23 would be derived
25 would be derived
24 :param date: ``datetime.datetime`` instance. Defaults to
26 :param date: ``datetime.datetime`` instance. Defaults to
25 ``datetime.datetime.now()``.
27 ``datetime.datetime.now()``.
26 :param branch: branch name, as string. If none given, default backend's
28 :param branch: branch name, as string. If none given, default backend's
27 branch would be used.
29 branch would be used.
28
30
29 :raises ``CommitError``: if any error occurs while committing
31 :raises ``CommitError``: if any error occurs while committing
30 """
32 """
31 self.check_integrity(parents)
33 self.check_integrity(parents)
32
34
33 if not isinstance(message, str):
35 if not isinstance(message, str):
34 raise RepositoryError('message must be a str - got %r' % type(message))
36 raise RepositoryError('message must be a str - got %r' % type(message))
35 if not isinstance(author, str):
37 if not isinstance(author, str):
36 raise RepositoryError('author must be a str - got %r' % type(author))
38 raise RepositoryError('author must be a str - got %r' % type(author))
37
39
38 from .repository import MercurialRepository
39 if branch is None:
40 if branch is None:
40 branch = MercurialRepository.DEFAULT_BRANCH_NAME
41 branch = repository.MercurialRepository.DEFAULT_BRANCH_NAME
41 kwargs[b'branch'] = safe_bytes(branch)
42 kwargs[b'branch'] = safe_bytes(branch)
42
43
43 def filectxfn(_repo, memctx, bytes_path):
44 def filectxfn(_repo, memctx, bytes_path):
44 """
45 """
45 Callback from Mercurial, returning ctx to commit for the given
46 Callback from Mercurial, returning ctx to commit for the given
46 path.
47 path.
47 """
48 """
48 path = safe_str(bytes_path)
49 path = safe_str(bytes_path)
49
50
50 # check if this path is removed
51 # check if this path is removed
51 if path in (node.path for node in self.removed):
52 if path in (node.path for node in self.removed):
52 return None
53 return None
53
54
54 # check if this path is added
55 # check if this path is added
55 for node in self.added:
56 for node in self.added:
56 if node.path == path:
57 if node.path == path:
57 return mercurial.context.memfilectx(_repo, memctx, path=bytes_path,
58 return mercurial.context.memfilectx(_repo, memctx, path=bytes_path,
58 data=node.content,
59 data=node.content,
59 islink=False,
60 islink=False,
60 isexec=node.is_executable,
61 isexec=node.is_executable,
61 copysource=False)
62 copysource=False)
62
63
63 # or changed
64 # or changed
64 for node in self.changed:
65 for node in self.changed:
65 if node.path == path:
66 if node.path == path:
66 return mercurial.context.memfilectx(_repo, memctx, path=bytes_path,
67 return mercurial.context.memfilectx(_repo, memctx, path=bytes_path,
67 data=node.content,
68 data=node.content,
68 islink=False,
69 islink=False,
69 isexec=node.is_executable,
70 isexec=node.is_executable,
70 copysource=False)
71 copysource=False)
71
72
72 raise RepositoryError("Given path haven't been marked as added, "
73 raise RepositoryError("Given path haven't been marked as added, "
73 "changed or removed (%s)" % path)
74 "changed or removed (%s)" % path)
74
75
75 parents = [None, None]
76 parents = [None, None]
76 for i, parent in enumerate(self.parents):
77 for i, parent in enumerate(self.parents):
77 if parent is not None:
78 if parent is not None:
78 parents[i] = parent._ctx.node()
79 parents[i] = parent._ctx.node()
79
80
80 if date and isinstance(date, datetime.datetime):
81 if date and isinstance(date, datetime.datetime):
81 date = safe_bytes(date.strftime('%a, %d %b %Y %H:%M:%S'))
82 date = safe_bytes(date.strftime('%a, %d %b %Y %H:%M:%S'))
82
83
83 commit_ctx = mercurial.context.memctx(
84 commit_ctx = mercurial.context.memctx(
84 repo=self.repository._repo,
85 repo=self.repository._repo,
85 parents=parents,
86 parents=parents,
86 text=b'',
87 text=b'',
87 files=[safe_bytes(x) for x in self.get_paths()],
88 files=[safe_bytes(x) for x in self.get_paths()],
88 filectxfn=filectxfn,
89 filectxfn=filectxfn,
89 user=safe_bytes(author),
90 user=safe_bytes(author),
90 date=date,
91 date=date,
91 extra=kwargs)
92 extra=kwargs)
92
93
93 # injecting given _repo params
94 # injecting given _repo params
94 commit_ctx._text = safe_bytes(message)
95 commit_ctx._text = safe_bytes(message)
95 commit_ctx._user = safe_bytes(author)
96 commit_ctx._user = safe_bytes(author)
96 commit_ctx._date = date
97 commit_ctx._date = date
97
98
98 # TODO: Catch exceptions!
99 # TODO: Catch exceptions!
99 n = self.repository._repo.commitctx(commit_ctx)
100 n = self.repository._repo.commitctx(commit_ctx)
100 # Returns mercurial node
101 # Returns mercurial node
101 self._commit_ctx = commit_ctx # For reference
102 self._commit_ctx = commit_ctx # For reference
102 # Update vcs repository object & recreate mercurial _repo
103 # Update vcs repository object & recreate mercurial _repo
103 # new_ctx = self.repository._repo[node]
104 # new_ctx = self.repository._repo[node]
104 # new_tip = ascii_str(self.repository.get_changeset(new_ctx.hex()))
105 # new_tip = ascii_str(self.repository.get_changeset(new_ctx.hex()))
105 self.repository.revisions.append(ascii_str(mercurial.node.hex(n)))
106 self.repository.revisions.append(ascii_str(mercurial.node.hex(n)))
106 self._repo = self.repository._get_repo(create=False)
107 self._repo = self.repository._get_repo(create=False)
107 self.repository.branches = self.repository._get_branches()
108 self.repository.branches = self.repository._get_branches()
108 tip = self.repository.get_changeset()
109 tip = self.repository.get_changeset()
109 self.reset()
110 self.reset()
110 return tip
111 return tip
@@ -1,665 +1,663 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 """
2 """
3 vcs.backends.hg.repository
3 vcs.backends.hg.repository
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
4 ~~~~~~~~~~~~~~~~~~~~~~~~~~
5
5
6 Mercurial repository implementation.
6 Mercurial repository implementation.
7
7
8 :created_on: Apr 8, 2010
8 :created_on: Apr 8, 2010
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
9 :copyright: (c) 2010-2011 by Marcin Kuzminski, Lukasz Balcerzak.
10 """
10 """
11
11
12 import datetime
12 import datetime
13 import logging
13 import logging
14 import os
14 import os
15 import time
15 import time
16 import urllib.error
16 import urllib.error
17 import urllib.parse
17 import urllib.parse
18 import urllib.request
18 import urllib.request
19 from collections import OrderedDict
19 from collections import OrderedDict
20
20
21 import mercurial.commands
21 import mercurial.commands
22 import mercurial.error
22 import mercurial.error
23 import mercurial.exchange
23 import mercurial.exchange
24 import mercurial.hg
24 import mercurial.hg
25 import mercurial.hgweb
25 import mercurial.hgweb
26 import mercurial.httppeer
26 import mercurial.httppeer
27 import mercurial.localrepo
27 import mercurial.localrepo
28 import mercurial.match
28 import mercurial.match
29 import mercurial.mdiff
29 import mercurial.mdiff
30 import mercurial.node
30 import mercurial.node
31 import mercurial.patch
31 import mercurial.patch
32 import mercurial.scmutil
32 import mercurial.scmutil
33 import mercurial.sshpeer
33 import mercurial.sshpeer
34 import mercurial.tags
34 import mercurial.tags
35 import mercurial.ui
35 import mercurial.ui
36 import mercurial.unionrepo
36 import mercurial.unionrepo
37 import mercurial.util
37 import mercurial.util
38
38
39 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
39 from kallithea.lib.vcs.backends.base import BaseRepository, CollectionGenerator
40 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
40 from kallithea.lib.vcs.exceptions import (BranchDoesNotExistError, ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError, TagAlreadyExistError,
41 TagDoesNotExistError, VCSError)
41 TagDoesNotExistError, VCSError)
42 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, author_email, author_name, date_fromtimestamp, makedate, safe_bytes, safe_str
42 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, author_email, author_name, date_fromtimestamp, makedate, safe_bytes, safe_str
43 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
43 from kallithea.lib.vcs.utils.helpers import get_urllib_request_handlers
44 from kallithea.lib.vcs.utils.lazy import LazyProperty
44 from kallithea.lib.vcs.utils.lazy import LazyProperty
45 from kallithea.lib.vcs.utils.paths import abspath
45 from kallithea.lib.vcs.utils.paths import abspath
46
46
47 from .changeset import MercurialChangeset
47 from . import changeset, inmemory, workdir
48 from .inmemory import MercurialInMemoryChangeset
49 from .workdir import MercurialWorkdir
50
48
51
49
52 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
53
51
54
52
55 class MercurialRepository(BaseRepository):
53 class MercurialRepository(BaseRepository):
56 """
54 """
57 Mercurial repository backend
55 Mercurial repository backend
58 """
56 """
59 DEFAULT_BRANCH_NAME = 'default'
57 DEFAULT_BRANCH_NAME = 'default'
60 scm = 'hg'
58 scm = 'hg'
61
59
62 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
60 def __init__(self, repo_path, create=False, baseui=None, src_url=None,
63 update_after_clone=False):
61 update_after_clone=False):
64 """
62 """
65 Raises RepositoryError if repository could not be find at the given
63 Raises RepositoryError if repository could not be find at the given
66 ``repo_path``.
64 ``repo_path``.
67
65
68 :param repo_path: local path of the repository
66 :param repo_path: local path of the repository
69 :param create=False: if set to True, would try to create repository if
67 :param create=False: if set to True, would try to create repository if
70 it does not exist rather than raising exception
68 it does not exist rather than raising exception
71 :param baseui=None: user data
69 :param baseui=None: user data
72 :param src_url=None: would try to clone repository from given location
70 :param src_url=None: would try to clone repository from given location
73 :param update_after_clone=False: sets update of working copy after
71 :param update_after_clone=False: sets update of working copy after
74 making a clone
72 making a clone
75 """
73 """
76
74
77 if not isinstance(repo_path, str):
75 if not isinstance(repo_path, str):
78 raise VCSError('Mercurial backend requires repository path to '
76 raise VCSError('Mercurial backend requires repository path to '
79 'be instance of <str> got %s instead' %
77 'be instance of <str> got %s instead' %
80 type(repo_path))
78 type(repo_path))
81 self.path = abspath(repo_path)
79 self.path = abspath(repo_path)
82 self.baseui = baseui or mercurial.ui.ui()
80 self.baseui = baseui or mercurial.ui.ui()
83 # We've set path and ui, now we can set _repo itself
81 # We've set path and ui, now we can set _repo itself
84 self._repo = self._get_repo(create, src_url, update_after_clone)
82 self._repo = self._get_repo(create, src_url, update_after_clone)
85
83
86 @property
84 @property
87 def _empty(self):
85 def _empty(self):
88 """
86 """
89 Checks if repository is empty ie. without any changesets
87 Checks if repository is empty ie. without any changesets
90 """
88 """
91 # TODO: Following raises errors when using InMemoryChangeset...
89 # TODO: Following raises errors when using InMemoryChangeset...
92 # return len(self._repo.changelog) == 0
90 # return len(self._repo.changelog) == 0
93 return len(self.revisions) == 0
91 return len(self.revisions) == 0
94
92
95 @LazyProperty
93 @LazyProperty
96 def revisions(self):
94 def revisions(self):
97 """
95 """
98 Returns list of revisions' ids, in ascending order. Being lazy
96 Returns list of revisions' ids, in ascending order. Being lazy
99 attribute allows external tools to inject shas from cache.
97 attribute allows external tools to inject shas from cache.
100 """
98 """
101 return self._get_all_revisions()
99 return self._get_all_revisions()
102
100
103 @LazyProperty
101 @LazyProperty
104 def name(self):
102 def name(self):
105 return os.path.basename(self.path)
103 return os.path.basename(self.path)
106
104
107 @LazyProperty
105 @LazyProperty
108 def branches(self):
106 def branches(self):
109 return self._get_branches()
107 return self._get_branches()
110
108
111 @LazyProperty
109 @LazyProperty
112 def closed_branches(self):
110 def closed_branches(self):
113 return self._get_branches(normal=False, closed=True)
111 return self._get_branches(normal=False, closed=True)
114
112
115 @LazyProperty
113 @LazyProperty
116 def allbranches(self):
114 def allbranches(self):
117 """
115 """
118 List all branches, including closed branches.
116 List all branches, including closed branches.
119 """
117 """
120 return self._get_branches(closed=True)
118 return self._get_branches(closed=True)
121
119
122 def _get_branches(self, normal=True, closed=False):
120 def _get_branches(self, normal=True, closed=False):
123 """
121 """
124 Gets branches for this repository
122 Gets branches for this repository
125 Returns only not closed branches by default
123 Returns only not closed branches by default
126
124
127 :param closed: return also closed branches for mercurial
125 :param closed: return also closed branches for mercurial
128 :param normal: return also normal branches
126 :param normal: return also normal branches
129 """
127 """
130
128
131 if self._empty:
129 if self._empty:
132 return {}
130 return {}
133
131
134 bt = OrderedDict()
132 bt = OrderedDict()
135 for bn, _heads, node, isclosed in sorted(self._repo.branchmap().iterbranches()):
133 for bn, _heads, node, isclosed in sorted(self._repo.branchmap().iterbranches()):
136 if isclosed:
134 if isclosed:
137 if closed:
135 if closed:
138 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
136 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
139 else:
137 else:
140 if normal:
138 if normal:
141 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
139 bt[safe_str(bn)] = ascii_str(mercurial.node.hex(node))
142 return bt
140 return bt
143
141
144 @LazyProperty
142 @LazyProperty
145 def tags(self):
143 def tags(self):
146 """
144 """
147 Gets tags for this repository
145 Gets tags for this repository
148 """
146 """
149 return self._get_tags()
147 return self._get_tags()
150
148
151 def _get_tags(self):
149 def _get_tags(self):
152 if self._empty:
150 if self._empty:
153 return {}
151 return {}
154
152
155 return OrderedDict(sorted(
153 return OrderedDict(sorted(
156 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()),
154 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo.tags().items()),
157 reverse=True,
155 reverse=True,
158 key=lambda x: x[0], # sort by name
156 key=lambda x: x[0], # sort by name
159 ))
157 ))
160
158
161 def tag(self, name, user, revision=None, message=None, date=None,
159 def tag(self, name, user, revision=None, message=None, date=None,
162 **kwargs):
160 **kwargs):
163 """
161 """
164 Creates and returns a tag for the given ``revision``.
162 Creates and returns a tag for the given ``revision``.
165
163
166 :param name: name for new tag
164 :param name: name for new tag
167 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
165 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
168 :param revision: changeset id for which new tag would be created
166 :param revision: changeset id for which new tag would be created
169 :param message: message of the tag's commit
167 :param message: message of the tag's commit
170 :param date: date of tag's commit
168 :param date: date of tag's commit
171
169
172 :raises TagAlreadyExistError: if tag with same name already exists
170 :raises TagAlreadyExistError: if tag with same name already exists
173 """
171 """
174 if name in self.tags:
172 if name in self.tags:
175 raise TagAlreadyExistError("Tag %s already exists" % name)
173 raise TagAlreadyExistError("Tag %s already exists" % name)
176 changeset = self.get_changeset(revision)
174 changeset = self.get_changeset(revision)
177 local = kwargs.setdefault('local', False)
175 local = kwargs.setdefault('local', False)
178
176
179 if message is None:
177 if message is None:
180 message = "Added tag %s for changeset %s" % (name,
178 message = "Added tag %s for changeset %s" % (name,
181 changeset.short_id)
179 changeset.short_id)
182
180
183 if date is None:
181 if date is None:
184 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
182 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
185
183
186 try:
184 try:
187 mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date)
185 mercurial.tags.tag(self._repo, safe_bytes(name), changeset._ctx.node(), safe_bytes(message), local, safe_bytes(user), date)
188 except mercurial.error.Abort as e:
186 except mercurial.error.Abort as e:
189 raise RepositoryError(e.args[0])
187 raise RepositoryError(e.args[0])
190
188
191 # Reinitialize tags
189 # Reinitialize tags
192 self.tags = self._get_tags()
190 self.tags = self._get_tags()
193 tag_id = self.tags[name]
191 tag_id = self.tags[name]
194
192
195 return self.get_changeset(revision=tag_id)
193 return self.get_changeset(revision=tag_id)
196
194
197 def remove_tag(self, name, user, message=None, date=None):
195 def remove_tag(self, name, user, message=None, date=None):
198 """
196 """
199 Removes tag with the given ``name``.
197 Removes tag with the given ``name``.
200
198
201 :param name: name of the tag to be removed
199 :param name: name of the tag to be removed
202 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
200 :param user: full username, i.e.: "Joe Doe <joe.doe@example.com>"
203 :param message: message of the tag's removal commit
201 :param message: message of the tag's removal commit
204 :param date: date of tag's removal commit
202 :param date: date of tag's removal commit
205
203
206 :raises TagDoesNotExistError: if tag with given name does not exists
204 :raises TagDoesNotExistError: if tag with given name does not exists
207 """
205 """
208 if name not in self.tags:
206 if name not in self.tags:
209 raise TagDoesNotExistError("Tag %s does not exist" % name)
207 raise TagDoesNotExistError("Tag %s does not exist" % name)
210 if message is None:
208 if message is None:
211 message = "Removed tag %s" % name
209 message = "Removed tag %s" % name
212 if date is None:
210 if date is None:
213 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
211 date = safe_bytes(datetime.datetime.now().strftime('%a, %d %b %Y %H:%M:%S'))
214 local = False
212 local = False
215
213
216 try:
214 try:
217 mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.commands.nullid, safe_bytes(message), local, safe_bytes(user), date)
215 mercurial.tags.tag(self._repo, safe_bytes(name), mercurial.commands.nullid, safe_bytes(message), local, safe_bytes(user), date)
218 self.tags = self._get_tags()
216 self.tags = self._get_tags()
219 except mercurial.error.Abort as e:
217 except mercurial.error.Abort as e:
220 raise RepositoryError(e.args[0])
218 raise RepositoryError(e.args[0])
221
219
222 @LazyProperty
220 @LazyProperty
223 def bookmarks(self):
221 def bookmarks(self):
224 """
222 """
225 Gets bookmarks for this repository
223 Gets bookmarks for this repository
226 """
224 """
227 return self._get_bookmarks()
225 return self._get_bookmarks()
228
226
229 def _get_bookmarks(self):
227 def _get_bookmarks(self):
230 if self._empty:
228 if self._empty:
231 return {}
229 return {}
232
230
233 return OrderedDict(sorted(
231 return OrderedDict(sorted(
234 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo._bookmarks.items()),
232 ((safe_str(n), ascii_str(mercurial.node.hex(h))) for n, h in self._repo._bookmarks.items()),
235 reverse=True,
233 reverse=True,
236 key=lambda x: x[0], # sort by name
234 key=lambda x: x[0], # sort by name
237 ))
235 ))
238
236
239 def _get_all_revisions(self):
237 def _get_all_revisions(self):
240 return [ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs()]
238 return [ascii_str(self._repo[x].hex()) for x in self._repo.filtered(b'visible').changelog.revs()]
241
239
242 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
240 def get_diff(self, rev1, rev2, path='', ignore_whitespace=False,
243 context=3):
241 context=3):
244 """
242 """
245 Returns (git like) *diff*, as plain text. Shows changes introduced by
243 Returns (git like) *diff*, as plain text. Shows changes introduced by
246 ``rev2`` since ``rev1``.
244 ``rev2`` since ``rev1``.
247
245
248 :param rev1: Entry point from which diff is shown. Can be
246 :param rev1: Entry point from which diff is shown. Can be
249 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
247 ``self.EMPTY_CHANGESET`` - in this case, patch showing all
250 the changes since empty state of the repository until ``rev2``
248 the changes since empty state of the repository until ``rev2``
251 :param rev2: Until which revision changes should be shown.
249 :param rev2: Until which revision changes should be shown.
252 :param ignore_whitespace: If set to ``True``, would not show whitespace
250 :param ignore_whitespace: If set to ``True``, would not show whitespace
253 changes. Defaults to ``False``.
251 changes. Defaults to ``False``.
254 :param context: How many lines before/after changed lines should be
252 :param context: How many lines before/after changed lines should be
255 shown. Defaults to ``3``. If negative value is passed-in, it will be
253 shown. Defaults to ``3``. If negative value is passed-in, it will be
256 set to ``0`` instead.
254 set to ``0`` instead.
257 """
255 """
258
256
259 # Negative context values make no sense, and will result in
257 # Negative context values make no sense, and will result in
260 # errors. Ensure this does not happen.
258 # errors. Ensure this does not happen.
261 if context < 0:
259 if context < 0:
262 context = 0
260 context = 0
263
261
264 if hasattr(rev1, 'raw_id'):
262 if hasattr(rev1, 'raw_id'):
265 rev1 = getattr(rev1, 'raw_id')
263 rev1 = getattr(rev1, 'raw_id')
266
264
267 if hasattr(rev2, 'raw_id'):
265 if hasattr(rev2, 'raw_id'):
268 rev2 = getattr(rev2, 'raw_id')
266 rev2 = getattr(rev2, 'raw_id')
269
267
270 # Check if given revisions are present at repository (may raise
268 # Check if given revisions are present at repository (may raise
271 # ChangesetDoesNotExistError)
269 # ChangesetDoesNotExistError)
272 if rev1 != self.EMPTY_CHANGESET:
270 if rev1 != self.EMPTY_CHANGESET:
273 self.get_changeset(rev1)
271 self.get_changeset(rev1)
274 self.get_changeset(rev2)
272 self.get_changeset(rev2)
275 if path:
273 if path:
276 file_filter = mercurial.match.exact([safe_bytes(path)])
274 file_filter = mercurial.match.exact([safe_bytes(path)])
277 else:
275 else:
278 file_filter = None
276 file_filter = None
279
277
280 return b''.join(mercurial.patch.diff(self._repo, rev1, rev2, match=file_filter,
278 return b''.join(mercurial.patch.diff(self._repo, rev1, rev2, match=file_filter,
281 opts=mercurial.mdiff.diffopts(git=True,
279 opts=mercurial.mdiff.diffopts(git=True,
282 showfunc=True,
280 showfunc=True,
283 ignorews=ignore_whitespace,
281 ignorews=ignore_whitespace,
284 context=context)))
282 context=context)))
285
283
286 @classmethod
284 @classmethod
287 def _check_url(cls, url, repoui=None):
285 def _check_url(cls, url, repoui=None):
288 """
286 """
289 Function will check given url and try to verify if it's a valid
287 Function will check given url and try to verify if it's a valid
290 link. Sometimes it may happened that mercurial will issue basic
288 link. Sometimes it may happened that mercurial will issue basic
291 auth request that can cause whole API to hang when used from python
289 auth request that can cause whole API to hang when used from python
292 or other external calls.
290 or other external calls.
293
291
294 On failures it'll raise urllib2.HTTPError, exception is also thrown
292 On failures it'll raise urllib2.HTTPError, exception is also thrown
295 when the return code is non 200
293 when the return code is non 200
296 """
294 """
297 # check first if it's not an local url
295 # check first if it's not an local url
298 url = safe_bytes(url)
296 url = safe_bytes(url)
299 if os.path.isdir(url) or url.startswith(b'file:'):
297 if os.path.isdir(url) or url.startswith(b'file:'):
300 return True
298 return True
301
299
302 if url.startswith(b'ssh:'):
300 if url.startswith(b'ssh:'):
303 # in case of invalid uri or authentication issues, sshpeer will
301 # in case of invalid uri or authentication issues, sshpeer will
304 # throw an exception.
302 # throw an exception.
305 mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
303 mercurial.sshpeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
306 return True
304 return True
307
305
308 url_prefix = None
306 url_prefix = None
309 if b'+' in url[:url.find(b'://')]:
307 if b'+' in url[:url.find(b'://')]:
310 url_prefix, url = url.split(b'+', 1)
308 url_prefix, url = url.split(b'+', 1)
311
309
312 url_obj = mercurial.util.url(url)
310 url_obj = mercurial.util.url(url)
313 test_uri, handlers = get_urllib_request_handlers(url_obj)
311 test_uri, handlers = get_urllib_request_handlers(url_obj)
314
312
315 url_obj.passwd = b'*****'
313 url_obj.passwd = b'*****'
316 cleaned_uri = str(url_obj)
314 cleaned_uri = str(url_obj)
317
315
318 o = urllib.request.build_opener(*handlers)
316 o = urllib.request.build_opener(*handlers)
319 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
317 o.addheaders = [('Content-Type', 'application/mercurial-0.1'),
320 ('Accept', 'application/mercurial-0.1')]
318 ('Accept', 'application/mercurial-0.1')]
321
319
322 req = urllib.request.Request(
320 req = urllib.request.Request(
323 "%s?%s" % (
321 "%s?%s" % (
324 safe_str(test_uri),
322 safe_str(test_uri),
325 urllib.parse.urlencode({
323 urllib.parse.urlencode({
326 'cmd': 'between',
324 'cmd': 'between',
327 'pairs': "%s-%s" % ('0' * 40, '0' * 40),
325 'pairs': "%s-%s" % ('0' * 40, '0' * 40),
328 })
326 })
329 ))
327 ))
330
328
331 try:
329 try:
332 resp = o.open(req)
330 resp = o.open(req)
333 if resp.code != 200:
331 if resp.code != 200:
334 raise Exception('Return Code is not 200')
332 raise Exception('Return Code is not 200')
335 except Exception as e:
333 except Exception as e:
336 # means it cannot be cloned
334 # means it cannot be cloned
337 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
335 raise urllib.error.URLError("[%s] org_exc: %s" % (cleaned_uri, e))
338
336
339 if not url_prefix: # skip git+http://... etc
337 if not url_prefix: # skip git+http://... etc
340 # now check if it's a proper hg repo
338 # now check if it's a proper hg repo
341 try:
339 try:
342 mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
340 mercurial.httppeer.instance(repoui or mercurial.ui.ui(), url, False).lookup(b'tip')
343 except Exception as e:
341 except Exception as e:
344 raise urllib.error.URLError(
342 raise urllib.error.URLError(
345 "url [%s] does not look like an hg repo org_exc: %s"
343 "url [%s] does not look like an hg repo org_exc: %s"
346 % (cleaned_uri, e))
344 % (cleaned_uri, e))
347
345
348 return True
346 return True
349
347
350 def _get_repo(self, create, src_url=None, update_after_clone=False):
348 def _get_repo(self, create, src_url=None, update_after_clone=False):
351 """
349 """
352 Function will check for mercurial repository in given path and return
350 Function will check for mercurial repository in given path and return
353 a localrepo object. If there is no repository in that path it will
351 a localrepo object. If there is no repository in that path it will
354 raise an exception unless ``create`` parameter is set to True - in
352 raise an exception unless ``create`` parameter is set to True - in
355 that case repository would be created and returned.
353 that case repository would be created and returned.
356 If ``src_url`` is given, would try to clone repository from the
354 If ``src_url`` is given, would try to clone repository from the
357 location at given clone_point. Additionally it'll make update to
355 location at given clone_point. Additionally it'll make update to
358 working copy accordingly to ``update_after_clone`` flag
356 working copy accordingly to ``update_after_clone`` flag
359 """
357 """
360 try:
358 try:
361 if src_url:
359 if src_url:
362 url = safe_bytes(self._get_url(src_url))
360 url = safe_bytes(self._get_url(src_url))
363 opts = {}
361 opts = {}
364 if not update_after_clone:
362 if not update_after_clone:
365 opts.update({'noupdate': True})
363 opts.update({'noupdate': True})
366 MercurialRepository._check_url(url, self.baseui)
364 MercurialRepository._check_url(url, self.baseui)
367 mercurial.commands.clone(self.baseui, url, safe_bytes(self.path), **opts)
365 mercurial.commands.clone(self.baseui, url, safe_bytes(self.path), **opts)
368
366
369 # Don't try to create if we've already cloned repo
367 # Don't try to create if we've already cloned repo
370 create = False
368 create = False
371 return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create)
369 return mercurial.localrepo.instance(self.baseui, safe_bytes(self.path), create=create)
372 except (mercurial.error.Abort, mercurial.error.RepoError) as err:
370 except (mercurial.error.Abort, mercurial.error.RepoError) as err:
373 if create:
371 if create:
374 msg = "Cannot create repository at %s. Original error was %s" \
372 msg = "Cannot create repository at %s. Original error was %s" \
375 % (self.name, err)
373 % (self.name, err)
376 else:
374 else:
377 msg = "Not valid repository at %s. Original error was %s" \
375 msg = "Not valid repository at %s. Original error was %s" \
378 % (self.name, err)
376 % (self.name, err)
379 raise RepositoryError(msg)
377 raise RepositoryError(msg)
380
378
381 @LazyProperty
379 @LazyProperty
382 def in_memory_changeset(self):
380 def in_memory_changeset(self):
383 return MercurialInMemoryChangeset(self)
381 return inmemory.MercurialInMemoryChangeset(self)
384
382
385 @LazyProperty
383 @LazyProperty
386 def description(self):
384 def description(self):
387 _desc = self._repo.ui.config(b'web', b'description', None, untrusted=True)
385 _desc = self._repo.ui.config(b'web', b'description', None, untrusted=True)
388 return safe_str(_desc or b'unknown')
386 return safe_str(_desc or b'unknown')
389
387
390 @LazyProperty
388 @LazyProperty
391 def contact(self):
389 def contact(self):
392 return safe_str(mercurial.hgweb.common.get_contact(self._repo.ui.config)
390 return safe_str(mercurial.hgweb.common.get_contact(self._repo.ui.config)
393 or b'Unknown')
391 or b'Unknown')
394
392
395 @LazyProperty
393 @LazyProperty
396 def last_change(self):
394 def last_change(self):
397 """
395 """
398 Returns last change made on this repository as datetime object
396 Returns last change made on this repository as datetime object
399 """
397 """
400 return date_fromtimestamp(self._get_mtime(), makedate()[1])
398 return date_fromtimestamp(self._get_mtime(), makedate()[1])
401
399
402 def _get_mtime(self):
400 def _get_mtime(self):
403 try:
401 try:
404 return time.mktime(self.get_changeset().date.timetuple())
402 return time.mktime(self.get_changeset().date.timetuple())
405 except RepositoryError:
403 except RepositoryError:
406 # fallback to filesystem
404 # fallback to filesystem
407 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
405 cl_path = os.path.join(self.path, '.hg', "00changelog.i")
408 st_path = os.path.join(self.path, '.hg', "store")
406 st_path = os.path.join(self.path, '.hg', "store")
409 if os.path.exists(cl_path):
407 if os.path.exists(cl_path):
410 return os.stat(cl_path).st_mtime
408 return os.stat(cl_path).st_mtime
411 else:
409 else:
412 return os.stat(st_path).st_mtime
410 return os.stat(st_path).st_mtime
413
411
414 def _get_revision(self, revision):
412 def _get_revision(self, revision):
415 """
413 """
416 Given any revision identifier, returns a 40 char string with revision hash.
414 Given any revision identifier, returns a 40 char string with revision hash.
417
415
418 :param revision: str or int or None
416 :param revision: str or int or None
419 """
417 """
420 if self._empty:
418 if self._empty:
421 raise EmptyRepositoryError("There are no changesets yet")
419 raise EmptyRepositoryError("There are no changesets yet")
422
420
423 if revision in [-1, None]:
421 if revision in [-1, None]:
424 revision = b'tip'
422 revision = b'tip'
425 elif isinstance(revision, str):
423 elif isinstance(revision, str):
426 revision = safe_bytes(revision)
424 revision = safe_bytes(revision)
427
425
428 try:
426 try:
429 if isinstance(revision, int):
427 if isinstance(revision, int):
430 return ascii_str(self._repo[revision].hex())
428 return ascii_str(self._repo[revision].hex())
431 return ascii_str(mercurial.scmutil.revsymbol(self._repo, revision).hex())
429 return ascii_str(mercurial.scmutil.revsymbol(self._repo, revision).hex())
432 except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError):
430 except (IndexError, ValueError, mercurial.error.RepoLookupError, TypeError):
433 msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name)
431 msg = "Revision %r does not exist for %s" % (safe_str(revision), self.name)
434 raise ChangesetDoesNotExistError(msg)
432 raise ChangesetDoesNotExistError(msg)
435 except (LookupError, ):
433 except (LookupError, ):
436 msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name)
434 msg = "Ambiguous identifier `%s` for %s" % (safe_str(revision), self.name)
437 raise ChangesetDoesNotExistError(msg)
435 raise ChangesetDoesNotExistError(msg)
438
436
439 def get_ref_revision(self, ref_type, ref_name):
437 def get_ref_revision(self, ref_type, ref_name):
440 """
438 """
441 Returns revision number for the given reference.
439 Returns revision number for the given reference.
442 """
440 """
443 if ref_type == 'rev' and not ref_name.strip('0'):
441 if ref_type == 'rev' and not ref_name.strip('0'):
444 return self.EMPTY_CHANGESET
442 return self.EMPTY_CHANGESET
445 # lookup up the exact node id
443 # lookup up the exact node id
446 _revset_predicates = {
444 _revset_predicates = {
447 'branch': 'branch',
445 'branch': 'branch',
448 'book': 'bookmark',
446 'book': 'bookmark',
449 'tag': 'tag',
447 'tag': 'tag',
450 'rev': 'id',
448 'rev': 'id',
451 }
449 }
452 # avoid expensive branch(x) iteration over whole repo
450 # avoid expensive branch(x) iteration over whole repo
453 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
451 rev_spec = "%%s & %s(%%s)" % _revset_predicates[ref_type]
454 try:
452 try:
455 revs = self._repo.revs(rev_spec, ref_name, ref_name)
453 revs = self._repo.revs(rev_spec, ref_name, ref_name)
456 except LookupError:
454 except LookupError:
457 msg = "Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name)
455 msg = "Ambiguous identifier %s:%s for %s" % (ref_type, ref_name, self.name)
458 raise ChangesetDoesNotExistError(msg)
456 raise ChangesetDoesNotExistError(msg)
459 except mercurial.error.RepoLookupError:
457 except mercurial.error.RepoLookupError:
460 msg = "Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name)
458 msg = "Revision %s:%s does not exist for %s" % (ref_type, ref_name, self.name)
461 raise ChangesetDoesNotExistError(msg)
459 raise ChangesetDoesNotExistError(msg)
462 if revs:
460 if revs:
463 revision = revs.last()
461 revision = revs.last()
464 else:
462 else:
465 # TODO: just report 'not found'?
463 # TODO: just report 'not found'?
466 revision = ref_name
464 revision = ref_name
467
465
468 return self._get_revision(revision)
466 return self._get_revision(revision)
469
467
470 def _get_archives(self, archive_name='tip'):
468 def _get_archives(self, archive_name='tip'):
471 allowed = self.baseui.configlist(b"web", b"allow_archive",
469 allowed = self.baseui.configlist(b"web", b"allow_archive",
472 untrusted=True)
470 untrusted=True)
473 for name, ext in [(b'zip', '.zip'), (b'gz', '.tar.gz'), (b'bz2', '.tar.bz2')]:
471 for name, ext in [(b'zip', '.zip'), (b'gz', '.tar.gz'), (b'bz2', '.tar.bz2')]:
474 if name in allowed or self._repo.ui.configbool(b"web",
472 if name in allowed or self._repo.ui.configbool(b"web",
475 b"allow" + name,
473 b"allow" + name,
476 untrusted=True):
474 untrusted=True):
477 yield {"type": safe_str(name), "extension": ext, "node": archive_name}
475 yield {"type": safe_str(name), "extension": ext, "node": archive_name}
478
476
479 def _get_url(self, url):
477 def _get_url(self, url):
480 """
478 """
481 Returns normalized url. If schema is not given, fall back to
479 Returns normalized url. If schema is not given, fall back to
482 filesystem (``file:///``) schema.
480 filesystem (``file:///``) schema.
483 """
481 """
484 if url != 'default' and '://' not in url:
482 if url != 'default' and '://' not in url:
485 url = "file:" + urllib.request.pathname2url(url)
483 url = "file:" + urllib.request.pathname2url(url)
486 return url
484 return url
487
485
488 def get_changeset(self, revision=None):
486 def get_changeset(self, revision=None):
489 """
487 """
490 Returns ``MercurialChangeset`` object representing repository's
488 Returns ``MercurialChangeset`` object representing repository's
491 changeset at the given ``revision``.
489 changeset at the given ``revision``.
492 """
490 """
493 return MercurialChangeset(repository=self, revision=self._get_revision(revision))
491 return changeset.MercurialChangeset(repository=self, revision=self._get_revision(revision))
494
492
495 def get_changesets(self, start=None, end=None, start_date=None,
493 def get_changesets(self, start=None, end=None, start_date=None,
496 end_date=None, branch_name=None, reverse=False, max_revisions=None):
494 end_date=None, branch_name=None, reverse=False, max_revisions=None):
497 """
495 """
498 Returns iterator of ``MercurialChangeset`` objects from start to end
496 Returns iterator of ``MercurialChangeset`` objects from start to end
499 (both are inclusive)
497 (both are inclusive)
500
498
501 :param start: None, str, int or mercurial lookup format
499 :param start: None, str, int or mercurial lookup format
502 :param end: None, str, int or mercurial lookup format
500 :param end: None, str, int or mercurial lookup format
503 :param start_date:
501 :param start_date:
504 :param end_date:
502 :param end_date:
505 :param branch_name:
503 :param branch_name:
506 :param reversed: return changesets in reversed order
504 :param reversed: return changesets in reversed order
507 """
505 """
508 start_raw_id = self._get_revision(start)
506 start_raw_id = self._get_revision(start)
509 start_pos = None if start is None else self.revisions.index(start_raw_id)
507 start_pos = None if start is None else self.revisions.index(start_raw_id)
510 end_raw_id = self._get_revision(end)
508 end_raw_id = self._get_revision(end)
511 end_pos = None if end is None else self.revisions.index(end_raw_id)
509 end_pos = None if end is None else self.revisions.index(end_raw_id)
512
510
513 if start_pos is not None and end_pos is not None and start_pos > end_pos:
511 if start_pos is not None and end_pos is not None and start_pos > end_pos:
514 raise RepositoryError("Start revision '%s' cannot be "
512 raise RepositoryError("Start revision '%s' cannot be "
515 "after end revision '%s'" % (start, end))
513 "after end revision '%s'" % (start, end))
516
514
517 if branch_name and branch_name not in self.allbranches:
515 if branch_name and branch_name not in self.allbranches:
518 msg = "Branch %r not found in %s" % (branch_name, self.name)
516 msg = "Branch %r not found in %s" % (branch_name, self.name)
519 raise BranchDoesNotExistError(msg)
517 raise BranchDoesNotExistError(msg)
520 if end_pos is not None:
518 if end_pos is not None:
521 end_pos += 1
519 end_pos += 1
522 # filter branches
520 # filter branches
523 filter_ = []
521 filter_ = []
524 if branch_name:
522 if branch_name:
525 filter_.append(b'branch("%s")' % safe_bytes(branch_name))
523 filter_.append(b'branch("%s")' % safe_bytes(branch_name))
526 if start_date:
524 if start_date:
527 filter_.append(b'date(">%s")' % safe_bytes(str(start_date)))
525 filter_.append(b'date(">%s")' % safe_bytes(str(start_date)))
528 if end_date:
526 if end_date:
529 filter_.append(b'date("<%s")' % safe_bytes(str(end_date)))
527 filter_.append(b'date("<%s")' % safe_bytes(str(end_date)))
530 if filter_ or max_revisions:
528 if filter_ or max_revisions:
531 if filter_:
529 if filter_:
532 revspec = b' and '.join(filter_)
530 revspec = b' and '.join(filter_)
533 else:
531 else:
534 revspec = b'all()'
532 revspec = b'all()'
535 if max_revisions:
533 if max_revisions:
536 revspec = b'limit(%s, %d)' % (revspec, max_revisions)
534 revspec = b'limit(%s, %d)' % (revspec, max_revisions)
537 revisions = mercurial.scmutil.revrange(self._repo, [revspec])
535 revisions = mercurial.scmutil.revrange(self._repo, [revspec])
538 else:
536 else:
539 revisions = self.revisions
537 revisions = self.revisions
540
538
541 # this is very much a hack to turn this into a list; a better solution
539 # this is very much a hack to turn this into a list; a better solution
542 # would be to get rid of this function entirely and use revsets
540 # would be to get rid of this function entirely and use revsets
543 revs = list(revisions)[start_pos:end_pos]
541 revs = list(revisions)[start_pos:end_pos]
544 if reverse:
542 if reverse:
545 revs.reverse()
543 revs.reverse()
546
544
547 return CollectionGenerator(self, revs)
545 return CollectionGenerator(self, revs)
548
546
549 def get_diff_changesets(self, org_rev, other_repo, other_rev):
547 def get_diff_changesets(self, org_rev, other_repo, other_rev):
550 """
548 """
551 Returns lists of changesets that can be merged from this repo @org_rev
549 Returns lists of changesets that can be merged from this repo @org_rev
552 to other_repo @other_rev
550 to other_repo @other_rev
553 ... and the other way
551 ... and the other way
554 ... and the ancestors that would be used for merge
552 ... and the ancestors that would be used for merge
555
553
556 :param org_rev: the revision we want our compare to be made
554 :param org_rev: the revision we want our compare to be made
557 :param other_repo: repo object, most likely the fork of org_repo. It has
555 :param other_repo: repo object, most likely the fork of org_repo. It has
558 all changesets that we need to obtain
556 all changesets that we need to obtain
559 :param other_rev: revision we want out compare to be made on other_repo
557 :param other_rev: revision we want out compare to be made on other_repo
560 """
558 """
561 ancestors = None
559 ancestors = None
562 if org_rev == other_rev:
560 if org_rev == other_rev:
563 org_changesets = []
561 org_changesets = []
564 other_changesets = []
562 other_changesets = []
565
563
566 else:
564 else:
567 # case two independent repos
565 # case two independent repos
568 if self != other_repo:
566 if self != other_repo:
569 hgrepo = mercurial.unionrepo.makeunionrepository(other_repo.baseui,
567 hgrepo = mercurial.unionrepo.makeunionrepository(other_repo.baseui,
570 safe_bytes(other_repo.path),
568 safe_bytes(other_repo.path),
571 safe_bytes(self.path))
569 safe_bytes(self.path))
572 # all ancestors of other_rev will be in other_repo and
570 # all ancestors of other_rev will be in other_repo and
573 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
571 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
574
572
575 # no remote compare do it on the same repository
573 # no remote compare do it on the same repository
576 else:
574 else:
577 hgrepo = other_repo._repo
575 hgrepo = other_repo._repo
578
576
579 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
577 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
580 hgrepo.revs(b"id(%s) & ::id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev))]
578 hgrepo.revs(b"id(%s) & ::id(%s)", ascii_bytes(other_rev), ascii_bytes(org_rev))]
581 if ancestors:
579 if ancestors:
582 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
580 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
583 else:
581 else:
584 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
582 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
585 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
583 ancestors = [ascii_str(hgrepo[ancestor].hex()) for ancestor in
586 hgrepo.revs(b"heads(::id(%s) & ::id(%s))", ascii_bytes(org_rev), ascii_bytes(other_rev))] # FIXME: expensive!
584 hgrepo.revs(b"heads(::id(%s) & ::id(%s))", ascii_bytes(org_rev), ascii_bytes(other_rev))] # FIXME: expensive!
587
585
588 other_changesets = [
586 other_changesets = [
589 other_repo.get_changeset(rev)
587 other_repo.get_changeset(rev)
590 for rev in hgrepo.revs(
588 for rev in hgrepo.revs(
591 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
589 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
592 ascii_bytes(other_rev), ascii_bytes(org_rev), ascii_bytes(org_rev))
590 ascii_bytes(other_rev), ascii_bytes(org_rev), ascii_bytes(org_rev))
593 ]
591 ]
594 org_changesets = [
592 org_changesets = [
595 self.get_changeset(ascii_str(hgrepo[rev].hex()))
593 self.get_changeset(ascii_str(hgrepo[rev].hex()))
596 for rev in hgrepo.revs(
594 for rev in hgrepo.revs(
597 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
595 b"ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
598 ascii_bytes(org_rev), ascii_bytes(other_rev), ascii_bytes(other_rev))
596 ascii_bytes(org_rev), ascii_bytes(other_rev), ascii_bytes(other_rev))
599 ]
597 ]
600
598
601 return other_changesets, org_changesets, ancestors
599 return other_changesets, org_changesets, ancestors
602
600
603 def pull(self, url):
601 def pull(self, url):
604 """
602 """
605 Tries to pull changes from external location.
603 Tries to pull changes from external location.
606 """
604 """
607 other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url)))
605 other = mercurial.hg.peer(self._repo, {}, safe_bytes(self._get_url(url)))
608 try:
606 try:
609 mercurial.exchange.pull(self._repo, other, heads=None, force=None)
607 mercurial.exchange.pull(self._repo, other, heads=None, force=None)
610 except mercurial.error.Abort as err:
608 except mercurial.error.Abort as err:
611 # Propagate error but with vcs's type
609 # Propagate error but with vcs's type
612 raise RepositoryError(str(err))
610 raise RepositoryError(str(err))
613
611
614 @LazyProperty
612 @LazyProperty
615 def workdir(self):
613 def workdir(self):
616 """
614 """
617 Returns ``Workdir`` instance for this repository.
615 Returns ``Workdir`` instance for this repository.
618 """
616 """
619 return MercurialWorkdir(self)
617 return workdir.MercurialWorkdir(self)
620
618
621 def get_config_value(self, section, name=None, config_file=None):
619 def get_config_value(self, section, name=None, config_file=None):
622 """
620 """
623 Returns configuration value for a given [``section``] and ``name``.
621 Returns configuration value for a given [``section``] and ``name``.
624
622
625 :param section: Section we want to retrieve value from
623 :param section: Section we want to retrieve value from
626 :param name: Name of configuration we want to retrieve
624 :param name: Name of configuration we want to retrieve
627 :param config_file: A path to file which should be used to retrieve
625 :param config_file: A path to file which should be used to retrieve
628 configuration from (might also be a list of file paths)
626 configuration from (might also be a list of file paths)
629 """
627 """
630 if config_file is None:
628 if config_file is None:
631 config_file = []
629 config_file = []
632 elif isinstance(config_file, str):
630 elif isinstance(config_file, str):
633 config_file = [config_file]
631 config_file = [config_file]
634
632
635 config = self._repo.ui
633 config = self._repo.ui
636 if config_file:
634 if config_file:
637 config = mercurial.ui.ui()
635 config = mercurial.ui.ui()
638 for path in config_file:
636 for path in config_file:
639 config.readconfig(safe_bytes(path))
637 config.readconfig(safe_bytes(path))
640 value = config.config(safe_bytes(section), safe_bytes(name))
638 value = config.config(safe_bytes(section), safe_bytes(name))
641 return value if value is None else safe_str(value)
639 return value if value is None else safe_str(value)
642
640
643 def get_user_name(self, config_file=None):
641 def get_user_name(self, config_file=None):
644 """
642 """
645 Returns user's name from global configuration file.
643 Returns user's name from global configuration file.
646
644
647 :param config_file: A path to file which should be used to retrieve
645 :param config_file: A path to file which should be used to retrieve
648 configuration from (might also be a list of file paths)
646 configuration from (might also be a list of file paths)
649 """
647 """
650 username = self.get_config_value('ui', 'username', config_file=config_file)
648 username = self.get_config_value('ui', 'username', config_file=config_file)
651 if username:
649 if username:
652 return author_name(username)
650 return author_name(username)
653 return None
651 return None
654
652
655 def get_user_email(self, config_file=None):
653 def get_user_email(self, config_file=None):
656 """
654 """
657 Returns user's email from global configuration file.
655 Returns user's email from global configuration file.
658
656
659 :param config_file: A path to file which should be used to retrieve
657 :param config_file: A path to file which should be used to retrieve
660 configuration from (might also be a list of file paths)
658 configuration from (might also be a list of file paths)
661 """
659 """
662 username = self.get_config_value('ui', 'username', config_file=config_file)
660 username = self.get_config_value('ui', 'username', config_file=config_file)
663 if username:
661 if username:
664 return author_email(username)
662 return author_email(username)
665 return None
663 return None
@@ -1,243 +1,243 b''
1 """
1 """
2 Utilities aimed to help achieve mostly basic tasks.
2 Utilities aimed to help achieve mostly basic tasks.
3 """
3 """
4
4
5 import datetime
5 import datetime
6 import logging
6 import logging
7 import os
7 import os
8 import re
8 import re
9 import time
9 import time
10 import urllib.request
10 import urllib.request
11
11
12 import mercurial.url
12 import mercurial.url
13 from pygments import highlight
13 from pygments import highlight
14 from pygments.formatters import TerminalFormatter
14 from pygments.formatters import TerminalFormatter
15 from pygments.lexers import ClassNotFound, guess_lexer_for_filename
15 from pygments.lexers import ClassNotFound, guess_lexer_for_filename
16
16
17 from kallithea.lib.vcs import backends
17 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError
18 from kallithea.lib.vcs.exceptions import RepositoryError, VCSError
18 from kallithea.lib.vcs.utils import safe_str
19 from kallithea.lib.vcs.utils import safe_str
19 from kallithea.lib.vcs.utils.paths import abspath
20 from kallithea.lib.vcs.utils.paths import abspath
20
21
21
22
22 ALIASES = ['hg', 'git']
23 ALIASES = ['hg', 'git']
23
24
24
25
25 def get_scm(path, search_up=False, explicit_alias=None):
26 def get_scm(path, search_up=False, explicit_alias=None):
26 """
27 """
27 Returns one of alias from ``ALIASES`` (in order of precedence same as
28 Returns one of alias from ``ALIASES`` (in order of precedence same as
28 shortcuts given in ``ALIASES``) and top working dir path for the given
29 shortcuts given in ``ALIASES``) and top working dir path for the given
29 argument. If no scm-specific directory is found or more than one scm is
30 argument. If no scm-specific directory is found or more than one scm is
30 found at that directory, ``VCSError`` is raised.
31 found at that directory, ``VCSError`` is raised.
31
32
32 :param search_up: if set to ``True``, this function would try to
33 :param search_up: if set to ``True``, this function would try to
33 move up to parent directory every time no scm is recognized for the
34 move up to parent directory every time no scm is recognized for the
34 currently checked path. Default: ``False``.
35 currently checked path. Default: ``False``.
35 :param explicit_alias: can be one of available backend aliases, when given
36 :param explicit_alias: can be one of available backend aliases, when given
36 it will return given explicit alias in repositories under more than one
37 it will return given explicit alias in repositories under more than one
37 version control, if explicit_alias is different than found it will raise
38 version control, if explicit_alias is different than found it will raise
38 VCSError
39 VCSError
39 """
40 """
40 if not os.path.isdir(path):
41 if not os.path.isdir(path):
41 raise VCSError("Given path %s is not a directory" % path)
42 raise VCSError("Given path %s is not a directory" % path)
42
43
43 while True:
44 while True:
44 found_scms = [(scm, path) for scm in get_scms_for_path(path)]
45 found_scms = [(scm, path) for scm in get_scms_for_path(path)]
45 if found_scms or not search_up:
46 if found_scms or not search_up:
46 break
47 break
47 newpath = abspath(path, '..')
48 newpath = abspath(path, '..')
48 if newpath == path:
49 if newpath == path:
49 break
50 break
50 path = newpath
51 path = newpath
51
52
52 if len(found_scms) > 1:
53 if len(found_scms) > 1:
53 for scm in found_scms:
54 for scm in found_scms:
54 if scm[0] == explicit_alias:
55 if scm[0] == explicit_alias:
55 return scm
56 return scm
56 raise VCSError('More than one [%s] scm found at given path %s'
57 raise VCSError('More than one [%s] scm found at given path %s'
57 % (', '.join((x[0] for x in found_scms)), path))
58 % (', '.join((x[0] for x in found_scms)), path))
58
59
59 if len(found_scms) == 0:
60 if len(found_scms) == 0:
60 raise VCSError('No scm found at given path %s' % path)
61 raise VCSError('No scm found at given path %s' % path)
61
62
62 return found_scms[0]
63 return found_scms[0]
63
64
64
65
65 def get_scms_for_path(path):
66 def get_scms_for_path(path):
66 """
67 """
67 Returns all scm's found at the given path. If no scm is recognized
68 Returns all scm's found at the given path. If no scm is recognized
68 - empty list is returned.
69 - empty list is returned.
69
70
70 :param path: path to directory which should be checked. May be callable.
71 :param path: path to directory which should be checked. May be callable.
71
72
72 :raises VCSError: if given ``path`` is not a directory
73 :raises VCSError: if given ``path`` is not a directory
73 """
74 """
74 from kallithea.lib.vcs.backends import get_backend
75 if hasattr(path, '__call__'):
75 if hasattr(path, '__call__'):
76 path = path()
76 path = path()
77 if not os.path.isdir(path):
77 if not os.path.isdir(path):
78 raise VCSError("Given path %r is not a directory" % path)
78 raise VCSError("Given path %r is not a directory" % path)
79
79
80 result = []
80 result = []
81 for key in ALIASES:
81 for key in ALIASES:
82 # find .hg / .git
82 # find .hg / .git
83 dirname = os.path.join(path, '.' + key)
83 dirname = os.path.join(path, '.' + key)
84 if os.path.isdir(dirname):
84 if os.path.isdir(dirname):
85 result.append(key)
85 result.append(key)
86 continue
86 continue
87 # find rm__.hg / rm__.git too - left overs from old method for deleting
87 # find rm__.hg / rm__.git too - left overs from old method for deleting
88 dirname = os.path.join(path, 'rm__.' + key)
88 dirname = os.path.join(path, 'rm__.' + key)
89 if os.path.isdir(dirname):
89 if os.path.isdir(dirname):
90 return result
90 return result
91 # We still need to check if it's not bare repository as
91 # We still need to check if it's not bare repository as
92 # bare repos don't have working directories
92 # bare repos don't have working directories
93 try:
93 try:
94 get_backend(key)(path)
94 backends.get_backend(key)(path)
95 result.append(key)
95 result.append(key)
96 continue
96 continue
97 except RepositoryError:
97 except RepositoryError:
98 # Wrong backend
98 # Wrong backend
99 pass
99 pass
100 except VCSError:
100 except VCSError:
101 # No backend at all
101 # No backend at all
102 pass
102 pass
103 return result
103 return result
104
104
105
105
106 def get_highlighted_code(name, code, type='terminal'):
106 def get_highlighted_code(name, code, type='terminal'):
107 """
107 """
108 If pygments are available on the system
108 If pygments are available on the system
109 then returned output is colored. Otherwise
109 then returned output is colored. Otherwise
110 unchanged content is returned.
110 unchanged content is returned.
111 """
111 """
112 try:
112 try:
113 lexer = guess_lexer_for_filename(name, code)
113 lexer = guess_lexer_for_filename(name, code)
114 formatter = TerminalFormatter()
114 formatter = TerminalFormatter()
115 content = highlight(code, lexer, formatter)
115 content = highlight(code, lexer, formatter)
116 except ClassNotFound:
116 except ClassNotFound:
117 logging.debug("Couldn't guess Lexer, will not use pygments.")
117 logging.debug("Couldn't guess Lexer, will not use pygments.")
118 content = code
118 content = code
119 return content
119 return content
120
120
121
121
122 def parse_changesets(text):
122 def parse_changesets(text):
123 """
123 """
124 Returns dictionary with *start*, *main* and *end* ids.
124 Returns dictionary with *start*, *main* and *end* ids.
125
125
126 Examples::
126 Examples::
127
127
128 >>> parse_changesets('aaabbb')
128 >>> parse_changesets('aaabbb')
129 {'start': None, 'main': 'aaabbb', 'end': None}
129 {'start': None, 'main': 'aaabbb', 'end': None}
130 >>> parse_changesets('aaabbb..cccddd')
130 >>> parse_changesets('aaabbb..cccddd')
131 {'start': 'aaabbb', 'end': 'cccddd', 'main': None}
131 {'start': 'aaabbb', 'end': 'cccddd', 'main': None}
132
132
133 """
133 """
134 text = text.strip()
134 text = text.strip()
135 CID_RE = r'[a-zA-Z0-9]+'
135 CID_RE = r'[a-zA-Z0-9]+'
136 if '..' not in text:
136 if '..' not in text:
137 m = re.match(r'^(?P<cid>%s)$' % CID_RE, text)
137 m = re.match(r'^(?P<cid>%s)$' % CID_RE, text)
138 if m:
138 if m:
139 return {
139 return {
140 'start': None,
140 'start': None,
141 'main': text,
141 'main': text,
142 'end': None,
142 'end': None,
143 }
143 }
144 else:
144 else:
145 RE = r'^(?P<start>%s)?\.{2,3}(?P<end>%s)?$' % (CID_RE, CID_RE)
145 RE = r'^(?P<start>%s)?\.{2,3}(?P<end>%s)?$' % (CID_RE, CID_RE)
146 m = re.match(RE, text)
146 m = re.match(RE, text)
147 if m:
147 if m:
148 result = m.groupdict()
148 result = m.groupdict()
149 result['main'] = None
149 result['main'] = None
150 return result
150 return result
151 raise ValueError("IDs not recognized")
151 raise ValueError("IDs not recognized")
152
152
153
153
154 def parse_datetime(text):
154 def parse_datetime(text):
155 """
155 """
156 Parses given text and returns ``datetime.datetime`` instance or raises
156 Parses given text and returns ``datetime.datetime`` instance or raises
157 ``ValueError``.
157 ``ValueError``.
158
158
159 :param text: string of desired date/datetime or something more verbose,
159 :param text: string of desired date/datetime or something more verbose,
160 like *yesterday*, *2weeks 3days*, etc.
160 like *yesterday*, *2weeks 3days*, etc.
161 """
161 """
162
162
163 text = text.strip().lower()
163 text = text.strip().lower()
164
164
165 INPUT_FORMATS = (
165 INPUT_FORMATS = (
166 '%Y-%m-%d %H:%M:%S',
166 '%Y-%m-%d %H:%M:%S',
167 '%Y-%m-%d %H:%M',
167 '%Y-%m-%d %H:%M',
168 '%Y-%m-%d',
168 '%Y-%m-%d',
169 '%m/%d/%Y %H:%M:%S',
169 '%m/%d/%Y %H:%M:%S',
170 '%m/%d/%Y %H:%M',
170 '%m/%d/%Y %H:%M',
171 '%m/%d/%Y',
171 '%m/%d/%Y',
172 '%m/%d/%y %H:%M:%S',
172 '%m/%d/%y %H:%M:%S',
173 '%m/%d/%y %H:%M',
173 '%m/%d/%y %H:%M',
174 '%m/%d/%y',
174 '%m/%d/%y',
175 )
175 )
176 for format in INPUT_FORMATS:
176 for format in INPUT_FORMATS:
177 try:
177 try:
178 return datetime.datetime(*time.strptime(text, format)[:6])
178 return datetime.datetime(*time.strptime(text, format)[:6])
179 except ValueError:
179 except ValueError:
180 pass
180 pass
181
181
182 # Try descriptive texts
182 # Try descriptive texts
183 if text == 'tomorrow':
183 if text == 'tomorrow':
184 future = datetime.datetime.now() + datetime.timedelta(days=1)
184 future = datetime.datetime.now() + datetime.timedelta(days=1)
185 args = future.timetuple()[:3] + (23, 59, 59)
185 args = future.timetuple()[:3] + (23, 59, 59)
186 return datetime.datetime(*args)
186 return datetime.datetime(*args)
187 elif text == 'today':
187 elif text == 'today':
188 return datetime.datetime(*datetime.datetime.today().timetuple()[:3])
188 return datetime.datetime(*datetime.datetime.today().timetuple()[:3])
189 elif text == 'now':
189 elif text == 'now':
190 return datetime.datetime.now()
190 return datetime.datetime.now()
191 elif text == 'yesterday':
191 elif text == 'yesterday':
192 past = datetime.datetime.now() - datetime.timedelta(days=1)
192 past = datetime.datetime.now() - datetime.timedelta(days=1)
193 return datetime.datetime(*past.timetuple()[:3])
193 return datetime.datetime(*past.timetuple()[:3])
194 else:
194 else:
195 days = 0
195 days = 0
196 matched = re.match(
196 matched = re.match(
197 r'^((?P<weeks>\d+) ?w(eeks?)?)? ?((?P<days>\d+) ?d(ays?)?)?$', text)
197 r'^((?P<weeks>\d+) ?w(eeks?)?)? ?((?P<days>\d+) ?d(ays?)?)?$', text)
198 if matched:
198 if matched:
199 groupdict = matched.groupdict()
199 groupdict = matched.groupdict()
200 if groupdict['days']:
200 if groupdict['days']:
201 days += int(matched.groupdict()['days'])
201 days += int(matched.groupdict()['days'])
202 if groupdict['weeks']:
202 if groupdict['weeks']:
203 days += int(matched.groupdict()['weeks']) * 7
203 days += int(matched.groupdict()['weeks']) * 7
204 past = datetime.datetime.now() - datetime.timedelta(days=days)
204 past = datetime.datetime.now() - datetime.timedelta(days=days)
205 return datetime.datetime(*past.timetuple()[:3])
205 return datetime.datetime(*past.timetuple()[:3])
206
206
207 raise ValueError('Wrong date: "%s"' % text)
207 raise ValueError('Wrong date: "%s"' % text)
208
208
209
209
210 def get_dict_for_attrs(obj, attrs):
210 def get_dict_for_attrs(obj, attrs):
211 """
211 """
212 Returns dictionary for each attribute from given ``obj``.
212 Returns dictionary for each attribute from given ``obj``.
213 """
213 """
214 data = {}
214 data = {}
215 for attr in attrs:
215 for attr in attrs:
216 data[attr] = getattr(obj, attr)
216 data[attr] = getattr(obj, attr)
217 return data
217 return data
218
218
219 def get_urllib_request_handlers(url_obj):
219 def get_urllib_request_handlers(url_obj):
220 handlers = []
220 handlers = []
221 test_uri, authinfo = url_obj.authinfo()
221 test_uri, authinfo = url_obj.authinfo()
222
222
223 if authinfo:
223 if authinfo:
224 # authinfo is a tuple (realm, uris, user, password) where 'uris' itself
224 # authinfo is a tuple (realm, uris, user, password) where 'uris' itself
225 # is a tuple of URIs.
225 # is a tuple of URIs.
226 # If url_obj is obtained via mercurial.util.url, the obtained authinfo
226 # If url_obj is obtained via mercurial.util.url, the obtained authinfo
227 # values will be bytes, e.g.
227 # values will be bytes, e.g.
228 # (None, (b'http://127.0.0.1/repo', b'127.0.0.1'), b'user', b'pass')
228 # (None, (b'http://127.0.0.1/repo', b'127.0.0.1'), b'user', b'pass')
229 # However, urllib expects strings, not bytes, so we must convert them.
229 # However, urllib expects strings, not bytes, so we must convert them.
230
230
231 # create a password manager
231 # create a password manager
232 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
232 passmgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
233 passmgr.add_password(
233 passmgr.add_password(
234 safe_str(authinfo[0]) if authinfo[0] else None, # realm
234 safe_str(authinfo[0]) if authinfo[0] else None, # realm
235 tuple(safe_str(x) for x in authinfo[1]), # uris
235 tuple(safe_str(x) for x in authinfo[1]), # uris
236 safe_str(authinfo[2]), # user
236 safe_str(authinfo[2]), # user
237 safe_str(authinfo[3]), # password
237 safe_str(authinfo[3]), # password
238 )
238 )
239
239
240 handlers.extend((mercurial.url.httpbasicauthhandler(passmgr),
240 handlers.extend((mercurial.url.httpbasicauthhandler(passmgr),
241 mercurial.url.httpdigestauthhandler(passmgr)))
241 mercurial.url.httpdigestauthhandler(passmgr)))
242
242
243 return test_uri, handlers
243 return test_uri, handlers
@@ -1,2303 +1,2302 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.db
15 kallithea.model.db
16 ~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~
17
17
18 Database Models for Kallithea
18 Database Models for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 08, 2010
22 :created_on: Apr 08, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import base64
28 import base64
29 import collections
29 import collections
30 import datetime
30 import datetime
31 import functools
31 import functools
32 import hashlib
32 import hashlib
33 import logging
33 import logging
34 import os
34 import os
35 import time
35 import time
36 import traceback
36 import traceback
37
37
38 import ipaddr
38 import ipaddr
39 import sqlalchemy
39 import sqlalchemy
40 import urlobject
40 from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Index, Integer, LargeBinary, String, Unicode, UnicodeText, UniqueConstraint
41 from sqlalchemy import Boolean, Column, DateTime, Float, ForeignKey, Index, Integer, LargeBinary, String, Unicode, UnicodeText, UniqueConstraint
41 from sqlalchemy.ext.hybrid import hybrid_property
42 from sqlalchemy.ext.hybrid import hybrid_property
42 from sqlalchemy.orm import class_mapper, joinedload, relationship, validates
43 from sqlalchemy.orm import class_mapper, joinedload, relationship, validates
43 from tg.i18n import lazy_ugettext as _
44 from tg.i18n import lazy_ugettext as _
44 from webob.exc import HTTPNotFound
45 from webob.exc import HTTPNotFound
45
46
46 import kallithea
47 import kallithea
47 from kallithea.lib import ext_json, ssh, webutils
48 from kallithea.lib import ext_json, ssh, webutils
48 from kallithea.lib.exceptions import DefaultUserException
49 from kallithea.lib.exceptions import DefaultUserException
49 from kallithea.lib.utils2 import asbool, ascii_bytes, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_bytes, safe_int, safe_str, urlreadable
50 from kallithea.lib.utils2 import asbool, ascii_bytes, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_bytes, safe_int, safe_str, urlreadable
50 from kallithea.lib.vcs import get_backend, get_repo
51 from kallithea.lib.vcs import get_backend, get_repo
51 from kallithea.lib.vcs.backends.base import EmptyChangeset
52 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
52 from kallithea.lib.vcs.utils import author_email, author_name
53 from kallithea.lib.vcs.utils import author_email, author_name
53 from kallithea.lib.vcs.utils.helpers import get_scm
54 from kallithea.lib.vcs.utils.helpers import get_scm
54 from kallithea.model import meta
55 from kallithea.model import meta
55
56
56
57
57 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
58
59
59 #==============================================================================
60 #==============================================================================
60 # BASE CLASSES
61 # BASE CLASSES
61 #==============================================================================
62 #==============================================================================
62
63
63 class BaseDbModel(object):
64 class BaseDbModel(object):
64 """
65 """
65 Base Model for all classes
66 Base Model for all classes
66 """
67 """
67
68
68 @classmethod
69 @classmethod
69 def _get_keys(cls):
70 def _get_keys(cls):
70 """return column names for this model """
71 """return column names for this model """
71 # Note: not a normal dict - iterator gives "users.firstname", but keys gives "firstname"
72 # Note: not a normal dict - iterator gives "users.firstname", but keys gives "firstname"
72 return class_mapper(cls).c.keys()
73 return class_mapper(cls).c.keys()
73
74
74 def get_dict(self):
75 def get_dict(self):
75 """
76 """
76 return dict with keys and values corresponding
77 return dict with keys and values corresponding
77 to this model data """
78 to this model data """
78
79
79 d = {}
80 d = {}
80 for k in self._get_keys():
81 for k in self._get_keys():
81 d[k] = getattr(self, k)
82 d[k] = getattr(self, k)
82
83
83 # also use __json__() if present to get additional fields
84 # also use __json__() if present to get additional fields
84 _json_attr = getattr(self, '__json__', None)
85 _json_attr = getattr(self, '__json__', None)
85 if _json_attr:
86 if _json_attr:
86 # update with attributes from __json__
87 # update with attributes from __json__
87 if callable(_json_attr):
88 if callable(_json_attr):
88 _json_attr = _json_attr()
89 _json_attr = _json_attr()
89 for k, val in _json_attr.items():
90 for k, val in _json_attr.items():
90 d[k] = val
91 d[k] = val
91 return d
92 return d
92
93
93 def get_appstruct(self):
94 def get_appstruct(self):
94 """return list with keys and values tuples corresponding
95 """return list with keys and values tuples corresponding
95 to this model data """
96 to this model data """
96
97
97 return [
98 return [
98 (k, getattr(self, k))
99 (k, getattr(self, k))
99 for k in self._get_keys()
100 for k in self._get_keys()
100 ]
101 ]
101
102
102 def populate_obj(self, populate_dict):
103 def populate_obj(self, populate_dict):
103 """populate model with data from given populate_dict"""
104 """populate model with data from given populate_dict"""
104
105
105 for k in self._get_keys():
106 for k in self._get_keys():
106 if k in populate_dict:
107 if k in populate_dict:
107 setattr(self, k, populate_dict[k])
108 setattr(self, k, populate_dict[k])
108
109
109 @classmethod
110 @classmethod
110 def query(cls):
111 def query(cls):
111 return meta.Session().query(cls)
112 return meta.Session().query(cls)
112
113
113 @classmethod
114 @classmethod
114 def get(cls, id_):
115 def get(cls, id_):
115 if id_:
116 if id_:
116 return cls.query().get(id_)
117 return cls.query().get(id_)
117
118
118 @classmethod
119 @classmethod
119 def guess_instance(cls, value, callback=None):
120 def guess_instance(cls, value, callback=None):
120 """Haphazardly attempt to convert `value` to a `cls` instance.
121 """Haphazardly attempt to convert `value` to a `cls` instance.
121
122
122 If `value` is None or already a `cls` instance, return it. If `value`
123 If `value` is None or already a `cls` instance, return it. If `value`
123 is a number (or looks like one if you squint just right), assume it's
124 is a number (or looks like one if you squint just right), assume it's
124 a database primary key and let SQLAlchemy sort things out. Otherwise,
125 a database primary key and let SQLAlchemy sort things out. Otherwise,
125 fall back to resolving it using `callback` (if specified); this could
126 fall back to resolving it using `callback` (if specified); this could
126 e.g. be a function that looks up instances by name (though that won't
127 e.g. be a function that looks up instances by name (though that won't
127 work if the name begins with a digit). Otherwise, raise Exception.
128 work if the name begins with a digit). Otherwise, raise Exception.
128 """
129 """
129
130
130 if value is None:
131 if value is None:
131 return None
132 return None
132 if isinstance(value, cls):
133 if isinstance(value, cls):
133 return value
134 return value
134 if isinstance(value, int):
135 if isinstance(value, int):
135 return cls.get(value)
136 return cls.get(value)
136 if isinstance(value, str) and value.isdigit():
137 if isinstance(value, str) and value.isdigit():
137 return cls.get(int(value))
138 return cls.get(int(value))
138 if callback is not None:
139 if callback is not None:
139 return callback(value)
140 return callback(value)
140
141
141 raise Exception(
142 raise Exception(
142 'given object must be int, long or Instance of %s '
143 'given object must be int, long or Instance of %s '
143 'got %s, no callback provided' % (cls, type(value))
144 'got %s, no callback provided' % (cls, type(value))
144 )
145 )
145
146
146 @classmethod
147 @classmethod
147 def get_or_404(cls, id_):
148 def get_or_404(cls, id_):
148 try:
149 try:
149 id_ = int(id_)
150 id_ = int(id_)
150 except (TypeError, ValueError):
151 except (TypeError, ValueError):
151 raise HTTPNotFound
152 raise HTTPNotFound
152
153
153 res = cls.query().get(id_)
154 res = cls.query().get(id_)
154 if res is None:
155 if res is None:
155 raise HTTPNotFound
156 raise HTTPNotFound
156 return res
157 return res
157
158
158 @classmethod
159 @classmethod
159 def delete(cls, id_):
160 def delete(cls, id_):
160 obj = cls.query().get(id_)
161 obj = cls.query().get(id_)
161 meta.Session().delete(obj)
162 meta.Session().delete(obj)
162
163
163 def __repr__(self):
164 def __repr__(self):
164 return '<DB:%s>' % (self.__class__.__name__)
165 return '<DB:%s>' % (self.__class__.__name__)
165
166
166
167
167 _table_args_default_dict = {'extend_existing': True,
168 _table_args_default_dict = {'extend_existing': True,
168 'mysql_engine': 'InnoDB',
169 'mysql_engine': 'InnoDB',
169 'sqlite_autoincrement': True,
170 'sqlite_autoincrement': True,
170 }
171 }
171
172
172 class Setting(meta.Base, BaseDbModel):
173 class Setting(meta.Base, BaseDbModel):
173 __tablename__ = 'settings'
174 __tablename__ = 'settings'
174 __table_args__ = (
175 __table_args__ = (
175 _table_args_default_dict,
176 _table_args_default_dict,
176 )
177 )
177
178
178 SETTINGS_TYPES = {
179 SETTINGS_TYPES = {
179 'str': safe_bytes,
180 'str': safe_bytes,
180 'int': safe_int,
181 'int': safe_int,
181 'unicode': safe_str,
182 'unicode': safe_str,
182 'bool': asbool,
183 'bool': asbool,
183 'list': functools.partial(aslist, sep=',')
184 'list': functools.partial(aslist, sep=',')
184 }
185 }
185
186
186 app_settings_id = Column(Integer(), primary_key=True)
187 app_settings_id = Column(Integer(), primary_key=True)
187 app_settings_name = Column(String(255), nullable=False, unique=True)
188 app_settings_name = Column(String(255), nullable=False, unique=True)
188 _app_settings_value = Column("app_settings_value", Unicode(4096), nullable=False)
189 _app_settings_value = Column("app_settings_value", Unicode(4096), nullable=False)
189 _app_settings_type = Column("app_settings_type", String(255), nullable=True) # FIXME: not nullable?
190 _app_settings_type = Column("app_settings_type", String(255), nullable=True) # FIXME: not nullable?
190
191
191 def __init__(self, key='', val='', type='unicode'):
192 def __init__(self, key='', val='', type='unicode'):
192 self.app_settings_name = key
193 self.app_settings_name = key
193 self.app_settings_value = val
194 self.app_settings_value = val
194 self.app_settings_type = type
195 self.app_settings_type = type
195
196
196 @validates('_app_settings_value')
197 @validates('_app_settings_value')
197 def validate_settings_value(self, key, val):
198 def validate_settings_value(self, key, val):
198 assert isinstance(val, str)
199 assert isinstance(val, str)
199 return val
200 return val
200
201
201 @hybrid_property
202 @hybrid_property
202 def app_settings_value(self):
203 def app_settings_value(self):
203 v = self._app_settings_value
204 v = self._app_settings_value
204 _type = self.app_settings_type
205 _type = self.app_settings_type
205 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
206 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
206 return converter(v)
207 return converter(v)
207
208
208 @app_settings_value.setter
209 @app_settings_value.setter
209 def app_settings_value(self, val):
210 def app_settings_value(self, val):
210 """
211 """
211 Setter that will always make sure we use str in app_settings_value
212 Setter that will always make sure we use str in app_settings_value
212 """
213 """
213 self._app_settings_value = safe_str(val)
214 self._app_settings_value = safe_str(val)
214
215
215 @hybrid_property
216 @hybrid_property
216 def app_settings_type(self):
217 def app_settings_type(self):
217 return self._app_settings_type
218 return self._app_settings_type
218
219
219 @app_settings_type.setter
220 @app_settings_type.setter
220 def app_settings_type(self, val):
221 def app_settings_type(self, val):
221 if val not in self.SETTINGS_TYPES:
222 if val not in self.SETTINGS_TYPES:
222 raise Exception('type must be one of %s got %s'
223 raise Exception('type must be one of %s got %s'
223 % (list(self.SETTINGS_TYPES), val))
224 % (list(self.SETTINGS_TYPES), val))
224 self._app_settings_type = val
225 self._app_settings_type = val
225
226
226 def __repr__(self):
227 def __repr__(self):
227 return "<%s %s.%s=%r>" % (
228 return "<%s %s.%s=%r>" % (
228 self.__class__.__name__,
229 self.__class__.__name__,
229 self.app_settings_name, self.app_settings_type, self.app_settings_value
230 self.app_settings_name, self.app_settings_type, self.app_settings_value
230 )
231 )
231
232
232 @classmethod
233 @classmethod
233 def get_by_name(cls, key):
234 def get_by_name(cls, key):
234 return cls.query() \
235 return cls.query() \
235 .filter(cls.app_settings_name == key).scalar()
236 .filter(cls.app_settings_name == key).scalar()
236
237
237 @classmethod
238 @classmethod
238 def get_by_name_or_create(cls, key, val='', type='unicode'):
239 def get_by_name_or_create(cls, key, val='', type='unicode'):
239 res = cls.get_by_name(key)
240 res = cls.get_by_name(key)
240 if res is None:
241 if res is None:
241 res = cls(key, val, type)
242 res = cls(key, val, type)
242 return res
243 return res
243
244
244 @classmethod
245 @classmethod
245 def create_or_update(cls, key, val=None, type=None):
246 def create_or_update(cls, key, val=None, type=None):
246 """
247 """
247 Creates or updates Kallithea setting. If updates are triggered, it will only
248 Creates or updates Kallithea setting. If updates are triggered, it will only
248 update parameters that are explicitly set. 'None' values will be skipped.
249 update parameters that are explicitly set. 'None' values will be skipped.
249
250
250 :param key:
251 :param key:
251 :param val:
252 :param val:
252 :param type:
253 :param type:
253 :return:
254 :return:
254 """
255 """
255 res = cls.get_by_name(key)
256 res = cls.get_by_name(key)
256 if res is None:
257 if res is None:
257 # new setting
258 # new setting
258 val = val if val is not None else ''
259 val = val if val is not None else ''
259 type = type if type is not None else 'unicode'
260 type = type if type is not None else 'unicode'
260 res = cls(key, val, type)
261 res = cls(key, val, type)
261 meta.Session().add(res)
262 meta.Session().add(res)
262 else:
263 else:
263 if val is not None:
264 if val is not None:
264 # update if set
265 # update if set
265 res.app_settings_value = val
266 res.app_settings_value = val
266 if type is not None:
267 if type is not None:
267 # update if set
268 # update if set
268 res.app_settings_type = type
269 res.app_settings_type = type
269 return res
270 return res
270
271
271 @classmethod
272 @classmethod
272 def get_app_settings(cls):
273 def get_app_settings(cls):
273
274
274 ret = cls.query()
275 ret = cls.query()
275 if ret is None:
276 if ret is None:
276 raise Exception('Could not get application settings !')
277 raise Exception('Could not get application settings !')
277 settings = {}
278 settings = {}
278 for each in ret:
279 for each in ret:
279 settings[each.app_settings_name] = \
280 settings[each.app_settings_name] = \
280 each.app_settings_value
281 each.app_settings_value
281
282
282 return settings
283 return settings
283
284
284 @classmethod
285 @classmethod
285 def get_auth_settings(cls):
286 def get_auth_settings(cls):
286 ret = cls.query() \
287 ret = cls.query() \
287 .filter(cls.app_settings_name.startswith('auth_')).all()
288 .filter(cls.app_settings_name.startswith('auth_')).all()
288 fd = {}
289 fd = {}
289 for row in ret:
290 for row in ret:
290 fd[row.app_settings_name] = row.app_settings_value
291 fd[row.app_settings_name] = row.app_settings_value
291 return fd
292 return fd
292
293
293 @classmethod
294 @classmethod
294 def get_default_repo_settings(cls, strip_prefix=False):
295 def get_default_repo_settings(cls, strip_prefix=False):
295 ret = cls.query() \
296 ret = cls.query() \
296 .filter(cls.app_settings_name.startswith('default_')).all()
297 .filter(cls.app_settings_name.startswith('default_')).all()
297 fd = {}
298 fd = {}
298 for row in ret:
299 for row in ret:
299 key = row.app_settings_name
300 key = row.app_settings_name
300 if strip_prefix:
301 if strip_prefix:
301 key = remove_prefix(key, prefix='default_')
302 key = remove_prefix(key, prefix='default_')
302 fd.update({key: row.app_settings_value})
303 fd.update({key: row.app_settings_value})
303
304
304 return fd
305 return fd
305
306
306 @classmethod
307 @classmethod
307 def get_server_info(cls):
308 def get_server_info(cls):
308 import platform
309 import platform
309
310
310 import pkg_resources
311 import pkg_resources
311
312
312 from kallithea.lib.utils import check_git_version
313 from kallithea.lib.utils import check_git_version
313 mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
314 mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
314 info = {
315 info = {
315 'modules': sorted(mods, key=lambda k: k[0].lower()),
316 'modules': sorted(mods, key=lambda k: k[0].lower()),
316 'py_version': platform.python_version(),
317 'py_version': platform.python_version(),
317 'platform': platform.platform(),
318 'platform': platform.platform(),
318 'kallithea_version': kallithea.__version__,
319 'kallithea_version': kallithea.__version__,
319 'git_version': str(check_git_version()),
320 'git_version': str(check_git_version()),
320 'git_path': kallithea.CONFIG.get('git_path')
321 'git_path': kallithea.CONFIG.get('git_path')
321 }
322 }
322 return info
323 return info
323
324
324
325
325 class Ui(meta.Base, BaseDbModel):
326 class Ui(meta.Base, BaseDbModel):
326 __tablename__ = 'ui'
327 __tablename__ = 'ui'
327 __table_args__ = (
328 __table_args__ = (
328 Index('ui_ui_section_ui_key_idx', 'ui_section', 'ui_key'),
329 Index('ui_ui_section_ui_key_idx', 'ui_section', 'ui_key'),
329 UniqueConstraint('ui_section', 'ui_key'),
330 UniqueConstraint('ui_section', 'ui_key'),
330 _table_args_default_dict,
331 _table_args_default_dict,
331 )
332 )
332
333
333 HOOK_UPDATE = 'changegroup.update'
334 HOOK_UPDATE = 'changegroup.update'
334 HOOK_REPO_SIZE = 'changegroup.repo_size'
335 HOOK_REPO_SIZE = 'changegroup.repo_size'
335
336
336 ui_id = Column(Integer(), primary_key=True)
337 ui_id = Column(Integer(), primary_key=True)
337 ui_section = Column(String(255), nullable=False)
338 ui_section = Column(String(255), nullable=False)
338 ui_key = Column(String(255), nullable=False)
339 ui_key = Column(String(255), nullable=False)
339 ui_value = Column(String(255), nullable=True) # FIXME: not nullable?
340 ui_value = Column(String(255), nullable=True) # FIXME: not nullable?
340 ui_active = Column(Boolean(), nullable=False, default=True)
341 ui_active = Column(Boolean(), nullable=False, default=True)
341
342
342 @classmethod
343 @classmethod
343 def get_by_key(cls, section, key):
344 def get_by_key(cls, section, key):
344 """ Return specified Ui object, or None if not found. """
345 """ Return specified Ui object, or None if not found. """
345 return cls.query().filter_by(ui_section=section, ui_key=key).scalar()
346 return cls.query().filter_by(ui_section=section, ui_key=key).scalar()
346
347
347 @classmethod
348 @classmethod
348 def get_or_create(cls, section, key):
349 def get_or_create(cls, section, key):
349 """ Return specified Ui object, creating it if necessary. """
350 """ Return specified Ui object, creating it if necessary. """
350 setting = cls.get_by_key(section, key)
351 setting = cls.get_by_key(section, key)
351 if setting is None:
352 if setting is None:
352 setting = cls(ui_section=section, ui_key=key)
353 setting = cls(ui_section=section, ui_key=key)
353 meta.Session().add(setting)
354 meta.Session().add(setting)
354 return setting
355 return setting
355
356
356 @classmethod
357 @classmethod
357 def get_builtin_hooks(cls):
358 def get_builtin_hooks(cls):
358 q = cls.query()
359 q = cls.query()
359 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
360 q = q.filter(cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
360 q = q.filter(cls.ui_section == 'hooks')
361 q = q.filter(cls.ui_section == 'hooks')
361 q = q.order_by(cls.ui_section, cls.ui_key)
362 q = q.order_by(cls.ui_section, cls.ui_key)
362 return q.all()
363 return q.all()
363
364
364 @classmethod
365 @classmethod
365 def get_custom_hooks(cls):
366 def get_custom_hooks(cls):
366 q = cls.query()
367 q = cls.query()
367 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
368 q = q.filter(~cls.ui_key.in_([cls.HOOK_UPDATE, cls.HOOK_REPO_SIZE]))
368 q = q.filter(cls.ui_section == 'hooks')
369 q = q.filter(cls.ui_section == 'hooks')
369 q = q.order_by(cls.ui_section, cls.ui_key)
370 q = q.order_by(cls.ui_section, cls.ui_key)
370 return q.all()
371 return q.all()
371
372
372 @classmethod
373 @classmethod
373 def get_repos_location(cls):
374 def get_repos_location(cls):
374 return cls.get_by_key('paths', '/').ui_value
375 return cls.get_by_key('paths', '/').ui_value
375
376
376 @classmethod
377 @classmethod
377 def create_or_update_hook(cls, key, val):
378 def create_or_update_hook(cls, key, val):
378 new_ui = cls.get_or_create('hooks', key)
379 new_ui = cls.get_or_create('hooks', key)
379 new_ui.ui_active = True
380 new_ui.ui_active = True
380 new_ui.ui_value = val
381 new_ui.ui_value = val
381
382
382 def __repr__(self):
383 def __repr__(self):
383 return '<%s %s.%s=%r>' % (
384 return '<%s %s.%s=%r>' % (
384 self.__class__.__name__,
385 self.__class__.__name__,
385 self.ui_section, self.ui_key, self.ui_value)
386 self.ui_section, self.ui_key, self.ui_value)
386
387
387
388
388 class User(meta.Base, BaseDbModel):
389 class User(meta.Base, BaseDbModel):
389 __tablename__ = 'users'
390 __tablename__ = 'users'
390 __table_args__ = (
391 __table_args__ = (
391 Index('u_username_idx', 'username'),
392 Index('u_username_idx', 'username'),
392 Index('u_email_idx', 'email'),
393 Index('u_email_idx', 'email'),
393 _table_args_default_dict,
394 _table_args_default_dict,
394 )
395 )
395
396
396 DEFAULT_USER_NAME = 'default'
397 DEFAULT_USER_NAME = 'default'
397 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
398 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
398 # The name of the default auth type in extern_type, 'internal' lives in auth_internal.py
399 # The name of the default auth type in extern_type, 'internal' lives in auth_internal.py
399 DEFAULT_AUTH_TYPE = 'internal'
400 DEFAULT_AUTH_TYPE = 'internal'
400
401
401 user_id = Column(Integer(), primary_key=True)
402 user_id = Column(Integer(), primary_key=True)
402 username = Column(String(255), nullable=False, unique=True)
403 username = Column(String(255), nullable=False, unique=True)
403 password = Column(String(255), nullable=False)
404 password = Column(String(255), nullable=False)
404 active = Column(Boolean(), nullable=False, default=True)
405 active = Column(Boolean(), nullable=False, default=True)
405 admin = Column(Boolean(), nullable=False, default=False)
406 admin = Column(Boolean(), nullable=False, default=False)
406 name = Column("firstname", Unicode(255), nullable=False)
407 name = Column("firstname", Unicode(255), nullable=False)
407 lastname = Column(Unicode(255), nullable=False)
408 lastname = Column(Unicode(255), nullable=False)
408 _email = Column("email", String(255), nullable=True, unique=True) # FIXME: not nullable?
409 _email = Column("email", String(255), nullable=True, unique=True) # FIXME: not nullable?
409 last_login = Column(DateTime(timezone=False), nullable=True)
410 last_login = Column(DateTime(timezone=False), nullable=True)
410 extern_type = Column(String(255), nullable=True) # FIXME: not nullable?
411 extern_type = Column(String(255), nullable=True) # FIXME: not nullable?
411 extern_name = Column(String(255), nullable=True) # FIXME: not nullable?
412 extern_name = Column(String(255), nullable=True) # FIXME: not nullable?
412 api_key = Column(String(255), nullable=False)
413 api_key = Column(String(255), nullable=False)
413 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
414 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
414 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
415 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
415
416
416 user_log = relationship('UserLog')
417 user_log = relationship('UserLog')
417 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
418 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
418
419
419 repositories = relationship('Repository')
420 repositories = relationship('Repository')
420 repo_groups = relationship('RepoGroup')
421 repo_groups = relationship('RepoGroup')
421 user_groups = relationship('UserGroup')
422 user_groups = relationship('UserGroup')
422 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
423 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
423 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
424 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
424
425
425 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
426 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
426 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
427 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
427
428
428 group_member = relationship('UserGroupMember', cascade='all')
429 group_member = relationship('UserGroupMember', cascade='all')
429
430
430 # comments created by this user
431 # comments created by this user
431 user_comments = relationship('ChangesetComment', cascade='all')
432 user_comments = relationship('ChangesetComment', cascade='all')
432 # extra emails for this user
433 # extra emails for this user
433 user_emails = relationship('UserEmailMap', cascade='all')
434 user_emails = relationship('UserEmailMap', cascade='all')
434 # extra API keys
435 # extra API keys
435 user_api_keys = relationship('UserApiKeys', cascade='all')
436 user_api_keys = relationship('UserApiKeys', cascade='all')
436 ssh_keys = relationship('UserSshKeys', cascade='all')
437 ssh_keys = relationship('UserSshKeys', cascade='all')
437
438
438 @hybrid_property
439 @hybrid_property
439 def email(self):
440 def email(self):
440 return self._email
441 return self._email
441
442
442 @email.setter
443 @email.setter
443 def email(self, val):
444 def email(self, val):
444 self._email = val.lower() if val else None
445 self._email = val.lower() if val else None
445
446
446 @property
447 @property
447 def firstname(self):
448 def firstname(self):
448 # alias for future
449 # alias for future
449 return self.name
450 return self.name
450
451
451 @property
452 @property
452 def emails(self):
453 def emails(self):
453 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
454 other = UserEmailMap.query().filter(UserEmailMap.user == self).all()
454 return [self.email] + [x.email for x in other]
455 return [self.email] + [x.email for x in other]
455
456
456 @property
457 @property
457 def api_keys(self):
458 def api_keys(self):
458 other = UserApiKeys.query().filter(UserApiKeys.user == self).all()
459 other = UserApiKeys.query().filter(UserApiKeys.user == self).all()
459 return [self.api_key] + [x.api_key for x in other]
460 return [self.api_key] + [x.api_key for x in other]
460
461
461 @property
462 @property
462 def ip_addresses(self):
463 def ip_addresses(self):
463 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
464 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
464 return [x.ip_addr for x in ret]
465 return [x.ip_addr for x in ret]
465
466
466 @property
467 @property
467 def full_name(self):
468 def full_name(self):
468 return '%s %s' % (self.firstname, self.lastname)
469 return '%s %s' % (self.firstname, self.lastname)
469
470
470 @property
471 @property
471 def full_name_or_username(self):
472 def full_name_or_username(self):
472 """
473 """
473 Show full name.
474 Show full name.
474 If full name is not set, fall back to username.
475 If full name is not set, fall back to username.
475 """
476 """
476 return ('%s %s' % (self.firstname, self.lastname)
477 return ('%s %s' % (self.firstname, self.lastname)
477 if (self.firstname and self.lastname) else self.username)
478 if (self.firstname and self.lastname) else self.username)
478
479
479 @property
480 @property
480 def full_name_and_username(self):
481 def full_name_and_username(self):
481 """
482 """
482 Show full name and username as 'Firstname Lastname (username)'.
483 Show full name and username as 'Firstname Lastname (username)'.
483 If full name is not set, fall back to username.
484 If full name is not set, fall back to username.
484 """
485 """
485 return ('%s %s (%s)' % (self.firstname, self.lastname, self.username)
486 return ('%s %s (%s)' % (self.firstname, self.lastname, self.username)
486 if (self.firstname and self.lastname) else self.username)
487 if (self.firstname and self.lastname) else self.username)
487
488
488 @property
489 @property
489 def full_contact(self):
490 def full_contact(self):
490 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
491 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
491
492
492 @property
493 @property
493 def short_contact(self):
494 def short_contact(self):
494 return '%s %s' % (self.firstname, self.lastname)
495 return '%s %s' % (self.firstname, self.lastname)
495
496
496 @property
497 @property
497 def is_admin(self):
498 def is_admin(self):
498 return self.admin
499 return self.admin
499
500
500 @hybrid_property
501 @hybrid_property
501 def is_default_user(self):
502 def is_default_user(self):
502 return self.username == User.DEFAULT_USER_NAME
503 return self.username == User.DEFAULT_USER_NAME
503
504
504 @hybrid_property
505 @hybrid_property
505 def user_data(self):
506 def user_data(self):
506 if not self._user_data:
507 if not self._user_data:
507 return {}
508 return {}
508
509
509 try:
510 try:
510 return ext_json.loads(self._user_data)
511 return ext_json.loads(self._user_data)
511 except TypeError:
512 except TypeError:
512 return {}
513 return {}
513
514
514 @user_data.setter
515 @user_data.setter
515 def user_data(self, val):
516 def user_data(self, val):
516 try:
517 try:
517 self._user_data = ascii_bytes(ext_json.dumps(val))
518 self._user_data = ascii_bytes(ext_json.dumps(val))
518 except Exception:
519 except Exception:
519 log.error(traceback.format_exc())
520 log.error(traceback.format_exc())
520
521
521 def __repr__(self):
522 def __repr__(self):
522 return "<%s %s: %r>" % (self.__class__.__name__, self.user_id, self.username)
523 return "<%s %s: %r>" % (self.__class__.__name__, self.user_id, self.username)
523
524
524 @classmethod
525 @classmethod
525 def guess_instance(cls, value):
526 def guess_instance(cls, value):
526 return super(User, cls).guess_instance(value, User.get_by_username)
527 return super(User, cls).guess_instance(value, User.get_by_username)
527
528
528 @classmethod
529 @classmethod
529 def get_or_404(cls, id_, allow_default=True):
530 def get_or_404(cls, id_, allow_default=True):
530 '''
531 '''
531 Overridden version of BaseDbModel.get_or_404, with an extra check on
532 Overridden version of BaseDbModel.get_or_404, with an extra check on
532 the default user.
533 the default user.
533 '''
534 '''
534 user = super(User, cls).get_or_404(id_)
535 user = super(User, cls).get_or_404(id_)
535 if not allow_default and user.is_default_user:
536 if not allow_default and user.is_default_user:
536 raise DefaultUserException()
537 raise DefaultUserException()
537 return user
538 return user
538
539
539 @classmethod
540 @classmethod
540 def get_by_username_or_email(cls, username_or_email, case_insensitive=True):
541 def get_by_username_or_email(cls, username_or_email, case_insensitive=True):
541 """
542 """
542 For anything that looks like an email address, look up by the email address (matching
543 For anything that looks like an email address, look up by the email address (matching
543 case insensitively).
544 case insensitively).
544 For anything else, try to look up by the user name.
545 For anything else, try to look up by the user name.
545
546
546 This assumes no normal username can have '@' symbol.
547 This assumes no normal username can have '@' symbol.
547 """
548 """
548 if '@' in username_or_email:
549 if '@' in username_or_email:
549 return User.get_by_email(username_or_email)
550 return User.get_by_email(username_or_email)
550 else:
551 else:
551 return User.get_by_username(username_or_email, case_insensitive=case_insensitive)
552 return User.get_by_username(username_or_email, case_insensitive=case_insensitive)
552
553
553 @classmethod
554 @classmethod
554 def get_by_username(cls, username, case_insensitive=False):
555 def get_by_username(cls, username, case_insensitive=False):
555 if case_insensitive:
556 if case_insensitive:
556 q = cls.query().filter(sqlalchemy.func.lower(cls.username) == sqlalchemy.func.lower(username))
557 q = cls.query().filter(sqlalchemy.func.lower(cls.username) == sqlalchemy.func.lower(username))
557 else:
558 else:
558 q = cls.query().filter(cls.username == username)
559 q = cls.query().filter(cls.username == username)
559 return q.scalar()
560 return q.scalar()
560
561
561 @classmethod
562 @classmethod
562 def get_by_api_key(cls, api_key, fallback=True):
563 def get_by_api_key(cls, api_key, fallback=True):
563 if len(api_key) != 40 or not api_key.isalnum():
564 if len(api_key) != 40 or not api_key.isalnum():
564 return None
565 return None
565
566
566 q = cls.query().filter(cls.api_key == api_key)
567 q = cls.query().filter(cls.api_key == api_key)
567 res = q.scalar()
568 res = q.scalar()
568
569
569 if fallback and not res:
570 if fallback and not res:
570 # fallback to additional keys
571 # fallback to additional keys
571 _res = UserApiKeys.query().filter_by(api_key=api_key, is_expired=False).first()
572 _res = UserApiKeys.query().filter_by(api_key=api_key, is_expired=False).first()
572 if _res:
573 if _res:
573 res = _res.user
574 res = _res.user
574 if res is None or not res.active or res.is_default_user:
575 if res is None or not res.active or res.is_default_user:
575 return None
576 return None
576 return res
577 return res
577
578
578 @classmethod
579 @classmethod
579 def get_by_email(cls, email, cache=False):
580 def get_by_email(cls, email, cache=False):
580 q = cls.query().filter(sqlalchemy.func.lower(cls.email) == sqlalchemy.func.lower(email))
581 q = cls.query().filter(sqlalchemy.func.lower(cls.email) == sqlalchemy.func.lower(email))
581 ret = q.scalar()
582 ret = q.scalar()
582 if ret is None:
583 if ret is None:
583 q = UserEmailMap.query()
584 q = UserEmailMap.query()
584 # try fetching in alternate email map
585 # try fetching in alternate email map
585 q = q.filter(sqlalchemy.func.lower(UserEmailMap.email) == sqlalchemy.func.lower(email))
586 q = q.filter(sqlalchemy.func.lower(UserEmailMap.email) == sqlalchemy.func.lower(email))
586 q = q.options(joinedload(UserEmailMap.user))
587 q = q.options(joinedload(UserEmailMap.user))
587 ret = getattr(q.scalar(), 'user', None)
588 ret = getattr(q.scalar(), 'user', None)
588
589
589 return ret
590 return ret
590
591
591 @classmethod
592 @classmethod
592 def get_from_cs_author(cls, author):
593 def get_from_cs_author(cls, author):
593 """
594 """
594 Tries to get User objects out of commit author string
595 Tries to get User objects out of commit author string
595 """
596 """
596 # Valid email in the attribute passed, see if they're in the system
597 # Valid email in the attribute passed, see if they're in the system
597 _email = author_email(author)
598 _email = author_email(author)
598 if _email:
599 if _email:
599 user = cls.get_by_email(_email)
600 user = cls.get_by_email(_email)
600 if user is not None:
601 if user is not None:
601 return user
602 return user
602 # Maybe we can match by username?
603 # Maybe we can match by username?
603 _author = author_name(author)
604 _author = author_name(author)
604 user = cls.get_by_username(_author, case_insensitive=True)
605 user = cls.get_by_username(_author, case_insensitive=True)
605 if user is not None:
606 if user is not None:
606 return user
607 return user
607
608
608 def update_lastlogin(self):
609 def update_lastlogin(self):
609 """Update user lastlogin"""
610 """Update user lastlogin"""
610 self.last_login = datetime.datetime.now()
611 self.last_login = datetime.datetime.now()
611 log.debug('updated user %s lastlogin', self.username)
612 log.debug('updated user %s lastlogin', self.username)
612
613
613 @classmethod
614 @classmethod
614 def get_first_admin(cls):
615 def get_first_admin(cls):
615 user = User.query().filter(User.admin == True).first()
616 user = User.query().filter(User.admin == True).first()
616 if user is None:
617 if user is None:
617 raise Exception('Missing administrative account!')
618 raise Exception('Missing administrative account!')
618 return user
619 return user
619
620
620 @classmethod
621 @classmethod
621 def get_default_user(cls):
622 def get_default_user(cls):
622 user = User.get_by_username(User.DEFAULT_USER_NAME)
623 user = User.get_by_username(User.DEFAULT_USER_NAME)
623 if user is None:
624 if user is None:
624 raise Exception('Missing default account!')
625 raise Exception('Missing default account!')
625 return user
626 return user
626
627
627 def get_api_data(self, details=False):
628 def get_api_data(self, details=False):
628 """
629 """
629 Common function for generating user related data for API
630 Common function for generating user related data for API
630 """
631 """
631 user = self
632 user = self
632 data = dict(
633 data = dict(
633 user_id=user.user_id,
634 user_id=user.user_id,
634 username=user.username,
635 username=user.username,
635 firstname=user.name,
636 firstname=user.name,
636 lastname=user.lastname,
637 lastname=user.lastname,
637 email=user.email,
638 email=user.email,
638 emails=user.emails,
639 emails=user.emails,
639 active=user.active,
640 active=user.active,
640 admin=user.admin,
641 admin=user.admin,
641 )
642 )
642 if details:
643 if details:
643 data.update(dict(
644 data.update(dict(
644 extern_type=user.extern_type,
645 extern_type=user.extern_type,
645 extern_name=user.extern_name,
646 extern_name=user.extern_name,
646 api_key=user.api_key,
647 api_key=user.api_key,
647 api_keys=user.api_keys,
648 api_keys=user.api_keys,
648 last_login=user.last_login,
649 last_login=user.last_login,
649 ip_addresses=user.ip_addresses
650 ip_addresses=user.ip_addresses
650 ))
651 ))
651 return data
652 return data
652
653
653 def __json__(self):
654 def __json__(self):
654 data = dict(
655 data = dict(
655 full_name=self.full_name,
656 full_name=self.full_name,
656 full_name_or_username=self.full_name_or_username,
657 full_name_or_username=self.full_name_or_username,
657 short_contact=self.short_contact,
658 short_contact=self.short_contact,
658 full_contact=self.full_contact
659 full_contact=self.full_contact
659 )
660 )
660 data.update(self.get_api_data())
661 data.update(self.get_api_data())
661 return data
662 return data
662
663
663
664
664 class UserApiKeys(meta.Base, BaseDbModel):
665 class UserApiKeys(meta.Base, BaseDbModel):
665 __tablename__ = 'user_api_keys'
666 __tablename__ = 'user_api_keys'
666 __table_args__ = (
667 __table_args__ = (
667 Index('uak_api_key_idx', 'api_key'),
668 Index('uak_api_key_idx', 'api_key'),
668 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
669 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
669 _table_args_default_dict,
670 _table_args_default_dict,
670 )
671 )
671
672
672 user_api_key_id = Column(Integer(), primary_key=True)
673 user_api_key_id = Column(Integer(), primary_key=True)
673 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
674 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
674 api_key = Column(String(255), nullable=False, unique=True)
675 api_key = Column(String(255), nullable=False, unique=True)
675 description = Column(UnicodeText(), nullable=False)
676 description = Column(UnicodeText(), nullable=False)
676 expires = Column(Float(53), nullable=False)
677 expires = Column(Float(53), nullable=False)
677 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
678 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
678
679
679 user = relationship('User')
680 user = relationship('User')
680
681
681 @hybrid_property
682 @hybrid_property
682 def is_expired(self):
683 def is_expired(self):
683 return (self.expires != -1) & (time.time() > self.expires)
684 return (self.expires != -1) & (time.time() > self.expires)
684
685
685
686
686 class UserEmailMap(meta.Base, BaseDbModel):
687 class UserEmailMap(meta.Base, BaseDbModel):
687 __tablename__ = 'user_email_map'
688 __tablename__ = 'user_email_map'
688 __table_args__ = (
689 __table_args__ = (
689 Index('uem_email_idx', 'email'),
690 Index('uem_email_idx', 'email'),
690 _table_args_default_dict,
691 _table_args_default_dict,
691 )
692 )
692
693
693 email_id = Column(Integer(), primary_key=True)
694 email_id = Column(Integer(), primary_key=True)
694 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
695 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
695 _email = Column("email", String(255), nullable=False, unique=True)
696 _email = Column("email", String(255), nullable=False, unique=True)
696 user = relationship('User')
697 user = relationship('User')
697
698
698 @validates('_email')
699 @validates('_email')
699 def validate_email(self, key, email):
700 def validate_email(self, key, email):
700 # check if this email is not main one
701 # check if this email is not main one
701 main_email = meta.Session().query(User).filter(User.email == email).scalar()
702 main_email = meta.Session().query(User).filter(User.email == email).scalar()
702 if main_email is not None:
703 if main_email is not None:
703 raise AttributeError('email %s is present is user table' % email)
704 raise AttributeError('email %s is present is user table' % email)
704 return email
705 return email
705
706
706 @hybrid_property
707 @hybrid_property
707 def email(self):
708 def email(self):
708 return self._email
709 return self._email
709
710
710 @email.setter
711 @email.setter
711 def email(self, val):
712 def email(self, val):
712 self._email = val.lower() if val else None
713 self._email = val.lower() if val else None
713
714
714
715
715 class UserIpMap(meta.Base, BaseDbModel):
716 class UserIpMap(meta.Base, BaseDbModel):
716 __tablename__ = 'user_ip_map'
717 __tablename__ = 'user_ip_map'
717 __table_args__ = (
718 __table_args__ = (
718 UniqueConstraint('user_id', 'ip_addr'),
719 UniqueConstraint('user_id', 'ip_addr'),
719 _table_args_default_dict,
720 _table_args_default_dict,
720 )
721 )
721
722
722 ip_id = Column(Integer(), primary_key=True)
723 ip_id = Column(Integer(), primary_key=True)
723 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
724 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
724 ip_addr = Column(String(255), nullable=False)
725 ip_addr = Column(String(255), nullable=False)
725 active = Column(Boolean(), nullable=False, default=True)
726 active = Column(Boolean(), nullable=False, default=True)
726 user = relationship('User')
727 user = relationship('User')
727
728
728 @classmethod
729 @classmethod
729 def _get_ip_range(cls, ip_addr):
730 def _get_ip_range(cls, ip_addr):
730 net = ipaddr.IPNetwork(address=ip_addr)
731 net = ipaddr.IPNetwork(address=ip_addr)
731 return [str(net.network), str(net.broadcast)]
732 return [str(net.network), str(net.broadcast)]
732
733
733 def __json__(self):
734 def __json__(self):
734 return dict(
735 return dict(
735 ip_addr=self.ip_addr,
736 ip_addr=self.ip_addr,
736 ip_range=self._get_ip_range(self.ip_addr)
737 ip_range=self._get_ip_range(self.ip_addr)
737 )
738 )
738
739
739 def __repr__(self):
740 def __repr__(self):
740 return "<%s %s: %s>" % (self.__class__.__name__, self.user_id, self.ip_addr)
741 return "<%s %s: %s>" % (self.__class__.__name__, self.user_id, self.ip_addr)
741
742
742
743
743 class UserLog(meta.Base, BaseDbModel):
744 class UserLog(meta.Base, BaseDbModel):
744 __tablename__ = 'user_logs'
745 __tablename__ = 'user_logs'
745 __table_args__ = (
746 __table_args__ = (
746 _table_args_default_dict,
747 _table_args_default_dict,
747 )
748 )
748
749
749 user_log_id = Column(Integer(), primary_key=True)
750 user_log_id = Column(Integer(), primary_key=True)
750 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
751 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
751 username = Column(String(255), nullable=False)
752 username = Column(String(255), nullable=False)
752 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
753 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
753 repository_name = Column(Unicode(255), nullable=False)
754 repository_name = Column(Unicode(255), nullable=False)
754 user_ip = Column(String(255), nullable=True)
755 user_ip = Column(String(255), nullable=True)
755 action = Column(UnicodeText(), nullable=False)
756 action = Column(UnicodeText(), nullable=False)
756 action_date = Column(DateTime(timezone=False), nullable=False)
757 action_date = Column(DateTime(timezone=False), nullable=False)
757
758
758 def __repr__(self):
759 def __repr__(self):
759 return "<%s %r: %r>" % (self.__class__.__name__,
760 return "<%s %r: %r>" % (self.__class__.__name__,
760 self.repository_name,
761 self.repository_name,
761 self.action)
762 self.action)
762
763
763 @property
764 @property
764 def action_as_day(self):
765 def action_as_day(self):
765 return datetime.date(*self.action_date.timetuple()[:3])
766 return datetime.date(*self.action_date.timetuple()[:3])
766
767
767 user = relationship('User')
768 user = relationship('User')
768 repository = relationship('Repository', cascade='')
769 repository = relationship('Repository', cascade='')
769
770
770
771
771 class UserGroup(meta.Base, BaseDbModel):
772 class UserGroup(meta.Base, BaseDbModel):
772 __tablename__ = 'users_groups'
773 __tablename__ = 'users_groups'
773 __table_args__ = (
774 __table_args__ = (
774 _table_args_default_dict,
775 _table_args_default_dict,
775 )
776 )
776
777
777 users_group_id = Column(Integer(), primary_key=True)
778 users_group_id = Column(Integer(), primary_key=True)
778 users_group_name = Column(Unicode(255), nullable=False, unique=True)
779 users_group_name = Column(Unicode(255), nullable=False, unique=True)
779 user_group_description = Column(Unicode(10000), nullable=True) # FIXME: not nullable?
780 user_group_description = Column(Unicode(10000), nullable=True) # FIXME: not nullable?
780 users_group_active = Column(Boolean(), nullable=False)
781 users_group_active = Column(Boolean(), nullable=False)
781 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
782 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
782 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
783 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
783 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
784 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
784
785
785 members = relationship('UserGroupMember', cascade="all, delete-orphan")
786 members = relationship('UserGroupMember', cascade="all, delete-orphan")
786 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
787 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
787 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
788 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
788 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
789 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
789 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
790 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
790 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
791 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
791
792
792 owner = relationship('User')
793 owner = relationship('User')
793
794
794 @hybrid_property
795 @hybrid_property
795 def group_data(self):
796 def group_data(self):
796 if not self._group_data:
797 if not self._group_data:
797 return {}
798 return {}
798
799
799 try:
800 try:
800 return ext_json.loads(self._group_data)
801 return ext_json.loads(self._group_data)
801 except TypeError:
802 except TypeError:
802 return {}
803 return {}
803
804
804 @group_data.setter
805 @group_data.setter
805 def group_data(self, val):
806 def group_data(self, val):
806 try:
807 try:
807 self._group_data = ascii_bytes(ext_json.dumps(val))
808 self._group_data = ascii_bytes(ext_json.dumps(val))
808 except Exception:
809 except Exception:
809 log.error(traceback.format_exc())
810 log.error(traceback.format_exc())
810
811
811 def __repr__(self):
812 def __repr__(self):
812 return "<%s %s: %r>" % (self.__class__.__name__,
813 return "<%s %s: %r>" % (self.__class__.__name__,
813 self.users_group_id,
814 self.users_group_id,
814 self.users_group_name)
815 self.users_group_name)
815
816
816 @classmethod
817 @classmethod
817 def guess_instance(cls, value):
818 def guess_instance(cls, value):
818 return super(UserGroup, cls).guess_instance(value, UserGroup.get_by_group_name)
819 return super(UserGroup, cls).guess_instance(value, UserGroup.get_by_group_name)
819
820
820 @classmethod
821 @classmethod
821 def get_by_group_name(cls, group_name, case_insensitive=False):
822 def get_by_group_name(cls, group_name, case_insensitive=False):
822 if case_insensitive:
823 if case_insensitive:
823 q = cls.query().filter(sqlalchemy.func.lower(cls.users_group_name) == sqlalchemy.func.lower(group_name))
824 q = cls.query().filter(sqlalchemy.func.lower(cls.users_group_name) == sqlalchemy.func.lower(group_name))
824 else:
825 else:
825 q = cls.query().filter(cls.users_group_name == group_name)
826 q = cls.query().filter(cls.users_group_name == group_name)
826 return q.scalar()
827 return q.scalar()
827
828
828 @classmethod
829 @classmethod
829 def get(cls, user_group_id):
830 def get(cls, user_group_id):
830 user_group = cls.query()
831 user_group = cls.query()
831 return user_group.get(user_group_id)
832 return user_group.get(user_group_id)
832
833
833 def get_api_data(self, with_members=True):
834 def get_api_data(self, with_members=True):
834 user_group = self
835 user_group = self
835
836
836 data = dict(
837 data = dict(
837 users_group_id=user_group.users_group_id,
838 users_group_id=user_group.users_group_id,
838 group_name=user_group.users_group_name,
839 group_name=user_group.users_group_name,
839 group_description=user_group.user_group_description,
840 group_description=user_group.user_group_description,
840 active=user_group.users_group_active,
841 active=user_group.users_group_active,
841 owner=user_group.owner.username,
842 owner=user_group.owner.username,
842 )
843 )
843 if with_members:
844 if with_members:
844 data['members'] = [
845 data['members'] = [
845 ugm.user.get_api_data()
846 ugm.user.get_api_data()
846 for ugm in user_group.members
847 for ugm in user_group.members
847 ]
848 ]
848
849
849 return data
850 return data
850
851
851
852
852 class UserGroupMember(meta.Base, BaseDbModel):
853 class UserGroupMember(meta.Base, BaseDbModel):
853 __tablename__ = 'users_groups_members'
854 __tablename__ = 'users_groups_members'
854 __table_args__ = (
855 __table_args__ = (
855 _table_args_default_dict,
856 _table_args_default_dict,
856 )
857 )
857
858
858 users_group_member_id = Column(Integer(), primary_key=True)
859 users_group_member_id = Column(Integer(), primary_key=True)
859 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
860 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
860 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
861 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
861
862
862 user = relationship('User')
863 user = relationship('User')
863 users_group = relationship('UserGroup')
864 users_group = relationship('UserGroup')
864
865
865 def __init__(self, gr_id='', u_id=''):
866 def __init__(self, gr_id='', u_id=''):
866 self.users_group_id = gr_id
867 self.users_group_id = gr_id
867 self.user_id = u_id
868 self.user_id = u_id
868
869
869
870
870 class RepositoryField(meta.Base, BaseDbModel):
871 class RepositoryField(meta.Base, BaseDbModel):
871 __tablename__ = 'repositories_fields'
872 __tablename__ = 'repositories_fields'
872 __table_args__ = (
873 __table_args__ = (
873 UniqueConstraint('repository_id', 'field_key'), # no-multi field
874 UniqueConstraint('repository_id', 'field_key'), # no-multi field
874 _table_args_default_dict,
875 _table_args_default_dict,
875 )
876 )
876
877
877 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
878 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
878
879
879 repo_field_id = Column(Integer(), primary_key=True)
880 repo_field_id = Column(Integer(), primary_key=True)
880 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
881 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
881 field_key = Column(String(250), nullable=False)
882 field_key = Column(String(250), nullable=False)
882 field_label = Column(String(1024), nullable=False)
883 field_label = Column(String(1024), nullable=False)
883 field_value = Column(String(10000), nullable=False)
884 field_value = Column(String(10000), nullable=False)
884 field_desc = Column(String(1024), nullable=False)
885 field_desc = Column(String(1024), nullable=False)
885 field_type = Column(String(255), nullable=False)
886 field_type = Column(String(255), nullable=False)
886 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
887 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
887
888
888 repository = relationship('Repository')
889 repository = relationship('Repository')
889
890
890 @property
891 @property
891 def field_key_prefixed(self):
892 def field_key_prefixed(self):
892 return 'ex_%s' % self.field_key
893 return 'ex_%s' % self.field_key
893
894
894 @classmethod
895 @classmethod
895 def un_prefix_key(cls, key):
896 def un_prefix_key(cls, key):
896 if key.startswith(cls.PREFIX):
897 if key.startswith(cls.PREFIX):
897 return key[len(cls.PREFIX):]
898 return key[len(cls.PREFIX):]
898 return key
899 return key
899
900
900 @classmethod
901 @classmethod
901 def get_by_key_name(cls, key, repo):
902 def get_by_key_name(cls, key, repo):
902 row = cls.query() \
903 row = cls.query() \
903 .filter(cls.repository == repo) \
904 .filter(cls.repository == repo) \
904 .filter(cls.field_key == key).scalar()
905 .filter(cls.field_key == key).scalar()
905 return row
906 return row
906
907
907
908
908 class Repository(meta.Base, BaseDbModel):
909 class Repository(meta.Base, BaseDbModel):
909 __tablename__ = 'repositories'
910 __tablename__ = 'repositories'
910 __table_args__ = (
911 __table_args__ = (
911 Index('r_repo_name_idx', 'repo_name'),
912 Index('r_repo_name_idx', 'repo_name'),
912 _table_args_default_dict,
913 _table_args_default_dict,
913 )
914 )
914
915
915 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
916 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
916 DEFAULT_CLONE_SSH = 'ssh://{system_user}@{hostname}/{repo}'
917 DEFAULT_CLONE_SSH = 'ssh://{system_user}@{hostname}/{repo}'
917
918
918 STATE_CREATED = 'repo_state_created'
919 STATE_CREATED = 'repo_state_created'
919 STATE_PENDING = 'repo_state_pending'
920 STATE_PENDING = 'repo_state_pending'
920 STATE_ERROR = 'repo_state_error'
921 STATE_ERROR = 'repo_state_error'
921
922
922 repo_id = Column(Integer(), primary_key=True)
923 repo_id = Column(Integer(), primary_key=True)
923 repo_name = Column(Unicode(255), nullable=False, unique=True)
924 repo_name = Column(Unicode(255), nullable=False, unique=True)
924 repo_state = Column(String(255), nullable=False)
925 repo_state = Column(String(255), nullable=False)
925
926
926 clone_uri = Column(String(255), nullable=True) # FIXME: not nullable?
927 clone_uri = Column(String(255), nullable=True) # FIXME: not nullable?
927 repo_type = Column(String(255), nullable=False) # 'hg' or 'git'
928 repo_type = Column(String(255), nullable=False) # 'hg' or 'git'
928 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
929 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
929 private = Column(Boolean(), nullable=False)
930 private = Column(Boolean(), nullable=False)
930 enable_statistics = Column("statistics", Boolean(), nullable=False, default=True)
931 enable_statistics = Column("statistics", Boolean(), nullable=False, default=True)
931 enable_downloads = Column("downloads", Boolean(), nullable=False, default=True)
932 enable_downloads = Column("downloads", Boolean(), nullable=False, default=True)
932 description = Column(Unicode(10000), nullable=False)
933 description = Column(Unicode(10000), nullable=False)
933 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
934 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
934 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
935 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
935 _landing_revision = Column("landing_revision", String(255), nullable=False)
936 _landing_revision = Column("landing_revision", String(255), nullable=False)
936 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
937 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data # FIXME: not nullable?
937
938
938 fork_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
939 fork_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
939 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=True)
940 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=True)
940
941
941 owner = relationship('User')
942 owner = relationship('User')
942 fork = relationship('Repository', remote_side=repo_id)
943 fork = relationship('Repository', remote_side=repo_id)
943 group = relationship('RepoGroup')
944 group = relationship('RepoGroup')
944 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
945 repo_to_perm = relationship('UserRepoToPerm', cascade='all', order_by='UserRepoToPerm.repo_to_perm_id')
945 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
946 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
946 stats = relationship('Statistics', cascade='all', uselist=False)
947 stats = relationship('Statistics', cascade='all', uselist=False)
947
948
948 followers = relationship('UserFollowing',
949 followers = relationship('UserFollowing',
949 primaryjoin='UserFollowing.follows_repository_id==Repository.repo_id',
950 primaryjoin='UserFollowing.follows_repository_id==Repository.repo_id',
950 cascade='all')
951 cascade='all')
951 extra_fields = relationship('RepositoryField',
952 extra_fields = relationship('RepositoryField',
952 cascade="all, delete-orphan")
953 cascade="all, delete-orphan")
953
954
954 logs = relationship('UserLog')
955 logs = relationship('UserLog')
955 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
956 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
956
957
957 pull_requests_org = relationship('PullRequest',
958 pull_requests_org = relationship('PullRequest',
958 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
959 primaryjoin='PullRequest.org_repo_id==Repository.repo_id',
959 cascade="all, delete-orphan")
960 cascade="all, delete-orphan")
960
961
961 pull_requests_other = relationship('PullRequest',
962 pull_requests_other = relationship('PullRequest',
962 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
963 primaryjoin='PullRequest.other_repo_id==Repository.repo_id',
963 cascade="all, delete-orphan")
964 cascade="all, delete-orphan")
964
965
965 def __repr__(self):
966 def __repr__(self):
966 return "<%s %s: %r>" % (self.__class__.__name__,
967 return "<%s %s: %r>" % (self.__class__.__name__,
967 self.repo_id, self.repo_name)
968 self.repo_id, self.repo_name)
968
969
969 @hybrid_property
970 @hybrid_property
970 def landing_rev(self):
971 def landing_rev(self):
971 # always should return [rev_type, rev]
972 # always should return [rev_type, rev]
972 if self._landing_revision:
973 if self._landing_revision:
973 _rev_info = self._landing_revision.split(':')
974 _rev_info = self._landing_revision.split(':')
974 if len(_rev_info) < 2:
975 if len(_rev_info) < 2:
975 _rev_info.insert(0, 'rev')
976 _rev_info.insert(0, 'rev')
976 return [_rev_info[0], _rev_info[1]]
977 return [_rev_info[0], _rev_info[1]]
977 return [None, None]
978 return [None, None]
978
979
979 @landing_rev.setter
980 @landing_rev.setter
980 def landing_rev(self, val):
981 def landing_rev(self, val):
981 if ':' not in val:
982 if ':' not in val:
982 raise ValueError('value must be delimited with `:` and consist '
983 raise ValueError('value must be delimited with `:` and consist '
983 'of <rev_type>:<rev>, got %s instead' % val)
984 'of <rev_type>:<rev>, got %s instead' % val)
984 self._landing_revision = val
985 self._landing_revision = val
985
986
986 @hybrid_property
987 @hybrid_property
987 def changeset_cache(self):
988 def changeset_cache(self):
988 try:
989 try:
989 cs_cache = ext_json.loads(self._changeset_cache) # might raise on bad data
990 cs_cache = ext_json.loads(self._changeset_cache) # might raise on bad data
990 cs_cache['raw_id'] # verify data, raise exception on error
991 cs_cache['raw_id'] # verify data, raise exception on error
991 return cs_cache
992 return cs_cache
992 except (TypeError, KeyError, ValueError):
993 except (TypeError, KeyError, ValueError):
993 return EmptyChangeset().__json__()
994 return EmptyChangeset().__json__()
994
995
995 @changeset_cache.setter
996 @changeset_cache.setter
996 def changeset_cache(self, val):
997 def changeset_cache(self, val):
997 try:
998 try:
998 self._changeset_cache = ascii_bytes(ext_json.dumps(val))
999 self._changeset_cache = ascii_bytes(ext_json.dumps(val))
999 except Exception:
1000 except Exception:
1000 log.error(traceback.format_exc())
1001 log.error(traceback.format_exc())
1001
1002
1002 @classmethod
1003 @classmethod
1003 def query(cls, sorted=False):
1004 def query(cls, sorted=False):
1004 """Add Repository-specific helpers for common query constructs.
1005 """Add Repository-specific helpers for common query constructs.
1005
1006
1006 sorted: if True, apply the default ordering (name, case insensitive).
1007 sorted: if True, apply the default ordering (name, case insensitive).
1007 """
1008 """
1008 q = super(Repository, cls).query()
1009 q = super(Repository, cls).query()
1009
1010
1010 if sorted:
1011 if sorted:
1011 q = q.order_by(sqlalchemy.func.lower(Repository.repo_name))
1012 q = q.order_by(sqlalchemy.func.lower(Repository.repo_name))
1012
1013
1013 return q
1014 return q
1014
1015
1015 @classmethod
1016 @classmethod
1016 def normalize_repo_name(cls, repo_name):
1017 def normalize_repo_name(cls, repo_name):
1017 """
1018 """
1018 Normalizes os specific repo_name to the format internally stored inside
1019 Normalizes os specific repo_name to the format internally stored inside
1019 database using URL_SEP
1020 database using URL_SEP
1020
1021
1021 :param cls:
1022 :param cls:
1022 :param repo_name:
1023 :param repo_name:
1023 """
1024 """
1024 return kallithea.URL_SEP.join(repo_name.split(os.sep))
1025 return kallithea.URL_SEP.join(repo_name.split(os.sep))
1025
1026
1026 @classmethod
1027 @classmethod
1027 def guess_instance(cls, value):
1028 def guess_instance(cls, value):
1028 return super(Repository, cls).guess_instance(value, Repository.get_by_repo_name)
1029 return super(Repository, cls).guess_instance(value, Repository.get_by_repo_name)
1029
1030
1030 @classmethod
1031 @classmethod
1031 def get_by_repo_name(cls, repo_name, case_insensitive=False):
1032 def get_by_repo_name(cls, repo_name, case_insensitive=False):
1032 """Get the repo, defaulting to database case sensitivity.
1033 """Get the repo, defaulting to database case sensitivity.
1033 case_insensitive will be slower and should only be specified if necessary."""
1034 case_insensitive will be slower and should only be specified if necessary."""
1034 if case_insensitive:
1035 if case_insensitive:
1035 q = meta.Session().query(cls).filter(sqlalchemy.func.lower(cls.repo_name) == sqlalchemy.func.lower(repo_name))
1036 q = meta.Session().query(cls).filter(sqlalchemy.func.lower(cls.repo_name) == sqlalchemy.func.lower(repo_name))
1036 else:
1037 else:
1037 q = meta.Session().query(cls).filter(cls.repo_name == repo_name)
1038 q = meta.Session().query(cls).filter(cls.repo_name == repo_name)
1038 q = q.options(joinedload(Repository.fork)) \
1039 q = q.options(joinedload(Repository.fork)) \
1039 .options(joinedload(Repository.owner)) \
1040 .options(joinedload(Repository.owner)) \
1040 .options(joinedload(Repository.group))
1041 .options(joinedload(Repository.group))
1041 return q.scalar()
1042 return q.scalar()
1042
1043
1043 @classmethod
1044 @classmethod
1044 def get_by_full_path(cls, repo_full_path):
1045 def get_by_full_path(cls, repo_full_path):
1045 base_full_path = os.path.realpath(kallithea.CONFIG['base_path'])
1046 base_full_path = os.path.realpath(kallithea.CONFIG['base_path'])
1046 repo_full_path = os.path.realpath(repo_full_path)
1047 repo_full_path = os.path.realpath(repo_full_path)
1047 assert repo_full_path.startswith(base_full_path + os.path.sep)
1048 assert repo_full_path.startswith(base_full_path + os.path.sep)
1048 repo_name = repo_full_path[len(base_full_path) + 1:]
1049 repo_name = repo_full_path[len(base_full_path) + 1:]
1049 repo_name = cls.normalize_repo_name(repo_name)
1050 repo_name = cls.normalize_repo_name(repo_name)
1050 return cls.get_by_repo_name(repo_name.strip(kallithea.URL_SEP))
1051 return cls.get_by_repo_name(repo_name.strip(kallithea.URL_SEP))
1051
1052
1052 @classmethod
1053 @classmethod
1053 def get_repo_forks(cls, repo_id):
1054 def get_repo_forks(cls, repo_id):
1054 return cls.query().filter(Repository.fork_id == repo_id)
1055 return cls.query().filter(Repository.fork_id == repo_id)
1055
1056
1056 @property
1057 @property
1057 def forks(self):
1058 def forks(self):
1058 """
1059 """
1059 Return forks of this repo
1060 Return forks of this repo
1060 """
1061 """
1061 return Repository.get_repo_forks(self.repo_id)
1062 return Repository.get_repo_forks(self.repo_id)
1062
1063
1063 @property
1064 @property
1064 def parent(self):
1065 def parent(self):
1065 """
1066 """
1066 Returns fork parent
1067 Returns fork parent
1067 """
1068 """
1068 return self.fork
1069 return self.fork
1069
1070
1070 @property
1071 @property
1071 def just_name(self):
1072 def just_name(self):
1072 return self.repo_name.split(kallithea.URL_SEP)[-1]
1073 return self.repo_name.split(kallithea.URL_SEP)[-1]
1073
1074
1074 @property
1075 @property
1075 def groups_with_parents(self):
1076 def groups_with_parents(self):
1076 groups = []
1077 groups = []
1077 group = self.group
1078 group = self.group
1078 while group is not None:
1079 while group is not None:
1079 groups.append(group)
1080 groups.append(group)
1080 group = group.parent_group
1081 group = group.parent_group
1081 assert group not in groups, group # avoid recursion on bad db content
1082 assert group not in groups, group # avoid recursion on bad db content
1082 groups.reverse()
1083 groups.reverse()
1083 return groups
1084 return groups
1084
1085
1085 @property
1086 @property
1086 def repo_full_path(self):
1087 def repo_full_path(self):
1087 """
1088 """
1088 Returns base full path for the repository - where it actually
1089 Returns base full path for the repository - where it actually
1089 exists on a filesystem.
1090 exists on a filesystem.
1090 """
1091 """
1091 p = [kallithea.CONFIG['base_path']]
1092 p = [kallithea.CONFIG['base_path']]
1092 # we need to split the name by / since this is how we store the
1093 # we need to split the name by / since this is how we store the
1093 # names in the database, but that eventually needs to be converted
1094 # names in the database, but that eventually needs to be converted
1094 # into a valid system path
1095 # into a valid system path
1095 p += self.repo_name.split(kallithea.URL_SEP)
1096 p += self.repo_name.split(kallithea.URL_SEP)
1096 return os.path.join(*p)
1097 return os.path.join(*p)
1097
1098
1098 def get_new_name(self, repo_name):
1099 def get_new_name(self, repo_name):
1099 """
1100 """
1100 returns new full repository name based on assigned group and new new
1101 returns new full repository name based on assigned group and new new
1101
1102
1102 :param group_name:
1103 :param group_name:
1103 """
1104 """
1104 path_prefix = self.group.full_path_splitted if self.group else []
1105 path_prefix = self.group.full_path_splitted if self.group else []
1105 return kallithea.URL_SEP.join(path_prefix + [repo_name])
1106 return kallithea.URL_SEP.join(path_prefix + [repo_name])
1106
1107
1107 @property
1108 @property
1108 def _ui(self):
1109 def _ui(self):
1109 """
1110 """
1110 Creates an db based ui object for this repository
1111 Creates an db based ui object for this repository
1111 """
1112 """
1112 from kallithea.lib.utils import make_ui
1113 from kallithea.lib.utils import make_ui
1113 return make_ui()
1114 return make_ui()
1114
1115
1115 @classmethod
1116 @classmethod
1116 def is_valid(cls, repo_name):
1117 def is_valid(cls, repo_name):
1117 """
1118 """
1118 returns True if given repo name is a valid filesystem repository
1119 returns True if given repo name is a valid filesystem repository
1119
1120
1120 :param cls:
1121 :param cls:
1121 :param repo_name:
1122 :param repo_name:
1122 """
1123 """
1123 from kallithea.lib.utils import is_valid_repo
1124 from kallithea.lib.utils import is_valid_repo
1124
1125
1125 return is_valid_repo(repo_name, kallithea.CONFIG['base_path'])
1126 return is_valid_repo(repo_name, kallithea.CONFIG['base_path'])
1126
1127
1127 def get_api_data(self, with_revision_names=False,
1128 def get_api_data(self, with_revision_names=False,
1128 with_pullrequests=False):
1129 with_pullrequests=False):
1129 """
1130 """
1130 Common function for generating repo api data.
1131 Common function for generating repo api data.
1131 Optionally, also return tags, branches, bookmarks and PRs.
1132 Optionally, also return tags, branches, bookmarks and PRs.
1132 """
1133 """
1133 repo = self
1134 repo = self
1134 data = dict(
1135 data = dict(
1135 repo_id=repo.repo_id,
1136 repo_id=repo.repo_id,
1136 repo_name=repo.repo_name,
1137 repo_name=repo.repo_name,
1137 repo_type=repo.repo_type,
1138 repo_type=repo.repo_type,
1138 clone_uri=repo.clone_uri,
1139 clone_uri=repo.clone_uri,
1139 private=repo.private,
1140 private=repo.private,
1140 created_on=repo.created_on,
1141 created_on=repo.created_on,
1141 description=repo.description,
1142 description=repo.description,
1142 landing_rev=repo.landing_rev,
1143 landing_rev=repo.landing_rev,
1143 owner=repo.owner.username,
1144 owner=repo.owner.username,
1144 fork_of=repo.fork.repo_name if repo.fork else None,
1145 fork_of=repo.fork.repo_name if repo.fork else None,
1145 enable_statistics=repo.enable_statistics,
1146 enable_statistics=repo.enable_statistics,
1146 enable_downloads=repo.enable_downloads,
1147 enable_downloads=repo.enable_downloads,
1147 last_changeset=repo.changeset_cache,
1148 last_changeset=repo.changeset_cache,
1148 )
1149 )
1149 if with_revision_names:
1150 if with_revision_names:
1150 scm_repo = repo.scm_instance_no_cache()
1151 scm_repo = repo.scm_instance_no_cache()
1151 data.update(dict(
1152 data.update(dict(
1152 tags=scm_repo.tags,
1153 tags=scm_repo.tags,
1153 branches=scm_repo.branches,
1154 branches=scm_repo.branches,
1154 bookmarks=scm_repo.bookmarks,
1155 bookmarks=scm_repo.bookmarks,
1155 ))
1156 ))
1156 if with_pullrequests:
1157 if with_pullrequests:
1157 data['pull_requests'] = repo.pull_requests_other
1158 data['pull_requests'] = repo.pull_requests_other
1158 settings = Setting.get_app_settings()
1159 settings = Setting.get_app_settings()
1159 repository_fields = asbool(settings.get('repository_fields'))
1160 repository_fields = asbool(settings.get('repository_fields'))
1160 if repository_fields:
1161 if repository_fields:
1161 for f in self.extra_fields:
1162 for f in self.extra_fields:
1162 data[f.field_key_prefixed] = f.field_value
1163 data[f.field_key_prefixed] = f.field_value
1163
1164
1164 return data
1165 return data
1165
1166
1166 @property
1167 @property
1167 def last_db_change(self):
1168 def last_db_change(self):
1168 return self.updated_on
1169 return self.updated_on
1169
1170
1170 @property
1171 @property
1171 def clone_uri_hidden(self):
1172 def clone_uri_hidden(self):
1172 clone_uri = self.clone_uri
1173 clone_uri = self.clone_uri
1173 if clone_uri:
1174 if clone_uri:
1174 import urlobject
1175 url_obj = urlobject.URLObject(self.clone_uri)
1175 url_obj = urlobject.URLObject(self.clone_uri)
1176 if url_obj.password:
1176 if url_obj.password:
1177 clone_uri = url_obj.with_password('*****')
1177 clone_uri = url_obj.with_password('*****')
1178 return clone_uri
1178 return clone_uri
1179
1179
1180 def clone_url(self, clone_uri_tmpl, with_id=False, username=None):
1180 def clone_url(self, clone_uri_tmpl, with_id=False, username=None):
1181 if '{repo}' not in clone_uri_tmpl and '_{repoid}' not in clone_uri_tmpl:
1181 if '{repo}' not in clone_uri_tmpl and '_{repoid}' not in clone_uri_tmpl:
1182 log.error("Configured clone_uri_tmpl %r has no '{repo}' or '_{repoid}' and cannot toggle to use repo id URLs", clone_uri_tmpl)
1182 log.error("Configured clone_uri_tmpl %r has no '{repo}' or '_{repoid}' and cannot toggle to use repo id URLs", clone_uri_tmpl)
1183 elif with_id:
1183 elif with_id:
1184 clone_uri_tmpl = clone_uri_tmpl.replace('{repo}', '_{repoid}')
1184 clone_uri_tmpl = clone_uri_tmpl.replace('{repo}', '_{repoid}')
1185 else:
1185 else:
1186 clone_uri_tmpl = clone_uri_tmpl.replace('_{repoid}', '{repo}')
1186 clone_uri_tmpl = clone_uri_tmpl.replace('_{repoid}', '{repo}')
1187
1187
1188 prefix_url = webutils.canonical_url('home')
1188 prefix_url = webutils.canonical_url('home')
1189
1189
1190 return get_clone_url(clone_uri_tmpl=clone_uri_tmpl,
1190 return get_clone_url(clone_uri_tmpl=clone_uri_tmpl,
1191 prefix_url=prefix_url,
1191 prefix_url=prefix_url,
1192 repo_name=self.repo_name,
1192 repo_name=self.repo_name,
1193 repo_id=self.repo_id,
1193 repo_id=self.repo_id,
1194 username=username)
1194 username=username)
1195
1195
1196 def set_state(self, state):
1196 def set_state(self, state):
1197 self.repo_state = state
1197 self.repo_state = state
1198
1198
1199 #==========================================================================
1199 #==========================================================================
1200 # SCM PROPERTIES
1200 # SCM PROPERTIES
1201 #==========================================================================
1201 #==========================================================================
1202
1202
1203 def get_changeset(self, rev=None):
1203 def get_changeset(self, rev=None):
1204 return get_changeset_safe(self.scm_instance, rev)
1204 return get_changeset_safe(self.scm_instance, rev)
1205
1205
1206 def get_landing_changeset(self):
1206 def get_landing_changeset(self):
1207 """
1207 """
1208 Returns landing changeset, or if that doesn't exist returns the tip
1208 Returns landing changeset, or if that doesn't exist returns the tip
1209 """
1209 """
1210 _rev_type, _rev = self.landing_rev
1210 _rev_type, _rev = self.landing_rev
1211 cs = self.get_changeset(_rev)
1211 cs = self.get_changeset(_rev)
1212 if isinstance(cs, EmptyChangeset):
1212 if isinstance(cs, EmptyChangeset):
1213 return self.get_changeset()
1213 return self.get_changeset()
1214 return cs
1214 return cs
1215
1215
1216 def update_changeset_cache(self, cs_cache=None):
1216 def update_changeset_cache(self, cs_cache=None):
1217 """
1217 """
1218 Update cache of last changeset for repository, keys should be::
1218 Update cache of last changeset for repository, keys should be::
1219
1219
1220 short_id
1220 short_id
1221 raw_id
1221 raw_id
1222 revision
1222 revision
1223 message
1223 message
1224 date
1224 date
1225 author
1225 author
1226
1226
1227 :param cs_cache:
1227 :param cs_cache:
1228 """
1228 """
1229 from kallithea.lib.vcs.backends.base import BaseChangeset
1230 if cs_cache is None:
1229 if cs_cache is None:
1231 cs_cache = EmptyChangeset()
1230 cs_cache = EmptyChangeset()
1232 # use no-cache version here
1231 # use no-cache version here
1233 scm_repo = self.scm_instance_no_cache()
1232 scm_repo = self.scm_instance_no_cache()
1234 if scm_repo:
1233 if scm_repo:
1235 cs_cache = scm_repo.get_changeset()
1234 cs_cache = scm_repo.get_changeset()
1236
1235
1237 if isinstance(cs_cache, BaseChangeset):
1236 if isinstance(cs_cache, BaseChangeset):
1238 cs_cache = cs_cache.__json__()
1237 cs_cache = cs_cache.__json__()
1239
1238
1240 if (not self.changeset_cache or cs_cache['raw_id'] != self.changeset_cache['raw_id']):
1239 if (not self.changeset_cache or cs_cache['raw_id'] != self.changeset_cache['raw_id']):
1241 _default = datetime.datetime.fromtimestamp(0)
1240 _default = datetime.datetime.fromtimestamp(0)
1242 last_change = cs_cache.get('date') or _default
1241 last_change = cs_cache.get('date') or _default
1243 log.debug('updated repo %s with new cs cache %s',
1242 log.debug('updated repo %s with new cs cache %s',
1244 self.repo_name, cs_cache)
1243 self.repo_name, cs_cache)
1245 self.updated_on = last_change
1244 self.updated_on = last_change
1246 self.changeset_cache = cs_cache
1245 self.changeset_cache = cs_cache
1247 meta.Session().commit()
1246 meta.Session().commit()
1248 else:
1247 else:
1249 log.debug('changeset_cache for %s already up to date with %s',
1248 log.debug('changeset_cache for %s already up to date with %s',
1250 self.repo_name, cs_cache['raw_id'])
1249 self.repo_name, cs_cache['raw_id'])
1251
1250
1252 @property
1251 @property
1253 def tip(self):
1252 def tip(self):
1254 return self.get_changeset('tip')
1253 return self.get_changeset('tip')
1255
1254
1256 @property
1255 @property
1257 def author(self):
1256 def author(self):
1258 return self.tip.author
1257 return self.tip.author
1259
1258
1260 @property
1259 @property
1261 def last_change(self):
1260 def last_change(self):
1262 return self.scm_instance.last_change
1261 return self.scm_instance.last_change
1263
1262
1264 def get_comments(self, revisions=None):
1263 def get_comments(self, revisions=None):
1265 """
1264 """
1266 Returns comments for this repository grouped by revisions
1265 Returns comments for this repository grouped by revisions
1267
1266
1268 :param revisions: filter query by revisions only
1267 :param revisions: filter query by revisions only
1269 """
1268 """
1270 cmts = ChangesetComment.query() \
1269 cmts = ChangesetComment.query() \
1271 .filter(ChangesetComment.repo == self)
1270 .filter(ChangesetComment.repo == self)
1272 if revisions is not None:
1271 if revisions is not None:
1273 if not revisions:
1272 if not revisions:
1274 return {} # don't use sql 'in' on empty set
1273 return {} # don't use sql 'in' on empty set
1275 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1274 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1276 grouped = collections.defaultdict(list)
1275 grouped = collections.defaultdict(list)
1277 for cmt in cmts.all():
1276 for cmt in cmts.all():
1278 grouped[cmt.revision].append(cmt)
1277 grouped[cmt.revision].append(cmt)
1279 return grouped
1278 return grouped
1280
1279
1281 def statuses(self, revisions):
1280 def statuses(self, revisions):
1282 """
1281 """
1283 Returns statuses for this repository.
1282 Returns statuses for this repository.
1284 PRs without any votes do _not_ show up as unreviewed.
1283 PRs without any votes do _not_ show up as unreviewed.
1285
1284
1286 :param revisions: list of revisions to get statuses for
1285 :param revisions: list of revisions to get statuses for
1287 """
1286 """
1288 if not revisions:
1287 if not revisions:
1289 return {}
1288 return {}
1290
1289
1291 statuses = ChangesetStatus.query() \
1290 statuses = ChangesetStatus.query() \
1292 .filter(ChangesetStatus.repo == self) \
1291 .filter(ChangesetStatus.repo == self) \
1293 .filter(ChangesetStatus.version == 0) \
1292 .filter(ChangesetStatus.version == 0) \
1294 .filter(ChangesetStatus.revision.in_(revisions))
1293 .filter(ChangesetStatus.revision.in_(revisions))
1295
1294
1296 grouped = {}
1295 grouped = {}
1297 for stat in statuses.all():
1296 for stat in statuses.all():
1298 pr_id = pr_nice_id = pr_repo = None
1297 pr_id = pr_nice_id = pr_repo = None
1299 if stat.pull_request:
1298 if stat.pull_request:
1300 pr_id = stat.pull_request.pull_request_id
1299 pr_id = stat.pull_request.pull_request_id
1301 pr_nice_id = PullRequest.make_nice_id(pr_id)
1300 pr_nice_id = PullRequest.make_nice_id(pr_id)
1302 pr_repo = stat.pull_request.other_repo.repo_name
1301 pr_repo = stat.pull_request.other_repo.repo_name
1303 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1302 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1304 pr_id, pr_repo, pr_nice_id,
1303 pr_id, pr_repo, pr_nice_id,
1305 stat.author]
1304 stat.author]
1306 return grouped
1305 return grouped
1307
1306
1308 def _repo_size(self):
1307 def _repo_size(self):
1309 log.debug('calculating repository size...')
1308 log.debug('calculating repository size...')
1310 return webutils.format_byte_size(self.scm_instance.size)
1309 return webutils.format_byte_size(self.scm_instance.size)
1311
1310
1312 #==========================================================================
1311 #==========================================================================
1313 # SCM CACHE INSTANCE
1312 # SCM CACHE INSTANCE
1314 #==========================================================================
1313 #==========================================================================
1315
1314
1316 def set_invalidate(self):
1315 def set_invalidate(self):
1317 """
1316 """
1318 Flush SA session caches of instances of on disk repo.
1317 Flush SA session caches of instances of on disk repo.
1319 """
1318 """
1320 try:
1319 try:
1321 del self._scm_instance
1320 del self._scm_instance
1322 except AttributeError:
1321 except AttributeError:
1323 pass
1322 pass
1324
1323
1325 _scm_instance = None # caching inside lifetime of SA session
1324 _scm_instance = None # caching inside lifetime of SA session
1326
1325
1327 @property
1326 @property
1328 def scm_instance(self):
1327 def scm_instance(self):
1329 if self._scm_instance is None:
1328 if self._scm_instance is None:
1330 return self.scm_instance_no_cache() # will populate self._scm_instance
1329 return self.scm_instance_no_cache() # will populate self._scm_instance
1331 return self._scm_instance
1330 return self._scm_instance
1332
1331
1333 def scm_instance_no_cache(self):
1332 def scm_instance_no_cache(self):
1334 repo_full_path = self.repo_full_path
1333 repo_full_path = self.repo_full_path
1335 alias = get_scm(repo_full_path)[0]
1334 alias = get_scm(repo_full_path)[0]
1336 log.debug('Creating instance of %s repository from %s',
1335 log.debug('Creating instance of %s repository from %s',
1337 alias, self.repo_full_path)
1336 alias, self.repo_full_path)
1338 backend = get_backend(alias)
1337 backend = get_backend(alias)
1339
1338
1340 if alias == 'hg':
1339 if alias == 'hg':
1341 self._scm_instance = backend(repo_full_path, create=False, baseui=self._ui)
1340 self._scm_instance = backend(repo_full_path, create=False, baseui=self._ui)
1342 else:
1341 else:
1343 self._scm_instance = backend(repo_full_path, create=False)
1342 self._scm_instance = backend(repo_full_path, create=False)
1344
1343
1345 return self._scm_instance
1344 return self._scm_instance
1346
1345
1347 def __json__(self):
1346 def __json__(self):
1348 return dict(
1347 return dict(
1349 repo_id=self.repo_id,
1348 repo_id=self.repo_id,
1350 repo_name=self.repo_name,
1349 repo_name=self.repo_name,
1351 landing_rev=self.landing_rev,
1350 landing_rev=self.landing_rev,
1352 )
1351 )
1353
1352
1354
1353
1355 class RepoGroup(meta.Base, BaseDbModel):
1354 class RepoGroup(meta.Base, BaseDbModel):
1356 __tablename__ = 'groups'
1355 __tablename__ = 'groups'
1357 __table_args__ = (
1356 __table_args__ = (
1358 _table_args_default_dict,
1357 _table_args_default_dict,
1359 )
1358 )
1360
1359
1361 SEP = ' &raquo; '
1360 SEP = ' &raquo; '
1362
1361
1363 group_id = Column(Integer(), primary_key=True)
1362 group_id = Column(Integer(), primary_key=True)
1364 group_name = Column(Unicode(255), nullable=False, unique=True) # full path
1363 group_name = Column(Unicode(255), nullable=False, unique=True) # full path
1365 parent_group_id = Column('group_parent_id', Integer(), ForeignKey('groups.group_id'), nullable=True)
1364 parent_group_id = Column('group_parent_id', Integer(), ForeignKey('groups.group_id'), nullable=True)
1366 group_description = Column(Unicode(10000), nullable=False)
1365 group_description = Column(Unicode(10000), nullable=False)
1367 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1366 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1368 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1367 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1369
1368
1370 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1369 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
1371 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1370 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1372 parent_group = relationship('RepoGroup', remote_side=group_id)
1371 parent_group = relationship('RepoGroup', remote_side=group_id)
1373 owner = relationship('User')
1372 owner = relationship('User')
1374
1373
1375 @classmethod
1374 @classmethod
1376 def query(cls, sorted=False):
1375 def query(cls, sorted=False):
1377 """Add RepoGroup-specific helpers for common query constructs.
1376 """Add RepoGroup-specific helpers for common query constructs.
1378
1377
1379 sorted: if True, apply the default ordering (name, case insensitive).
1378 sorted: if True, apply the default ordering (name, case insensitive).
1380 """
1379 """
1381 q = super(RepoGroup, cls).query()
1380 q = super(RepoGroup, cls).query()
1382
1381
1383 if sorted:
1382 if sorted:
1384 q = q.order_by(sqlalchemy.func.lower(RepoGroup.group_name))
1383 q = q.order_by(sqlalchemy.func.lower(RepoGroup.group_name))
1385
1384
1386 return q
1385 return q
1387
1386
1388 def __init__(self, group_name='', parent_group=None):
1387 def __init__(self, group_name='', parent_group=None):
1389 self.group_name = group_name
1388 self.group_name = group_name
1390 self.parent_group = parent_group
1389 self.parent_group = parent_group
1391
1390
1392 def __repr__(self):
1391 def __repr__(self):
1393 return "<%s %s: %s>" % (self.__class__.__name__,
1392 return "<%s %s: %s>" % (self.__class__.__name__,
1394 self.group_id, self.group_name)
1393 self.group_id, self.group_name)
1395
1394
1396 @classmethod
1395 @classmethod
1397 def _generate_choice(cls, repo_group):
1396 def _generate_choice(cls, repo_group):
1398 """Return tuple with group_id and name as html literal"""
1397 """Return tuple with group_id and name as html literal"""
1399 if repo_group is None:
1398 if repo_group is None:
1400 return (-1, '-- %s --' % _('top level'))
1399 return (-1, '-- %s --' % _('top level'))
1401 return repo_group.group_id, webutils.literal(cls.SEP.join(repo_group.full_path_splitted))
1400 return repo_group.group_id, webutils.literal(cls.SEP.join(repo_group.full_path_splitted))
1402
1401
1403 @classmethod
1402 @classmethod
1404 def groups_choices(cls, groups):
1403 def groups_choices(cls, groups):
1405 """Return tuples with group_id and name as html literal."""
1404 """Return tuples with group_id and name as html literal."""
1406 return sorted((cls._generate_choice(g) for g in groups),
1405 return sorted((cls._generate_choice(g) for g in groups),
1407 key=lambda c: c[1].split(cls.SEP))
1406 key=lambda c: c[1].split(cls.SEP))
1408
1407
1409 @classmethod
1408 @classmethod
1410 def guess_instance(cls, value):
1409 def guess_instance(cls, value):
1411 return super(RepoGroup, cls).guess_instance(value, RepoGroup.get_by_group_name)
1410 return super(RepoGroup, cls).guess_instance(value, RepoGroup.get_by_group_name)
1412
1411
1413 @classmethod
1412 @classmethod
1414 def get_by_group_name(cls, group_name, case_insensitive=False):
1413 def get_by_group_name(cls, group_name, case_insensitive=False):
1415 group_name = group_name.rstrip('/')
1414 group_name = group_name.rstrip('/')
1416 if case_insensitive:
1415 if case_insensitive:
1417 gr = cls.query() \
1416 gr = cls.query() \
1418 .filter(sqlalchemy.func.lower(cls.group_name) == sqlalchemy.func.lower(group_name))
1417 .filter(sqlalchemy.func.lower(cls.group_name) == sqlalchemy.func.lower(group_name))
1419 else:
1418 else:
1420 gr = cls.query() \
1419 gr = cls.query() \
1421 .filter(cls.group_name == group_name)
1420 .filter(cls.group_name == group_name)
1422 return gr.scalar()
1421 return gr.scalar()
1423
1422
1424 @property
1423 @property
1425 def parents(self):
1424 def parents(self):
1426 groups = []
1425 groups = []
1427 group = self.parent_group
1426 group = self.parent_group
1428 while group is not None:
1427 while group is not None:
1429 groups.append(group)
1428 groups.append(group)
1430 group = group.parent_group
1429 group = group.parent_group
1431 assert group not in groups, group # avoid recursion on bad db content
1430 assert group not in groups, group # avoid recursion on bad db content
1432 groups.reverse()
1431 groups.reverse()
1433 return groups
1432 return groups
1434
1433
1435 @property
1434 @property
1436 def children(self):
1435 def children(self):
1437 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1436 return RepoGroup.query().filter(RepoGroup.parent_group == self)
1438
1437
1439 @property
1438 @property
1440 def name(self):
1439 def name(self):
1441 return self.group_name.split(kallithea.URL_SEP)[-1]
1440 return self.group_name.split(kallithea.URL_SEP)[-1]
1442
1441
1443 @property
1442 @property
1444 def full_path(self):
1443 def full_path(self):
1445 return self.group_name
1444 return self.group_name
1446
1445
1447 @property
1446 @property
1448 def full_path_splitted(self):
1447 def full_path_splitted(self):
1449 return self.group_name.split(kallithea.URL_SEP)
1448 return self.group_name.split(kallithea.URL_SEP)
1450
1449
1451 @property
1450 @property
1452 def repositories(self):
1451 def repositories(self):
1453 return Repository.query(sorted=True).filter_by(group=self)
1452 return Repository.query(sorted=True).filter_by(group=self)
1454
1453
1455 @property
1454 @property
1456 def repositories_recursive_count(self):
1455 def repositories_recursive_count(self):
1457 cnt = self.repositories.count()
1456 cnt = self.repositories.count()
1458
1457
1459 def children_count(group):
1458 def children_count(group):
1460 cnt = 0
1459 cnt = 0
1461 for child in group.children:
1460 for child in group.children:
1462 cnt += child.repositories.count()
1461 cnt += child.repositories.count()
1463 cnt += children_count(child)
1462 cnt += children_count(child)
1464 return cnt
1463 return cnt
1465
1464
1466 return cnt + children_count(self)
1465 return cnt + children_count(self)
1467
1466
1468 def _recursive_objects(self, include_repos=True):
1467 def _recursive_objects(self, include_repos=True):
1469 all_ = []
1468 all_ = []
1470
1469
1471 def _get_members(root_gr):
1470 def _get_members(root_gr):
1472 if include_repos:
1471 if include_repos:
1473 for r in root_gr.repositories:
1472 for r in root_gr.repositories:
1474 all_.append(r)
1473 all_.append(r)
1475 childs = root_gr.children.all()
1474 childs = root_gr.children.all()
1476 if childs:
1475 if childs:
1477 for gr in childs:
1476 for gr in childs:
1478 all_.append(gr)
1477 all_.append(gr)
1479 _get_members(gr)
1478 _get_members(gr)
1480
1479
1481 _get_members(self)
1480 _get_members(self)
1482 return [self] + all_
1481 return [self] + all_
1483
1482
1484 def recursive_groups_and_repos(self):
1483 def recursive_groups_and_repos(self):
1485 """
1484 """
1486 Recursive return all groups, with repositories in those groups
1485 Recursive return all groups, with repositories in those groups
1487 """
1486 """
1488 return self._recursive_objects()
1487 return self._recursive_objects()
1489
1488
1490 def recursive_groups(self):
1489 def recursive_groups(self):
1491 """
1490 """
1492 Returns all children groups for this group including children of children
1491 Returns all children groups for this group including children of children
1493 """
1492 """
1494 return self._recursive_objects(include_repos=False)
1493 return self._recursive_objects(include_repos=False)
1495
1494
1496 def get_new_name(self, group_name):
1495 def get_new_name(self, group_name):
1497 """
1496 """
1498 returns new full group name based on parent and new name
1497 returns new full group name based on parent and new name
1499
1498
1500 :param group_name:
1499 :param group_name:
1501 """
1500 """
1502 path_prefix = (self.parent_group.full_path_splitted if
1501 path_prefix = (self.parent_group.full_path_splitted if
1503 self.parent_group else [])
1502 self.parent_group else [])
1504 return kallithea.URL_SEP.join(path_prefix + [group_name])
1503 return kallithea.URL_SEP.join(path_prefix + [group_name])
1505
1504
1506 def get_api_data(self):
1505 def get_api_data(self):
1507 """
1506 """
1508 Common function for generating api data
1507 Common function for generating api data
1509
1508
1510 """
1509 """
1511 group = self
1510 group = self
1512 data = dict(
1511 data = dict(
1513 group_id=group.group_id,
1512 group_id=group.group_id,
1514 group_name=group.group_name,
1513 group_name=group.group_name,
1515 group_description=group.group_description,
1514 group_description=group.group_description,
1516 parent_group=group.parent_group.group_name if group.parent_group else None,
1515 parent_group=group.parent_group.group_name if group.parent_group else None,
1517 repositories=[x.repo_name for x in group.repositories],
1516 repositories=[x.repo_name for x in group.repositories],
1518 owner=group.owner.username
1517 owner=group.owner.username
1519 )
1518 )
1520 return data
1519 return data
1521
1520
1522
1521
1523 class Permission(meta.Base, BaseDbModel):
1522 class Permission(meta.Base, BaseDbModel):
1524 __tablename__ = 'permissions'
1523 __tablename__ = 'permissions'
1525 __table_args__ = (
1524 __table_args__ = (
1526 Index('p_perm_name_idx', 'permission_name'),
1525 Index('p_perm_name_idx', 'permission_name'),
1527 _table_args_default_dict,
1526 _table_args_default_dict,
1528 )
1527 )
1529
1528
1530 PERMS = (
1529 PERMS = (
1531 ('hg.admin', _('Kallithea Administrator')),
1530 ('hg.admin', _('Kallithea Administrator')),
1532
1531
1533 ('repository.none', _('Default user has no access to new repositories')),
1532 ('repository.none', _('Default user has no access to new repositories')),
1534 ('repository.read', _('Default user has read access to new repositories')),
1533 ('repository.read', _('Default user has read access to new repositories')),
1535 ('repository.write', _('Default user has write access to new repositories')),
1534 ('repository.write', _('Default user has write access to new repositories')),
1536 ('repository.admin', _('Default user has admin access to new repositories')),
1535 ('repository.admin', _('Default user has admin access to new repositories')),
1537
1536
1538 ('group.none', _('Default user has no access to new repository groups')),
1537 ('group.none', _('Default user has no access to new repository groups')),
1539 ('group.read', _('Default user has read access to new repository groups')),
1538 ('group.read', _('Default user has read access to new repository groups')),
1540 ('group.write', _('Default user has write access to new repository groups')),
1539 ('group.write', _('Default user has write access to new repository groups')),
1541 ('group.admin', _('Default user has admin access to new repository groups')),
1540 ('group.admin', _('Default user has admin access to new repository groups')),
1542
1541
1543 ('usergroup.none', _('Default user has no access to new user groups')),
1542 ('usergroup.none', _('Default user has no access to new user groups')),
1544 ('usergroup.read', _('Default user has read access to new user groups')),
1543 ('usergroup.read', _('Default user has read access to new user groups')),
1545 ('usergroup.write', _('Default user has write access to new user groups')),
1544 ('usergroup.write', _('Default user has write access to new user groups')),
1546 ('usergroup.admin', _('Default user has admin access to new user groups')),
1545 ('usergroup.admin', _('Default user has admin access to new user groups')),
1547
1546
1548 ('hg.usergroup.create.false', _('Only admins can create user groups')),
1547 ('hg.usergroup.create.false', _('Only admins can create user groups')),
1549 ('hg.usergroup.create.true', _('Non-admins can create user groups')),
1548 ('hg.usergroup.create.true', _('Non-admins can create user groups')),
1550
1549
1551 ('hg.create.none', _('Only admins can create top level repositories')),
1550 ('hg.create.none', _('Only admins can create top level repositories')),
1552 ('hg.create.repository', _('Non-admins can create top level repositories')),
1551 ('hg.create.repository', _('Non-admins can create top level repositories')),
1553
1552
1554 ('hg.fork.none', _('Only admins can fork repositories')),
1553 ('hg.fork.none', _('Only admins can fork repositories')),
1555 ('hg.fork.repository', _('Non-admins can fork repositories')),
1554 ('hg.fork.repository', _('Non-admins can fork repositories')),
1556
1555
1557 ('hg.register.none', _('Registration disabled')),
1556 ('hg.register.none', _('Registration disabled')),
1558 ('hg.register.manual_activate', _('User registration with manual account activation')),
1557 ('hg.register.manual_activate', _('User registration with manual account activation')),
1559 ('hg.register.auto_activate', _('User registration with automatic account activation')),
1558 ('hg.register.auto_activate', _('User registration with automatic account activation')),
1560
1559
1561 ('hg.extern_activate.manual', _('Manual activation of external account')),
1560 ('hg.extern_activate.manual', _('Manual activation of external account')),
1562 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1561 ('hg.extern_activate.auto', _('Automatic activation of external account')),
1563 )
1562 )
1564
1563
1565 # definition of system default permissions for DEFAULT user
1564 # definition of system default permissions for DEFAULT user
1566 DEFAULT_USER_PERMISSIONS = (
1565 DEFAULT_USER_PERMISSIONS = (
1567 'repository.read',
1566 'repository.read',
1568 'group.read',
1567 'group.read',
1569 'usergroup.read',
1568 'usergroup.read',
1570 'hg.create.repository',
1569 'hg.create.repository',
1571 'hg.fork.repository',
1570 'hg.fork.repository',
1572 'hg.register.manual_activate',
1571 'hg.register.manual_activate',
1573 'hg.extern_activate.auto',
1572 'hg.extern_activate.auto',
1574 )
1573 )
1575
1574
1576 # defines which permissions are more important higher the more important
1575 # defines which permissions are more important higher the more important
1577 # Weight defines which permissions are more important.
1576 # Weight defines which permissions are more important.
1578 # The higher number the more important.
1577 # The higher number the more important.
1579 PERM_WEIGHTS = {
1578 PERM_WEIGHTS = {
1580 'repository.none': 0,
1579 'repository.none': 0,
1581 'repository.read': 1,
1580 'repository.read': 1,
1582 'repository.write': 3,
1581 'repository.write': 3,
1583 'repository.admin': 4,
1582 'repository.admin': 4,
1584
1583
1585 'group.none': 0,
1584 'group.none': 0,
1586 'group.read': 1,
1585 'group.read': 1,
1587 'group.write': 3,
1586 'group.write': 3,
1588 'group.admin': 4,
1587 'group.admin': 4,
1589
1588
1590 'usergroup.none': 0,
1589 'usergroup.none': 0,
1591 'usergroup.read': 1,
1590 'usergroup.read': 1,
1592 'usergroup.write': 3,
1591 'usergroup.write': 3,
1593 'usergroup.admin': 4,
1592 'usergroup.admin': 4,
1594
1593
1595 'hg.usergroup.create.false': 0,
1594 'hg.usergroup.create.false': 0,
1596 'hg.usergroup.create.true': 1,
1595 'hg.usergroup.create.true': 1,
1597
1596
1598 'hg.fork.none': 0,
1597 'hg.fork.none': 0,
1599 'hg.fork.repository': 1,
1598 'hg.fork.repository': 1,
1600
1599
1601 'hg.create.none': 0,
1600 'hg.create.none': 0,
1602 'hg.create.repository': 1,
1601 'hg.create.repository': 1,
1603
1602
1604 'hg.register.none': 0,
1603 'hg.register.none': 0,
1605 'hg.register.manual_activate': 1,
1604 'hg.register.manual_activate': 1,
1606 'hg.register.auto_activate': 2,
1605 'hg.register.auto_activate': 2,
1607
1606
1608 'hg.extern_activate.manual': 0,
1607 'hg.extern_activate.manual': 0,
1609 'hg.extern_activate.auto': 1,
1608 'hg.extern_activate.auto': 1,
1610 }
1609 }
1611
1610
1612 permission_id = Column(Integer(), primary_key=True)
1611 permission_id = Column(Integer(), primary_key=True)
1613 permission_name = Column(String(255), nullable=False)
1612 permission_name = Column(String(255), nullable=False)
1614
1613
1615 def __repr__(self):
1614 def __repr__(self):
1616 return "<%s %s: %r>" % (
1615 return "<%s %s: %r>" % (
1617 self.__class__.__name__, self.permission_id, self.permission_name
1616 self.__class__.__name__, self.permission_id, self.permission_name
1618 )
1617 )
1619
1618
1620 @classmethod
1619 @classmethod
1621 def guess_instance(cls, value):
1620 def guess_instance(cls, value):
1622 return super(Permission, cls).guess_instance(value, Permission.get_by_key)
1621 return super(Permission, cls).guess_instance(value, Permission.get_by_key)
1623
1622
1624 @classmethod
1623 @classmethod
1625 def get_by_key(cls, key):
1624 def get_by_key(cls, key):
1626 return cls.query().filter(cls.permission_name == key).scalar()
1625 return cls.query().filter(cls.permission_name == key).scalar()
1627
1626
1628 @classmethod
1627 @classmethod
1629 def get_default_perms(cls, default_user_id):
1628 def get_default_perms(cls, default_user_id):
1630 q = meta.Session().query(UserRepoToPerm) \
1629 q = meta.Session().query(UserRepoToPerm) \
1631 .options(joinedload(UserRepoToPerm.repository)) \
1630 .options(joinedload(UserRepoToPerm.repository)) \
1632 .options(joinedload(UserRepoToPerm.permission)) \
1631 .options(joinedload(UserRepoToPerm.permission)) \
1633 .filter(UserRepoToPerm.user_id == default_user_id)
1632 .filter(UserRepoToPerm.user_id == default_user_id)
1634
1633
1635 return q.all()
1634 return q.all()
1636
1635
1637 @classmethod
1636 @classmethod
1638 def get_default_group_perms(cls, default_user_id):
1637 def get_default_group_perms(cls, default_user_id):
1639 q = meta.Session().query(UserRepoGroupToPerm) \
1638 q = meta.Session().query(UserRepoGroupToPerm) \
1640 .options(joinedload(UserRepoGroupToPerm.group)) \
1639 .options(joinedload(UserRepoGroupToPerm.group)) \
1641 .options(joinedload(UserRepoGroupToPerm.permission)) \
1640 .options(joinedload(UserRepoGroupToPerm.permission)) \
1642 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1641 .filter(UserRepoGroupToPerm.user_id == default_user_id)
1643
1642
1644 return q.all()
1643 return q.all()
1645
1644
1646 @classmethod
1645 @classmethod
1647 def get_default_user_group_perms(cls, default_user_id):
1646 def get_default_user_group_perms(cls, default_user_id):
1648 q = meta.Session().query(UserUserGroupToPerm) \
1647 q = meta.Session().query(UserUserGroupToPerm) \
1649 .options(joinedload(UserUserGroupToPerm.user_group)) \
1648 .options(joinedload(UserUserGroupToPerm.user_group)) \
1650 .options(joinedload(UserUserGroupToPerm.permission)) \
1649 .options(joinedload(UserUserGroupToPerm.permission)) \
1651 .filter(UserUserGroupToPerm.user_id == default_user_id)
1650 .filter(UserUserGroupToPerm.user_id == default_user_id)
1652
1651
1653 return q.all()
1652 return q.all()
1654
1653
1655
1654
1656 class UserRepoToPerm(meta.Base, BaseDbModel):
1655 class UserRepoToPerm(meta.Base, BaseDbModel):
1657 __tablename__ = 'repo_to_perm'
1656 __tablename__ = 'repo_to_perm'
1658 __table_args__ = (
1657 __table_args__ = (
1659 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1658 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
1660 _table_args_default_dict,
1659 _table_args_default_dict,
1661 )
1660 )
1662
1661
1663 repo_to_perm_id = Column(Integer(), primary_key=True)
1662 repo_to_perm_id = Column(Integer(), primary_key=True)
1664 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1663 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1665 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1664 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1666 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1665 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1667
1666
1668 user = relationship('User')
1667 user = relationship('User')
1669 repository = relationship('Repository')
1668 repository = relationship('Repository')
1670 permission = relationship('Permission')
1669 permission = relationship('Permission')
1671
1670
1672 @classmethod
1671 @classmethod
1673 def create(cls, user, repository, permission):
1672 def create(cls, user, repository, permission):
1674 n = cls()
1673 n = cls()
1675 n.user = user
1674 n.user = user
1676 n.repository = repository
1675 n.repository = repository
1677 n.permission = permission
1676 n.permission = permission
1678 meta.Session().add(n)
1677 meta.Session().add(n)
1679 return n
1678 return n
1680
1679
1681 def __repr__(self):
1680 def __repr__(self):
1682 return '<%s %s at %s: %s>' % (
1681 return '<%s %s at %s: %s>' % (
1683 self.__class__.__name__, self.user, self.repository, self.permission)
1682 self.__class__.__name__, self.user, self.repository, self.permission)
1684
1683
1685
1684
1686 class UserUserGroupToPerm(meta.Base, BaseDbModel):
1685 class UserUserGroupToPerm(meta.Base, BaseDbModel):
1687 __tablename__ = 'user_user_group_to_perm'
1686 __tablename__ = 'user_user_group_to_perm'
1688 __table_args__ = (
1687 __table_args__ = (
1689 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1688 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
1690 _table_args_default_dict,
1689 _table_args_default_dict,
1691 )
1690 )
1692
1691
1693 user_user_group_to_perm_id = Column(Integer(), primary_key=True)
1692 user_user_group_to_perm_id = Column(Integer(), primary_key=True)
1694 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1693 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1695 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1694 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1696 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1695 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1697
1696
1698 user = relationship('User')
1697 user = relationship('User')
1699 user_group = relationship('UserGroup')
1698 user_group = relationship('UserGroup')
1700 permission = relationship('Permission')
1699 permission = relationship('Permission')
1701
1700
1702 @classmethod
1701 @classmethod
1703 def create(cls, user, user_group, permission):
1702 def create(cls, user, user_group, permission):
1704 n = cls()
1703 n = cls()
1705 n.user = user
1704 n.user = user
1706 n.user_group = user_group
1705 n.user_group = user_group
1707 n.permission = permission
1706 n.permission = permission
1708 meta.Session().add(n)
1707 meta.Session().add(n)
1709 return n
1708 return n
1710
1709
1711 def __repr__(self):
1710 def __repr__(self):
1712 return '<%s %s at %s: %s>' % (
1711 return '<%s %s at %s: %s>' % (
1713 self.__class__.__name__, self.user, self.user_group, self.permission)
1712 self.__class__.__name__, self.user, self.user_group, self.permission)
1714
1713
1715
1714
1716 class UserToPerm(meta.Base, BaseDbModel):
1715 class UserToPerm(meta.Base, BaseDbModel):
1717 __tablename__ = 'user_to_perm'
1716 __tablename__ = 'user_to_perm'
1718 __table_args__ = (
1717 __table_args__ = (
1719 UniqueConstraint('user_id', 'permission_id'),
1718 UniqueConstraint('user_id', 'permission_id'),
1720 _table_args_default_dict,
1719 _table_args_default_dict,
1721 )
1720 )
1722
1721
1723 user_to_perm_id = Column(Integer(), primary_key=True)
1722 user_to_perm_id = Column(Integer(), primary_key=True)
1724 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1723 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1725 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1724 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1726
1725
1727 user = relationship('User')
1726 user = relationship('User')
1728 permission = relationship('Permission')
1727 permission = relationship('Permission')
1729
1728
1730 def __repr__(self):
1729 def __repr__(self):
1731 return '<%s %s: %s>' % (
1730 return '<%s %s: %s>' % (
1732 self.__class__.__name__, self.user, self.permission)
1731 self.__class__.__name__, self.user, self.permission)
1733
1732
1734
1733
1735 class UserGroupRepoToPerm(meta.Base, BaseDbModel):
1734 class UserGroupRepoToPerm(meta.Base, BaseDbModel):
1736 __tablename__ = 'users_group_repo_to_perm'
1735 __tablename__ = 'users_group_repo_to_perm'
1737 __table_args__ = (
1736 __table_args__ = (
1738 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1737 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
1739 _table_args_default_dict,
1738 _table_args_default_dict,
1740 )
1739 )
1741
1740
1742 users_group_to_perm_id = Column(Integer(), primary_key=True)
1741 users_group_to_perm_id = Column(Integer(), primary_key=True)
1743 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1742 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1744 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1743 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1745 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1744 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1746
1745
1747 users_group = relationship('UserGroup')
1746 users_group = relationship('UserGroup')
1748 permission = relationship('Permission')
1747 permission = relationship('Permission')
1749 repository = relationship('Repository')
1748 repository = relationship('Repository')
1750
1749
1751 @classmethod
1750 @classmethod
1752 def create(cls, users_group, repository, permission):
1751 def create(cls, users_group, repository, permission):
1753 n = cls()
1752 n = cls()
1754 n.users_group = users_group
1753 n.users_group = users_group
1755 n.repository = repository
1754 n.repository = repository
1756 n.permission = permission
1755 n.permission = permission
1757 meta.Session().add(n)
1756 meta.Session().add(n)
1758 return n
1757 return n
1759
1758
1760 def __repr__(self):
1759 def __repr__(self):
1761 return '<%s %s at %s: %s>' % (
1760 return '<%s %s at %s: %s>' % (
1762 self.__class__.__name__, self.users_group, self.repository, self.permission)
1761 self.__class__.__name__, self.users_group, self.repository, self.permission)
1763
1762
1764
1763
1765 class UserGroupUserGroupToPerm(meta.Base, BaseDbModel):
1764 class UserGroupUserGroupToPerm(meta.Base, BaseDbModel):
1766 __tablename__ = 'user_group_user_group_to_perm'
1765 __tablename__ = 'user_group_user_group_to_perm'
1767 __table_args__ = (
1766 __table_args__ = (
1768 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1767 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
1769 _table_args_default_dict,
1768 _table_args_default_dict,
1770 )
1769 )
1771
1770
1772 user_group_user_group_to_perm_id = Column(Integer(), primary_key=True)
1771 user_group_user_group_to_perm_id = Column(Integer(), primary_key=True)
1773 target_user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1772 target_user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1774 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1773 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1775 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1774 user_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1776
1775
1777 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1776 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
1778 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1777 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
1779 permission = relationship('Permission')
1778 permission = relationship('Permission')
1780
1779
1781 @classmethod
1780 @classmethod
1782 def create(cls, target_user_group, user_group, permission):
1781 def create(cls, target_user_group, user_group, permission):
1783 n = cls()
1782 n = cls()
1784 n.target_user_group = target_user_group
1783 n.target_user_group = target_user_group
1785 n.user_group = user_group
1784 n.user_group = user_group
1786 n.permission = permission
1785 n.permission = permission
1787 meta.Session().add(n)
1786 meta.Session().add(n)
1788 return n
1787 return n
1789
1788
1790 def __repr__(self):
1789 def __repr__(self):
1791 return '<%s %s at %s: %s>' % (
1790 return '<%s %s at %s: %s>' % (
1792 self.__class__.__name__, self.user_group, self.target_user_group, self.permission)
1791 self.__class__.__name__, self.user_group, self.target_user_group, self.permission)
1793
1792
1794
1793
1795 class UserGroupToPerm(meta.Base, BaseDbModel):
1794 class UserGroupToPerm(meta.Base, BaseDbModel):
1796 __tablename__ = 'users_group_to_perm'
1795 __tablename__ = 'users_group_to_perm'
1797 __table_args__ = (
1796 __table_args__ = (
1798 UniqueConstraint('users_group_id', 'permission_id',),
1797 UniqueConstraint('users_group_id', 'permission_id',),
1799 _table_args_default_dict,
1798 _table_args_default_dict,
1800 )
1799 )
1801
1800
1802 users_group_to_perm_id = Column(Integer(), primary_key=True)
1801 users_group_to_perm_id = Column(Integer(), primary_key=True)
1803 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1802 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1804 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1803 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1805
1804
1806 users_group = relationship('UserGroup')
1805 users_group = relationship('UserGroup')
1807 permission = relationship('Permission')
1806 permission = relationship('Permission')
1808
1807
1809
1808
1810 class UserRepoGroupToPerm(meta.Base, BaseDbModel):
1809 class UserRepoGroupToPerm(meta.Base, BaseDbModel):
1811 __tablename__ = 'user_repo_group_to_perm'
1810 __tablename__ = 'user_repo_group_to_perm'
1812 __table_args__ = (
1811 __table_args__ = (
1813 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1812 UniqueConstraint('user_id', 'group_id', 'permission_id'),
1814 _table_args_default_dict,
1813 _table_args_default_dict,
1815 )
1814 )
1816
1815
1817 group_to_perm_id = Column(Integer(), primary_key=True)
1816 group_to_perm_id = Column(Integer(), primary_key=True)
1818 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1817 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1819 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1818 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1820 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1819 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1821
1820
1822 user = relationship('User')
1821 user = relationship('User')
1823 group = relationship('RepoGroup')
1822 group = relationship('RepoGroup')
1824 permission = relationship('Permission')
1823 permission = relationship('Permission')
1825
1824
1826 @classmethod
1825 @classmethod
1827 def create(cls, user, repository_group, permission):
1826 def create(cls, user, repository_group, permission):
1828 n = cls()
1827 n = cls()
1829 n.user = user
1828 n.user = user
1830 n.group = repository_group
1829 n.group = repository_group
1831 n.permission = permission
1830 n.permission = permission
1832 meta.Session().add(n)
1831 meta.Session().add(n)
1833 return n
1832 return n
1834
1833
1835
1834
1836 class UserGroupRepoGroupToPerm(meta.Base, BaseDbModel):
1835 class UserGroupRepoGroupToPerm(meta.Base, BaseDbModel):
1837 __tablename__ = 'users_group_repo_group_to_perm'
1836 __tablename__ = 'users_group_repo_group_to_perm'
1838 __table_args__ = (
1837 __table_args__ = (
1839 UniqueConstraint('users_group_id', 'group_id'),
1838 UniqueConstraint('users_group_id', 'group_id'),
1840 _table_args_default_dict,
1839 _table_args_default_dict,
1841 )
1840 )
1842
1841
1843 users_group_repo_group_to_perm_id = Column(Integer(), primary_key=True)
1842 users_group_repo_group_to_perm_id = Column(Integer(), primary_key=True)
1844 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1843 users_group_id = Column(Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
1845 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1844 group_id = Column(Integer(), ForeignKey('groups.group_id'), nullable=False)
1846 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1845 permission_id = Column(Integer(), ForeignKey('permissions.permission_id'), nullable=False)
1847
1846
1848 users_group = relationship('UserGroup')
1847 users_group = relationship('UserGroup')
1849 permission = relationship('Permission')
1848 permission = relationship('Permission')
1850 group = relationship('RepoGroup')
1849 group = relationship('RepoGroup')
1851
1850
1852 @classmethod
1851 @classmethod
1853 def create(cls, user_group, repository_group, permission):
1852 def create(cls, user_group, repository_group, permission):
1854 n = cls()
1853 n = cls()
1855 n.users_group = user_group
1854 n.users_group = user_group
1856 n.group = repository_group
1855 n.group = repository_group
1857 n.permission = permission
1856 n.permission = permission
1858 meta.Session().add(n)
1857 meta.Session().add(n)
1859 return n
1858 return n
1860
1859
1861
1860
1862 class Statistics(meta.Base, BaseDbModel):
1861 class Statistics(meta.Base, BaseDbModel):
1863 __tablename__ = 'statistics'
1862 __tablename__ = 'statistics'
1864 __table_args__ = (
1863 __table_args__ = (
1865 _table_args_default_dict,
1864 _table_args_default_dict,
1866 )
1865 )
1867
1866
1868 stat_id = Column(Integer(), primary_key=True)
1867 stat_id = Column(Integer(), primary_key=True)
1869 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True)
1868 repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True)
1870 stat_on_revision = Column(Integer(), nullable=False)
1869 stat_on_revision = Column(Integer(), nullable=False)
1871 commit_activity = Column(LargeBinary(1000000), nullable=False) # JSON data
1870 commit_activity = Column(LargeBinary(1000000), nullable=False) # JSON data
1872 commit_activity_combined = Column(LargeBinary(), nullable=False) # JSON data
1871 commit_activity_combined = Column(LargeBinary(), nullable=False) # JSON data
1873 languages = Column(LargeBinary(1000000), nullable=False) # JSON data
1872 languages = Column(LargeBinary(1000000), nullable=False) # JSON data
1874
1873
1875 repository = relationship('Repository', single_parent=True)
1874 repository = relationship('Repository', single_parent=True)
1876
1875
1877
1876
1878 class UserFollowing(meta.Base, BaseDbModel):
1877 class UserFollowing(meta.Base, BaseDbModel):
1879 __tablename__ = 'user_followings'
1878 __tablename__ = 'user_followings'
1880 __table_args__ = (
1879 __table_args__ = (
1881 UniqueConstraint('user_id', 'follows_repository_id', name='uq_user_followings_user_repo'),
1880 UniqueConstraint('user_id', 'follows_repository_id', name='uq_user_followings_user_repo'),
1882 UniqueConstraint('user_id', 'follows_user_id', name='uq_user_followings_user_user'),
1881 UniqueConstraint('user_id', 'follows_user_id', name='uq_user_followings_user_user'),
1883 _table_args_default_dict,
1882 _table_args_default_dict,
1884 )
1883 )
1885
1884
1886 user_following_id = Column(Integer(), primary_key=True)
1885 user_following_id = Column(Integer(), primary_key=True)
1887 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1886 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1888 follows_repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1887 follows_repository_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1889 follows_user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
1888 follows_user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=True)
1890 follows_from = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1889 follows_from = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1891
1890
1892 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1891 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
1893
1892
1894 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1893 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
1895 follows_repository = relationship('Repository', order_by=lambda: sqlalchemy.func.lower(Repository.repo_name))
1894 follows_repository = relationship('Repository', order_by=lambda: sqlalchemy.func.lower(Repository.repo_name))
1896
1895
1897 @classmethod
1896 @classmethod
1898 def get_repo_followers(cls, repo_id):
1897 def get_repo_followers(cls, repo_id):
1899 return cls.query().filter(cls.follows_repository_id == repo_id)
1898 return cls.query().filter(cls.follows_repository_id == repo_id)
1900
1899
1901
1900
1902 class ChangesetComment(meta.Base, BaseDbModel):
1901 class ChangesetComment(meta.Base, BaseDbModel):
1903 __tablename__ = 'changeset_comments'
1902 __tablename__ = 'changeset_comments'
1904 __table_args__ = (
1903 __table_args__ = (
1905 Index('cc_revision_idx', 'revision'),
1904 Index('cc_revision_idx', 'revision'),
1906 Index('cc_pull_request_id_idx', 'pull_request_id'),
1905 Index('cc_pull_request_id_idx', 'pull_request_id'),
1907 _table_args_default_dict,
1906 _table_args_default_dict,
1908 )
1907 )
1909
1908
1910 comment_id = Column(Integer(), primary_key=True)
1909 comment_id = Column(Integer(), primary_key=True)
1911 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1910 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1912 revision = Column(String(40), nullable=True)
1911 revision = Column(String(40), nullable=True)
1913 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1912 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1914 line_no = Column(Unicode(10), nullable=True)
1913 line_no = Column(Unicode(10), nullable=True)
1915 f_path = Column(Unicode(1000), nullable=True)
1914 f_path = Column(Unicode(1000), nullable=True)
1916 author_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1915 author_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
1917 text = Column(UnicodeText(), nullable=False)
1916 text = Column(UnicodeText(), nullable=False)
1918 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1917 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1919 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1918 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1920
1919
1921 author = relationship('User')
1920 author = relationship('User')
1922 repo = relationship('Repository')
1921 repo = relationship('Repository')
1923 # status_change is frequently used directly in templates - make it a lazy
1922 # status_change is frequently used directly in templates - make it a lazy
1924 # join to avoid fetching each related ChangesetStatus on demand.
1923 # join to avoid fetching each related ChangesetStatus on demand.
1925 # There will only be one ChangesetStatus referencing each comment so the join will not explode.
1924 # There will only be one ChangesetStatus referencing each comment so the join will not explode.
1926 status_change = relationship('ChangesetStatus',
1925 status_change = relationship('ChangesetStatus',
1927 cascade="all, delete-orphan", lazy='joined')
1926 cascade="all, delete-orphan", lazy='joined')
1928 pull_request = relationship('PullRequest')
1927 pull_request = relationship('PullRequest')
1929
1928
1930 def url(self):
1929 def url(self):
1931 anchor = "comment-%s" % self.comment_id
1930 anchor = "comment-%s" % self.comment_id
1932 if self.revision:
1931 if self.revision:
1933 return webutils.url('changeset_home', repo_name=self.repo.repo_name, revision=self.revision, anchor=anchor)
1932 return webutils.url('changeset_home', repo_name=self.repo.repo_name, revision=self.revision, anchor=anchor)
1934 elif self.pull_request_id is not None:
1933 elif self.pull_request_id is not None:
1935 return self.pull_request.url(anchor=anchor)
1934 return self.pull_request.url(anchor=anchor)
1936
1935
1937 def __json__(self):
1936 def __json__(self):
1938 return dict(
1937 return dict(
1939 comment_id=self.comment_id,
1938 comment_id=self.comment_id,
1940 username=self.author.username,
1939 username=self.author.username,
1941 text=self.text,
1940 text=self.text,
1942 )
1941 )
1943
1942
1944 def deletable(self):
1943 def deletable(self):
1945 return self.created_on > datetime.datetime.now() - datetime.timedelta(minutes=5)
1944 return self.created_on > datetime.datetime.now() - datetime.timedelta(minutes=5)
1946
1945
1947
1946
1948 class ChangesetStatus(meta.Base, BaseDbModel):
1947 class ChangesetStatus(meta.Base, BaseDbModel):
1949 __tablename__ = 'changeset_statuses'
1948 __tablename__ = 'changeset_statuses'
1950 __table_args__ = (
1949 __table_args__ = (
1951 Index('cs_revision_idx', 'revision'),
1950 Index('cs_revision_idx', 'revision'),
1952 Index('cs_version_idx', 'version'),
1951 Index('cs_version_idx', 'version'),
1953 Index('cs_pull_request_id_idx', 'pull_request_id'),
1952 Index('cs_pull_request_id_idx', 'pull_request_id'),
1954 Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
1953 Index('cs_changeset_comment_id_idx', 'changeset_comment_id'),
1955 Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
1954 Index('cs_pull_request_id_user_id_version_idx', 'pull_request_id', 'user_id', 'version'),
1956 Index('cs_repo_id_pull_request_id_idx', 'repo_id', 'pull_request_id'),
1955 Index('cs_repo_id_pull_request_id_idx', 'repo_id', 'pull_request_id'),
1957 UniqueConstraint('repo_id', 'revision', 'version'),
1956 UniqueConstraint('repo_id', 'revision', 'version'),
1958 _table_args_default_dict,
1957 _table_args_default_dict,
1959 )
1958 )
1960
1959
1961 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1960 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
1962 STATUS_APPROVED = 'approved'
1961 STATUS_APPROVED = 'approved'
1963 STATUS_REJECTED = 'rejected' # is shown as "Not approved" - TODO: change database content / scheme
1962 STATUS_REJECTED = 'rejected' # is shown as "Not approved" - TODO: change database content / scheme
1964 STATUS_UNDER_REVIEW = 'under_review'
1963 STATUS_UNDER_REVIEW = 'under_review'
1965
1964
1966 STATUSES = [
1965 STATUSES = [
1967 (STATUS_NOT_REVIEWED, _("Not reviewed")), # (no icon) and default
1966 (STATUS_NOT_REVIEWED, _("Not reviewed")), # (no icon) and default
1968 (STATUS_UNDER_REVIEW, _("Under review")),
1967 (STATUS_UNDER_REVIEW, _("Under review")),
1969 (STATUS_REJECTED, _("Not approved")),
1968 (STATUS_REJECTED, _("Not approved")),
1970 (STATUS_APPROVED, _("Approved")),
1969 (STATUS_APPROVED, _("Approved")),
1971 ]
1970 ]
1972 STATUSES_DICT = dict(STATUSES)
1971 STATUSES_DICT = dict(STATUSES)
1973
1972
1974 changeset_status_id = Column(Integer(), primary_key=True)
1973 changeset_status_id = Column(Integer(), primary_key=True)
1975 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1974 repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
1976 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1975 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
1977 revision = Column(String(40), nullable=True)
1976 revision = Column(String(40), nullable=True)
1978 status = Column(String(128), nullable=False, default=DEFAULT)
1977 status = Column(String(128), nullable=False, default=DEFAULT)
1979 comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
1978 comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
1980 modified_at = Column(DateTime(), nullable=False, default=datetime.datetime.now)
1979 modified_at = Column(DateTime(), nullable=False, default=datetime.datetime.now)
1981 version = Column(Integer(), nullable=False, default=0)
1980 version = Column(Integer(), nullable=False, default=0)
1982 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1981 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
1983
1982
1984 author = relationship('User')
1983 author = relationship('User')
1985 repo = relationship('Repository')
1984 repo = relationship('Repository')
1986 comment = relationship('ChangesetComment')
1985 comment = relationship('ChangesetComment')
1987 pull_request = relationship('PullRequest')
1986 pull_request = relationship('PullRequest')
1988
1987
1989 def __repr__(self):
1988 def __repr__(self):
1990 return "<%s %r by %r>" % (
1989 return "<%s %r by %r>" % (
1991 self.__class__.__name__,
1990 self.__class__.__name__,
1992 self.status, self.author
1991 self.status, self.author
1993 )
1992 )
1994
1993
1995 @classmethod
1994 @classmethod
1996 def get_status_lbl(cls, value):
1995 def get_status_lbl(cls, value):
1997 return cls.STATUSES_DICT.get(value)
1996 return cls.STATUSES_DICT.get(value)
1998
1997
1999 @property
1998 @property
2000 def status_lbl(self):
1999 def status_lbl(self):
2001 return ChangesetStatus.get_status_lbl(self.status)
2000 return ChangesetStatus.get_status_lbl(self.status)
2002
2001
2003 def __json__(self):
2002 def __json__(self):
2004 return dict(
2003 return dict(
2005 status=self.status,
2004 status=self.status,
2006 modified_at=self.modified_at.replace(microsecond=0),
2005 modified_at=self.modified_at.replace(microsecond=0),
2007 reviewer=self.author.username,
2006 reviewer=self.author.username,
2008 )
2007 )
2009
2008
2010
2009
2011 class PullRequest(meta.Base, BaseDbModel):
2010 class PullRequest(meta.Base, BaseDbModel):
2012 __tablename__ = 'pull_requests'
2011 __tablename__ = 'pull_requests'
2013 __table_args__ = (
2012 __table_args__ = (
2014 Index('pr_org_repo_id_idx', 'org_repo_id'),
2013 Index('pr_org_repo_id_idx', 'org_repo_id'),
2015 Index('pr_other_repo_id_idx', 'other_repo_id'),
2014 Index('pr_other_repo_id_idx', 'other_repo_id'),
2016 _table_args_default_dict,
2015 _table_args_default_dict,
2017 )
2016 )
2018
2017
2019 # values for .status
2018 # values for .status
2020 STATUS_NEW = 'new'
2019 STATUS_NEW = 'new'
2021 STATUS_CLOSED = 'closed'
2020 STATUS_CLOSED = 'closed'
2022
2021
2023 pull_request_id = Column(Integer(), primary_key=True)
2022 pull_request_id = Column(Integer(), primary_key=True)
2024 title = Column(Unicode(255), nullable=False)
2023 title = Column(Unicode(255), nullable=False)
2025 description = Column(UnicodeText(), nullable=False)
2024 description = Column(UnicodeText(), nullable=False)
2026 status = Column(Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
2025 status = Column(Unicode(255), nullable=False, default=STATUS_NEW) # only for closedness, not approve/reject/etc
2027 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2026 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2028 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2027 updated_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2029 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2028 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2030 _revisions = Column('revisions', UnicodeText(), nullable=False)
2029 _revisions = Column('revisions', UnicodeText(), nullable=False)
2031 org_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2030 org_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2032 org_ref = Column(Unicode(255), nullable=False)
2031 org_ref = Column(Unicode(255), nullable=False)
2033 other_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2032 other_repo_id = Column(Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2034 other_ref = Column(Unicode(255), nullable=False)
2033 other_ref = Column(Unicode(255), nullable=False)
2035
2034
2036 @hybrid_property
2035 @hybrid_property
2037 def revisions(self):
2036 def revisions(self):
2038 return self._revisions.split(':')
2037 return self._revisions.split(':')
2039
2038
2040 @revisions.setter
2039 @revisions.setter
2041 def revisions(self, val):
2040 def revisions(self, val):
2042 self._revisions = ':'.join(val)
2041 self._revisions = ':'.join(val)
2043
2042
2044 @property
2043 @property
2045 def org_ref_parts(self):
2044 def org_ref_parts(self):
2046 return self.org_ref.split(':')
2045 return self.org_ref.split(':')
2047
2046
2048 @property
2047 @property
2049 def other_ref_parts(self):
2048 def other_ref_parts(self):
2050 return self.other_ref.split(':')
2049 return self.other_ref.split(':')
2051
2050
2052 owner = relationship('User')
2051 owner = relationship('User')
2053 reviewers = relationship('PullRequestReviewer',
2052 reviewers = relationship('PullRequestReviewer',
2054 cascade="all, delete-orphan")
2053 cascade="all, delete-orphan")
2055 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2054 org_repo = relationship('Repository', primaryjoin='PullRequest.org_repo_id==Repository.repo_id')
2056 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2055 other_repo = relationship('Repository', primaryjoin='PullRequest.other_repo_id==Repository.repo_id')
2057 statuses = relationship('ChangesetStatus', order_by='ChangesetStatus.changeset_status_id')
2056 statuses = relationship('ChangesetStatus', order_by='ChangesetStatus.changeset_status_id')
2058 comments = relationship('ChangesetComment', order_by='ChangesetComment.comment_id',
2057 comments = relationship('ChangesetComment', order_by='ChangesetComment.comment_id',
2059 cascade="all, delete-orphan")
2058 cascade="all, delete-orphan")
2060
2059
2061 @classmethod
2060 @classmethod
2062 def query(cls, reviewer_id=None, include_closed=True, sorted=False):
2061 def query(cls, reviewer_id=None, include_closed=True, sorted=False):
2063 """Add PullRequest-specific helpers for common query constructs.
2062 """Add PullRequest-specific helpers for common query constructs.
2064
2063
2065 reviewer_id: only PRs with the specified user added as reviewer.
2064 reviewer_id: only PRs with the specified user added as reviewer.
2066
2065
2067 include_closed: if False, do not include closed PRs.
2066 include_closed: if False, do not include closed PRs.
2068
2067
2069 sorted: if True, apply the default ordering (newest first).
2068 sorted: if True, apply the default ordering (newest first).
2070 """
2069 """
2071 q = super(PullRequest, cls).query()
2070 q = super(PullRequest, cls).query()
2072
2071
2073 if reviewer_id is not None:
2072 if reviewer_id is not None:
2074 q = q.join(PullRequestReviewer).filter(PullRequestReviewer.user_id == reviewer_id)
2073 q = q.join(PullRequestReviewer).filter(PullRequestReviewer.user_id == reviewer_id)
2075
2074
2076 if not include_closed:
2075 if not include_closed:
2077 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
2076 q = q.filter(PullRequest.status != PullRequest.STATUS_CLOSED)
2078
2077
2079 if sorted:
2078 if sorted:
2080 q = q.order_by(PullRequest.created_on.desc())
2079 q = q.order_by(PullRequest.created_on.desc())
2081
2080
2082 return q
2081 return q
2083
2082
2084 def get_reviewer_users(self):
2083 def get_reviewer_users(self):
2085 """Like .reviewers, but actually returning the users"""
2084 """Like .reviewers, but actually returning the users"""
2086 return User.query() \
2085 return User.query() \
2087 .join(PullRequestReviewer) \
2086 .join(PullRequestReviewer) \
2088 .filter(PullRequestReviewer.pull_request == self) \
2087 .filter(PullRequestReviewer.pull_request == self) \
2089 .order_by(PullRequestReviewer.pull_request_reviewers_id) \
2088 .order_by(PullRequestReviewer.pull_request_reviewers_id) \
2090 .all()
2089 .all()
2091
2090
2092 def is_closed(self):
2091 def is_closed(self):
2093 return self.status == self.STATUS_CLOSED
2092 return self.status == self.STATUS_CLOSED
2094
2093
2095 def user_review_status(self, user_id):
2094 def user_review_status(self, user_id):
2096 """Return the user's latest status votes on PR"""
2095 """Return the user's latest status votes on PR"""
2097 # note: no filtering on repo - that would be redundant
2096 # note: no filtering on repo - that would be redundant
2098 status = ChangesetStatus.query() \
2097 status = ChangesetStatus.query() \
2099 .filter(ChangesetStatus.pull_request == self) \
2098 .filter(ChangesetStatus.pull_request == self) \
2100 .filter(ChangesetStatus.user_id == user_id) \
2099 .filter(ChangesetStatus.user_id == user_id) \
2101 .order_by(ChangesetStatus.version) \
2100 .order_by(ChangesetStatus.version) \
2102 .first()
2101 .first()
2103 return str(status.status) if status else ''
2102 return str(status.status) if status else ''
2104
2103
2105 @classmethod
2104 @classmethod
2106 def make_nice_id(cls, pull_request_id):
2105 def make_nice_id(cls, pull_request_id):
2107 '''Return pull request id nicely formatted for displaying'''
2106 '''Return pull request id nicely formatted for displaying'''
2108 return '#%s' % pull_request_id
2107 return '#%s' % pull_request_id
2109
2108
2110 def nice_id(self):
2109 def nice_id(self):
2111 '''Return the id of this pull request, nicely formatted for displaying'''
2110 '''Return the id of this pull request, nicely formatted for displaying'''
2112 return self.make_nice_id(self.pull_request_id)
2111 return self.make_nice_id(self.pull_request_id)
2113
2112
2114 def get_api_data(self):
2113 def get_api_data(self):
2115 return self.__json__()
2114 return self.__json__()
2116
2115
2117 def __json__(self):
2116 def __json__(self):
2118 clone_uri_tmpl = kallithea.CONFIG.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI
2117 clone_uri_tmpl = kallithea.CONFIG.get('clone_uri_tmpl') or Repository.DEFAULT_CLONE_URI
2119 return dict(
2118 return dict(
2120 pull_request_id=self.pull_request_id,
2119 pull_request_id=self.pull_request_id,
2121 url=self.url(),
2120 url=self.url(),
2122 reviewers=self.reviewers,
2121 reviewers=self.reviewers,
2123 revisions=self.revisions,
2122 revisions=self.revisions,
2124 owner=self.owner.username,
2123 owner=self.owner.username,
2125 title=self.title,
2124 title=self.title,
2126 description=self.description,
2125 description=self.description,
2127 org_repo_url=self.org_repo.clone_url(clone_uri_tmpl=clone_uri_tmpl),
2126 org_repo_url=self.org_repo.clone_url(clone_uri_tmpl=clone_uri_tmpl),
2128 org_ref_parts=self.org_ref_parts,
2127 org_ref_parts=self.org_ref_parts,
2129 other_ref_parts=self.other_ref_parts,
2128 other_ref_parts=self.other_ref_parts,
2130 status=self.status,
2129 status=self.status,
2131 comments=self.comments,
2130 comments=self.comments,
2132 statuses=self.statuses,
2131 statuses=self.statuses,
2133 created_on=self.created_on.replace(microsecond=0),
2132 created_on=self.created_on.replace(microsecond=0),
2134 updated_on=self.updated_on.replace(microsecond=0),
2133 updated_on=self.updated_on.replace(microsecond=0),
2135 )
2134 )
2136
2135
2137 def url(self, **kwargs):
2136 def url(self, **kwargs):
2138 canonical = kwargs.pop('canonical', None)
2137 canonical = kwargs.pop('canonical', None)
2139 b = self.org_ref_parts[1]
2138 b = self.org_ref_parts[1]
2140 if b != self.other_ref_parts[1]:
2139 if b != self.other_ref_parts[1]:
2141 s = '/_/' + b
2140 s = '/_/' + b
2142 else:
2141 else:
2143 s = '/_/' + self.title
2142 s = '/_/' + self.title
2144 kwargs['extra'] = urlreadable(s)
2143 kwargs['extra'] = urlreadable(s)
2145 if canonical:
2144 if canonical:
2146 return webutils.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
2145 return webutils.canonical_url('pullrequest_show', repo_name=self.other_repo.repo_name,
2147 pull_request_id=self.pull_request_id, **kwargs)
2146 pull_request_id=self.pull_request_id, **kwargs)
2148 return webutils.url('pullrequest_show', repo_name=self.other_repo.repo_name,
2147 return webutils.url('pullrequest_show', repo_name=self.other_repo.repo_name,
2149 pull_request_id=self.pull_request_id, **kwargs)
2148 pull_request_id=self.pull_request_id, **kwargs)
2150
2149
2151
2150
2152 class PullRequestReviewer(meta.Base, BaseDbModel):
2151 class PullRequestReviewer(meta.Base, BaseDbModel):
2153 __tablename__ = 'pull_request_reviewers'
2152 __tablename__ = 'pull_request_reviewers'
2154 __table_args__ = (
2153 __table_args__ = (
2155 Index('pull_request_reviewers_user_id_idx', 'user_id'),
2154 Index('pull_request_reviewers_user_id_idx', 'user_id'),
2156 UniqueConstraint('pull_request_id', 'user_id'),
2155 UniqueConstraint('pull_request_id', 'user_id'),
2157 _table_args_default_dict,
2156 _table_args_default_dict,
2158 )
2157 )
2159
2158
2160 def __init__(self, user=None, pull_request=None):
2159 def __init__(self, user=None, pull_request=None):
2161 self.user = user
2160 self.user = user
2162 self.pull_request = pull_request
2161 self.pull_request = pull_request
2163
2162
2164 pull_request_reviewers_id = Column('pull_requests_reviewers_id', Integer(), primary_key=True)
2163 pull_request_reviewers_id = Column('pull_requests_reviewers_id', Integer(), primary_key=True)
2165 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2164 pull_request_id = Column(Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=False)
2166 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2165 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2167
2166
2168 user = relationship('User')
2167 user = relationship('User')
2169 pull_request = relationship('PullRequest')
2168 pull_request = relationship('PullRequest')
2170
2169
2171 def __json__(self):
2170 def __json__(self):
2172 return dict(
2171 return dict(
2173 username=self.user.username if self.user else None,
2172 username=self.user.username if self.user else None,
2174 )
2173 )
2175
2174
2176
2175
2177 class Notification(object):
2176 class Notification(object):
2178 __tablename__ = 'notifications'
2177 __tablename__ = 'notifications'
2179
2178
2180 class UserNotification(object):
2179 class UserNotification(object):
2181 __tablename__ = 'user_to_notification'
2180 __tablename__ = 'user_to_notification'
2182
2181
2183
2182
2184 class Gist(meta.Base, BaseDbModel):
2183 class Gist(meta.Base, BaseDbModel):
2185 __tablename__ = 'gists'
2184 __tablename__ = 'gists'
2186 __table_args__ = (
2185 __table_args__ = (
2187 Index('g_gist_access_id_idx', 'gist_access_id'),
2186 Index('g_gist_access_id_idx', 'gist_access_id'),
2188 Index('g_created_on_idx', 'created_on'),
2187 Index('g_created_on_idx', 'created_on'),
2189 _table_args_default_dict,
2188 _table_args_default_dict,
2190 )
2189 )
2191
2190
2192 GIST_STORE_LOC = '.rc_gist_store'
2191 GIST_STORE_LOC = '.rc_gist_store'
2193 GIST_METADATA_FILE = '.rc_gist_metadata'
2192 GIST_METADATA_FILE = '.rc_gist_metadata'
2194
2193
2195 GIST_PUBLIC = 'public'
2194 GIST_PUBLIC = 'public'
2196 GIST_PRIVATE = 'private'
2195 GIST_PRIVATE = 'private'
2197 DEFAULT_FILENAME = 'gistfile1.txt'
2196 DEFAULT_FILENAME = 'gistfile1.txt'
2198
2197
2199 gist_id = Column(Integer(), primary_key=True)
2198 gist_id = Column(Integer(), primary_key=True)
2200 gist_access_id = Column(Unicode(250), nullable=False)
2199 gist_access_id = Column(Unicode(250), nullable=False)
2201 gist_description = Column(UnicodeText(), nullable=False)
2200 gist_description = Column(UnicodeText(), nullable=False)
2202 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2201 owner_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2203 gist_expires = Column(Float(53), nullable=False)
2202 gist_expires = Column(Float(53), nullable=False)
2204 gist_type = Column(Unicode(128), nullable=False)
2203 gist_type = Column(Unicode(128), nullable=False)
2205 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2204 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2206 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2205 modified_at = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2207
2206
2208 owner = relationship('User')
2207 owner = relationship('User')
2209
2208
2210 @hybrid_property
2209 @hybrid_property
2211 def is_expired(self):
2210 def is_expired(self):
2212 return (self.gist_expires != -1) & (time.time() > self.gist_expires)
2211 return (self.gist_expires != -1) & (time.time() > self.gist_expires)
2213
2212
2214 def __repr__(self):
2213 def __repr__(self):
2215 return "<%s %s %s>" % (
2214 return "<%s %s %s>" % (
2216 self.__class__.__name__,
2215 self.__class__.__name__,
2217 self.gist_type, self.gist_access_id)
2216 self.gist_type, self.gist_access_id)
2218
2217
2219 @classmethod
2218 @classmethod
2220 def guess_instance(cls, value):
2219 def guess_instance(cls, value):
2221 return super(Gist, cls).guess_instance(value, Gist.get_by_access_id)
2220 return super(Gist, cls).guess_instance(value, Gist.get_by_access_id)
2222
2221
2223 @classmethod
2222 @classmethod
2224 def get_or_404(cls, id_):
2223 def get_or_404(cls, id_):
2225 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2224 res = cls.query().filter(cls.gist_access_id == id_).scalar()
2226 if res is None:
2225 if res is None:
2227 raise HTTPNotFound
2226 raise HTTPNotFound
2228 return res
2227 return res
2229
2228
2230 @classmethod
2229 @classmethod
2231 def get_by_access_id(cls, gist_access_id):
2230 def get_by_access_id(cls, gist_access_id):
2232 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2231 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
2233
2232
2234 def gist_url(self):
2233 def gist_url(self):
2235 alias_url = kallithea.CONFIG.get('gist_alias_url')
2234 alias_url = kallithea.CONFIG.get('gist_alias_url')
2236 if alias_url:
2235 if alias_url:
2237 return alias_url.replace('{gistid}', self.gist_access_id)
2236 return alias_url.replace('{gistid}', self.gist_access_id)
2238
2237
2239 return webutils.canonical_url('gist', gist_id=self.gist_access_id)
2238 return webutils.canonical_url('gist', gist_id=self.gist_access_id)
2240
2239
2241 def get_api_data(self):
2240 def get_api_data(self):
2242 """
2241 """
2243 Common function for generating gist related data for API
2242 Common function for generating gist related data for API
2244 """
2243 """
2245 gist = self
2244 gist = self
2246 data = dict(
2245 data = dict(
2247 gist_id=gist.gist_id,
2246 gist_id=gist.gist_id,
2248 type=gist.gist_type,
2247 type=gist.gist_type,
2249 access_id=gist.gist_access_id,
2248 access_id=gist.gist_access_id,
2250 description=gist.gist_description,
2249 description=gist.gist_description,
2251 url=gist.gist_url(),
2250 url=gist.gist_url(),
2252 expires=gist.gist_expires,
2251 expires=gist.gist_expires,
2253 created_on=gist.created_on,
2252 created_on=gist.created_on,
2254 )
2253 )
2255 return data
2254 return data
2256
2255
2257 def __json__(self):
2256 def __json__(self):
2258 data = dict(
2257 data = dict(
2259 )
2258 )
2260 data.update(self.get_api_data())
2259 data.update(self.get_api_data())
2261 return data
2260 return data
2262
2261
2263 ## SCM functions
2262 ## SCM functions
2264
2263
2265 @property
2264 @property
2266 def scm_instance(self):
2265 def scm_instance(self):
2267 gist_base_path = os.path.join(kallithea.CONFIG['base_path'], self.GIST_STORE_LOC)
2266 gist_base_path = os.path.join(kallithea.CONFIG['base_path'], self.GIST_STORE_LOC)
2268 return get_repo(os.path.join(gist_base_path, self.gist_access_id))
2267 return get_repo(os.path.join(gist_base_path, self.gist_access_id))
2269
2268
2270
2269
2271 class UserSshKeys(meta.Base, BaseDbModel):
2270 class UserSshKeys(meta.Base, BaseDbModel):
2272 __tablename__ = 'user_ssh_keys'
2271 __tablename__ = 'user_ssh_keys'
2273 __table_args__ = (
2272 __table_args__ = (
2274 Index('usk_fingerprint_idx', 'fingerprint'),
2273 Index('usk_fingerprint_idx', 'fingerprint'),
2275 _table_args_default_dict
2274 _table_args_default_dict
2276 )
2275 )
2277 __mapper_args__ = {}
2276 __mapper_args__ = {}
2278
2277
2279 user_ssh_key_id = Column(Integer(), primary_key=True)
2278 user_ssh_key_id = Column(Integer(), primary_key=True)
2280 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2279 user_id = Column(Integer(), ForeignKey('users.user_id'), nullable=False)
2281 _public_key = Column('public_key', UnicodeText(), nullable=False)
2280 _public_key = Column('public_key', UnicodeText(), nullable=False)
2282 description = Column(UnicodeText(), nullable=False)
2281 description = Column(UnicodeText(), nullable=False)
2283 fingerprint = Column(String(255), nullable=False, unique=True)
2282 fingerprint = Column(String(255), nullable=False, unique=True)
2284 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2283 created_on = Column(DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2285 last_seen = Column(DateTime(timezone=False), nullable=True)
2284 last_seen = Column(DateTime(timezone=False), nullable=True)
2286
2285
2287 user = relationship('User')
2286 user = relationship('User')
2288
2287
2289 @property
2288 @property
2290 def public_key(self):
2289 def public_key(self):
2291 return self._public_key
2290 return self._public_key
2292
2291
2293 @public_key.setter
2292 @public_key.setter
2294 def public_key(self, full_key):
2293 def public_key(self, full_key):
2295 """The full public key is too long to be suitable as database key.
2294 """The full public key is too long to be suitable as database key.
2296 Instead, as a side-effect of setting the public key string, compute the
2295 Instead, as a side-effect of setting the public key string, compute the
2297 fingerprints according to https://tools.ietf.org/html/rfc4716#section-4
2296 fingerprints according to https://tools.ietf.org/html/rfc4716#section-4
2298 BUT using sha256 instead of md5, similar to 'ssh-keygen -E sha256 -lf
2297 BUT using sha256 instead of md5, similar to 'ssh-keygen -E sha256 -lf
2299 ~/.ssh/id_rsa.pub' .
2298 ~/.ssh/id_rsa.pub' .
2300 """
2299 """
2301 keytype, key_bytes, comment = ssh.parse_pub_key(full_key)
2300 keytype, key_bytes, comment = ssh.parse_pub_key(full_key)
2302 self._public_key = full_key
2301 self._public_key = full_key
2303 self.fingerprint = base64.b64encode(hashlib.sha256(key_bytes).digest()).replace(b'\n', b'').rstrip(b'=').decode()
2302 self.fingerprint = base64.b64encode(hashlib.sha256(key_bytes).digest()).replace(b'\n', b'').rstrip(b'=').decode()
@@ -1,505 +1,503 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.model.user
15 kallithea.model.user
16 ~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~
17
17
18 users model for Kallithea
18 users model for Kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Apr 9, 2010
22 :created_on: Apr 9, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28
28
29 import hashlib
29 import hashlib
30 import hmac
30 import hmac
31 import logging
31 import logging
32 import time
32 import time
33 import traceback
33 import traceback
34
34
35 from sqlalchemy.exc import DatabaseError
35 from sqlalchemy.exc import DatabaseError
36 from tg import config
36 from tg import config
37 from tg.i18n import ugettext as _
37 from tg.i18n import ugettext as _
38
38
39 from kallithea.lib import webutils
39 from kallithea.lib import webutils
40 from kallithea.lib.exceptions import DefaultUserException, UserOwnsReposException
40 from kallithea.lib.exceptions import DefaultUserException, UserOwnsReposException
41 from kallithea.lib.utils2 import generate_api_key, get_current_authuser
41 from kallithea.lib.utils2 import generate_api_key, get_current_authuser
42 from kallithea.model import db, meta
42 from kallithea.model import db, forms, meta
43
43
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 class UserModel(object):
48 class UserModel(object):
49 password_reset_token_lifetime = 86400 # 24 hours
49 password_reset_token_lifetime = 86400 # 24 hours
50
50
51 def get(self, user_id):
51 def get(self, user_id):
52 user = db.User.query()
52 user = db.User.query()
53 return user.get(user_id)
53 return user.get(user_id)
54
54
55 def get_user(self, user):
55 def get_user(self, user):
56 return db.User.guess_instance(user)
56 return db.User.guess_instance(user)
57
57
58 def create(self, form_data, cur_user=None):
58 def create(self, form_data, cur_user=None):
59 if not cur_user:
59 if not cur_user:
60 cur_user = getattr(get_current_authuser(), 'username', None)
60 cur_user = getattr(get_current_authuser(), 'username', None)
61
61
62 from kallithea.lib.hooks import check_allowed_create_user, log_create_user
62 from kallithea.lib.hooks import check_allowed_create_user, log_create_user
63 _fd = form_data
63 _fd = form_data
64 user_data = {
64 user_data = {
65 'username': _fd['username'],
65 'username': _fd['username'],
66 'password': _fd['password'],
66 'password': _fd['password'],
67 'email': _fd['email'],
67 'email': _fd['email'],
68 'firstname': _fd['firstname'],
68 'firstname': _fd['firstname'],
69 'lastname': _fd['lastname'],
69 'lastname': _fd['lastname'],
70 'active': _fd['active'],
70 'active': _fd['active'],
71 'admin': False
71 'admin': False
72 }
72 }
73 # raises UserCreationError if it's not allowed
73 # raises UserCreationError if it's not allowed
74 check_allowed_create_user(user_data, cur_user)
74 check_allowed_create_user(user_data, cur_user)
75 from kallithea.lib.auth import get_crypt_password
75 from kallithea.lib.auth import get_crypt_password
76
76
77 new_user = db.User()
77 new_user = db.User()
78 for k, v in form_data.items():
78 for k, v in form_data.items():
79 if k == 'password':
79 if k == 'password':
80 v = get_crypt_password(v)
80 v = get_crypt_password(v)
81 if k == 'firstname':
81 if k == 'firstname':
82 k = 'name'
82 k = 'name'
83 setattr(new_user, k, v)
83 setattr(new_user, k, v)
84
84
85 new_user.api_key = generate_api_key()
85 new_user.api_key = generate_api_key()
86 meta.Session().add(new_user)
86 meta.Session().add(new_user)
87 meta.Session().flush() # make database assign new_user.user_id
87 meta.Session().flush() # make database assign new_user.user_id
88
88
89 log_create_user(new_user.get_dict(), cur_user)
89 log_create_user(new_user.get_dict(), cur_user)
90 return new_user
90 return new_user
91
91
92 def create_or_update(self, username, password, email, firstname='',
92 def create_or_update(self, username, password, email, firstname='',
93 lastname='', active=True, admin=False,
93 lastname='', active=True, admin=False,
94 extern_type=None, extern_name=None, cur_user=None):
94 extern_type=None, extern_name=None, cur_user=None):
95 """
95 """
96 Creates a new instance if not found, or updates current one
96 Creates a new instance if not found, or updates current one
97
97
98 :param username:
98 :param username:
99 :param password:
99 :param password:
100 :param email:
100 :param email:
101 :param active:
101 :param active:
102 :param firstname:
102 :param firstname:
103 :param lastname:
103 :param lastname:
104 :param active:
104 :param active:
105 :param admin:
105 :param admin:
106 :param extern_name:
106 :param extern_name:
107 :param extern_type:
107 :param extern_type:
108 :param cur_user:
108 :param cur_user:
109 """
109 """
110 if not cur_user:
110 if not cur_user:
111 cur_user = getattr(get_current_authuser(), 'username', None)
111 cur_user = getattr(get_current_authuser(), 'username', None)
112
112
113 from kallithea.lib.auth import check_password, get_crypt_password
113 from kallithea.lib.auth import check_password, get_crypt_password
114 from kallithea.lib.hooks import check_allowed_create_user, log_create_user
114 from kallithea.lib.hooks import check_allowed_create_user, log_create_user
115 user_data = {
115 user_data = {
116 'username': username, 'password': password,
116 'username': username, 'password': password,
117 'email': email, 'firstname': firstname, 'lastname': lastname,
117 'email': email, 'firstname': firstname, 'lastname': lastname,
118 'active': active, 'admin': admin
118 'active': active, 'admin': admin
119 }
119 }
120 # raises UserCreationError if it's not allowed
120 # raises UserCreationError if it's not allowed
121 check_allowed_create_user(user_data, cur_user)
121 check_allowed_create_user(user_data, cur_user)
122
122
123 log.debug('Checking for %s account in Kallithea database', username)
123 log.debug('Checking for %s account in Kallithea database', username)
124 user = db.User.get_by_username(username, case_insensitive=True)
124 user = db.User.get_by_username(username, case_insensitive=True)
125 if user is None:
125 if user is None:
126 log.debug('creating new user %s', username)
126 log.debug('creating new user %s', username)
127 new_user = db.User()
127 new_user = db.User()
128 edit = False
128 edit = False
129 else:
129 else:
130 log.debug('updating user %s', username)
130 log.debug('updating user %s', username)
131 new_user = user
131 new_user = user
132 edit = True
132 edit = True
133
133
134 try:
134 try:
135 new_user.username = username
135 new_user.username = username
136 new_user.admin = admin
136 new_user.admin = admin
137 new_user.email = email
137 new_user.email = email
138 new_user.active = active
138 new_user.active = active
139 new_user.extern_name = extern_name
139 new_user.extern_name = extern_name
140 new_user.extern_type = extern_type
140 new_user.extern_type = extern_type
141 new_user.name = firstname
141 new_user.name = firstname
142 new_user.lastname = lastname
142 new_user.lastname = lastname
143
143
144 if not edit:
144 if not edit:
145 new_user.api_key = generate_api_key()
145 new_user.api_key = generate_api_key()
146
146
147 # set password only if creating an user or password is changed
147 # set password only if creating an user or password is changed
148 password_change = new_user.password and \
148 password_change = new_user.password and \
149 not check_password(password, new_user.password)
149 not check_password(password, new_user.password)
150 if not edit or password_change:
150 if not edit or password_change:
151 reason = 'new password' if edit else 'new user'
151 reason = 'new password' if edit else 'new user'
152 log.debug('Updating password reason=>%s', reason)
152 log.debug('Updating password reason=>%s', reason)
153 new_user.password = get_crypt_password(password) \
153 new_user.password = get_crypt_password(password) \
154 if password else ''
154 if password else ''
155
155
156 if user is None:
156 if user is None:
157 meta.Session().add(new_user)
157 meta.Session().add(new_user)
158 meta.Session().flush() # make database assign new_user.user_id
158 meta.Session().flush() # make database assign new_user.user_id
159
159
160 if not edit:
160 if not edit:
161 log_create_user(new_user.get_dict(), cur_user)
161 log_create_user(new_user.get_dict(), cur_user)
162
162
163 return new_user
163 return new_user
164 except (DatabaseError,):
164 except (DatabaseError,):
165 log.error(traceback.format_exc())
165 log.error(traceback.format_exc())
166 raise
166 raise
167
167
168 def create_registration(self, form_data):
168 def create_registration(self, form_data):
169 from kallithea.model import notification
169 from kallithea.model import notification
170
170
171 form_data['admin'] = False
171 form_data['admin'] = False
172 form_data['extern_type'] = db.User.DEFAULT_AUTH_TYPE
172 form_data['extern_type'] = db.User.DEFAULT_AUTH_TYPE
173 form_data['extern_name'] = ''
173 form_data['extern_name'] = ''
174 new_user = self.create(form_data)
174 new_user = self.create(form_data)
175
175
176 # notification to admins
176 # notification to admins
177 subject = _('New user registration')
177 subject = _('New user registration')
178 body = (
178 body = (
179 'New user registration\n'
179 'New user registration\n'
180 '---------------------\n'
180 '---------------------\n'
181 '- Username: {user.username}\n'
181 '- Username: {user.username}\n'
182 '- Full Name: {user.full_name}\n'
182 '- Full Name: {user.full_name}\n'
183 '- Email: {user.email}\n'
183 '- Email: {user.email}\n'
184 ).format(user=new_user)
184 ).format(user=new_user)
185 edit_url = webutils.canonical_url('edit_user', id=new_user.user_id)
185 edit_url = webutils.canonical_url('edit_user', id=new_user.user_id)
186 email_kwargs = {
186 email_kwargs = {
187 'registered_user_url': edit_url,
187 'registered_user_url': edit_url,
188 'new_username': new_user.username,
188 'new_username': new_user.username,
189 'new_email': new_user.email,
189 'new_email': new_user.email,
190 'new_full_name': new_user.full_name}
190 'new_full_name': new_user.full_name}
191 notification.NotificationModel().create(created_by=new_user, subject=subject,
191 notification.NotificationModel().create(created_by=new_user, subject=subject,
192 body=body, recipients=None,
192 body=body, recipients=None,
193 type_=notification.NotificationModel.TYPE_REGISTRATION,
193 type_=notification.NotificationModel.TYPE_REGISTRATION,
194 email_kwargs=email_kwargs)
194 email_kwargs=email_kwargs)
195
195
196 def update(self, user_id, form_data, skip_attrs=None):
196 def update(self, user_id, form_data, skip_attrs=None):
197 from kallithea.lib.auth import get_crypt_password
197 from kallithea.lib.auth import get_crypt_password
198 skip_attrs = skip_attrs or []
198 skip_attrs = skip_attrs or []
199 user = self.get(user_id)
199 user = self.get(user_id)
200 if user.is_default_user:
200 if user.is_default_user:
201 raise DefaultUserException(
201 raise DefaultUserException(
202 _("You can't edit this user since it's "
202 _("You can't edit this user since it's "
203 "crucial for entire application"))
203 "crucial for entire application"))
204
204
205 for k, v in form_data.items():
205 for k, v in form_data.items():
206 if k in skip_attrs:
206 if k in skip_attrs:
207 continue
207 continue
208 if k == 'new_password' and v:
208 if k == 'new_password' and v:
209 user.password = get_crypt_password(v)
209 user.password = get_crypt_password(v)
210 else:
210 else:
211 # old legacy thing orm models store firstname as name,
211 # old legacy thing orm models store firstname as name,
212 # need proper refactor to username
212 # need proper refactor to username
213 if k == 'firstname':
213 if k == 'firstname':
214 k = 'name'
214 k = 'name'
215 setattr(user, k, v)
215 setattr(user, k, v)
216
216
217 def update_user(self, user, **kwargs):
217 def update_user(self, user, **kwargs):
218 from kallithea.lib.auth import get_crypt_password
218 from kallithea.lib.auth import get_crypt_password
219
219
220 user = db.User.guess_instance(user)
220 user = db.User.guess_instance(user)
221 if user.is_default_user:
221 if user.is_default_user:
222 raise DefaultUserException(
222 raise DefaultUserException(
223 _("You can't edit this user since it's"
223 _("You can't edit this user since it's"
224 " crucial for entire application")
224 " crucial for entire application")
225 )
225 )
226
226
227 for k, v in kwargs.items():
227 for k, v in kwargs.items():
228 if k == 'password' and v:
228 if k == 'password' and v:
229 v = get_crypt_password(v)
229 v = get_crypt_password(v)
230
230
231 setattr(user, k, v)
231 setattr(user, k, v)
232 return user
232 return user
233
233
234 def delete(self, user, cur_user=None):
234 def delete(self, user, cur_user=None):
235 if cur_user is None:
235 if cur_user is None:
236 cur_user = getattr(get_current_authuser(), 'username', None)
236 cur_user = getattr(get_current_authuser(), 'username', None)
237 user = db.User.guess_instance(user)
237 user = db.User.guess_instance(user)
238
238
239 if user.is_default_user:
239 if user.is_default_user:
240 raise DefaultUserException(
240 raise DefaultUserException(
241 _("You can't remove this user since it is"
241 _("You can't remove this user since it is"
242 " crucial for the entire application"))
242 " crucial for the entire application"))
243 if user.repositories:
243 if user.repositories:
244 repos = [x.repo_name for x in user.repositories]
244 repos = [x.repo_name for x in user.repositories]
245 raise UserOwnsReposException(
245 raise UserOwnsReposException(
246 _('User "%s" still owns %s repositories and cannot be '
246 _('User "%s" still owns %s repositories and cannot be '
247 'removed. Switch owners or remove those repositories: %s')
247 'removed. Switch owners or remove those repositories: %s')
248 % (user.username, len(repos), ', '.join(repos)))
248 % (user.username, len(repos), ', '.join(repos)))
249 if user.repo_groups:
249 if user.repo_groups:
250 repogroups = [x.group_name for x in user.repo_groups]
250 repogroups = [x.group_name for x in user.repo_groups]
251 raise UserOwnsReposException(_(
251 raise UserOwnsReposException(_(
252 'User "%s" still owns %s repository groups and cannot be '
252 'User "%s" still owns %s repository groups and cannot be '
253 'removed. Switch owners or remove those repository groups: %s')
253 'removed. Switch owners or remove those repository groups: %s')
254 % (user.username, len(repogroups), ', '.join(repogroups)))
254 % (user.username, len(repogroups), ', '.join(repogroups)))
255 if user.user_groups:
255 if user.user_groups:
256 usergroups = [x.users_group_name for x in user.user_groups]
256 usergroups = [x.users_group_name for x in user.user_groups]
257 raise UserOwnsReposException(
257 raise UserOwnsReposException(
258 _('User "%s" still owns %s user groups and cannot be '
258 _('User "%s" still owns %s user groups and cannot be '
259 'removed. Switch owners or remove those user groups: %s')
259 'removed. Switch owners or remove those user groups: %s')
260 % (user.username, len(usergroups), ', '.join(usergroups)))
260 % (user.username, len(usergroups), ', '.join(usergroups)))
261 meta.Session().delete(user)
261 meta.Session().delete(user)
262
262
263 from kallithea.lib.hooks import log_delete_user
263 from kallithea.lib.hooks import log_delete_user
264 log_delete_user(user.get_dict(), cur_user)
264 log_delete_user(user.get_dict(), cur_user)
265
265
266 def can_change_password(self, user):
266 def can_change_password(self, user):
267 from kallithea.lib import auth_modules
267 from kallithea.lib import auth_modules
268 managed_fields = auth_modules.get_managed_fields(user)
268 managed_fields = auth_modules.get_managed_fields(user)
269 return 'password' not in managed_fields
269 return 'password' not in managed_fields
270
270
271 def get_reset_password_token(self, user, timestamp, session_id):
271 def get_reset_password_token(self, user, timestamp, session_id):
272 """
272 """
273 The token is a 40-digit hexstring, calculated as a HMAC-SHA1.
273 The token is a 40-digit hexstring, calculated as a HMAC-SHA1.
274
274
275 In a traditional HMAC scenario, an attacker is unable to know or
275 In a traditional HMAC scenario, an attacker is unable to know or
276 influence the secret key, but can know or influence the message
276 influence the secret key, but can know or influence the message
277 and token. This scenario is slightly different (in particular
277 and token. This scenario is slightly different (in particular
278 since the message sender is also the message recipient), but
278 since the message sender is also the message recipient), but
279 sufficiently similar to use an HMAC. Benefits compared to a plain
279 sufficiently similar to use an HMAC. Benefits compared to a plain
280 SHA1 hash includes resistance against a length extension attack.
280 SHA1 hash includes resistance against a length extension attack.
281
281
282 The HMAC key consists of the following values (known only to the
282 The HMAC key consists of the following values (known only to the
283 server and authorized users):
283 server and authorized users):
284
284
285 * per-application secret (the `app_instance_uuid` setting), without
285 * per-application secret (the `app_instance_uuid` setting), without
286 which an attacker cannot counterfeit tokens
286 which an attacker cannot counterfeit tokens
287 * hashed user password, invalidating the token upon password change
287 * hashed user password, invalidating the token upon password change
288
288
289 The HMAC message consists of the following values (potentially known
289 The HMAC message consists of the following values (potentially known
290 to an attacker):
290 to an attacker):
291
291
292 * session ID (the anti-CSRF token), requiring an attacker to have
292 * session ID (the anti-CSRF token), requiring an attacker to have
293 access to the browser session in which the token was created
293 access to the browser session in which the token was created
294 * numeric user ID, limiting the token to a specific user (yet allowing
294 * numeric user ID, limiting the token to a specific user (yet allowing
295 users to be renamed)
295 users to be renamed)
296 * user email address
296 * user email address
297 * time of token issue (a Unix timestamp, to enable token expiration)
297 * time of token issue (a Unix timestamp, to enable token expiration)
298
298
299 The key and message values are separated by NUL characters, which are
299 The key and message values are separated by NUL characters, which are
300 guaranteed not to occur in any of the values.
300 guaranteed not to occur in any of the values.
301 """
301 """
302 app_secret = config.get('app_instance_uuid')
302 app_secret = config.get('app_instance_uuid')
303 return hmac.new(
303 return hmac.new(
304 '\0'.join([app_secret, user.password]).encode('utf-8'),
304 '\0'.join([app_secret, user.password]).encode('utf-8'),
305 msg='\0'.join([session_id, str(user.user_id), user.email, str(timestamp)]).encode('utf-8'),
305 msg='\0'.join([session_id, str(user.user_id), user.email, str(timestamp)]).encode('utf-8'),
306 digestmod=hashlib.sha1,
306 digestmod=hashlib.sha1,
307 ).hexdigest()
307 ).hexdigest()
308
308
309 def send_reset_password_email(self, data):
309 def send_reset_password_email(self, data):
310 """
310 """
311 Sends email with a password reset token and link to the password
311 Sends email with a password reset token and link to the password
312 reset confirmation page with all information (including the token)
312 reset confirmation page with all information (including the token)
313 pre-filled. Also returns URL of that page, only without the token,
313 pre-filled. Also returns URL of that page, only without the token,
314 allowing users to copy-paste or manually enter the token from the
314 allowing users to copy-paste or manually enter the token from the
315 email.
315 email.
316 """
316 """
317 from kallithea.lib.celerylib import tasks
317 from kallithea.lib.celerylib import tasks
318 from kallithea.model import notification
318 from kallithea.model import notification
319
319
320 user_email = data['email']
320 user_email = data['email']
321 user = db.User.get_by_email(user_email)
321 user = db.User.get_by_email(user_email)
322 timestamp = int(time.time())
322 timestamp = int(time.time())
323 if user is not None:
323 if user is not None:
324 if self.can_change_password(user):
324 if self.can_change_password(user):
325 log.debug('password reset user %s found', user)
325 log.debug('password reset user %s found', user)
326 token = self.get_reset_password_token(user,
326 token = self.get_reset_password_token(user,
327 timestamp,
327 timestamp,
328 webutils.session_csrf_secret_token())
328 webutils.session_csrf_secret_token())
329 # URL must be fully qualified; but since the token is locked to
329 # URL must be fully qualified; but since the token is locked to
330 # the current browser session, we must provide a URL with the
330 # the current browser session, we must provide a URL with the
331 # current scheme and hostname, rather than the canonical_url.
331 # current scheme and hostname, rather than the canonical_url.
332 link = webutils.url('reset_password_confirmation', qualified=True,
332 link = webutils.url('reset_password_confirmation', qualified=True,
333 email=user_email,
333 email=user_email,
334 timestamp=timestamp,
334 timestamp=timestamp,
335 token=token)
335 token=token)
336 else:
336 else:
337 log.debug('password reset user %s found but was managed', user)
337 log.debug('password reset user %s found but was managed', user)
338 token = link = None
338 token = link = None
339 reg_type = notification.EmailNotificationModel.TYPE_PASSWORD_RESET
339 reg_type = notification.EmailNotificationModel.TYPE_PASSWORD_RESET
340 body = notification.EmailNotificationModel().get_email_tmpl(
340 body = notification.EmailNotificationModel().get_email_tmpl(
341 reg_type, 'txt',
341 reg_type, 'txt',
342 user=user.short_contact,
342 user=user.short_contact,
343 reset_token=token,
343 reset_token=token,
344 reset_url=link)
344 reset_url=link)
345 html_body = notification.EmailNotificationModel().get_email_tmpl(
345 html_body = notification.EmailNotificationModel().get_email_tmpl(
346 reg_type, 'html',
346 reg_type, 'html',
347 user=user.short_contact,
347 user=user.short_contact,
348 reset_token=token,
348 reset_token=token,
349 reset_url=link)
349 reset_url=link)
350 log.debug('sending email')
350 log.debug('sending email')
351 tasks.send_email([user_email], _("Password reset link"), body, html_body)
351 tasks.send_email([user_email], _("Password reset link"), body, html_body)
352 log.info('send new password mail to %s', user_email)
352 log.info('send new password mail to %s', user_email)
353 else:
353 else:
354 log.debug("password reset email %s not found", user_email)
354 log.debug("password reset email %s not found", user_email)
355
355
356 return webutils.url('reset_password_confirmation',
356 return webutils.url('reset_password_confirmation',
357 email=user_email,
357 email=user_email,
358 timestamp=timestamp)
358 timestamp=timestamp)
359
359
360 def verify_reset_password_token(self, email, timestamp, token):
360 def verify_reset_password_token(self, email, timestamp, token):
361 user = db.User.get_by_email(email)
361 user = db.User.get_by_email(email)
362 if user is None:
362 if user is None:
363 log.debug("user with email %s not found", email)
363 log.debug("user with email %s not found", email)
364 return False
364 return False
365
365
366 token_age = int(time.time()) - int(timestamp)
366 token_age = int(time.time()) - int(timestamp)
367
367
368 if token_age < 0:
368 if token_age < 0:
369 log.debug('timestamp is from the future')
369 log.debug('timestamp is from the future')
370 return False
370 return False
371
371
372 if token_age > UserModel.password_reset_token_lifetime:
372 if token_age > UserModel.password_reset_token_lifetime:
373 log.debug('password reset token expired')
373 log.debug('password reset token expired')
374 return False
374 return False
375
375
376 expected_token = self.get_reset_password_token(user,
376 expected_token = self.get_reset_password_token(user,
377 timestamp,
377 timestamp,
378 webutils.session_csrf_secret_token())
378 webutils.session_csrf_secret_token())
379 log.debug('computed password reset token: %s', expected_token)
379 log.debug('computed password reset token: %s', expected_token)
380 log.debug('received password reset token: %s', token)
380 log.debug('received password reset token: %s', token)
381 return expected_token == token
381 return expected_token == token
382
382
383 def reset_password(self, user_email, new_passwd):
383 def reset_password(self, user_email, new_passwd):
384 from kallithea.lib import auth
384 from kallithea.lib import auth
385 from kallithea.lib.celerylib import tasks
385 from kallithea.lib.celerylib import tasks
386 user = db.User.get_by_email(user_email)
386 user = db.User.get_by_email(user_email)
387 if user is not None:
387 if user is not None:
388 if not self.can_change_password(user):
388 if not self.can_change_password(user):
389 raise Exception('trying to change password for external user')
389 raise Exception('trying to change password for external user')
390 user.password = auth.get_crypt_password(new_passwd)
390 user.password = auth.get_crypt_password(new_passwd)
391 meta.Session().commit()
391 meta.Session().commit()
392 log.info('change password for %s', user_email)
392 log.info('change password for %s', user_email)
393 if new_passwd is None:
393 if new_passwd is None:
394 raise Exception('unable to set new password')
394 raise Exception('unable to set new password')
395
395
396 tasks.send_email([user_email],
396 tasks.send_email([user_email],
397 _('Password reset notification'),
397 _('Password reset notification'),
398 _('The password to your account %s has been changed using password reset form.') % (user.username,))
398 _('The password to your account %s has been changed using password reset form.') % (user.username,))
399 log.info('send password reset mail to %s', user_email)
399 log.info('send password reset mail to %s', user_email)
400
400
401 return True
401 return True
402
402
403 def has_perm(self, user, perm):
403 def has_perm(self, user, perm):
404 perm = db.Permission.guess_instance(perm)
404 perm = db.Permission.guess_instance(perm)
405 user = db.User.guess_instance(user)
405 user = db.User.guess_instance(user)
406
406
407 return db.UserToPerm.query().filter(db.UserToPerm.user == user) \
407 return db.UserToPerm.query().filter(db.UserToPerm.user == user) \
408 .filter(db.UserToPerm.permission == perm).scalar() is not None
408 .filter(db.UserToPerm.permission == perm).scalar() is not None
409
409
410 def grant_perm(self, user, perm):
410 def grant_perm(self, user, perm):
411 """
411 """
412 Grant user global permissions
412 Grant user global permissions
413
413
414 :param user:
414 :param user:
415 :param perm:
415 :param perm:
416 """
416 """
417 user = db.User.guess_instance(user)
417 user = db.User.guess_instance(user)
418 perm = db.Permission.guess_instance(perm)
418 perm = db.Permission.guess_instance(perm)
419 # if this permission is already granted skip it
419 # if this permission is already granted skip it
420 _perm = db.UserToPerm.query() \
420 _perm = db.UserToPerm.query() \
421 .filter(db.UserToPerm.user == user) \
421 .filter(db.UserToPerm.user == user) \
422 .filter(db.UserToPerm.permission == perm) \
422 .filter(db.UserToPerm.permission == perm) \
423 .scalar()
423 .scalar()
424 if _perm:
424 if _perm:
425 return
425 return
426 new = db.UserToPerm()
426 new = db.UserToPerm()
427 new.user = user
427 new.user = user
428 new.permission = perm
428 new.permission = perm
429 meta.Session().add(new)
429 meta.Session().add(new)
430 return new
430 return new
431
431
432 def revoke_perm(self, user, perm):
432 def revoke_perm(self, user, perm):
433 """
433 """
434 Revoke users global permissions
434 Revoke users global permissions
435
435
436 :param user:
436 :param user:
437 :param perm:
437 :param perm:
438 """
438 """
439 user = db.User.guess_instance(user)
439 user = db.User.guess_instance(user)
440 perm = db.Permission.guess_instance(perm)
440 perm = db.Permission.guess_instance(perm)
441
441
442 db.UserToPerm.query().filter(
442 db.UserToPerm.query().filter(
443 db.UserToPerm.user == user,
443 db.UserToPerm.user == user,
444 db.UserToPerm.permission == perm,
444 db.UserToPerm.permission == perm,
445 ).delete()
445 ).delete()
446
446
447 def add_extra_email(self, user, email):
447 def add_extra_email(self, user, email):
448 """
448 """
449 Adds email address to UserEmailMap
449 Adds email address to UserEmailMap
450
450
451 :param user:
451 :param user:
452 :param email:
452 :param email:
453 """
453 """
454 from kallithea.model import forms
455 form = forms.UserExtraEmailForm()()
454 form = forms.UserExtraEmailForm()()
456 data = form.to_python(dict(email=email))
455 data = form.to_python(dict(email=email))
457 user = db.User.guess_instance(user)
456 user = db.User.guess_instance(user)
458
457
459 obj = db.UserEmailMap()
458 obj = db.UserEmailMap()
460 obj.user = user
459 obj.user = user
461 obj.email = data['email']
460 obj.email = data['email']
462 meta.Session().add(obj)
461 meta.Session().add(obj)
463 return obj
462 return obj
464
463
465 def delete_extra_email(self, user, email_id):
464 def delete_extra_email(self, user, email_id):
466 """
465 """
467 Removes email address from UserEmailMap
466 Removes email address from UserEmailMap
468
467
469 :param user:
468 :param user:
470 :param email_id:
469 :param email_id:
471 """
470 """
472 user = db.User.guess_instance(user)
471 user = db.User.guess_instance(user)
473 obj = db.UserEmailMap.query().get(email_id)
472 obj = db.UserEmailMap.query().get(email_id)
474 if obj is not None:
473 if obj is not None:
475 meta.Session().delete(obj)
474 meta.Session().delete(obj)
476
475
477 def add_extra_ip(self, user, ip):
476 def add_extra_ip(self, user, ip):
478 """
477 """
479 Adds IP address to UserIpMap
478 Adds IP address to UserIpMap
480
479
481 :param user:
480 :param user:
482 :param ip:
481 :param ip:
483 """
482 """
484 from kallithea.model import forms
485 form = forms.UserExtraIpForm()()
483 form = forms.UserExtraIpForm()()
486 data = form.to_python(dict(ip=ip))
484 data = form.to_python(dict(ip=ip))
487 user = db.User.guess_instance(user)
485 user = db.User.guess_instance(user)
488
486
489 obj = db.UserIpMap()
487 obj = db.UserIpMap()
490 obj.user = user
488 obj.user = user
491 obj.ip_addr = data['ip']
489 obj.ip_addr = data['ip']
492 meta.Session().add(obj)
490 meta.Session().add(obj)
493 return obj
491 return obj
494
492
495 def delete_extra_ip(self, user, ip_id):
493 def delete_extra_ip(self, user, ip_id):
496 """
494 """
497 Removes IP address from UserIpMap
495 Removes IP address from UserIpMap
498
496
499 :param user:
497 :param user:
500 :param ip_id:
498 :param ip_id:
501 """
499 """
502 user = db.User.guess_instance(user)
500 user = db.User.guess_instance(user)
503 obj = db.UserIpMap.query().get(ip_id)
501 obj = db.UserIpMap.query().get(ip_id)
504 if obj:
502 if obj:
505 meta.Session().delete(obj)
503 meta.Session().delete(obj)
@@ -1,799 +1,799 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 Set of generic validators
15 Set of generic validators
16 """
16 """
17
17
18 import logging
18 import logging
19 import os
19 import os
20 import re
20 import re
21 from collections import defaultdict
21 from collections import defaultdict
22
22
23 import formencode
23 import formencode
24 import ipaddr
24 import ipaddr
25 import sqlalchemy
25 import sqlalchemy
26 from formencode.validators import CIDR, Bool, Email, FancyValidator, Int, IPAddress, NotEmpty, Number, OneOf, Regex, Set, String, StringBoolean, UnicodeString
26 from formencode.validators import CIDR, Bool, Email, FancyValidator, Int, IPAddress, NotEmpty, Number, OneOf, Regex, Set, String, StringBoolean, UnicodeString
27 from sqlalchemy import func
27 from sqlalchemy import func
28 from tg.i18n import ugettext as _
28 from tg.i18n import ugettext as _
29
29
30 import kallithea
30 import kallithea
31 from kallithea.lib.auth import HasPermissionAny, HasRepoGroupPermissionLevel
31 from kallithea.lib import auth
32 from kallithea.lib.compat import OrderedSet
32 from kallithea.lib.compat import OrderedSet
33 from kallithea.lib.exceptions import InvalidCloneUriException, LdapImportError
33 from kallithea.lib.exceptions import InvalidCloneUriException, LdapImportError
34 from kallithea.lib.utils import is_valid_repo_uri
34 from kallithea.lib.utils import is_valid_repo_uri
35 from kallithea.lib.utils2 import asbool, aslist, repo_name_slug
35 from kallithea.lib.utils2 import asbool, aslist, repo_name_slug
36 from kallithea.model import db
36 from kallithea.model import db
37
37
38
38
39 # silence warnings and pylint
39 # silence warnings and pylint
40 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
40 UnicodeString, OneOf, Int, Number, Regex, Email, Bool, StringBoolean, Set, \
41 NotEmpty, IPAddress, CIDR, String, FancyValidator
41 NotEmpty, IPAddress, CIDR, String, FancyValidator
42
42
43 log = logging.getLogger(__name__)
43 log = logging.getLogger(__name__)
44
44
45
45
46 def UniqueListFromString():
46 def UniqueListFromString():
47 class _UniqueListFromString(formencode.FancyValidator):
47 class _UniqueListFromString(formencode.FancyValidator):
48 """
48 """
49 Split value on ',' and make unique while preserving order
49 Split value on ',' and make unique while preserving order
50 """
50 """
51 messages = dict(
51 messages = dict(
52 empty=_('Value cannot be an empty list'),
52 empty=_('Value cannot be an empty list'),
53 missing_value=_('Value cannot be an empty list'),
53 missing_value=_('Value cannot be an empty list'),
54 )
54 )
55
55
56 def _convert_to_python(self, value, state):
56 def _convert_to_python(self, value, state):
57 value = aslist(value, ',')
57 value = aslist(value, ',')
58 seen = set()
58 seen = set()
59 return [c for c in value if not (c in seen or seen.add(c))]
59 return [c for c in value if not (c in seen or seen.add(c))]
60
60
61 def empty_value(self, value):
61 def empty_value(self, value):
62 return []
62 return []
63
63
64 return _UniqueListFromString
64 return _UniqueListFromString
65
65
66
66
67 def ValidUsername(edit=False, old_data=None):
67 def ValidUsername(edit=False, old_data=None):
68 old_data = old_data or {}
68 old_data = old_data or {}
69
69
70 class _validator(formencode.validators.FancyValidator):
70 class _validator(formencode.validators.FancyValidator):
71 messages = {
71 messages = {
72 'username_exists': _('Username "%(username)s" already exists'),
72 'username_exists': _('Username "%(username)s" already exists'),
73 'system_invalid_username':
73 'system_invalid_username':
74 _('Username "%(username)s" cannot be used'),
74 _('Username "%(username)s" cannot be used'),
75 'invalid_username':
75 'invalid_username':
76 _('Username may only contain alphanumeric characters '
76 _('Username may only contain alphanumeric characters '
77 'underscores, periods or dashes and must begin with an '
77 'underscores, periods or dashes and must begin with an '
78 'alphanumeric character or underscore')
78 'alphanumeric character or underscore')
79 }
79 }
80
80
81 def _validate_python(self, value, state):
81 def _validate_python(self, value, state):
82 if value in ['default', 'new_user']:
82 if value in ['default', 'new_user']:
83 msg = self.message('system_invalid_username', state, username=value)
83 msg = self.message('system_invalid_username', state, username=value)
84 raise formencode.Invalid(msg, value, state)
84 raise formencode.Invalid(msg, value, state)
85 # check if user is unique
85 # check if user is unique
86 old_un = None
86 old_un = None
87 if edit:
87 if edit:
88 old_un = db.User.get(old_data.get('user_id')).username
88 old_un = db.User.get(old_data.get('user_id')).username
89
89
90 if old_un != value or not edit:
90 if old_un != value or not edit:
91 if db.User.get_by_username(value, case_insensitive=True):
91 if db.User.get_by_username(value, case_insensitive=True):
92 msg = self.message('username_exists', state, username=value)
92 msg = self.message('username_exists', state, username=value)
93 raise formencode.Invalid(msg, value, state)
93 raise formencode.Invalid(msg, value, state)
94
94
95 if re.match(r'^[a-zA-Z0-9\_]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
95 if re.match(r'^[a-zA-Z0-9\_]{1}[a-zA-Z0-9\-\_\.]*$', value) is None:
96 msg = self.message('invalid_username', state)
96 msg = self.message('invalid_username', state)
97 raise formencode.Invalid(msg, value, state)
97 raise formencode.Invalid(msg, value, state)
98 return _validator
98 return _validator
99
99
100
100
101 def ValidRegex(msg=None):
101 def ValidRegex(msg=None):
102 class _validator(formencode.validators.Regex):
102 class _validator(formencode.validators.Regex):
103 messages = dict(invalid=msg or _('The input is not valid'))
103 messages = dict(invalid=msg or _('The input is not valid'))
104 return _validator
104 return _validator
105
105
106
106
107 def ValidRepoUser():
107 def ValidRepoUser():
108 class _validator(formencode.validators.FancyValidator):
108 class _validator(formencode.validators.FancyValidator):
109 messages = {
109 messages = {
110 'invalid_username': _('Username %(username)s is not valid')
110 'invalid_username': _('Username %(username)s is not valid')
111 }
111 }
112
112
113 def _validate_python(self, value, state):
113 def _validate_python(self, value, state):
114 try:
114 try:
115 db.User.query().filter(db.User.active == True) \
115 db.User.query().filter(db.User.active == True) \
116 .filter(db.User.username == value).one()
116 .filter(db.User.username == value).one()
117 except sqlalchemy.exc.InvalidRequestError: # NoResultFound/MultipleResultsFound
117 except sqlalchemy.exc.InvalidRequestError: # NoResultFound/MultipleResultsFound
118 msg = self.message('invalid_username', state, username=value)
118 msg = self.message('invalid_username', state, username=value)
119 raise formencode.Invalid(msg, value, state,
119 raise formencode.Invalid(msg, value, state,
120 error_dict=dict(username=msg)
120 error_dict=dict(username=msg)
121 )
121 )
122
122
123 return _validator
123 return _validator
124
124
125
125
126 def ValidUserGroup(edit=False, old_data=None):
126 def ValidUserGroup(edit=False, old_data=None):
127 old_data = old_data or {}
127 old_data = old_data or {}
128
128
129 class _validator(formencode.validators.FancyValidator):
129 class _validator(formencode.validators.FancyValidator):
130 messages = {
130 messages = {
131 'invalid_group': _('Invalid user group name'),
131 'invalid_group': _('Invalid user group name'),
132 'group_exist': _('User group "%(usergroup)s" already exists'),
132 'group_exist': _('User group "%(usergroup)s" already exists'),
133 'invalid_usergroup_name':
133 'invalid_usergroup_name':
134 _('user group name may only contain alphanumeric '
134 _('user group name may only contain alphanumeric '
135 'characters underscores, periods or dashes and must begin '
135 'characters underscores, periods or dashes and must begin '
136 'with alphanumeric character')
136 'with alphanumeric character')
137 }
137 }
138
138
139 def _validate_python(self, value, state):
139 def _validate_python(self, value, state):
140 if value in ['default']:
140 if value in ['default']:
141 msg = self.message('invalid_group', state)
141 msg = self.message('invalid_group', state)
142 raise formencode.Invalid(msg, value, state,
142 raise formencode.Invalid(msg, value, state,
143 error_dict=dict(users_group_name=msg)
143 error_dict=dict(users_group_name=msg)
144 )
144 )
145 # check if group is unique
145 # check if group is unique
146 old_ugname = None
146 old_ugname = None
147 if edit:
147 if edit:
148 old_id = old_data.get('users_group_id')
148 old_id = old_data.get('users_group_id')
149 old_ugname = db.UserGroup.get(old_id).users_group_name
149 old_ugname = db.UserGroup.get(old_id).users_group_name
150
150
151 if old_ugname != value or not edit:
151 if old_ugname != value or not edit:
152 is_existing_group = db.UserGroup.get_by_group_name(value,
152 is_existing_group = db.UserGroup.get_by_group_name(value,
153 case_insensitive=True)
153 case_insensitive=True)
154 if is_existing_group:
154 if is_existing_group:
155 msg = self.message('group_exist', state, usergroup=value)
155 msg = self.message('group_exist', state, usergroup=value)
156 raise formencode.Invalid(msg, value, state,
156 raise formencode.Invalid(msg, value, state,
157 error_dict=dict(users_group_name=msg)
157 error_dict=dict(users_group_name=msg)
158 )
158 )
159
159
160 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
160 if re.match(r'^[a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+$', value) is None:
161 msg = self.message('invalid_usergroup_name', state)
161 msg = self.message('invalid_usergroup_name', state)
162 raise formencode.Invalid(msg, value, state,
162 raise formencode.Invalid(msg, value, state,
163 error_dict=dict(users_group_name=msg)
163 error_dict=dict(users_group_name=msg)
164 )
164 )
165
165
166 return _validator
166 return _validator
167
167
168
168
169 def ValidRepoGroup(edit=False, old_data=None):
169 def ValidRepoGroup(edit=False, old_data=None):
170 old_data = old_data or {}
170 old_data = old_data or {}
171
171
172 class _validator(formencode.validators.FancyValidator):
172 class _validator(formencode.validators.FancyValidator):
173 messages = {
173 messages = {
174 'parent_group_id': _('Cannot assign this group as parent'),
174 'parent_group_id': _('Cannot assign this group as parent'),
175 'group_exists': _('Group "%(group_name)s" already exists'),
175 'group_exists': _('Group "%(group_name)s" already exists'),
176 'repo_exists':
176 'repo_exists':
177 _('Repository with name "%(group_name)s" already exists')
177 _('Repository with name "%(group_name)s" already exists')
178 }
178 }
179
179
180 def _validate_python(self, value, state):
180 def _validate_python(self, value, state):
181 # TODO WRITE VALIDATIONS
181 # TODO WRITE VALIDATIONS
182 group_name = value.get('group_name')
182 group_name = value.get('group_name')
183 parent_group_id = value.get('parent_group_id')
183 parent_group_id = value.get('parent_group_id')
184
184
185 # slugify repo group just in case :)
185 # slugify repo group just in case :)
186 slug = repo_name_slug(group_name)
186 slug = repo_name_slug(group_name)
187
187
188 # check for parent of self
188 # check for parent of self
189 if edit and parent_group_id and old_data['group_id'] == parent_group_id:
189 if edit and parent_group_id and old_data['group_id'] == parent_group_id:
190 msg = self.message('parent_group_id', state)
190 msg = self.message('parent_group_id', state)
191 raise formencode.Invalid(msg, value, state,
191 raise formencode.Invalid(msg, value, state,
192 error_dict=dict(parent_group_id=msg)
192 error_dict=dict(parent_group_id=msg)
193 )
193 )
194
194
195 old_gname = None
195 old_gname = None
196 if edit:
196 if edit:
197 old_gname = db.RepoGroup.get(old_data.get('group_id')).group_name
197 old_gname = db.RepoGroup.get(old_data.get('group_id')).group_name
198
198
199 if old_gname != group_name or not edit:
199 if old_gname != group_name or not edit:
200
200
201 # check group
201 # check group
202 gr = db.RepoGroup.query() \
202 gr = db.RepoGroup.query() \
203 .filter(func.lower(db.RepoGroup.group_name) == func.lower(slug)) \
203 .filter(func.lower(db.RepoGroup.group_name) == func.lower(slug)) \
204 .filter(db.RepoGroup.parent_group_id == parent_group_id) \
204 .filter(db.RepoGroup.parent_group_id == parent_group_id) \
205 .scalar()
205 .scalar()
206 if gr is not None:
206 if gr is not None:
207 msg = self.message('group_exists', state, group_name=slug)
207 msg = self.message('group_exists', state, group_name=slug)
208 raise formencode.Invalid(msg, value, state,
208 raise formencode.Invalid(msg, value, state,
209 error_dict=dict(group_name=msg)
209 error_dict=dict(group_name=msg)
210 )
210 )
211
211
212 # check for same repo
212 # check for same repo
213 repo = db.Repository.query() \
213 repo = db.Repository.query() \
214 .filter(func.lower(db.Repository.repo_name) == func.lower(slug)) \
214 .filter(func.lower(db.Repository.repo_name) == func.lower(slug)) \
215 .scalar()
215 .scalar()
216 if repo is not None:
216 if repo is not None:
217 msg = self.message('repo_exists', state, group_name=slug)
217 msg = self.message('repo_exists', state, group_name=slug)
218 raise formencode.Invalid(msg, value, state,
218 raise formencode.Invalid(msg, value, state,
219 error_dict=dict(group_name=msg)
219 error_dict=dict(group_name=msg)
220 )
220 )
221
221
222 return _validator
222 return _validator
223
223
224
224
225 def ValidPassword():
225 def ValidPassword():
226 class _validator(formencode.validators.FancyValidator):
226 class _validator(formencode.validators.FancyValidator):
227 messages = {
227 messages = {
228 'invalid_password':
228 'invalid_password':
229 _('Invalid characters (non-ascii) in password')
229 _('Invalid characters (non-ascii) in password')
230 }
230 }
231
231
232 def _validate_python(self, value, state):
232 def _validate_python(self, value, state):
233 try:
233 try:
234 (value or '').encode('ascii')
234 (value or '').encode('ascii')
235 except UnicodeError:
235 except UnicodeError:
236 msg = self.message('invalid_password', state)
236 msg = self.message('invalid_password', state)
237 raise formencode.Invalid(msg, value, state,)
237 raise formencode.Invalid(msg, value, state,)
238 return _validator
238 return _validator
239
239
240
240
241 def ValidOldPassword(username):
241 def ValidOldPassword(username):
242 class _validator(formencode.validators.FancyValidator):
242 class _validator(formencode.validators.FancyValidator):
243 messages = {
243 messages = {
244 'invalid_password': _('Invalid old password')
244 'invalid_password': _('Invalid old password')
245 }
245 }
246
246
247 def _validate_python(self, value, state):
247 def _validate_python(self, value, state):
248 from kallithea.lib import auth_modules
248 from kallithea.lib import auth_modules
249 if auth_modules.authenticate(username, value, '') is None:
249 if auth_modules.authenticate(username, value, '') is None:
250 msg = self.message('invalid_password', state)
250 msg = self.message('invalid_password', state)
251 raise formencode.Invalid(msg, value, state,
251 raise formencode.Invalid(msg, value, state,
252 error_dict=dict(current_password=msg)
252 error_dict=dict(current_password=msg)
253 )
253 )
254 return _validator
254 return _validator
255
255
256
256
257 def ValidPasswordsMatch(password_field, password_confirmation_field):
257 def ValidPasswordsMatch(password_field, password_confirmation_field):
258 class _validator(formencode.validators.FancyValidator):
258 class _validator(formencode.validators.FancyValidator):
259 messages = {
259 messages = {
260 'password_mismatch': _('Passwords do not match'),
260 'password_mismatch': _('Passwords do not match'),
261 }
261 }
262
262
263 def _validate_python(self, value, state):
263 def _validate_python(self, value, state):
264 if value.get(password_field) != value[password_confirmation_field]:
264 if value.get(password_field) != value[password_confirmation_field]:
265 msg = self.message('password_mismatch', state)
265 msg = self.message('password_mismatch', state)
266 raise formencode.Invalid(msg, value, state,
266 raise formencode.Invalid(msg, value, state,
267 error_dict={password_field: msg, password_confirmation_field: msg}
267 error_dict={password_field: msg, password_confirmation_field: msg}
268 )
268 )
269 return _validator
269 return _validator
270
270
271
271
272 def ValidAuth():
272 def ValidAuth():
273 class _validator(formencode.validators.FancyValidator):
273 class _validator(formencode.validators.FancyValidator):
274 messages = {
274 messages = {
275 'invalid_auth': _('Invalid username or password'),
275 'invalid_auth': _('Invalid username or password'),
276 }
276 }
277
277
278 def _validate_python(self, value, state):
278 def _validate_python(self, value, state):
279 from kallithea.lib import auth_modules
279 from kallithea.lib import auth_modules
280
280
281 password = value['password']
281 password = value['password']
282 username = value['username']
282 username = value['username']
283
283
284 # authenticate returns unused dict but has called
284 # authenticate returns unused dict but has called
285 # plugin._authenticate which has create_or_update'ed the username user in db
285 # plugin._authenticate which has create_or_update'ed the username user in db
286 if auth_modules.authenticate(username, password) is None:
286 if auth_modules.authenticate(username, password) is None:
287 user = db.User.get_by_username_or_email(username)
287 user = db.User.get_by_username_or_email(username)
288 if user and not user.active:
288 if user and not user.active:
289 log.warning('user %s is disabled', username)
289 log.warning('user %s is disabled', username)
290 msg = self.message('invalid_auth', state)
290 msg = self.message('invalid_auth', state)
291 raise formencode.Invalid(msg, value, state,
291 raise formencode.Invalid(msg, value, state,
292 error_dict=dict(username=' ', password=msg)
292 error_dict=dict(username=' ', password=msg)
293 )
293 )
294 else:
294 else:
295 log.warning('user %s failed to authenticate', username)
295 log.warning('user %s failed to authenticate', username)
296 msg = self.message('invalid_auth', state)
296 msg = self.message('invalid_auth', state)
297 raise formencode.Invalid(msg, value, state,
297 raise formencode.Invalid(msg, value, state,
298 error_dict=dict(username=' ', password=msg)
298 error_dict=dict(username=' ', password=msg)
299 )
299 )
300 return _validator
300 return _validator
301
301
302
302
303 def ValidRepoName(edit=False, old_data=None):
303 def ValidRepoName(edit=False, old_data=None):
304 old_data = old_data or {}
304 old_data = old_data or {}
305
305
306 class _validator(formencode.validators.FancyValidator):
306 class _validator(formencode.validators.FancyValidator):
307 messages = {
307 messages = {
308 'invalid_repo_name':
308 'invalid_repo_name':
309 _('Repository name %(repo)s is not allowed'),
309 _('Repository name %(repo)s is not allowed'),
310 'repository_exists':
310 'repository_exists':
311 _('Repository named %(repo)s already exists'),
311 _('Repository named %(repo)s already exists'),
312 'repository_in_group_exists': _('Repository "%(repo)s" already '
312 'repository_in_group_exists': _('Repository "%(repo)s" already '
313 'exists in group "%(group)s"'),
313 'exists in group "%(group)s"'),
314 'same_group_exists': _('Repository group with name "%(repo)s" '
314 'same_group_exists': _('Repository group with name "%(repo)s" '
315 'already exists')
315 'already exists')
316 }
316 }
317
317
318 def _convert_to_python(self, value, state):
318 def _convert_to_python(self, value, state):
319 repo_name = repo_name_slug(value.get('repo_name', ''))
319 repo_name = repo_name_slug(value.get('repo_name', ''))
320 repo_group = value.get('repo_group')
320 repo_group = value.get('repo_group')
321 if repo_group:
321 if repo_group:
322 gr = db.RepoGroup.get(repo_group)
322 gr = db.RepoGroup.get(repo_group)
323 group_path = gr.full_path
323 group_path = gr.full_path
324 group_name = gr.group_name
324 group_name = gr.group_name
325 # value needs to be aware of group name in order to check
325 # value needs to be aware of group name in order to check
326 # db key This is an actual just the name to store in the
326 # db key This is an actual just the name to store in the
327 # database
327 # database
328 repo_name_full = group_path + kallithea.URL_SEP + repo_name
328 repo_name_full = group_path + kallithea.URL_SEP + repo_name
329 else:
329 else:
330 group_name = group_path = ''
330 group_name = group_path = ''
331 repo_name_full = repo_name
331 repo_name_full = repo_name
332
332
333 value['repo_name'] = repo_name
333 value['repo_name'] = repo_name
334 value['repo_name_full'] = repo_name_full
334 value['repo_name_full'] = repo_name_full
335 value['group_path'] = group_path
335 value['group_path'] = group_path
336 value['group_name'] = group_name
336 value['group_name'] = group_name
337 return value
337 return value
338
338
339 def _validate_python(self, value, state):
339 def _validate_python(self, value, state):
340 repo_name = value.get('repo_name')
340 repo_name = value.get('repo_name')
341 repo_name_full = value.get('repo_name_full')
341 repo_name_full = value.get('repo_name_full')
342 group_path = value.get('group_path')
342 group_path = value.get('group_path')
343 group_name = value.get('group_name')
343 group_name = value.get('group_name')
344
344
345 if repo_name in [kallithea.ADMIN_PREFIX, '']:
345 if repo_name in [kallithea.ADMIN_PREFIX, '']:
346 msg = self.message('invalid_repo_name', state, repo=repo_name)
346 msg = self.message('invalid_repo_name', state, repo=repo_name)
347 raise formencode.Invalid(msg, value, state,
347 raise formencode.Invalid(msg, value, state,
348 error_dict=dict(repo_name=msg)
348 error_dict=dict(repo_name=msg)
349 )
349 )
350
350
351 rename = old_data.get('repo_name') != repo_name_full
351 rename = old_data.get('repo_name') != repo_name_full
352 create = not edit
352 create = not edit
353 if rename or create:
353 if rename or create:
354 repo = db.Repository.get_by_repo_name(repo_name_full, case_insensitive=True)
354 repo = db.Repository.get_by_repo_name(repo_name_full, case_insensitive=True)
355 repo_group = db.RepoGroup.get_by_group_name(repo_name_full, case_insensitive=True)
355 repo_group = db.RepoGroup.get_by_group_name(repo_name_full, case_insensitive=True)
356 if group_path != '':
356 if group_path != '':
357 if repo is not None:
357 if repo is not None:
358 msg = self.message('repository_in_group_exists', state,
358 msg = self.message('repository_in_group_exists', state,
359 repo=repo.repo_name, group=group_name)
359 repo=repo.repo_name, group=group_name)
360 raise formencode.Invalid(msg, value, state,
360 raise formencode.Invalid(msg, value, state,
361 error_dict=dict(repo_name=msg)
361 error_dict=dict(repo_name=msg)
362 )
362 )
363 elif repo_group is not None:
363 elif repo_group is not None:
364 msg = self.message('same_group_exists', state,
364 msg = self.message('same_group_exists', state,
365 repo=repo_name)
365 repo=repo_name)
366 raise formencode.Invalid(msg, value, state,
366 raise formencode.Invalid(msg, value, state,
367 error_dict=dict(repo_name=msg)
367 error_dict=dict(repo_name=msg)
368 )
368 )
369 elif repo is not None:
369 elif repo is not None:
370 msg = self.message('repository_exists', state,
370 msg = self.message('repository_exists', state,
371 repo=repo.repo_name)
371 repo=repo.repo_name)
372 raise formencode.Invalid(msg, value, state,
372 raise formencode.Invalid(msg, value, state,
373 error_dict=dict(repo_name=msg)
373 error_dict=dict(repo_name=msg)
374 )
374 )
375 return value
375 return value
376 return _validator
376 return _validator
377
377
378
378
379 def ValidForkName(*args, **kwargs):
379 def ValidForkName(*args, **kwargs):
380 return ValidRepoName(*args, **kwargs)
380 return ValidRepoName(*args, **kwargs)
381
381
382
382
383 def SlugifyName():
383 def SlugifyName():
384 class _validator(formencode.validators.FancyValidator):
384 class _validator(formencode.validators.FancyValidator):
385
385
386 def _convert_to_python(self, value, state):
386 def _convert_to_python(self, value, state):
387 return repo_name_slug(value)
387 return repo_name_slug(value)
388
388
389 def _validate_python(self, value, state):
389 def _validate_python(self, value, state):
390 pass
390 pass
391
391
392 return _validator
392 return _validator
393
393
394
394
395 def ValidCloneUri():
395 def ValidCloneUri():
396 from kallithea.lib.utils import make_ui
396 from kallithea.lib.utils import make_ui
397
397
398 class _validator(formencode.validators.FancyValidator):
398 class _validator(formencode.validators.FancyValidator):
399 messages = {
399 messages = {
400 'clone_uri': _('Invalid repository URL'),
400 'clone_uri': _('Invalid repository URL'),
401 'invalid_clone_uri': _('Invalid repository URL. It must be a '
401 'invalid_clone_uri': _('Invalid repository URL. It must be a '
402 'valid http, https, or ssh URL'),
402 'valid http, https, or ssh URL'),
403 }
403 }
404
404
405 def _validate_python(self, value, state):
405 def _validate_python(self, value, state):
406 repo_type = value.get('repo_type')
406 repo_type = value.get('repo_type')
407 url = value.get('clone_uri')
407 url = value.get('clone_uri')
408
408
409 if url and url != value.get('clone_uri_hidden'):
409 if url and url != value.get('clone_uri_hidden'):
410 try:
410 try:
411 is_valid_repo_uri(repo_type, url, make_ui())
411 is_valid_repo_uri(repo_type, url, make_ui())
412 except InvalidCloneUriException as e:
412 except InvalidCloneUriException as e:
413 log.warning('validation of clone URL %r failed: %s', url, e)
413 log.warning('validation of clone URL %r failed: %s', url, e)
414 msg = self.message('clone_uri', state)
414 msg = self.message('clone_uri', state)
415 raise formencode.Invalid(msg, value, state,
415 raise formencode.Invalid(msg, value, state,
416 error_dict=dict(clone_uri=msg)
416 error_dict=dict(clone_uri=msg)
417 )
417 )
418 return _validator
418 return _validator
419
419
420
420
421 def ValidForkType(old_data=None):
421 def ValidForkType(old_data=None):
422 old_data = old_data or {}
422 old_data = old_data or {}
423
423
424 class _validator(formencode.validators.FancyValidator):
424 class _validator(formencode.validators.FancyValidator):
425 messages = {
425 messages = {
426 'invalid_fork_type': _('Fork has to be the same type as parent')
426 'invalid_fork_type': _('Fork has to be the same type as parent')
427 }
427 }
428
428
429 def _validate_python(self, value, state):
429 def _validate_python(self, value, state):
430 if old_data['repo_type'] != value:
430 if old_data['repo_type'] != value:
431 msg = self.message('invalid_fork_type', state)
431 msg = self.message('invalid_fork_type', state)
432 raise formencode.Invalid(msg, value, state,
432 raise formencode.Invalid(msg, value, state,
433 error_dict=dict(repo_type=msg)
433 error_dict=dict(repo_type=msg)
434 )
434 )
435 return _validator
435 return _validator
436
436
437
437
438 def CanWriteGroup(old_data=None):
438 def CanWriteGroup(old_data=None):
439 class _validator(formencode.validators.FancyValidator):
439 class _validator(formencode.validators.FancyValidator):
440 messages = {
440 messages = {
441 'permission_denied': _("You don't have permissions "
441 'permission_denied': _("You don't have permissions "
442 "to create repository in this group"),
442 "to create repository in this group"),
443 'permission_denied_root': _("no permission to create repository "
443 'permission_denied_root': _("no permission to create repository "
444 "in root location")
444 "in root location")
445 }
445 }
446
446
447 def _convert_to_python(self, value, state):
447 def _convert_to_python(self, value, state):
448 # root location
448 # root location
449 if value == -1:
449 if value == -1:
450 return None
450 return None
451 return value
451 return value
452
452
453 def _validate_python(self, value, state):
453 def _validate_python(self, value, state):
454 gr = db.RepoGroup.get(value)
454 gr = db.RepoGroup.get(value)
455 gr_name = gr.group_name if gr is not None else None # None means ROOT location
455 gr_name = gr.group_name if gr is not None else None # None means ROOT location
456
456
457 # create repositories with write permission on group is set to true
457 # create repositories with write permission on group is set to true
458 group_admin = HasRepoGroupPermissionLevel('admin')(gr_name,
458 group_admin = auth.HasRepoGroupPermissionLevel('admin')(gr_name,
459 'can write into group validator')
459 'can write into group validator')
460 group_write = HasRepoGroupPermissionLevel('write')(gr_name,
460 group_write = auth.HasRepoGroupPermissionLevel('write')(gr_name,
461 'can write into group validator')
461 'can write into group validator')
462 forbidden = not (group_admin or group_write)
462 forbidden = not (group_admin or group_write)
463 can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
463 can_create_repos = auth.HasPermissionAny('hg.admin', 'hg.create.repository')
464 gid = (old_data['repo_group'].get('group_id')
464 gid = (old_data['repo_group'].get('group_id')
465 if (old_data and 'repo_group' in old_data) else None)
465 if (old_data and 'repo_group' in old_data) else None)
466 value_changed = gid != value
466 value_changed = gid != value
467 new = not old_data
467 new = not old_data
468 # do check if we changed the value, there's a case that someone got
468 # do check if we changed the value, there's a case that someone got
469 # revoked write permissions to a repository, he still created, we
469 # revoked write permissions to a repository, he still created, we
470 # don't need to check permission if he didn't change the value of
470 # don't need to check permission if he didn't change the value of
471 # groups in form box
471 # groups in form box
472 if value_changed or new:
472 if value_changed or new:
473 # parent group need to be existing
473 # parent group need to be existing
474 if gr and forbidden:
474 if gr and forbidden:
475 msg = self.message('permission_denied', state)
475 msg = self.message('permission_denied', state)
476 raise formencode.Invalid(msg, value, state,
476 raise formencode.Invalid(msg, value, state,
477 error_dict=dict(repo_type=msg)
477 error_dict=dict(repo_type=msg)
478 )
478 )
479 ## check if we can write to root location !
479 ## check if we can write to root location !
480 elif gr is None and not can_create_repos():
480 elif gr is None and not can_create_repos():
481 msg = self.message('permission_denied_root', state)
481 msg = self.message('permission_denied_root', state)
482 raise formencode.Invalid(msg, value, state,
482 raise formencode.Invalid(msg, value, state,
483 error_dict=dict(repo_type=msg)
483 error_dict=dict(repo_type=msg)
484 )
484 )
485
485
486 return _validator
486 return _validator
487
487
488
488
489 def CanCreateGroup(can_create_in_root=False):
489 def CanCreateGroup(can_create_in_root=False):
490 class _validator(formencode.validators.FancyValidator):
490 class _validator(formencode.validators.FancyValidator):
491 messages = {
491 messages = {
492 'permission_denied': _("You don't have permissions "
492 'permission_denied': _("You don't have permissions "
493 "to create a group in this location")
493 "to create a group in this location")
494 }
494 }
495
495
496 def to_python(self, value, state):
496 def to_python(self, value, state):
497 # root location
497 # root location
498 if value == -1:
498 if value == -1:
499 return None
499 return None
500 return value
500 return value
501
501
502 def _validate_python(self, value, state):
502 def _validate_python(self, value, state):
503 gr = db.RepoGroup.get(value)
503 gr = db.RepoGroup.get(value)
504 gr_name = gr.group_name if gr is not None else None # None means ROOT location
504 gr_name = gr.group_name if gr is not None else None # None means ROOT location
505
505
506 if can_create_in_root and gr is None:
506 if can_create_in_root and gr is None:
507 # we can create in root, we're fine no validations required
507 # we can create in root, we're fine no validations required
508 return
508 return
509
509
510 forbidden_in_root = gr is None and not can_create_in_root
510 forbidden_in_root = gr is None and not can_create_in_root
511 forbidden = not HasRepoGroupPermissionLevel('admin')(gr_name, 'can create group validator')
511 forbidden = not auth.HasRepoGroupPermissionLevel('admin')(gr_name, 'can create group validator')
512 if forbidden_in_root or forbidden:
512 if forbidden_in_root or forbidden:
513 msg = self.message('permission_denied', state)
513 msg = self.message('permission_denied', state)
514 raise formencode.Invalid(msg, value, state,
514 raise formencode.Invalid(msg, value, state,
515 error_dict=dict(parent_group_id=msg)
515 error_dict=dict(parent_group_id=msg)
516 )
516 )
517
517
518 return _validator
518 return _validator
519
519
520
520
521 def ValidPerms(type_='repo'):
521 def ValidPerms(type_='repo'):
522 if type_ == 'repo_group':
522 if type_ == 'repo_group':
523 EMPTY_PERM = 'group.none'
523 EMPTY_PERM = 'group.none'
524 elif type_ == 'repo':
524 elif type_ == 'repo':
525 EMPTY_PERM = 'repository.none'
525 EMPTY_PERM = 'repository.none'
526 elif type_ == 'user_group':
526 elif type_ == 'user_group':
527 EMPTY_PERM = 'usergroup.none'
527 EMPTY_PERM = 'usergroup.none'
528
528
529 class _validator(formencode.validators.FancyValidator):
529 class _validator(formencode.validators.FancyValidator):
530 messages = {
530 messages = {
531 'perm_new_member_name':
531 'perm_new_member_name':
532 _('This username or user group name is not valid')
532 _('This username or user group name is not valid')
533 }
533 }
534
534
535 def to_python(self, value, state):
535 def to_python(self, value, state):
536 perms_update = OrderedSet()
536 perms_update = OrderedSet()
537 perms_new = OrderedSet()
537 perms_new = OrderedSet()
538 # build a list of permission to update and new permission to create
538 # build a list of permission to update and new permission to create
539
539
540 # CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
540 # CLEAN OUT ORG VALUE FROM NEW MEMBERS, and group them using
541 new_perms_group = defaultdict(dict)
541 new_perms_group = defaultdict(dict)
542 for k, v in value.copy().items():
542 for k, v in value.copy().items():
543 if k.startswith('perm_new_member'):
543 if k.startswith('perm_new_member'):
544 del value[k]
544 del value[k]
545 _type, part = k.split('perm_new_member_')
545 _type, part = k.split('perm_new_member_')
546 args = part.split('_')
546 args = part.split('_')
547 if len(args) == 1:
547 if len(args) == 1:
548 new_perms_group[args[0]]['perm'] = v
548 new_perms_group[args[0]]['perm'] = v
549 elif len(args) == 2:
549 elif len(args) == 2:
550 _key, pos = args
550 _key, pos = args
551 new_perms_group[pos][_key] = v
551 new_perms_group[pos][_key] = v
552
552
553 # fill new permissions in order of how they were added
553 # fill new permissions in order of how they were added
554 for k in sorted(new_perms_group, key=lambda k: int(k)):
554 for k in sorted(new_perms_group, key=lambda k: int(k)):
555 perm_dict = new_perms_group[k]
555 perm_dict = new_perms_group[k]
556 new_member = perm_dict.get('name')
556 new_member = perm_dict.get('name')
557 new_perm = perm_dict.get('perm')
557 new_perm = perm_dict.get('perm')
558 new_type = perm_dict.get('type')
558 new_type = perm_dict.get('type')
559 if new_member and new_perm and new_type:
559 if new_member and new_perm and new_type:
560 perms_new.add((new_member, new_perm, new_type))
560 perms_new.add((new_member, new_perm, new_type))
561
561
562 for k, v in value.items():
562 for k, v in value.items():
563 if k.startswith('u_perm_') or k.startswith('g_perm_'):
563 if k.startswith('u_perm_') or k.startswith('g_perm_'):
564 member_name = k[7:]
564 member_name = k[7:]
565 t = {'u': 'user',
565 t = {'u': 'user',
566 'g': 'users_group'
566 'g': 'users_group'
567 }[k[0]]
567 }[k[0]]
568 if member_name == db.User.DEFAULT_USER_NAME:
568 if member_name == db.User.DEFAULT_USER_NAME:
569 if asbool(value.get('repo_private')):
569 if asbool(value.get('repo_private')):
570 # set none for default when updating to
570 # set none for default when updating to
571 # private repo protects against form manipulation
571 # private repo protects against form manipulation
572 v = EMPTY_PERM
572 v = EMPTY_PERM
573 perms_update.add((member_name, v, t))
573 perms_update.add((member_name, v, t))
574
574
575 value['perms_updates'] = list(perms_update)
575 value['perms_updates'] = list(perms_update)
576 value['perms_new'] = list(perms_new)
576 value['perms_new'] = list(perms_new)
577
577
578 # update permissions
578 # update permissions
579 for k, v, t in perms_new:
579 for k, v, t in perms_new:
580 try:
580 try:
581 if t == 'user':
581 if t == 'user':
582 _user_db = db.User.query() \
582 _user_db = db.User.query() \
583 .filter(db.User.active == True) \
583 .filter(db.User.active == True) \
584 .filter(db.User.username == k).one()
584 .filter(db.User.username == k).one()
585 if t == 'users_group':
585 if t == 'users_group':
586 _user_db = db.UserGroup.query() \
586 _user_db = db.UserGroup.query() \
587 .filter(db.UserGroup.users_group_active == True) \
587 .filter(db.UserGroup.users_group_active == True) \
588 .filter(db.UserGroup.users_group_name == k).one()
588 .filter(db.UserGroup.users_group_name == k).one()
589
589
590 except Exception as e:
590 except Exception as e:
591 log.warning('Error validating %s permission %s', t, k)
591 log.warning('Error validating %s permission %s', t, k)
592 msg = self.message('perm_new_member_type', state)
592 msg = self.message('perm_new_member_type', state)
593 raise formencode.Invalid(msg, value, state,
593 raise formencode.Invalid(msg, value, state,
594 error_dict=dict(perm_new_member_name=msg)
594 error_dict=dict(perm_new_member_name=msg)
595 )
595 )
596 return value
596 return value
597 return _validator
597 return _validator
598
598
599
599
600 def ValidSettings():
600 def ValidSettings():
601 class _validator(formencode.validators.FancyValidator):
601 class _validator(formencode.validators.FancyValidator):
602 def _convert_to_python(self, value, state):
602 def _convert_to_python(self, value, state):
603 # settings form for users that are not admin
603 # settings form for users that are not admin
604 # can't edit certain parameters, it's extra backup if they mangle
604 # can't edit certain parameters, it's extra backup if they mangle
605 # with forms
605 # with forms
606
606
607 forbidden_params = [
607 forbidden_params = [
608 'user', 'repo_type',
608 'user', 'repo_type',
609 'repo_enable_downloads', 'repo_enable_statistics'
609 'repo_enable_downloads', 'repo_enable_statistics'
610 ]
610 ]
611
611
612 for param in forbidden_params:
612 for param in forbidden_params:
613 if param in value:
613 if param in value:
614 del value[param]
614 del value[param]
615 return value
615 return value
616
616
617 def _validate_python(self, value, state):
617 def _validate_python(self, value, state):
618 pass
618 pass
619 return _validator
619 return _validator
620
620
621
621
622 def ValidPath():
622 def ValidPath():
623 class _validator(formencode.validators.FancyValidator):
623 class _validator(formencode.validators.FancyValidator):
624 messages = {
624 messages = {
625 'invalid_path': _('This is not a valid path')
625 'invalid_path': _('This is not a valid path')
626 }
626 }
627
627
628 def _validate_python(self, value, state):
628 def _validate_python(self, value, state):
629 if not os.path.isdir(value):
629 if not os.path.isdir(value):
630 msg = self.message('invalid_path', state)
630 msg = self.message('invalid_path', state)
631 raise formencode.Invalid(msg, value, state,
631 raise formencode.Invalid(msg, value, state,
632 error_dict=dict(paths_root_path=msg)
632 error_dict=dict(paths_root_path=msg)
633 )
633 )
634 return _validator
634 return _validator
635
635
636
636
637 def UniqSystemEmail(old_data=None):
637 def UniqSystemEmail(old_data=None):
638 old_data = old_data or {}
638 old_data = old_data or {}
639
639
640 class _validator(formencode.validators.FancyValidator):
640 class _validator(formencode.validators.FancyValidator):
641 messages = {
641 messages = {
642 'email_taken': _('This email address is already in use')
642 'email_taken': _('This email address is already in use')
643 }
643 }
644
644
645 def _convert_to_python(self, value, state):
645 def _convert_to_python(self, value, state):
646 return value.lower()
646 return value.lower()
647
647
648 def _validate_python(self, value, state):
648 def _validate_python(self, value, state):
649 if (old_data.get('email') or '').lower() != value:
649 if (old_data.get('email') or '').lower() != value:
650 user = db.User.get_by_email(value)
650 user = db.User.get_by_email(value)
651 if user is not None:
651 if user is not None:
652 msg = self.message('email_taken', state)
652 msg = self.message('email_taken', state)
653 raise formencode.Invalid(msg, value, state,
653 raise formencode.Invalid(msg, value, state,
654 error_dict=dict(email=msg)
654 error_dict=dict(email=msg)
655 )
655 )
656 return _validator
656 return _validator
657
657
658
658
659 def ValidSystemEmail():
659 def ValidSystemEmail():
660 class _validator(formencode.validators.FancyValidator):
660 class _validator(formencode.validators.FancyValidator):
661 messages = {
661 messages = {
662 'non_existing_email': _('Email address "%(email)s" not found')
662 'non_existing_email': _('Email address "%(email)s" not found')
663 }
663 }
664
664
665 def _convert_to_python(self, value, state):
665 def _convert_to_python(self, value, state):
666 return value.lower()
666 return value.lower()
667
667
668 def _validate_python(self, value, state):
668 def _validate_python(self, value, state):
669 user = db.User.get_by_email(value)
669 user = db.User.get_by_email(value)
670 if user is None:
670 if user is None:
671 msg = self.message('non_existing_email', state, email=value)
671 msg = self.message('non_existing_email', state, email=value)
672 raise formencode.Invalid(msg, value, state,
672 raise formencode.Invalid(msg, value, state,
673 error_dict=dict(email=msg)
673 error_dict=dict(email=msg)
674 )
674 )
675
675
676 return _validator
676 return _validator
677
677
678
678
679 def LdapLibValidator():
679 def LdapLibValidator():
680 class _validator(formencode.validators.FancyValidator):
680 class _validator(formencode.validators.FancyValidator):
681 messages = {
681 messages = {
682
682
683 }
683 }
684
684
685 def _validate_python(self, value, state):
685 def _validate_python(self, value, state):
686 try:
686 try:
687 import ldap
687 import ldap
688 ldap # pyflakes silence !
688 ldap # pyflakes silence !
689 except ImportError:
689 except ImportError:
690 raise LdapImportError()
690 raise LdapImportError()
691
691
692 return _validator
692 return _validator
693
693
694
694
695 def AttrLoginValidator():
695 def AttrLoginValidator():
696 class _validator(formencode.validators.UnicodeString):
696 class _validator(formencode.validators.UnicodeString):
697 messages = {
697 messages = {
698 'invalid_cn':
698 'invalid_cn':
699 _('The LDAP Login attribute of the CN must be specified - '
699 _('The LDAP Login attribute of the CN must be specified - '
700 'this is the name of the attribute that is equivalent '
700 'this is the name of the attribute that is equivalent '
701 'to "username"')
701 'to "username"')
702 }
702 }
703 messages['empty'] = messages['invalid_cn']
703 messages['empty'] = messages['invalid_cn']
704
704
705 return _validator
705 return _validator
706
706
707
707
708 def ValidIp():
708 def ValidIp():
709 class _validator(CIDR):
709 class _validator(CIDR):
710 messages = dict(
710 messages = dict(
711 badFormat=_('Please enter a valid IPv4 or IPv6 address'),
711 badFormat=_('Please enter a valid IPv4 or IPv6 address'),
712 illegalBits=_('The network size (bits) must be within the range'
712 illegalBits=_('The network size (bits) must be within the range'
713 ' of 0-32 (not %(bits)r)')
713 ' of 0-32 (not %(bits)r)')
714 )
714 )
715
715
716 def to_python(self, value, state):
716 def to_python(self, value, state):
717 v = super(_validator, self).to_python(value, state)
717 v = super(_validator, self).to_python(value, state)
718 v = v.strip()
718 v = v.strip()
719 net = ipaddr.IPNetwork(address=v)
719 net = ipaddr.IPNetwork(address=v)
720 if isinstance(net, ipaddr.IPv4Network):
720 if isinstance(net, ipaddr.IPv4Network):
721 # if IPv4 doesn't end with a mask, add /32
721 # if IPv4 doesn't end with a mask, add /32
722 if '/' not in value:
722 if '/' not in value:
723 v += '/32'
723 v += '/32'
724 if isinstance(net, ipaddr.IPv6Network):
724 if isinstance(net, ipaddr.IPv6Network):
725 # if IPv6 doesn't end with a mask, add /128
725 # if IPv6 doesn't end with a mask, add /128
726 if '/' not in value:
726 if '/' not in value:
727 v += '/128'
727 v += '/128'
728 return v
728 return v
729
729
730 def _validate_python(self, value, state):
730 def _validate_python(self, value, state):
731 try:
731 try:
732 addr = value.strip()
732 addr = value.strip()
733 # this raises an ValueError if address is not IPv4 or IPv6
733 # this raises an ValueError if address is not IPv4 or IPv6
734 ipaddr.IPNetwork(address=addr)
734 ipaddr.IPNetwork(address=addr)
735 except ValueError:
735 except ValueError:
736 raise formencode.Invalid(self.message('badFormat', state),
736 raise formencode.Invalid(self.message('badFormat', state),
737 value, state)
737 value, state)
738
738
739 return _validator
739 return _validator
740
740
741
741
742 def FieldKey():
742 def FieldKey():
743 class _validator(formencode.validators.FancyValidator):
743 class _validator(formencode.validators.FancyValidator):
744 messages = dict(
744 messages = dict(
745 badFormat=_('Key name can only consist of letters, '
745 badFormat=_('Key name can only consist of letters, '
746 'underscore, dash or numbers')
746 'underscore, dash or numbers')
747 )
747 )
748
748
749 def _validate_python(self, value, state):
749 def _validate_python(self, value, state):
750 if not re.match('[a-zA-Z0-9_-]+$', value):
750 if not re.match('[a-zA-Z0-9_-]+$', value):
751 raise formencode.Invalid(self.message('badFormat', state),
751 raise formencode.Invalid(self.message('badFormat', state),
752 value, state)
752 value, state)
753 return _validator
753 return _validator
754
754
755
755
756 def BasePath():
756 def BasePath():
757 class _validator(formencode.validators.FancyValidator):
757 class _validator(formencode.validators.FancyValidator):
758 messages = dict(
758 messages = dict(
759 badPath=_('Filename cannot be inside a directory')
759 badPath=_('Filename cannot be inside a directory')
760 )
760 )
761
761
762 def _convert_to_python(self, value, state):
762 def _convert_to_python(self, value, state):
763 return value
763 return value
764
764
765 def _validate_python(self, value, state):
765 def _validate_python(self, value, state):
766 if value != os.path.basename(value):
766 if value != os.path.basename(value):
767 raise formencode.Invalid(self.message('badPath', state),
767 raise formencode.Invalid(self.message('badPath', state),
768 value, state)
768 value, state)
769 return _validator
769 return _validator
770
770
771
771
772 def ValidAuthPlugins():
772 def ValidAuthPlugins():
773 class _validator(formencode.validators.FancyValidator):
773 class _validator(formencode.validators.FancyValidator):
774 messages = dict(
774 messages = dict(
775 import_duplicate=_('Plugins %(loaded)s and %(next_to_load)s both export the same name')
775 import_duplicate=_('Plugins %(loaded)s and %(next_to_load)s both export the same name')
776 )
776 )
777
777
778 def _convert_to_python(self, value, state):
778 def _convert_to_python(self, value, state):
779 # filter empty values
779 # filter empty values
780 return [s for s in value if s not in [None, '']]
780 return [s for s in value if s not in [None, '']]
781
781
782 def _validate_python(self, value, state):
782 def _validate_python(self, value, state):
783 from kallithea.lib import auth_modules
783 from kallithea.lib import auth_modules
784 module_list = value
784 module_list = value
785 unique_names = {}
785 unique_names = {}
786 try:
786 try:
787 for module in module_list:
787 for module in module_list:
788 plugin = auth_modules.loadplugin(module)
788 plugin = auth_modules.loadplugin(module)
789 plugin_name = plugin.name
789 plugin_name = plugin.name
790 if plugin_name in unique_names:
790 if plugin_name in unique_names:
791 msg = self.message('import_duplicate', state,
791 msg = self.message('import_duplicate', state,
792 loaded=unique_names[plugin_name],
792 loaded=unique_names[plugin_name],
793 next_to_load=plugin_name)
793 next_to_load=plugin_name)
794 raise formencode.Invalid(msg, value, state)
794 raise formencode.Invalid(msg, value, state)
795 unique_names[plugin_name] = plugin
795 unique_names[plugin_name] = plugin
796 except (ImportError, AttributeError, TypeError) as e:
796 except (ImportError, AttributeError, TypeError) as e:
797 raise formencode.Invalid(str(e), value, state)
797 raise formencode.Invalid(str(e), value, state)
798
798
799 return _validator
799 return _validator
@@ -1,2821 +1,2821 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
15 """
15 """
16 Tests for the JSON-RPC web api.
16 Tests for the JSON-RPC web api.
17 """
17 """
18
18
19 import os
19 import os
20 import random
20 import random
21 import re
21 import re
22 import string
22
23
23 import mock
24 import mock
24 import pytest
25 import pytest
25
26
26 from kallithea.lib import ext_json
27 from kallithea.lib import ext_json
27 from kallithea.lib.auth import AuthUser
28 from kallithea.lib.auth import AuthUser
28 from kallithea.lib.utils2 import ascii_bytes
29 from kallithea.lib.utils2 import ascii_bytes
29 from kallithea.model import db, meta
30 from kallithea.model import db, meta
30 from kallithea.model.changeset_status import ChangesetStatusModel
31 from kallithea.model.changeset_status import ChangesetStatusModel
31 from kallithea.model.gist import GistModel
32 from kallithea.model.gist import GistModel
32 from kallithea.model.pull_request import PullRequestModel
33 from kallithea.model.pull_request import PullRequestModel
33 from kallithea.model.repo import RepoModel
34 from kallithea.model.repo import RepoModel
34 from kallithea.model.repo_group import RepoGroupModel
35 from kallithea.model.repo_group import RepoGroupModel
35 from kallithea.model.scm import ScmModel
36 from kallithea.model.scm import ScmModel
36 from kallithea.model.user import UserModel
37 from kallithea.model.user import UserModel
37 from kallithea.model.user_group import UserGroupModel
38 from kallithea.model.user_group import UserGroupModel
38 from kallithea.tests import base
39 from kallithea.tests import base
39 from kallithea.tests.fixture import Fixture, raise_exception
40 from kallithea.tests.fixture import Fixture, raise_exception
40
41
41
42
42 API_URL = '/_admin/api'
43 API_URL = '/_admin/api'
43 TEST_USER_GROUP = 'test_user_group'
44 TEST_USER_GROUP = 'test_user_group'
44 TEST_REPO_GROUP = 'test_repo_group'
45 TEST_REPO_GROUP = 'test_repo_group'
45
46
46 fixture = Fixture()
47 fixture = Fixture()
47
48
48
49
49 def _build_data(apikey, method, **kw):
50 def _build_data(apikey, method, **kw):
50 """
51 """
51 Builds API data with given random ID
52 Builds API data with given random ID
52 For convenience, the json is returned as str
53 For convenience, the json is returned as str
53 """
54 """
54 random_id = random.randrange(1, 9999)
55 random_id = random.randrange(1, 9999)
55 return random_id, ext_json.dumps({
56 return random_id, ext_json.dumps({
56 "id": random_id,
57 "id": random_id,
57 "api_key": apikey,
58 "api_key": apikey,
58 "method": method,
59 "method": method,
59 "args": kw
60 "args": kw
60 })
61 })
61
62
62
63
63 jsonify = lambda obj: ext_json.loads(ext_json.dumps(obj))
64 jsonify = lambda obj: ext_json.loads(ext_json.dumps(obj))
64
65
65
66
66 def api_call(test_obj, params):
67 def api_call(test_obj, params):
67 response = test_obj.app.post(API_URL, content_type='application/json',
68 response = test_obj.app.post(API_URL, content_type='application/json',
68 params=params)
69 params=params)
69 return response
70 return response
70
71
71
72
72 ## helpers
73 ## helpers
73 def make_user_group(name=TEST_USER_GROUP):
74 def make_user_group(name=TEST_USER_GROUP):
74 gr = fixture.create_user_group(name, cur_user=base.TEST_USER_ADMIN_LOGIN)
75 gr = fixture.create_user_group(name, cur_user=base.TEST_USER_ADMIN_LOGIN)
75 UserGroupModel().add_user_to_group(user_group=gr,
76 UserGroupModel().add_user_to_group(user_group=gr,
76 user=base.TEST_USER_ADMIN_LOGIN)
77 user=base.TEST_USER_ADMIN_LOGIN)
77 meta.Session().commit()
78 meta.Session().commit()
78 return gr
79 return gr
79
80
80
81
81 def make_repo_group(name=TEST_REPO_GROUP):
82 def make_repo_group(name=TEST_REPO_GROUP):
82 gr = fixture.create_repo_group(name, cur_user=base.TEST_USER_ADMIN_LOGIN)
83 gr = fixture.create_repo_group(name, cur_user=base.TEST_USER_ADMIN_LOGIN)
83 meta.Session().commit()
84 meta.Session().commit()
84 return gr
85 return gr
85
86
86
87
87 class _BaseTestApi(object):
88 class _BaseTestApi(object):
88 REPO = None
89 REPO = None
89 REPO_TYPE = None
90 REPO_TYPE = None
90
91
91 @classmethod
92 @classmethod
92 def setup_class(cls):
93 def setup_class(cls):
93 cls.usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
94 cls.usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
94 cls.apikey = cls.usr.api_key
95 cls.apikey = cls.usr.api_key
95 cls.test_user = UserModel().create_or_update(
96 cls.test_user = UserModel().create_or_update(
96 username='test-api',
97 username='test-api',
97 password='test',
98 password='test',
98 email='test@example.com',
99 email='test@example.com',
99 firstname='first',
100 firstname='first',
100 lastname='last'
101 lastname='last'
101 )
102 )
102 meta.Session().commit()
103 meta.Session().commit()
103 cls.TEST_USER_LOGIN = cls.test_user.username
104 cls.TEST_USER_LOGIN = cls.test_user.username
104 cls.apikey_regular = cls.test_user.api_key
105 cls.apikey_regular = cls.test_user.api_key
105
106
106 @classmethod
107 @classmethod
107 def teardown_class(cls):
108 def teardown_class(cls):
108 pass
109 pass
109
110
110 def setup_method(self, method):
111 def setup_method(self, method):
111 make_user_group()
112 make_user_group()
112 make_repo_group()
113 make_repo_group()
113
114
114 def teardown_method(self, method):
115 def teardown_method(self, method):
115 fixture.destroy_user_group(TEST_USER_GROUP)
116 fixture.destroy_user_group(TEST_USER_GROUP)
116 fixture.destroy_gists()
117 fixture.destroy_gists()
117 fixture.destroy_repo_group(TEST_REPO_GROUP)
118 fixture.destroy_repo_group(TEST_REPO_GROUP)
118
119
119 def _compare_ok(self, id_, expected, given):
120 def _compare_ok(self, id_, expected, given):
120 expected = jsonify({
121 expected = jsonify({
121 'id': id_,
122 'id': id_,
122 'error': None,
123 'error': None,
123 'result': expected
124 'result': expected
124 })
125 })
125 given = ext_json.loads(given)
126 given = ext_json.loads(given)
126 assert expected == given, (expected, given)
127 assert expected == given, (expected, given)
127
128
128 def _compare_error(self, id_, expected, given):
129 def _compare_error(self, id_, expected, given):
129 expected = jsonify({
130 expected = jsonify({
130 'id': id_,
131 'id': id_,
131 'error': expected,
132 'error': expected,
132 'result': None
133 'result': None
133 })
134 })
134 given = ext_json.loads(given)
135 given = ext_json.loads(given)
135 assert expected == given, (expected, given)
136 assert expected == given, (expected, given)
136
137
137 def test_api_wrong_key(self):
138 def test_api_wrong_key(self):
138 id_, params = _build_data('trololo', 'get_user')
139 id_, params = _build_data('trololo', 'get_user')
139 response = api_call(self, params)
140 response = api_call(self, params)
140
141
141 expected = 'Invalid API key'
142 expected = 'Invalid API key'
142 self._compare_error(id_, expected, given=response.body)
143 self._compare_error(id_, expected, given=response.body)
143
144
144 def test_api_missing_non_optional_param(self):
145 def test_api_missing_non_optional_param(self):
145 id_, params = _build_data(self.apikey, 'get_repo')
146 id_, params = _build_data(self.apikey, 'get_repo')
146 response = api_call(self, params)
147 response = api_call(self, params)
147
148
148 expected = 'Missing non optional `repoid` arg in JSON DATA'
149 expected = 'Missing non optional `repoid` arg in JSON DATA'
149 self._compare_error(id_, expected, given=response.body)
150 self._compare_error(id_, expected, given=response.body)
150
151
151 def test_api_missing_non_optional_param_args_null(self):
152 def test_api_missing_non_optional_param_args_null(self):
152 id_, params = _build_data(self.apikey, 'get_repo')
153 id_, params = _build_data(self.apikey, 'get_repo')
153 params = params.replace('"args": {}', '"args": null')
154 params = params.replace('"args": {}', '"args": null')
154 response = api_call(self, params)
155 response = api_call(self, params)
155
156
156 expected = 'Missing non optional `repoid` arg in JSON DATA'
157 expected = 'Missing non optional `repoid` arg in JSON DATA'
157 self._compare_error(id_, expected, given=response.body)
158 self._compare_error(id_, expected, given=response.body)
158
159
159 def test_api_missing_non_optional_param_args_bad(self):
160 def test_api_missing_non_optional_param_args_bad(self):
160 id_, params = _build_data(self.apikey, 'get_repo')
161 id_, params = _build_data(self.apikey, 'get_repo')
161 params = params.replace('"args": {}', '"args": 1')
162 params = params.replace('"args": {}', '"args": 1')
162 response = api_call(self, params)
163 response = api_call(self, params)
163
164
164 expected = 'Missing non optional `repoid` arg in JSON DATA'
165 expected = 'Missing non optional `repoid` arg in JSON DATA'
165 self._compare_error(id_, expected, given=response.body)
166 self._compare_error(id_, expected, given=response.body)
166
167
167 def test_api_args_is_null(self):
168 def test_api_args_is_null(self):
168 id_, params = _build_data(self.apikey, 'get_users', )
169 id_, params = _build_data(self.apikey, 'get_users', )
169 params = params.replace('"args": {}', '"args": null')
170 params = params.replace('"args": {}', '"args": null')
170 response = api_call(self, params)
171 response = api_call(self, params)
171 assert response.status == '200 OK'
172 assert response.status == '200 OK'
172
173
173 def test_api_args_is_bad(self):
174 def test_api_args_is_bad(self):
174 id_, params = _build_data(self.apikey, 'get_users', )
175 id_, params = _build_data(self.apikey, 'get_users', )
175 params = params.replace('"args": {}', '"args": 1')
176 params = params.replace('"args": {}', '"args": 1')
176 response = api_call(self, params)
177 response = api_call(self, params)
177 assert response.status == '200 OK'
178 assert response.status == '200 OK'
178
179
179 def test_api_args_different_args(self):
180 def test_api_args_different_args(self):
180 import string
181 expected = {
181 expected = {
182 'ascii_letters': string.ascii_letters,
182 'ascii_letters': string.ascii_letters,
183 'ws': string.whitespace,
183 'ws': string.whitespace,
184 'printables': string.printable
184 'printables': string.printable
185 }
185 }
186 id_, params = _build_data(self.apikey, 'test', args=expected)
186 id_, params = _build_data(self.apikey, 'test', args=expected)
187 response = api_call(self, params)
187 response = api_call(self, params)
188 assert response.status == '200 OK'
188 assert response.status == '200 OK'
189 self._compare_ok(id_, expected, response.body)
189 self._compare_ok(id_, expected, response.body)
190
190
191 def test_api_get_users(self):
191 def test_api_get_users(self):
192 id_, params = _build_data(self.apikey, 'get_users', )
192 id_, params = _build_data(self.apikey, 'get_users', )
193 response = api_call(self, params)
193 response = api_call(self, params)
194 ret_all = []
194 ret_all = []
195 _users = db.User.query().filter_by(is_default_user=False) \
195 _users = db.User.query().filter_by(is_default_user=False) \
196 .order_by(db.User.username).all()
196 .order_by(db.User.username).all()
197 for usr in _users:
197 for usr in _users:
198 ret = usr.get_api_data()
198 ret = usr.get_api_data()
199 ret_all.append(jsonify(ret))
199 ret_all.append(jsonify(ret))
200 expected = ret_all
200 expected = ret_all
201 self._compare_ok(id_, expected, given=response.body)
201 self._compare_ok(id_, expected, given=response.body)
202
202
203 def test_api_get_user(self):
203 def test_api_get_user(self):
204 id_, params = _build_data(self.apikey, 'get_user',
204 id_, params = _build_data(self.apikey, 'get_user',
205 userid=base.TEST_USER_ADMIN_LOGIN)
205 userid=base.TEST_USER_ADMIN_LOGIN)
206 response = api_call(self, params)
206 response = api_call(self, params)
207
207
208 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
208 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
209 ret = usr.get_api_data()
209 ret = usr.get_api_data()
210 ret['permissions'] = AuthUser(dbuser=usr).permissions
210 ret['permissions'] = AuthUser(dbuser=usr).permissions
211
211
212 expected = ret
212 expected = ret
213 self._compare_ok(id_, expected, given=response.body)
213 self._compare_ok(id_, expected, given=response.body)
214
214
215 def test_api_get_user_that_does_not_exist(self):
215 def test_api_get_user_that_does_not_exist(self):
216 id_, params = _build_data(self.apikey, 'get_user',
216 id_, params = _build_data(self.apikey, 'get_user',
217 userid='trololo')
217 userid='trololo')
218 response = api_call(self, params)
218 response = api_call(self, params)
219
219
220 expected = "user `%s` does not exist" % 'trololo'
220 expected = "user `%s` does not exist" % 'trololo'
221 self._compare_error(id_, expected, given=response.body)
221 self._compare_error(id_, expected, given=response.body)
222
222
223 def test_api_get_user_without_giving_userid(self):
223 def test_api_get_user_without_giving_userid(self):
224 id_, params = _build_data(self.apikey, 'get_user')
224 id_, params = _build_data(self.apikey, 'get_user')
225 response = api_call(self, params)
225 response = api_call(self, params)
226
226
227 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
227 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
228 ret = usr.get_api_data()
228 ret = usr.get_api_data()
229 ret['permissions'] = AuthUser(dbuser=usr).permissions
229 ret['permissions'] = AuthUser(dbuser=usr).permissions
230
230
231 expected = ret
231 expected = ret
232 self._compare_ok(id_, expected, given=response.body)
232 self._compare_ok(id_, expected, given=response.body)
233
233
234 def test_api_get_user_without_giving_userid_non_admin(self):
234 def test_api_get_user_without_giving_userid_non_admin(self):
235 id_, params = _build_data(self.apikey_regular, 'get_user')
235 id_, params = _build_data(self.apikey_regular, 'get_user')
236 response = api_call(self, params)
236 response = api_call(self, params)
237
237
238 usr = db.User.get_by_username(self.TEST_USER_LOGIN)
238 usr = db.User.get_by_username(self.TEST_USER_LOGIN)
239 ret = usr.get_api_data()
239 ret = usr.get_api_data()
240 ret['permissions'] = AuthUser(dbuser=usr).permissions
240 ret['permissions'] = AuthUser(dbuser=usr).permissions
241
241
242 expected = ret
242 expected = ret
243 self._compare_ok(id_, expected, given=response.body)
243 self._compare_ok(id_, expected, given=response.body)
244
244
245 def test_api_get_user_with_giving_userid_non_admin(self):
245 def test_api_get_user_with_giving_userid_non_admin(self):
246 id_, params = _build_data(self.apikey_regular, 'get_user',
246 id_, params = _build_data(self.apikey_regular, 'get_user',
247 userid=self.TEST_USER_LOGIN)
247 userid=self.TEST_USER_LOGIN)
248 response = api_call(self, params)
248 response = api_call(self, params)
249
249
250 expected = 'userid is not the same as your user'
250 expected = 'userid is not the same as your user'
251 self._compare_error(id_, expected, given=response.body)
251 self._compare_error(id_, expected, given=response.body)
252
252
253 def test_api_pull_remote(self):
253 def test_api_pull_remote(self):
254 # Note: pulling from local repos is a mis-feature - it will bypass access control
254 # Note: pulling from local repos is a mis-feature - it will bypass access control
255 # ... but ok, if the path already has been set in the database
255 # ... but ok, if the path already has been set in the database
256 repo_name = 'test_pull'
256 repo_name = 'test_pull'
257 r = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
257 r = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
258 # hack around that clone_uri can't be set to to a local path
258 # hack around that clone_uri can't be set to to a local path
259 # (as shown by test_api_create_repo_clone_uri_local)
259 # (as shown by test_api_create_repo_clone_uri_local)
260 r.clone_uri = os.path.join(db.Ui.get_by_key('paths', '/').ui_value, self.REPO)
260 r.clone_uri = os.path.join(db.Ui.get_by_key('paths', '/').ui_value, self.REPO)
261 meta.Session().commit()
261 meta.Session().commit()
262
262
263 pre_cached_tip = [repo.get_api_data()['last_changeset']['short_id'] for repo in db.Repository.query().filter(db.Repository.repo_name == repo_name)]
263 pre_cached_tip = [repo.get_api_data()['last_changeset']['short_id'] for repo in db.Repository.query().filter(db.Repository.repo_name == repo_name)]
264
264
265 id_, params = _build_data(self.apikey, 'pull',
265 id_, params = _build_data(self.apikey, 'pull',
266 repoid=repo_name,)
266 repoid=repo_name,)
267 response = api_call(self, params)
267 response = api_call(self, params)
268
268
269 expected = {'msg': 'Pulled from `%s`' % repo_name,
269 expected = {'msg': 'Pulled from `%s`' % repo_name,
270 'repository': repo_name}
270 'repository': repo_name}
271 self._compare_ok(id_, expected, given=response.body)
271 self._compare_ok(id_, expected, given=response.body)
272
272
273 post_cached_tip = [repo.get_api_data()['last_changeset']['short_id'] for repo in db.Repository.query().filter(db.Repository.repo_name == repo_name)]
273 post_cached_tip = [repo.get_api_data()['last_changeset']['short_id'] for repo in db.Repository.query().filter(db.Repository.repo_name == repo_name)]
274
274
275 fixture.destroy_repo(repo_name)
275 fixture.destroy_repo(repo_name)
276
276
277 assert pre_cached_tip != post_cached_tip
277 assert pre_cached_tip != post_cached_tip
278
278
279 def test_api_pull_fork(self):
279 def test_api_pull_fork(self):
280 fork_name = 'fork'
280 fork_name = 'fork'
281 fixture.create_fork(self.REPO, fork_name)
281 fixture.create_fork(self.REPO, fork_name)
282 id_, params = _build_data(self.apikey, 'pull',
282 id_, params = _build_data(self.apikey, 'pull',
283 repoid=fork_name,)
283 repoid=fork_name,)
284 response = api_call(self, params)
284 response = api_call(self, params)
285
285
286 expected = {'msg': 'Pulled from `%s`' % fork_name,
286 expected = {'msg': 'Pulled from `%s`' % fork_name,
287 'repository': fork_name}
287 'repository': fork_name}
288 self._compare_ok(id_, expected, given=response.body)
288 self._compare_ok(id_, expected, given=response.body)
289
289
290 fixture.destroy_repo(fork_name)
290 fixture.destroy_repo(fork_name)
291
291
292 def test_api_pull_error_no_remote_no_fork(self):
292 def test_api_pull_error_no_remote_no_fork(self):
293 # should fail because no clone_uri is set
293 # should fail because no clone_uri is set
294 id_, params = _build_data(self.apikey, 'pull',
294 id_, params = _build_data(self.apikey, 'pull',
295 repoid=self.REPO, )
295 repoid=self.REPO, )
296 response = api_call(self, params)
296 response = api_call(self, params)
297
297
298 expected = 'Unable to pull changes from `%s`' % self.REPO
298 expected = 'Unable to pull changes from `%s`' % self.REPO
299 self._compare_error(id_, expected, given=response.body)
299 self._compare_error(id_, expected, given=response.body)
300
300
301 def test_api_pull_custom_remote(self):
301 def test_api_pull_custom_remote(self):
302 repo_name = 'test_pull_custom_remote'
302 repo_name = 'test_pull_custom_remote'
303 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
303 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
304
304
305 custom_remote_path = os.path.join(db.Ui.get_by_key('paths', '/').ui_value, self.REPO)
305 custom_remote_path = os.path.join(db.Ui.get_by_key('paths', '/').ui_value, self.REPO)
306
306
307 id_, params = _build_data(self.apikey, 'pull',
307 id_, params = _build_data(self.apikey, 'pull',
308 repoid=repo_name,
308 repoid=repo_name,
309 clone_uri=custom_remote_path)
309 clone_uri=custom_remote_path)
310 response = api_call(self, params)
310 response = api_call(self, params)
311
311
312 expected = {'msg': 'Pulled from `%s`' % repo_name,
312 expected = {'msg': 'Pulled from `%s`' % repo_name,
313 'repository': repo_name}
313 'repository': repo_name}
314 self._compare_ok(id_, expected, given=response.body)
314 self._compare_ok(id_, expected, given=response.body)
315
315
316 fixture.destroy_repo(repo_name)
316 fixture.destroy_repo(repo_name)
317
317
318 def test_api_rescan_repos(self):
318 def test_api_rescan_repos(self):
319 id_, params = _build_data(self.apikey, 'rescan_repos')
319 id_, params = _build_data(self.apikey, 'rescan_repos')
320 response = api_call(self, params)
320 response = api_call(self, params)
321
321
322 expected = {'added': [], 'removed': []}
322 expected = {'added': [], 'removed': []}
323 self._compare_ok(id_, expected, given=response.body)
323 self._compare_ok(id_, expected, given=response.body)
324
324
325 @mock.patch.object(ScmModel, 'repo_scan', raise_exception)
325 @mock.patch.object(ScmModel, 'repo_scan', raise_exception)
326 def test_api_rescann_error(self):
326 def test_api_rescann_error(self):
327 id_, params = _build_data(self.apikey, 'rescan_repos', )
327 id_, params = _build_data(self.apikey, 'rescan_repos', )
328 response = api_call(self, params)
328 response = api_call(self, params)
329
329
330 expected = 'Error occurred during rescan repositories action'
330 expected = 'Error occurred during rescan repositories action'
331 self._compare_error(id_, expected, given=response.body)
331 self._compare_error(id_, expected, given=response.body)
332
332
333 def test_api_create_existing_user(self):
333 def test_api_create_existing_user(self):
334 id_, params = _build_data(self.apikey, 'create_user',
334 id_, params = _build_data(self.apikey, 'create_user',
335 username=base.TEST_USER_ADMIN_LOGIN,
335 username=base.TEST_USER_ADMIN_LOGIN,
336 email='test@example.com',
336 email='test@example.com',
337 password='trololo')
337 password='trololo')
338 response = api_call(self, params)
338 response = api_call(self, params)
339
339
340 expected = "user `%s` already exist" % base.TEST_USER_ADMIN_LOGIN
340 expected = "user `%s` already exist" % base.TEST_USER_ADMIN_LOGIN
341 self._compare_error(id_, expected, given=response.body)
341 self._compare_error(id_, expected, given=response.body)
342
342
343 def test_api_create_user_with_existing_email(self):
343 def test_api_create_user_with_existing_email(self):
344 id_, params = _build_data(self.apikey, 'create_user',
344 id_, params = _build_data(self.apikey, 'create_user',
345 username=base.TEST_USER_ADMIN_LOGIN + 'new',
345 username=base.TEST_USER_ADMIN_LOGIN + 'new',
346 email=base.TEST_USER_REGULAR_EMAIL,
346 email=base.TEST_USER_REGULAR_EMAIL,
347 password='trololo')
347 password='trololo')
348 response = api_call(self, params)
348 response = api_call(self, params)
349
349
350 expected = "email `%s` already exist" % base.TEST_USER_REGULAR_EMAIL
350 expected = "email `%s` already exist" % base.TEST_USER_REGULAR_EMAIL
351 self._compare_error(id_, expected, given=response.body)
351 self._compare_error(id_, expected, given=response.body)
352
352
353 def test_api_create_user(self):
353 def test_api_create_user(self):
354 username = 'test_new_api_user'
354 username = 'test_new_api_user'
355 email = username + "@example.com"
355 email = username + "@example.com"
356
356
357 id_, params = _build_data(self.apikey, 'create_user',
357 id_, params = _build_data(self.apikey, 'create_user',
358 username=username,
358 username=username,
359 email=email,
359 email=email,
360 password='trololo')
360 password='trololo')
361 response = api_call(self, params)
361 response = api_call(self, params)
362
362
363 usr = db.User.get_by_username(username)
363 usr = db.User.get_by_username(username)
364 ret = dict(
364 ret = dict(
365 msg='created new user `%s`' % username,
365 msg='created new user `%s`' % username,
366 user=jsonify(usr.get_api_data())
366 user=jsonify(usr.get_api_data())
367 )
367 )
368
368
369 try:
369 try:
370 expected = ret
370 expected = ret
371 self._compare_ok(id_, expected, given=response.body)
371 self._compare_ok(id_, expected, given=response.body)
372 finally:
372 finally:
373 fixture.destroy_user(usr.user_id)
373 fixture.destroy_user(usr.user_id)
374
374
375 def test_api_create_user_without_password(self):
375 def test_api_create_user_without_password(self):
376 username = 'test_new_api_user_passwordless'
376 username = 'test_new_api_user_passwordless'
377 email = username + "@example.com"
377 email = username + "@example.com"
378
378
379 id_, params = _build_data(self.apikey, 'create_user',
379 id_, params = _build_data(self.apikey, 'create_user',
380 username=username,
380 username=username,
381 email=email)
381 email=email)
382 response = api_call(self, params)
382 response = api_call(self, params)
383
383
384 usr = db.User.get_by_username(username)
384 usr = db.User.get_by_username(username)
385 ret = dict(
385 ret = dict(
386 msg='created new user `%s`' % username,
386 msg='created new user `%s`' % username,
387 user=jsonify(usr.get_api_data())
387 user=jsonify(usr.get_api_data())
388 )
388 )
389 try:
389 try:
390 expected = ret
390 expected = ret
391 self._compare_ok(id_, expected, given=response.body)
391 self._compare_ok(id_, expected, given=response.body)
392 finally:
392 finally:
393 fixture.destroy_user(usr.user_id)
393 fixture.destroy_user(usr.user_id)
394
394
395 def test_api_create_user_with_extern_name(self):
395 def test_api_create_user_with_extern_name(self):
396 username = 'test_new_api_user_passwordless'
396 username = 'test_new_api_user_passwordless'
397 email = username + "@example.com"
397 email = username + "@example.com"
398
398
399 id_, params = _build_data(self.apikey, 'create_user',
399 id_, params = _build_data(self.apikey, 'create_user',
400 username=username,
400 username=username,
401 email=email, extern_name='internal')
401 email=email, extern_name='internal')
402 response = api_call(self, params)
402 response = api_call(self, params)
403
403
404 usr = db.User.get_by_username(username)
404 usr = db.User.get_by_username(username)
405 ret = dict(
405 ret = dict(
406 msg='created new user `%s`' % username,
406 msg='created new user `%s`' % username,
407 user=jsonify(usr.get_api_data())
407 user=jsonify(usr.get_api_data())
408 )
408 )
409 try:
409 try:
410 expected = ret
410 expected = ret
411 self._compare_ok(id_, expected, given=response.body)
411 self._compare_ok(id_, expected, given=response.body)
412 finally:
412 finally:
413 fixture.destroy_user(usr.user_id)
413 fixture.destroy_user(usr.user_id)
414
414
415 @mock.patch.object(UserModel, 'create_or_update', raise_exception)
415 @mock.patch.object(UserModel, 'create_or_update', raise_exception)
416 def test_api_create_user_when_exception_happened(self):
416 def test_api_create_user_when_exception_happened(self):
417
417
418 username = 'test_new_api_user'
418 username = 'test_new_api_user'
419 email = username + "@example.com"
419 email = username + "@example.com"
420
420
421 id_, params = _build_data(self.apikey, 'create_user',
421 id_, params = _build_data(self.apikey, 'create_user',
422 username=username,
422 username=username,
423 email=email,
423 email=email,
424 password='trololo')
424 password='trololo')
425 response = api_call(self, params)
425 response = api_call(self, params)
426 expected = 'failed to create user `%s`' % username
426 expected = 'failed to create user `%s`' % username
427 self._compare_error(id_, expected, given=response.body)
427 self._compare_error(id_, expected, given=response.body)
428
428
429 def test_api_delete_user(self):
429 def test_api_delete_user(self):
430 usr = UserModel().create_or_update(username='test_user',
430 usr = UserModel().create_or_update(username='test_user',
431 password='qweqwe',
431 password='qweqwe',
432 email='u232@example.com',
432 email='u232@example.com',
433 firstname='u1', lastname='u1')
433 firstname='u1', lastname='u1')
434 meta.Session().commit()
434 meta.Session().commit()
435 username = usr.username
435 username = usr.username
436 email = usr.email
436 email = usr.email
437 usr_id = usr.user_id
437 usr_id = usr.user_id
438 ## DELETE THIS USER NOW
438 ## DELETE THIS USER NOW
439
439
440 id_, params = _build_data(self.apikey, 'delete_user',
440 id_, params = _build_data(self.apikey, 'delete_user',
441 userid=username, )
441 userid=username, )
442 response = api_call(self, params)
442 response = api_call(self, params)
443
443
444 ret = {'msg': 'deleted user ID:%s %s' % (usr_id, username),
444 ret = {'msg': 'deleted user ID:%s %s' % (usr_id, username),
445 'user': None}
445 'user': None}
446 expected = ret
446 expected = ret
447 self._compare_ok(id_, expected, given=response.body)
447 self._compare_ok(id_, expected, given=response.body)
448
448
449 @mock.patch.object(UserModel, 'delete', raise_exception)
449 @mock.patch.object(UserModel, 'delete', raise_exception)
450 def test_api_delete_user_when_exception_happened(self):
450 def test_api_delete_user_when_exception_happened(self):
451 usr = UserModel().create_or_update(username='test_user',
451 usr = UserModel().create_or_update(username='test_user',
452 password='qweqwe',
452 password='qweqwe',
453 email='u232@example.com',
453 email='u232@example.com',
454 firstname='u1', lastname='u1')
454 firstname='u1', lastname='u1')
455 meta.Session().commit()
455 meta.Session().commit()
456 username = usr.username
456 username = usr.username
457
457
458 id_, params = _build_data(self.apikey, 'delete_user',
458 id_, params = _build_data(self.apikey, 'delete_user',
459 userid=username, )
459 userid=username, )
460 response = api_call(self, params)
460 response = api_call(self, params)
461 ret = 'failed to delete user ID:%s %s' % (usr.user_id,
461 ret = 'failed to delete user ID:%s %s' % (usr.user_id,
462 usr.username)
462 usr.username)
463 expected = ret
463 expected = ret
464 self._compare_error(id_, expected, given=response.body)
464 self._compare_error(id_, expected, given=response.body)
465
465
466 @base.parametrize('name,expected', [
466 @base.parametrize('name,expected', [
467 ('firstname', 'new_username'),
467 ('firstname', 'new_username'),
468 ('lastname', 'new_username'),
468 ('lastname', 'new_username'),
469 ('email', 'new_username'),
469 ('email', 'new_username'),
470 ('admin', True),
470 ('admin', True),
471 ('admin', False),
471 ('admin', False),
472 ('extern_type', 'ldap'),
472 ('extern_type', 'ldap'),
473 ('extern_type', None),
473 ('extern_type', None),
474 ('extern_name', 'test'),
474 ('extern_name', 'test'),
475 ('extern_name', None),
475 ('extern_name', None),
476 ('active', False),
476 ('active', False),
477 ('active', True),
477 ('active', True),
478 ('password', 'newpass'),
478 ('password', 'newpass'),
479 ])
479 ])
480 def test_api_update_user(self, name, expected):
480 def test_api_update_user(self, name, expected):
481 usr = db.User.get_by_username(self.TEST_USER_LOGIN)
481 usr = db.User.get_by_username(self.TEST_USER_LOGIN)
482 kw = {name: expected,
482 kw = {name: expected,
483 'userid': usr.user_id}
483 'userid': usr.user_id}
484 id_, params = _build_data(self.apikey, 'update_user', **kw)
484 id_, params = _build_data(self.apikey, 'update_user', **kw)
485 response = api_call(self, params)
485 response = api_call(self, params)
486
486
487 ret = {
487 ret = {
488 'msg': 'updated user ID:%s %s' % (
488 'msg': 'updated user ID:%s %s' % (
489 usr.user_id, self.TEST_USER_LOGIN),
489 usr.user_id, self.TEST_USER_LOGIN),
490 'user': jsonify(db.User \
490 'user': jsonify(db.User \
491 .get_by_username(self.TEST_USER_LOGIN) \
491 .get_by_username(self.TEST_USER_LOGIN) \
492 .get_api_data())
492 .get_api_data())
493 }
493 }
494
494
495 expected = ret
495 expected = ret
496 self._compare_ok(id_, expected, given=response.body)
496 self._compare_ok(id_, expected, given=response.body)
497
497
498 def test_api_update_user_no_changed_params(self):
498 def test_api_update_user_no_changed_params(self):
499 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
499 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
500 ret = jsonify(usr.get_api_data())
500 ret = jsonify(usr.get_api_data())
501 id_, params = _build_data(self.apikey, 'update_user',
501 id_, params = _build_data(self.apikey, 'update_user',
502 userid=base.TEST_USER_ADMIN_LOGIN)
502 userid=base.TEST_USER_ADMIN_LOGIN)
503
503
504 response = api_call(self, params)
504 response = api_call(self, params)
505 ret = {
505 ret = {
506 'msg': 'updated user ID:%s %s' % (
506 'msg': 'updated user ID:%s %s' % (
507 usr.user_id, base.TEST_USER_ADMIN_LOGIN),
507 usr.user_id, base.TEST_USER_ADMIN_LOGIN),
508 'user': ret
508 'user': ret
509 }
509 }
510 expected = ret
510 expected = ret
511 self._compare_ok(id_, expected, given=response.body)
511 self._compare_ok(id_, expected, given=response.body)
512
512
513 def test_api_update_user_by_user_id(self):
513 def test_api_update_user_by_user_id(self):
514 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
514 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
515 ret = jsonify(usr.get_api_data())
515 ret = jsonify(usr.get_api_data())
516 id_, params = _build_data(self.apikey, 'update_user',
516 id_, params = _build_data(self.apikey, 'update_user',
517 userid=usr.user_id)
517 userid=usr.user_id)
518
518
519 response = api_call(self, params)
519 response = api_call(self, params)
520 ret = {
520 ret = {
521 'msg': 'updated user ID:%s %s' % (
521 'msg': 'updated user ID:%s %s' % (
522 usr.user_id, base.TEST_USER_ADMIN_LOGIN),
522 usr.user_id, base.TEST_USER_ADMIN_LOGIN),
523 'user': ret
523 'user': ret
524 }
524 }
525 expected = ret
525 expected = ret
526 self._compare_ok(id_, expected, given=response.body)
526 self._compare_ok(id_, expected, given=response.body)
527
527
528 def test_api_update_user_default_user(self):
528 def test_api_update_user_default_user(self):
529 usr = db.User.get_default_user()
529 usr = db.User.get_default_user()
530 id_, params = _build_data(self.apikey, 'update_user',
530 id_, params = _build_data(self.apikey, 'update_user',
531 userid=usr.user_id)
531 userid=usr.user_id)
532
532
533 response = api_call(self, params)
533 response = api_call(self, params)
534 expected = 'editing default user is forbidden'
534 expected = 'editing default user is forbidden'
535 self._compare_error(id_, expected, given=response.body)
535 self._compare_error(id_, expected, given=response.body)
536
536
537 @mock.patch.object(UserModel, 'update_user', raise_exception)
537 @mock.patch.object(UserModel, 'update_user', raise_exception)
538 def test_api_update_user_when_exception_happens(self):
538 def test_api_update_user_when_exception_happens(self):
539 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
539 usr = db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
540 ret = jsonify(usr.get_api_data())
540 ret = jsonify(usr.get_api_data())
541 id_, params = _build_data(self.apikey, 'update_user',
541 id_, params = _build_data(self.apikey, 'update_user',
542 userid=usr.user_id)
542 userid=usr.user_id)
543
543
544 response = api_call(self, params)
544 response = api_call(self, params)
545 ret = 'failed to update user `%s`' % usr.user_id
545 ret = 'failed to update user `%s`' % usr.user_id
546
546
547 expected = ret
547 expected = ret
548 self._compare_error(id_, expected, given=response.body)
548 self._compare_error(id_, expected, given=response.body)
549
549
550 def test_api_get_repo(self):
550 def test_api_get_repo(self):
551 new_group = 'some_new_group'
551 new_group = 'some_new_group'
552 make_user_group(new_group)
552 make_user_group(new_group)
553 RepoModel().grant_user_group_permission(repo=self.REPO,
553 RepoModel().grant_user_group_permission(repo=self.REPO,
554 group_name=new_group,
554 group_name=new_group,
555 perm='repository.read')
555 perm='repository.read')
556 meta.Session().commit()
556 meta.Session().commit()
557 id_, params = _build_data(self.apikey, 'get_repo',
557 id_, params = _build_data(self.apikey, 'get_repo',
558 repoid=self.REPO)
558 repoid=self.REPO)
559 response = api_call(self, params)
559 response = api_call(self, params)
560 assert "tags" not in response.json['result']
560 assert "tags" not in response.json['result']
561 assert 'pull_requests' not in response.json['result']
561 assert 'pull_requests' not in response.json['result']
562
562
563 repo = RepoModel().get_by_repo_name(self.REPO)
563 repo = RepoModel().get_by_repo_name(self.REPO)
564 ret = repo.get_api_data()
564 ret = repo.get_api_data()
565
565
566 members = []
566 members = []
567 followers = []
567 followers = []
568 for user in repo.repo_to_perm:
568 for user in repo.repo_to_perm:
569 perm = user.permission.permission_name
569 perm = user.permission.permission_name
570 user = user.user
570 user = user.user
571 user_data = {'name': user.username, 'type': "user",
571 user_data = {'name': user.username, 'type': "user",
572 'permission': perm}
572 'permission': perm}
573 members.append(user_data)
573 members.append(user_data)
574
574
575 for user_group in repo.users_group_to_perm:
575 for user_group in repo.users_group_to_perm:
576 perm = user_group.permission.permission_name
576 perm = user_group.permission.permission_name
577 user_group = user_group.users_group
577 user_group = user_group.users_group
578 user_group_data = {'name': user_group.users_group_name,
578 user_group_data = {'name': user_group.users_group_name,
579 'type': "user_group", 'permission': perm}
579 'type': "user_group", 'permission': perm}
580 members.append(user_group_data)
580 members.append(user_group_data)
581
581
582 for user in repo.followers:
582 for user in repo.followers:
583 followers.append(user.user.get_api_data())
583 followers.append(user.user.get_api_data())
584
584
585 ret['members'] = members
585 ret['members'] = members
586 ret['followers'] = followers
586 ret['followers'] = followers
587
587
588 expected = ret
588 expected = ret
589 self._compare_ok(id_, expected, given=response.body)
589 self._compare_ok(id_, expected, given=response.body)
590 fixture.destroy_user_group(new_group)
590 fixture.destroy_user_group(new_group)
591
591
592 id_, params = _build_data(self.apikey, 'get_repo', repoid=self.REPO,
592 id_, params = _build_data(self.apikey, 'get_repo', repoid=self.REPO,
593 with_revision_names=True,
593 with_revision_names=True,
594 with_pullrequests=True)
594 with_pullrequests=True)
595 response = api_call(self, params)
595 response = api_call(self, params)
596 assert "v0.2.0" in response.json['result']['tags']
596 assert "v0.2.0" in response.json['result']['tags']
597 assert 'pull_requests' in response.json['result']
597 assert 'pull_requests' in response.json['result']
598
598
599 @base.parametrize('grant_perm', [
599 @base.parametrize('grant_perm', [
600 ('repository.admin'),
600 ('repository.admin'),
601 ('repository.write'),
601 ('repository.write'),
602 ('repository.read'),
602 ('repository.read'),
603 ])
603 ])
604 def test_api_get_repo_by_non_admin(self, grant_perm):
604 def test_api_get_repo_by_non_admin(self, grant_perm):
605 RepoModel().grant_user_permission(repo=self.REPO,
605 RepoModel().grant_user_permission(repo=self.REPO,
606 user=self.TEST_USER_LOGIN,
606 user=self.TEST_USER_LOGIN,
607 perm=grant_perm)
607 perm=grant_perm)
608 meta.Session().commit()
608 meta.Session().commit()
609 id_, params = _build_data(self.apikey_regular, 'get_repo',
609 id_, params = _build_data(self.apikey_regular, 'get_repo',
610 repoid=self.REPO)
610 repoid=self.REPO)
611 response = api_call(self, params)
611 response = api_call(self, params)
612
612
613 repo = RepoModel().get_by_repo_name(self.REPO)
613 repo = RepoModel().get_by_repo_name(self.REPO)
614 assert len(repo.repo_to_perm) >= 2 # make sure we actually are testing something - probably the default 2 permissions, possibly more
614 assert len(repo.repo_to_perm) >= 2 # make sure we actually are testing something - probably the default 2 permissions, possibly more
615
615
616 expected = repo.get_api_data()
616 expected = repo.get_api_data()
617
617
618 members = []
618 members = []
619 for user in repo.repo_to_perm:
619 for user in repo.repo_to_perm:
620 perm = user.permission.permission_name
620 perm = user.permission.permission_name
621 user_obj = user.user
621 user_obj = user.user
622 user_data = {'name': user_obj.username, 'type': "user",
622 user_data = {'name': user_obj.username, 'type': "user",
623 'permission': perm}
623 'permission': perm}
624 members.append(user_data)
624 members.append(user_data)
625 for user_group in repo.users_group_to_perm:
625 for user_group in repo.users_group_to_perm:
626 perm = user_group.permission.permission_name
626 perm = user_group.permission.permission_name
627 user_group_obj = user_group.users_group
627 user_group_obj = user_group.users_group
628 user_group_data = {'name': user_group_obj.users_group_name,
628 user_group_data = {'name': user_group_obj.users_group_name,
629 'type': "user_group", 'permission': perm}
629 'type': "user_group", 'permission': perm}
630 members.append(user_group_data)
630 members.append(user_group_data)
631 expected['members'] = members
631 expected['members'] = members
632
632
633 followers = []
633 followers = []
634
634
635 for user in repo.followers:
635 for user in repo.followers:
636 followers.append(user.user.get_api_data())
636 followers.append(user.user.get_api_data())
637
637
638 expected['followers'] = followers
638 expected['followers'] = followers
639
639
640 try:
640 try:
641 self._compare_ok(id_, expected, given=response.body)
641 self._compare_ok(id_, expected, given=response.body)
642 finally:
642 finally:
643 RepoModel().revoke_user_permission(self.REPO, self.TEST_USER_LOGIN)
643 RepoModel().revoke_user_permission(self.REPO, self.TEST_USER_LOGIN)
644
644
645 def test_api_get_repo_by_non_admin_no_permission_to_repo(self):
645 def test_api_get_repo_by_non_admin_no_permission_to_repo(self):
646 RepoModel().grant_user_permission(repo=self.REPO,
646 RepoModel().grant_user_permission(repo=self.REPO,
647 user=db.User.DEFAULT_USER_NAME,
647 user=db.User.DEFAULT_USER_NAME,
648 perm='repository.none')
648 perm='repository.none')
649 try:
649 try:
650 RepoModel().grant_user_permission(repo=self.REPO,
650 RepoModel().grant_user_permission(repo=self.REPO,
651 user=self.TEST_USER_LOGIN,
651 user=self.TEST_USER_LOGIN,
652 perm='repository.none')
652 perm='repository.none')
653
653
654 id_, params = _build_data(self.apikey_regular, 'get_repo',
654 id_, params = _build_data(self.apikey_regular, 'get_repo',
655 repoid=self.REPO)
655 repoid=self.REPO)
656 response = api_call(self, params)
656 response = api_call(self, params)
657
657
658 expected = 'repository `%s` does not exist' % (self.REPO)
658 expected = 'repository `%s` does not exist' % (self.REPO)
659 self._compare_error(id_, expected, given=response.body)
659 self._compare_error(id_, expected, given=response.body)
660 finally:
660 finally:
661 RepoModel().grant_user_permission(repo=self.REPO,
661 RepoModel().grant_user_permission(repo=self.REPO,
662 user=db.User.DEFAULT_USER_NAME,
662 user=db.User.DEFAULT_USER_NAME,
663 perm='repository.read')
663 perm='repository.read')
664
664
665 def test_api_get_repo_that_doesn_not_exist(self):
665 def test_api_get_repo_that_doesn_not_exist(self):
666 id_, params = _build_data(self.apikey, 'get_repo',
666 id_, params = _build_data(self.apikey, 'get_repo',
667 repoid='no-such-repo')
667 repoid='no-such-repo')
668 response = api_call(self, params)
668 response = api_call(self, params)
669
669
670 ret = 'repository `%s` does not exist' % 'no-such-repo'
670 ret = 'repository `%s` does not exist' % 'no-such-repo'
671 expected = ret
671 expected = ret
672 self._compare_error(id_, expected, given=response.body)
672 self._compare_error(id_, expected, given=response.body)
673
673
674 def test_api_get_repos(self):
674 def test_api_get_repos(self):
675 id_, params = _build_data(self.apikey, 'get_repos')
675 id_, params = _build_data(self.apikey, 'get_repos')
676 response = api_call(self, params)
676 response = api_call(self, params)
677
677
678 expected = jsonify([
678 expected = jsonify([
679 repo.get_api_data()
679 repo.get_api_data()
680 for repo in db.Repository.query()
680 for repo in db.Repository.query()
681 ])
681 ])
682
682
683 self._compare_ok(id_, expected, given=response.body)
683 self._compare_ok(id_, expected, given=response.body)
684
684
685 def test_api_get_repos_non_admin(self):
685 def test_api_get_repos_non_admin(self):
686 id_, params = _build_data(self.apikey_regular, 'get_repos')
686 id_, params = _build_data(self.apikey_regular, 'get_repos')
687 response = api_call(self, params)
687 response = api_call(self, params)
688
688
689 expected = jsonify([
689 expected = jsonify([
690 repo.get_api_data()
690 repo.get_api_data()
691 for repo in RepoModel().get_all_user_repos(self.TEST_USER_LOGIN)
691 for repo in RepoModel().get_all_user_repos(self.TEST_USER_LOGIN)
692 ])
692 ])
693
693
694 self._compare_ok(id_, expected, given=response.body)
694 self._compare_ok(id_, expected, given=response.body)
695
695
696 @base.parametrize('name,ret_type', [
696 @base.parametrize('name,ret_type', [
697 ('all', 'all'),
697 ('all', 'all'),
698 ('dirs', 'dirs'),
698 ('dirs', 'dirs'),
699 ('files', 'files'),
699 ('files', 'files'),
700 ])
700 ])
701 def test_api_get_repo_nodes(self, name, ret_type):
701 def test_api_get_repo_nodes(self, name, ret_type):
702 rev = 'tip'
702 rev = 'tip'
703 path = '/'
703 path = '/'
704 id_, params = _build_data(self.apikey, 'get_repo_nodes',
704 id_, params = _build_data(self.apikey, 'get_repo_nodes',
705 repoid=self.REPO, revision=rev,
705 repoid=self.REPO, revision=rev,
706 root_path=path,
706 root_path=path,
707 ret_type=ret_type)
707 ret_type=ret_type)
708 response = api_call(self, params)
708 response = api_call(self, params)
709
709
710 # we don't the actual return types here since it's tested somewhere
710 # we don't the actual return types here since it's tested somewhere
711 # else
711 # else
712 expected = response.json['result']
712 expected = response.json['result']
713 self._compare_ok(id_, expected, given=response.body)
713 self._compare_ok(id_, expected, given=response.body)
714
714
715 def test_api_get_repo_nodes_bad_revisions(self):
715 def test_api_get_repo_nodes_bad_revisions(self):
716 rev = 'i-dont-exist'
716 rev = 'i-dont-exist'
717 path = '/'
717 path = '/'
718 id_, params = _build_data(self.apikey, 'get_repo_nodes',
718 id_, params = _build_data(self.apikey, 'get_repo_nodes',
719 repoid=self.REPO, revision=rev,
719 repoid=self.REPO, revision=rev,
720 root_path=path, )
720 root_path=path, )
721 response = api_call(self, params)
721 response = api_call(self, params)
722
722
723 expected = 'failed to get repo: `%s` nodes' % self.REPO
723 expected = 'failed to get repo: `%s` nodes' % self.REPO
724 self._compare_error(id_, expected, given=response.body)
724 self._compare_error(id_, expected, given=response.body)
725
725
726 def test_api_get_repo_nodes_bad_path(self):
726 def test_api_get_repo_nodes_bad_path(self):
727 rev = 'tip'
727 rev = 'tip'
728 path = '/idontexits'
728 path = '/idontexits'
729 id_, params = _build_data(self.apikey, 'get_repo_nodes',
729 id_, params = _build_data(self.apikey, 'get_repo_nodes',
730 repoid=self.REPO, revision=rev,
730 repoid=self.REPO, revision=rev,
731 root_path=path, )
731 root_path=path, )
732 response = api_call(self, params)
732 response = api_call(self, params)
733
733
734 expected = 'failed to get repo: `%s` nodes' % self.REPO
734 expected = 'failed to get repo: `%s` nodes' % self.REPO
735 self._compare_error(id_, expected, given=response.body)
735 self._compare_error(id_, expected, given=response.body)
736
736
737 def test_api_get_repo_nodes_bad_ret_type(self):
737 def test_api_get_repo_nodes_bad_ret_type(self):
738 rev = 'tip'
738 rev = 'tip'
739 path = '/'
739 path = '/'
740 ret_type = 'error'
740 ret_type = 'error'
741 id_, params = _build_data(self.apikey, 'get_repo_nodes',
741 id_, params = _build_data(self.apikey, 'get_repo_nodes',
742 repoid=self.REPO, revision=rev,
742 repoid=self.REPO, revision=rev,
743 root_path=path,
743 root_path=path,
744 ret_type=ret_type)
744 ret_type=ret_type)
745 response = api_call(self, params)
745 response = api_call(self, params)
746
746
747 expected = ('ret_type must be one of %s'
747 expected = ('ret_type must be one of %s'
748 % (','.join(sorted(['files', 'dirs', 'all']))))
748 % (','.join(sorted(['files', 'dirs', 'all']))))
749 self._compare_error(id_, expected, given=response.body)
749 self._compare_error(id_, expected, given=response.body)
750
750
751 @base.parametrize('name,ret_type,grant_perm', [
751 @base.parametrize('name,ret_type,grant_perm', [
752 ('all', 'all', 'repository.write'),
752 ('all', 'all', 'repository.write'),
753 ('dirs', 'dirs', 'repository.admin'),
753 ('dirs', 'dirs', 'repository.admin'),
754 ('files', 'files', 'repository.read'),
754 ('files', 'files', 'repository.read'),
755 ])
755 ])
756 def test_api_get_repo_nodes_by_regular_user(self, name, ret_type, grant_perm):
756 def test_api_get_repo_nodes_by_regular_user(self, name, ret_type, grant_perm):
757 RepoModel().grant_user_permission(repo=self.REPO,
757 RepoModel().grant_user_permission(repo=self.REPO,
758 user=self.TEST_USER_LOGIN,
758 user=self.TEST_USER_LOGIN,
759 perm=grant_perm)
759 perm=grant_perm)
760 meta.Session().commit()
760 meta.Session().commit()
761
761
762 rev = 'tip'
762 rev = 'tip'
763 path = '/'
763 path = '/'
764 id_, params = _build_data(self.apikey_regular, 'get_repo_nodes',
764 id_, params = _build_data(self.apikey_regular, 'get_repo_nodes',
765 repoid=self.REPO, revision=rev,
765 repoid=self.REPO, revision=rev,
766 root_path=path,
766 root_path=path,
767 ret_type=ret_type)
767 ret_type=ret_type)
768 response = api_call(self, params)
768 response = api_call(self, params)
769
769
770 # we don't the actual return types here since it's tested somewhere
770 # we don't the actual return types here since it's tested somewhere
771 # else
771 # else
772 expected = response.json['result']
772 expected = response.json['result']
773 try:
773 try:
774 self._compare_ok(id_, expected, given=response.body)
774 self._compare_ok(id_, expected, given=response.body)
775 finally:
775 finally:
776 RepoModel().revoke_user_permission(self.REPO, self.TEST_USER_LOGIN)
776 RepoModel().revoke_user_permission(self.REPO, self.TEST_USER_LOGIN)
777
777
778 def test_api_create_repo(self):
778 def test_api_create_repo(self):
779 repo_name = 'api-repo'
779 repo_name = 'api-repo'
780 id_, params = _build_data(self.apikey, 'create_repo',
780 id_, params = _build_data(self.apikey, 'create_repo',
781 repo_name=repo_name,
781 repo_name=repo_name,
782 owner=base.TEST_USER_ADMIN_LOGIN,
782 owner=base.TEST_USER_ADMIN_LOGIN,
783 repo_type=self.REPO_TYPE,
783 repo_type=self.REPO_TYPE,
784 )
784 )
785 response = api_call(self, params)
785 response = api_call(self, params)
786
786
787 repo = RepoModel().get_by_repo_name(repo_name)
787 repo = RepoModel().get_by_repo_name(repo_name)
788 assert repo is not None
788 assert repo is not None
789 ret = {
789 ret = {
790 'msg': 'Created new repository `%s`' % repo_name,
790 'msg': 'Created new repository `%s`' % repo_name,
791 'success': True,
791 'success': True,
792 'task': None,
792 'task': None,
793 }
793 }
794 expected = ret
794 expected = ret
795 self._compare_ok(id_, expected, given=response.body)
795 self._compare_ok(id_, expected, given=response.body)
796 fixture.destroy_repo(repo_name)
796 fixture.destroy_repo(repo_name)
797
797
798 @base.parametrize('repo_name', [
798 @base.parametrize('repo_name', [
799 '',
799 '',
800 '.',
800 '.',
801 '..',
801 '..',
802 ':',
802 ':',
803 '/',
803 '/',
804 '<test>',
804 '<test>',
805 ])
805 ])
806 def test_api_create_repo_bad_names(self, repo_name):
806 def test_api_create_repo_bad_names(self, repo_name):
807 id_, params = _build_data(self.apikey, 'create_repo',
807 id_, params = _build_data(self.apikey, 'create_repo',
808 repo_name=repo_name,
808 repo_name=repo_name,
809 owner=base.TEST_USER_ADMIN_LOGIN,
809 owner=base.TEST_USER_ADMIN_LOGIN,
810 repo_type=self.REPO_TYPE,
810 repo_type=self.REPO_TYPE,
811 )
811 )
812 response = api_call(self, params)
812 response = api_call(self, params)
813 if repo_name == '/':
813 if repo_name == '/':
814 expected = "repo group `` not found"
814 expected = "repo group `` not found"
815 self._compare_error(id_, expected, given=response.body)
815 self._compare_error(id_, expected, given=response.body)
816 else:
816 else:
817 expected = "failed to create repository `%s`" % repo_name
817 expected = "failed to create repository `%s`" % repo_name
818 self._compare_error(id_, expected, given=response.body)
818 self._compare_error(id_, expected, given=response.body)
819 fixture.destroy_repo(repo_name)
819 fixture.destroy_repo(repo_name)
820
820
821 def test_api_create_repo_clone_uri_local(self):
821 def test_api_create_repo_clone_uri_local(self):
822 # cloning from local repos was a mis-feature - it would bypass access control
822 # cloning from local repos was a mis-feature - it would bypass access control
823 # TODO: introduce other test coverage of actual remote cloning
823 # TODO: introduce other test coverage of actual remote cloning
824 clone_uri = os.path.join(base.TESTS_TMP_PATH, self.REPO)
824 clone_uri = os.path.join(base.TESTS_TMP_PATH, self.REPO)
825 repo_name = 'api-repo'
825 repo_name = 'api-repo'
826 id_, params = _build_data(self.apikey, 'create_repo',
826 id_, params = _build_data(self.apikey, 'create_repo',
827 repo_name=repo_name,
827 repo_name=repo_name,
828 owner=base.TEST_USER_ADMIN_LOGIN,
828 owner=base.TEST_USER_ADMIN_LOGIN,
829 repo_type=self.REPO_TYPE,
829 repo_type=self.REPO_TYPE,
830 clone_uri=clone_uri,
830 clone_uri=clone_uri,
831 )
831 )
832 response = api_call(self, params)
832 response = api_call(self, params)
833 expected = "failed to create repository `%s`" % repo_name
833 expected = "failed to create repository `%s`" % repo_name
834 self._compare_error(id_, expected, given=response.body)
834 self._compare_error(id_, expected, given=response.body)
835 fixture.destroy_repo(repo_name)
835 fixture.destroy_repo(repo_name)
836
836
837 def test_api_create_repo_and_repo_group(self):
837 def test_api_create_repo_and_repo_group(self):
838 repo_group_name = 'my_gr'
838 repo_group_name = 'my_gr'
839 repo_name = '%s/api-repo' % repo_group_name
839 repo_name = '%s/api-repo' % repo_group_name
840
840
841 # repo creation can no longer also create repo group
841 # repo creation can no longer also create repo group
842 id_, params = _build_data(self.apikey, 'create_repo',
842 id_, params = _build_data(self.apikey, 'create_repo',
843 repo_name=repo_name,
843 repo_name=repo_name,
844 owner=base.TEST_USER_ADMIN_LOGIN,
844 owner=base.TEST_USER_ADMIN_LOGIN,
845 repo_type=self.REPO_TYPE,)
845 repo_type=self.REPO_TYPE,)
846 response = api_call(self, params)
846 response = api_call(self, params)
847 expected = 'repo group `%s` not found' % repo_group_name
847 expected = 'repo group `%s` not found' % repo_group_name
848 self._compare_error(id_, expected, given=response.body)
848 self._compare_error(id_, expected, given=response.body)
849 assert RepoModel().get_by_repo_name(repo_name) is None
849 assert RepoModel().get_by_repo_name(repo_name) is None
850
850
851 # create group before creating repo
851 # create group before creating repo
852 rg = fixture.create_repo_group(repo_group_name)
852 rg = fixture.create_repo_group(repo_group_name)
853 meta.Session().commit()
853 meta.Session().commit()
854
854
855 id_, params = _build_data(self.apikey, 'create_repo',
855 id_, params = _build_data(self.apikey, 'create_repo',
856 repo_name=repo_name,
856 repo_name=repo_name,
857 owner=base.TEST_USER_ADMIN_LOGIN,
857 owner=base.TEST_USER_ADMIN_LOGIN,
858 repo_type=self.REPO_TYPE,)
858 repo_type=self.REPO_TYPE,)
859 response = api_call(self, params)
859 response = api_call(self, params)
860 expected = {
860 expected = {
861 'msg': 'Created new repository `%s`' % repo_name,
861 'msg': 'Created new repository `%s`' % repo_name,
862 'success': True,
862 'success': True,
863 'task': None,
863 'task': None,
864 }
864 }
865 self._compare_ok(id_, expected, given=response.body)
865 self._compare_ok(id_, expected, given=response.body)
866 repo = RepoModel().get_by_repo_name(repo_name)
866 repo = RepoModel().get_by_repo_name(repo_name)
867 assert repo is not None
867 assert repo is not None
868
868
869 fixture.destroy_repo(repo_name)
869 fixture.destroy_repo(repo_name)
870 fixture.destroy_repo_group(repo_group_name)
870 fixture.destroy_repo_group(repo_group_name)
871
871
872 def test_api_create_repo_in_repo_group_without_permission(self):
872 def test_api_create_repo_in_repo_group_without_permission(self):
873 repo_group_basename = 'api-repo-repo'
873 repo_group_basename = 'api-repo-repo'
874 repo_group_name = '%s/%s' % (TEST_REPO_GROUP, repo_group_basename)
874 repo_group_name = '%s/%s' % (TEST_REPO_GROUP, repo_group_basename)
875 repo_name = '%s/api-repo' % repo_group_name
875 repo_name = '%s/api-repo' % repo_group_name
876
876
877 top_group = db.RepoGroup.get_by_group_name(TEST_REPO_GROUP)
877 top_group = db.RepoGroup.get_by_group_name(TEST_REPO_GROUP)
878 assert top_group
878 assert top_group
879 rg = fixture.create_repo_group(repo_group_basename, parent_group_id=top_group)
879 rg = fixture.create_repo_group(repo_group_basename, parent_group_id=top_group)
880 meta.Session().commit()
880 meta.Session().commit()
881 RepoGroupModel().grant_user_permission(repo_group_name,
881 RepoGroupModel().grant_user_permission(repo_group_name,
882 self.TEST_USER_LOGIN,
882 self.TEST_USER_LOGIN,
883 'group.none')
883 'group.none')
884 meta.Session().commit()
884 meta.Session().commit()
885
885
886 id_, params = _build_data(self.apikey_regular, 'create_repo',
886 id_, params = _build_data(self.apikey_regular, 'create_repo',
887 repo_name=repo_name,
887 repo_name=repo_name,
888 repo_type=self.REPO_TYPE,
888 repo_type=self.REPO_TYPE,
889 )
889 )
890 response = api_call(self, params)
890 response = api_call(self, params)
891
891
892 # Current result when API access control is different from Web:
892 # Current result when API access control is different from Web:
893 ret = {
893 ret = {
894 'msg': 'Created new repository `%s`' % repo_name,
894 'msg': 'Created new repository `%s`' % repo_name,
895 'success': True,
895 'success': True,
896 'task': None,
896 'task': None,
897 }
897 }
898 expected = ret
898 expected = ret
899 self._compare_ok(id_, expected, given=response.body)
899 self._compare_ok(id_, expected, given=response.body)
900 fixture.destroy_repo(repo_name)
900 fixture.destroy_repo(repo_name)
901
901
902 # Expected and arguably more correct result:
902 # Expected and arguably more correct result:
903 #expected = 'failed to create repository `%s`' % repo_name
903 #expected = 'failed to create repository `%s`' % repo_name
904 #self._compare_error(id_, expected, given=response.body)
904 #self._compare_error(id_, expected, given=response.body)
905
905
906 fixture.destroy_repo_group(repo_group_name)
906 fixture.destroy_repo_group(repo_group_name)
907
907
908 def test_api_create_repo_unknown_owner(self):
908 def test_api_create_repo_unknown_owner(self):
909 repo_name = 'api-repo'
909 repo_name = 'api-repo'
910 owner = 'i-dont-exist'
910 owner = 'i-dont-exist'
911 id_, params = _build_data(self.apikey, 'create_repo',
911 id_, params = _build_data(self.apikey, 'create_repo',
912 repo_name=repo_name,
912 repo_name=repo_name,
913 owner=owner,
913 owner=owner,
914 repo_type=self.REPO_TYPE,
914 repo_type=self.REPO_TYPE,
915 )
915 )
916 response = api_call(self, params)
916 response = api_call(self, params)
917 expected = 'user `%s` does not exist' % owner
917 expected = 'user `%s` does not exist' % owner
918 self._compare_error(id_, expected, given=response.body)
918 self._compare_error(id_, expected, given=response.body)
919
919
920 def test_api_create_repo_dont_specify_owner(self):
920 def test_api_create_repo_dont_specify_owner(self):
921 repo_name = 'api-repo'
921 repo_name = 'api-repo'
922 owner = 'i-dont-exist'
922 owner = 'i-dont-exist'
923 id_, params = _build_data(self.apikey, 'create_repo',
923 id_, params = _build_data(self.apikey, 'create_repo',
924 repo_name=repo_name,
924 repo_name=repo_name,
925 repo_type=self.REPO_TYPE,
925 repo_type=self.REPO_TYPE,
926 )
926 )
927 response = api_call(self, params)
927 response = api_call(self, params)
928
928
929 repo = RepoModel().get_by_repo_name(repo_name)
929 repo = RepoModel().get_by_repo_name(repo_name)
930 assert repo is not None
930 assert repo is not None
931 ret = {
931 ret = {
932 'msg': 'Created new repository `%s`' % repo_name,
932 'msg': 'Created new repository `%s`' % repo_name,
933 'success': True,
933 'success': True,
934 'task': None,
934 'task': None,
935 }
935 }
936 expected = ret
936 expected = ret
937 self._compare_ok(id_, expected, given=response.body)
937 self._compare_ok(id_, expected, given=response.body)
938 fixture.destroy_repo(repo_name)
938 fixture.destroy_repo(repo_name)
939
939
940 def test_api_create_repo_by_non_admin(self):
940 def test_api_create_repo_by_non_admin(self):
941 repo_name = 'api-repo'
941 repo_name = 'api-repo'
942 owner = 'i-dont-exist'
942 owner = 'i-dont-exist'
943 id_, params = _build_data(self.apikey_regular, 'create_repo',
943 id_, params = _build_data(self.apikey_regular, 'create_repo',
944 repo_name=repo_name,
944 repo_name=repo_name,
945 repo_type=self.REPO_TYPE,
945 repo_type=self.REPO_TYPE,
946 )
946 )
947 response = api_call(self, params)
947 response = api_call(self, params)
948
948
949 repo = RepoModel().get_by_repo_name(repo_name)
949 repo = RepoModel().get_by_repo_name(repo_name)
950 assert repo is not None
950 assert repo is not None
951 ret = {
951 ret = {
952 'msg': 'Created new repository `%s`' % repo_name,
952 'msg': 'Created new repository `%s`' % repo_name,
953 'success': True,
953 'success': True,
954 'task': None,
954 'task': None,
955 }
955 }
956 expected = ret
956 expected = ret
957 self._compare_ok(id_, expected, given=response.body)
957 self._compare_ok(id_, expected, given=response.body)
958 fixture.destroy_repo(repo_name)
958 fixture.destroy_repo(repo_name)
959
959
960 def test_api_create_repo_by_non_admin_specify_owner(self):
960 def test_api_create_repo_by_non_admin_specify_owner(self):
961 repo_name = 'api-repo'
961 repo_name = 'api-repo'
962 owner = 'i-dont-exist'
962 owner = 'i-dont-exist'
963 id_, params = _build_data(self.apikey_regular, 'create_repo',
963 id_, params = _build_data(self.apikey_regular, 'create_repo',
964 repo_name=repo_name,
964 repo_name=repo_name,
965 repo_type=self.REPO_TYPE,
965 repo_type=self.REPO_TYPE,
966 owner=owner)
966 owner=owner)
967 response = api_call(self, params)
967 response = api_call(self, params)
968
968
969 expected = 'Only Kallithea admin can specify `owner` param'
969 expected = 'Only Kallithea admin can specify `owner` param'
970 self._compare_error(id_, expected, given=response.body)
970 self._compare_error(id_, expected, given=response.body)
971 fixture.destroy_repo(repo_name)
971 fixture.destroy_repo(repo_name)
972
972
973 def test_api_create_repo_exists(self):
973 def test_api_create_repo_exists(self):
974 repo_name = self.REPO
974 repo_name = self.REPO
975 id_, params = _build_data(self.apikey, 'create_repo',
975 id_, params = _build_data(self.apikey, 'create_repo',
976 repo_name=repo_name,
976 repo_name=repo_name,
977 owner=base.TEST_USER_ADMIN_LOGIN,
977 owner=base.TEST_USER_ADMIN_LOGIN,
978 repo_type=self.REPO_TYPE,)
978 repo_type=self.REPO_TYPE,)
979 response = api_call(self, params)
979 response = api_call(self, params)
980 expected = "repo `%s` already exist" % repo_name
980 expected = "repo `%s` already exist" % repo_name
981 self._compare_error(id_, expected, given=response.body)
981 self._compare_error(id_, expected, given=response.body)
982
982
983 def test_api_create_repo_dot_dot(self):
983 def test_api_create_repo_dot_dot(self):
984 # it is only possible to create repositories in existing repo groups - and '..' can't be used
984 # it is only possible to create repositories in existing repo groups - and '..' can't be used
985 group_name = '%s/..' % TEST_REPO_GROUP
985 group_name = '%s/..' % TEST_REPO_GROUP
986 repo_name = '%s/%s' % (group_name, 'could-be-outside')
986 repo_name = '%s/%s' % (group_name, 'could-be-outside')
987 id_, params = _build_data(self.apikey, 'create_repo',
987 id_, params = _build_data(self.apikey, 'create_repo',
988 repo_name=repo_name,
988 repo_name=repo_name,
989 owner=base.TEST_USER_ADMIN_LOGIN,
989 owner=base.TEST_USER_ADMIN_LOGIN,
990 repo_type=self.REPO_TYPE,)
990 repo_type=self.REPO_TYPE,)
991 response = api_call(self, params)
991 response = api_call(self, params)
992 expected = 'repo group `%s` not found' % group_name
992 expected = 'repo group `%s` not found' % group_name
993 self._compare_error(id_, expected, given=response.body)
993 self._compare_error(id_, expected, given=response.body)
994 fixture.destroy_repo(repo_name)
994 fixture.destroy_repo(repo_name)
995
995
996 @mock.patch.object(RepoModel, 'create', raise_exception)
996 @mock.patch.object(RepoModel, 'create', raise_exception)
997 def test_api_create_repo_exception_occurred(self):
997 def test_api_create_repo_exception_occurred(self):
998 repo_name = 'api-repo'
998 repo_name = 'api-repo'
999 id_, params = _build_data(self.apikey, 'create_repo',
999 id_, params = _build_data(self.apikey, 'create_repo',
1000 repo_name=repo_name,
1000 repo_name=repo_name,
1001 owner=base.TEST_USER_ADMIN_LOGIN,
1001 owner=base.TEST_USER_ADMIN_LOGIN,
1002 repo_type=self.REPO_TYPE,)
1002 repo_type=self.REPO_TYPE,)
1003 response = api_call(self, params)
1003 response = api_call(self, params)
1004 expected = 'failed to create repository `%s`' % repo_name
1004 expected = 'failed to create repository `%s`' % repo_name
1005 self._compare_error(id_, expected, given=response.body)
1005 self._compare_error(id_, expected, given=response.body)
1006
1006
1007 @base.parametrize('changing_attr,updates', [
1007 @base.parametrize('changing_attr,updates', [
1008 ('owner', {'owner': base.TEST_USER_REGULAR_LOGIN}),
1008 ('owner', {'owner': base.TEST_USER_REGULAR_LOGIN}),
1009 ('description', {'description': 'new description'}),
1009 ('description', {'description': 'new description'}),
1010 ('clone_uri', {'clone_uri': 'http://example.com/repo'}), # will fail - pulling from non-existing repo should fail
1010 ('clone_uri', {'clone_uri': 'http://example.com/repo'}), # will fail - pulling from non-existing repo should fail
1011 ('clone_uri', {'clone_uri': '/repo'}), # will fail - pulling from local repo was a mis-feature - it would bypass access control
1011 ('clone_uri', {'clone_uri': '/repo'}), # will fail - pulling from local repo was a mis-feature - it would bypass access control
1012 ('clone_uri', {'clone_uri': None}),
1012 ('clone_uri', {'clone_uri': None}),
1013 ('landing_rev', {'landing_rev': 'branch:master'}),
1013 ('landing_rev', {'landing_rev': 'branch:master'}),
1014 ('enable_statistics', {'enable_statistics': True}),
1014 ('enable_statistics', {'enable_statistics': True}),
1015 ('enable_downloads', {'enable_downloads': True}),
1015 ('enable_downloads', {'enable_downloads': True}),
1016 ('name', {'name': 'new_repo_name'}),
1016 ('name', {'name': 'new_repo_name'}),
1017 ('repo_group', {'group': 'test_group_for_update'}),
1017 ('repo_group', {'group': 'test_group_for_update'}),
1018 ])
1018 ])
1019 def test_api_update_repo(self, changing_attr, updates):
1019 def test_api_update_repo(self, changing_attr, updates):
1020 repo_name = 'api_update_me'
1020 repo_name = 'api_update_me'
1021 repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1021 repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1022 if changing_attr == 'repo_group':
1022 if changing_attr == 'repo_group':
1023 fixture.create_repo_group(updates['group'])
1023 fixture.create_repo_group(updates['group'])
1024
1024
1025 id_, params = _build_data(self.apikey, 'update_repo',
1025 id_, params = _build_data(self.apikey, 'update_repo',
1026 repoid=repo_name, **updates)
1026 repoid=repo_name, **updates)
1027 response = api_call(self, params)
1027 response = api_call(self, params)
1028 if changing_attr == 'name':
1028 if changing_attr == 'name':
1029 repo_name = updates['name']
1029 repo_name = updates['name']
1030 if changing_attr == 'repo_group':
1030 if changing_attr == 'repo_group':
1031 repo_name = '/'.join([updates['group'], repo_name])
1031 repo_name = '/'.join([updates['group'], repo_name])
1032 try:
1032 try:
1033 if changing_attr == 'clone_uri' and updates['clone_uri']:
1033 if changing_attr == 'clone_uri' and updates['clone_uri']:
1034 expected = 'failed to update repo `%s`' % repo_name
1034 expected = 'failed to update repo `%s`' % repo_name
1035 self._compare_error(id_, expected, given=response.body)
1035 self._compare_error(id_, expected, given=response.body)
1036 else:
1036 else:
1037 expected = {
1037 expected = {
1038 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
1038 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
1039 'repository': repo.get_api_data()
1039 'repository': repo.get_api_data()
1040 }
1040 }
1041 self._compare_ok(id_, expected, given=response.body)
1041 self._compare_ok(id_, expected, given=response.body)
1042 finally:
1042 finally:
1043 fixture.destroy_repo(repo_name)
1043 fixture.destroy_repo(repo_name)
1044 if changing_attr == 'repo_group':
1044 if changing_attr == 'repo_group':
1045 fixture.destroy_repo_group(updates['group'])
1045 fixture.destroy_repo_group(updates['group'])
1046
1046
1047 @base.parametrize('changing_attr,updates', [
1047 @base.parametrize('changing_attr,updates', [
1048 ('owner', {'owner': base.TEST_USER_REGULAR_LOGIN}),
1048 ('owner', {'owner': base.TEST_USER_REGULAR_LOGIN}),
1049 ('description', {'description': 'new description'}),
1049 ('description', {'description': 'new description'}),
1050 ('clone_uri', {'clone_uri': 'http://example.com/repo'}), # will fail - pulling from non-existing repo should fail
1050 ('clone_uri', {'clone_uri': 'http://example.com/repo'}), # will fail - pulling from non-existing repo should fail
1051 ('clone_uri', {'clone_uri': '/repo'}), # will fail - pulling from local repo was a mis-feature - it would bypass access control
1051 ('clone_uri', {'clone_uri': '/repo'}), # will fail - pulling from local repo was a mis-feature - it would bypass access control
1052 ('clone_uri', {'clone_uri': None}),
1052 ('clone_uri', {'clone_uri': None}),
1053 ('landing_rev', {'landing_rev': 'branch:master'}),
1053 ('landing_rev', {'landing_rev': 'branch:master'}),
1054 ('enable_statistics', {'enable_statistics': True}),
1054 ('enable_statistics', {'enable_statistics': True}),
1055 ('enable_downloads', {'enable_downloads': True}),
1055 ('enable_downloads', {'enable_downloads': True}),
1056 ('name', {'name': 'new_repo_name'}),
1056 ('name', {'name': 'new_repo_name'}),
1057 ('repo_group', {'group': 'test_group_for_update'}),
1057 ('repo_group', {'group': 'test_group_for_update'}),
1058 ])
1058 ])
1059 def test_api_update_group_repo(self, changing_attr, updates):
1059 def test_api_update_group_repo(self, changing_attr, updates):
1060 group_name = 'lololo'
1060 group_name = 'lololo'
1061 fixture.create_repo_group(group_name)
1061 fixture.create_repo_group(group_name)
1062 repo_name = '%s/api_update_me' % group_name
1062 repo_name = '%s/api_update_me' % group_name
1063 repo = fixture.create_repo(repo_name, repo_group=group_name, repo_type=self.REPO_TYPE)
1063 repo = fixture.create_repo(repo_name, repo_group=group_name, repo_type=self.REPO_TYPE)
1064 if changing_attr == 'repo_group':
1064 if changing_attr == 'repo_group':
1065 fixture.create_repo_group(updates['group'])
1065 fixture.create_repo_group(updates['group'])
1066
1066
1067 id_, params = _build_data(self.apikey, 'update_repo',
1067 id_, params = _build_data(self.apikey, 'update_repo',
1068 repoid=repo_name, **updates)
1068 repoid=repo_name, **updates)
1069 response = api_call(self, params)
1069 response = api_call(self, params)
1070 if changing_attr == 'name':
1070 if changing_attr == 'name':
1071 repo_name = '%s/%s' % (group_name, updates['name'])
1071 repo_name = '%s/%s' % (group_name, updates['name'])
1072 if changing_attr == 'repo_group':
1072 if changing_attr == 'repo_group':
1073 repo_name = '/'.join([updates['group'], repo_name.rsplit('/', 1)[-1]])
1073 repo_name = '/'.join([updates['group'], repo_name.rsplit('/', 1)[-1]])
1074 try:
1074 try:
1075 if changing_attr == 'clone_uri' and updates['clone_uri']:
1075 if changing_attr == 'clone_uri' and updates['clone_uri']:
1076 expected = 'failed to update repo `%s`' % repo_name
1076 expected = 'failed to update repo `%s`' % repo_name
1077 self._compare_error(id_, expected, given=response.body)
1077 self._compare_error(id_, expected, given=response.body)
1078 else:
1078 else:
1079 expected = {
1079 expected = {
1080 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
1080 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo_name),
1081 'repository': repo.get_api_data()
1081 'repository': repo.get_api_data()
1082 }
1082 }
1083 self._compare_ok(id_, expected, given=response.body)
1083 self._compare_ok(id_, expected, given=response.body)
1084 finally:
1084 finally:
1085 fixture.destroy_repo(repo_name)
1085 fixture.destroy_repo(repo_name)
1086 if changing_attr == 'repo_group':
1086 if changing_attr == 'repo_group':
1087 fixture.destroy_repo_group(updates['group'])
1087 fixture.destroy_repo_group(updates['group'])
1088 fixture.destroy_repo_group(group_name)
1088 fixture.destroy_repo_group(group_name)
1089
1089
1090 def test_api_update_repo_repo_group_does_not_exist(self):
1090 def test_api_update_repo_repo_group_does_not_exist(self):
1091 repo_name = 'admin_owned'
1091 repo_name = 'admin_owned'
1092 fixture.create_repo(repo_name)
1092 fixture.create_repo(repo_name)
1093 updates = {'group': 'test_group_for_update'}
1093 updates = {'group': 'test_group_for_update'}
1094 id_, params = _build_data(self.apikey, 'update_repo',
1094 id_, params = _build_data(self.apikey, 'update_repo',
1095 repoid=repo_name, **updates)
1095 repoid=repo_name, **updates)
1096 response = api_call(self, params)
1096 response = api_call(self, params)
1097 try:
1097 try:
1098 expected = 'repository group `%s` does not exist' % updates['group']
1098 expected = 'repository group `%s` does not exist' % updates['group']
1099 self._compare_error(id_, expected, given=response.body)
1099 self._compare_error(id_, expected, given=response.body)
1100 finally:
1100 finally:
1101 fixture.destroy_repo(repo_name)
1101 fixture.destroy_repo(repo_name)
1102
1102
1103 def test_api_update_repo_regular_user_not_allowed(self):
1103 def test_api_update_repo_regular_user_not_allowed(self):
1104 repo_name = 'admin_owned'
1104 repo_name = 'admin_owned'
1105 fixture.create_repo(repo_name)
1105 fixture.create_repo(repo_name)
1106 updates = {'description': 'something else'}
1106 updates = {'description': 'something else'}
1107 id_, params = _build_data(self.apikey_regular, 'update_repo',
1107 id_, params = _build_data(self.apikey_regular, 'update_repo',
1108 repoid=repo_name, **updates)
1108 repoid=repo_name, **updates)
1109 response = api_call(self, params)
1109 response = api_call(self, params)
1110 try:
1110 try:
1111 expected = 'repository `%s` does not exist' % repo_name
1111 expected = 'repository `%s` does not exist' % repo_name
1112 self._compare_error(id_, expected, given=response.body)
1112 self._compare_error(id_, expected, given=response.body)
1113 finally:
1113 finally:
1114 fixture.destroy_repo(repo_name)
1114 fixture.destroy_repo(repo_name)
1115
1115
1116 @mock.patch.object(RepoModel, 'update', raise_exception)
1116 @mock.patch.object(RepoModel, 'update', raise_exception)
1117 def test_api_update_repo_exception_occurred(self):
1117 def test_api_update_repo_exception_occurred(self):
1118 repo_name = 'api_update_me'
1118 repo_name = 'api_update_me'
1119 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1119 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1120 id_, params = _build_data(self.apikey, 'update_repo',
1120 id_, params = _build_data(self.apikey, 'update_repo',
1121 repoid=repo_name, owner=base.TEST_USER_ADMIN_LOGIN,)
1121 repoid=repo_name, owner=base.TEST_USER_ADMIN_LOGIN,)
1122 response = api_call(self, params)
1122 response = api_call(self, params)
1123 try:
1123 try:
1124 expected = 'failed to update repo `%s`' % repo_name
1124 expected = 'failed to update repo `%s`' % repo_name
1125 self._compare_error(id_, expected, given=response.body)
1125 self._compare_error(id_, expected, given=response.body)
1126 finally:
1126 finally:
1127 fixture.destroy_repo(repo_name)
1127 fixture.destroy_repo(repo_name)
1128
1128
1129 def test_api_update_repo_regular_user_change_repo_name(self):
1129 def test_api_update_repo_regular_user_change_repo_name(self):
1130 repo_name = 'admin_owned'
1130 repo_name = 'admin_owned'
1131 new_repo_name = 'new_repo_name'
1131 new_repo_name = 'new_repo_name'
1132 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1132 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1133 RepoModel().grant_user_permission(repo=repo_name,
1133 RepoModel().grant_user_permission(repo=repo_name,
1134 user=self.TEST_USER_LOGIN,
1134 user=self.TEST_USER_LOGIN,
1135 perm='repository.admin')
1135 perm='repository.admin')
1136 UserModel().revoke_perm('default', 'hg.create.repository')
1136 UserModel().revoke_perm('default', 'hg.create.repository')
1137 UserModel().grant_perm('default', 'hg.create.none')
1137 UserModel().grant_perm('default', 'hg.create.none')
1138 updates = {'name': new_repo_name}
1138 updates = {'name': new_repo_name}
1139 id_, params = _build_data(self.apikey_regular, 'update_repo',
1139 id_, params = _build_data(self.apikey_regular, 'update_repo',
1140 repoid=repo_name, **updates)
1140 repoid=repo_name, **updates)
1141 response = api_call(self, params)
1141 response = api_call(self, params)
1142 try:
1142 try:
1143 expected = 'no permission to create (or move) repositories'
1143 expected = 'no permission to create (or move) repositories'
1144 self._compare_error(id_, expected, given=response.body)
1144 self._compare_error(id_, expected, given=response.body)
1145 finally:
1145 finally:
1146 fixture.destroy_repo(repo_name)
1146 fixture.destroy_repo(repo_name)
1147 fixture.destroy_repo(new_repo_name)
1147 fixture.destroy_repo(new_repo_name)
1148
1148
1149 def test_api_update_repo_regular_user_change_repo_name_allowed(self):
1149 def test_api_update_repo_regular_user_change_repo_name_allowed(self):
1150 repo_name = 'admin_owned'
1150 repo_name = 'admin_owned'
1151 new_repo_name = 'new_repo_name'
1151 new_repo_name = 'new_repo_name'
1152 repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1152 repo = fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1153 RepoModel().grant_user_permission(repo=repo_name,
1153 RepoModel().grant_user_permission(repo=repo_name,
1154 user=self.TEST_USER_LOGIN,
1154 user=self.TEST_USER_LOGIN,
1155 perm='repository.admin')
1155 perm='repository.admin')
1156 UserModel().revoke_perm('default', 'hg.create.none')
1156 UserModel().revoke_perm('default', 'hg.create.none')
1157 UserModel().grant_perm('default', 'hg.create.repository')
1157 UserModel().grant_perm('default', 'hg.create.repository')
1158 updates = {'name': new_repo_name}
1158 updates = {'name': new_repo_name}
1159 id_, params = _build_data(self.apikey_regular, 'update_repo',
1159 id_, params = _build_data(self.apikey_regular, 'update_repo',
1160 repoid=repo_name, **updates)
1160 repoid=repo_name, **updates)
1161 response = api_call(self, params)
1161 response = api_call(self, params)
1162 try:
1162 try:
1163 expected = {
1163 expected = {
1164 'msg': 'updated repo ID:%s %s' % (repo.repo_id, new_repo_name),
1164 'msg': 'updated repo ID:%s %s' % (repo.repo_id, new_repo_name),
1165 'repository': repo.get_api_data()
1165 'repository': repo.get_api_data()
1166 }
1166 }
1167 self._compare_ok(id_, expected, given=response.body)
1167 self._compare_ok(id_, expected, given=response.body)
1168 finally:
1168 finally:
1169 fixture.destroy_repo(repo_name)
1169 fixture.destroy_repo(repo_name)
1170 fixture.destroy_repo(new_repo_name)
1170 fixture.destroy_repo(new_repo_name)
1171
1171
1172 def test_api_update_repo_regular_user_change_owner(self):
1172 def test_api_update_repo_regular_user_change_owner(self):
1173 repo_name = 'admin_owned'
1173 repo_name = 'admin_owned'
1174 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1174 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1175 RepoModel().grant_user_permission(repo=repo_name,
1175 RepoModel().grant_user_permission(repo=repo_name,
1176 user=self.TEST_USER_LOGIN,
1176 user=self.TEST_USER_LOGIN,
1177 perm='repository.admin')
1177 perm='repository.admin')
1178 updates = {'owner': base.TEST_USER_ADMIN_LOGIN}
1178 updates = {'owner': base.TEST_USER_ADMIN_LOGIN}
1179 id_, params = _build_data(self.apikey_regular, 'update_repo',
1179 id_, params = _build_data(self.apikey_regular, 'update_repo',
1180 repoid=repo_name, **updates)
1180 repoid=repo_name, **updates)
1181 response = api_call(self, params)
1181 response = api_call(self, params)
1182 try:
1182 try:
1183 expected = 'Only Kallithea admin can specify `owner` param'
1183 expected = 'Only Kallithea admin can specify `owner` param'
1184 self._compare_error(id_, expected, given=response.body)
1184 self._compare_error(id_, expected, given=response.body)
1185 finally:
1185 finally:
1186 fixture.destroy_repo(repo_name)
1186 fixture.destroy_repo(repo_name)
1187
1187
1188 def test_api_delete_repo(self):
1188 def test_api_delete_repo(self):
1189 repo_name = 'api_delete_me'
1189 repo_name = 'api_delete_me'
1190 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1190 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1191
1191
1192 id_, params = _build_data(self.apikey, 'delete_repo',
1192 id_, params = _build_data(self.apikey, 'delete_repo',
1193 repoid=repo_name, )
1193 repoid=repo_name, )
1194 response = api_call(self, params)
1194 response = api_call(self, params)
1195
1195
1196 ret = {
1196 ret = {
1197 'msg': 'Deleted repository `%s`' % repo_name,
1197 'msg': 'Deleted repository `%s`' % repo_name,
1198 'success': True
1198 'success': True
1199 }
1199 }
1200 try:
1200 try:
1201 expected = ret
1201 expected = ret
1202 self._compare_ok(id_, expected, given=response.body)
1202 self._compare_ok(id_, expected, given=response.body)
1203 finally:
1203 finally:
1204 fixture.destroy_repo(repo_name)
1204 fixture.destroy_repo(repo_name)
1205
1205
1206 def test_api_delete_repo_by_non_admin(self):
1206 def test_api_delete_repo_by_non_admin(self):
1207 repo_name = 'api_delete_me'
1207 repo_name = 'api_delete_me'
1208 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE,
1208 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE,
1209 cur_user=self.TEST_USER_LOGIN)
1209 cur_user=self.TEST_USER_LOGIN)
1210 id_, params = _build_data(self.apikey_regular, 'delete_repo',
1210 id_, params = _build_data(self.apikey_regular, 'delete_repo',
1211 repoid=repo_name, )
1211 repoid=repo_name, )
1212 response = api_call(self, params)
1212 response = api_call(self, params)
1213
1213
1214 ret = {
1214 ret = {
1215 'msg': 'Deleted repository `%s`' % repo_name,
1215 'msg': 'Deleted repository `%s`' % repo_name,
1216 'success': True
1216 'success': True
1217 }
1217 }
1218 try:
1218 try:
1219 expected = ret
1219 expected = ret
1220 self._compare_ok(id_, expected, given=response.body)
1220 self._compare_ok(id_, expected, given=response.body)
1221 finally:
1221 finally:
1222 fixture.destroy_repo(repo_name)
1222 fixture.destroy_repo(repo_name)
1223
1223
1224 def test_api_delete_repo_by_non_admin_no_permission(self):
1224 def test_api_delete_repo_by_non_admin_no_permission(self):
1225 repo_name = 'api_delete_me'
1225 repo_name = 'api_delete_me'
1226 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1226 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1227 try:
1227 try:
1228 id_, params = _build_data(self.apikey_regular, 'delete_repo',
1228 id_, params = _build_data(self.apikey_regular, 'delete_repo',
1229 repoid=repo_name, )
1229 repoid=repo_name, )
1230 response = api_call(self, params)
1230 response = api_call(self, params)
1231 expected = 'repository `%s` does not exist' % (repo_name)
1231 expected = 'repository `%s` does not exist' % (repo_name)
1232 self._compare_error(id_, expected, given=response.body)
1232 self._compare_error(id_, expected, given=response.body)
1233 finally:
1233 finally:
1234 fixture.destroy_repo(repo_name)
1234 fixture.destroy_repo(repo_name)
1235
1235
1236 def test_api_delete_repo_exception_occurred(self):
1236 def test_api_delete_repo_exception_occurred(self):
1237 repo_name = 'api_delete_me'
1237 repo_name = 'api_delete_me'
1238 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1238 fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
1239 try:
1239 try:
1240 with mock.patch.object(RepoModel, 'delete', raise_exception):
1240 with mock.patch.object(RepoModel, 'delete', raise_exception):
1241 id_, params = _build_data(self.apikey, 'delete_repo',
1241 id_, params = _build_data(self.apikey, 'delete_repo',
1242 repoid=repo_name, )
1242 repoid=repo_name, )
1243 response = api_call(self, params)
1243 response = api_call(self, params)
1244
1244
1245 expected = 'failed to delete repository `%s`' % repo_name
1245 expected = 'failed to delete repository `%s`' % repo_name
1246 self._compare_error(id_, expected, given=response.body)
1246 self._compare_error(id_, expected, given=response.body)
1247 finally:
1247 finally:
1248 fixture.destroy_repo(repo_name)
1248 fixture.destroy_repo(repo_name)
1249
1249
1250 def test_api_fork_repo(self):
1250 def test_api_fork_repo(self):
1251 fork_name = 'api-repo-fork'
1251 fork_name = 'api-repo-fork'
1252 id_, params = _build_data(self.apikey, 'fork_repo',
1252 id_, params = _build_data(self.apikey, 'fork_repo',
1253 repoid=self.REPO,
1253 repoid=self.REPO,
1254 fork_name=fork_name,
1254 fork_name=fork_name,
1255 owner=base.TEST_USER_ADMIN_LOGIN,
1255 owner=base.TEST_USER_ADMIN_LOGIN,
1256 )
1256 )
1257 response = api_call(self, params)
1257 response = api_call(self, params)
1258
1258
1259 ret = {
1259 ret = {
1260 'msg': 'Created fork of `%s` as `%s`' % (self.REPO,
1260 'msg': 'Created fork of `%s` as `%s`' % (self.REPO,
1261 fork_name),
1261 fork_name),
1262 'success': True,
1262 'success': True,
1263 'task': None,
1263 'task': None,
1264 }
1264 }
1265 expected = ret
1265 expected = ret
1266 self._compare_ok(id_, expected, given=response.body)
1266 self._compare_ok(id_, expected, given=response.body)
1267 fixture.destroy_repo(fork_name)
1267 fixture.destroy_repo(fork_name)
1268
1268
1269 @base.parametrize('fork_name', [
1269 @base.parametrize('fork_name', [
1270 'api-repo-fork',
1270 'api-repo-fork',
1271 '%s/api-repo-fork' % TEST_REPO_GROUP,
1271 '%s/api-repo-fork' % TEST_REPO_GROUP,
1272 ])
1272 ])
1273 def test_api_fork_repo_non_admin(self, fork_name):
1273 def test_api_fork_repo_non_admin(self, fork_name):
1274 id_, params = _build_data(self.apikey_regular, 'fork_repo',
1274 id_, params = _build_data(self.apikey_regular, 'fork_repo',
1275 repoid=self.REPO,
1275 repoid=self.REPO,
1276 fork_name=fork_name,
1276 fork_name=fork_name,
1277 )
1277 )
1278 response = api_call(self, params)
1278 response = api_call(self, params)
1279
1279
1280 ret = {
1280 ret = {
1281 'msg': 'Created fork of `%s` as `%s`' % (self.REPO,
1281 'msg': 'Created fork of `%s` as `%s`' % (self.REPO,
1282 fork_name),
1282 fork_name),
1283 'success': True,
1283 'success': True,
1284 'task': None,
1284 'task': None,
1285 }
1285 }
1286 expected = ret
1286 expected = ret
1287 self._compare_ok(id_, expected, given=response.body)
1287 self._compare_ok(id_, expected, given=response.body)
1288 fixture.destroy_repo(fork_name)
1288 fixture.destroy_repo(fork_name)
1289
1289
1290 def test_api_fork_repo_non_admin_specify_owner(self):
1290 def test_api_fork_repo_non_admin_specify_owner(self):
1291 fork_name = 'api-repo-fork'
1291 fork_name = 'api-repo-fork'
1292 id_, params = _build_data(self.apikey_regular, 'fork_repo',
1292 id_, params = _build_data(self.apikey_regular, 'fork_repo',
1293 repoid=self.REPO,
1293 repoid=self.REPO,
1294 fork_name=fork_name,
1294 fork_name=fork_name,
1295 owner=base.TEST_USER_ADMIN_LOGIN,
1295 owner=base.TEST_USER_ADMIN_LOGIN,
1296 )
1296 )
1297 response = api_call(self, params)
1297 response = api_call(self, params)
1298 expected = 'Only Kallithea admin can specify `owner` param'
1298 expected = 'Only Kallithea admin can specify `owner` param'
1299 self._compare_error(id_, expected, given=response.body)
1299 self._compare_error(id_, expected, given=response.body)
1300 fixture.destroy_repo(fork_name)
1300 fixture.destroy_repo(fork_name)
1301
1301
1302 def test_api_fork_repo_non_admin_no_permission_to_fork(self):
1302 def test_api_fork_repo_non_admin_no_permission_to_fork(self):
1303 RepoModel().grant_user_permission(repo=self.REPO,
1303 RepoModel().grant_user_permission(repo=self.REPO,
1304 user=db.User.DEFAULT_USER_NAME,
1304 user=db.User.DEFAULT_USER_NAME,
1305 perm='repository.none')
1305 perm='repository.none')
1306 try:
1306 try:
1307 fork_name = 'api-repo-fork'
1307 fork_name = 'api-repo-fork'
1308 id_, params = _build_data(self.apikey_regular, 'fork_repo',
1308 id_, params = _build_data(self.apikey_regular, 'fork_repo',
1309 repoid=self.REPO,
1309 repoid=self.REPO,
1310 fork_name=fork_name,
1310 fork_name=fork_name,
1311 )
1311 )
1312 response = api_call(self, params)
1312 response = api_call(self, params)
1313 expected = 'repository `%s` does not exist' % (self.REPO)
1313 expected = 'repository `%s` does not exist' % (self.REPO)
1314 self._compare_error(id_, expected, given=response.body)
1314 self._compare_error(id_, expected, given=response.body)
1315 finally:
1315 finally:
1316 RepoModel().grant_user_permission(repo=self.REPO,
1316 RepoModel().grant_user_permission(repo=self.REPO,
1317 user=db.User.DEFAULT_USER_NAME,
1317 user=db.User.DEFAULT_USER_NAME,
1318 perm='repository.read')
1318 perm='repository.read')
1319 fixture.destroy_repo(fork_name)
1319 fixture.destroy_repo(fork_name)
1320
1320
1321 @base.parametrize('name,perm', [
1321 @base.parametrize('name,perm', [
1322 ('read', 'repository.read'),
1322 ('read', 'repository.read'),
1323 ('write', 'repository.write'),
1323 ('write', 'repository.write'),
1324 ('admin', 'repository.admin'),
1324 ('admin', 'repository.admin'),
1325 ])
1325 ])
1326 def test_api_fork_repo_non_admin_no_create_repo_permission(self, name, perm):
1326 def test_api_fork_repo_non_admin_no_create_repo_permission(self, name, perm):
1327 fork_name = 'api-repo-fork'
1327 fork_name = 'api-repo-fork'
1328 # regardless of base repository permission, forking is disallowed
1328 # regardless of base repository permission, forking is disallowed
1329 # when repository creation is disabled
1329 # when repository creation is disabled
1330 RepoModel().grant_user_permission(repo=self.REPO,
1330 RepoModel().grant_user_permission(repo=self.REPO,
1331 user=self.TEST_USER_LOGIN,
1331 user=self.TEST_USER_LOGIN,
1332 perm=perm)
1332 perm=perm)
1333 UserModel().revoke_perm('default', 'hg.create.repository')
1333 UserModel().revoke_perm('default', 'hg.create.repository')
1334 UserModel().grant_perm('default', 'hg.create.none')
1334 UserModel().grant_perm('default', 'hg.create.none')
1335 id_, params = _build_data(self.apikey_regular, 'fork_repo',
1335 id_, params = _build_data(self.apikey_regular, 'fork_repo',
1336 repoid=self.REPO,
1336 repoid=self.REPO,
1337 fork_name=fork_name,
1337 fork_name=fork_name,
1338 )
1338 )
1339 response = api_call(self, params)
1339 response = api_call(self, params)
1340 expected = 'no permission to create repositories'
1340 expected = 'no permission to create repositories'
1341 self._compare_error(id_, expected, given=response.body)
1341 self._compare_error(id_, expected, given=response.body)
1342 fixture.destroy_repo(fork_name)
1342 fixture.destroy_repo(fork_name)
1343
1343
1344 def test_api_fork_repo_unknown_owner(self):
1344 def test_api_fork_repo_unknown_owner(self):
1345 fork_name = 'api-repo-fork'
1345 fork_name = 'api-repo-fork'
1346 owner = 'i-dont-exist'
1346 owner = 'i-dont-exist'
1347 id_, params = _build_data(self.apikey, 'fork_repo',
1347 id_, params = _build_data(self.apikey, 'fork_repo',
1348 repoid=self.REPO,
1348 repoid=self.REPO,
1349 fork_name=fork_name,
1349 fork_name=fork_name,
1350 owner=owner,
1350 owner=owner,
1351 )
1351 )
1352 response = api_call(self, params)
1352 response = api_call(self, params)
1353 expected = 'user `%s` does not exist' % owner
1353 expected = 'user `%s` does not exist' % owner
1354 self._compare_error(id_, expected, given=response.body)
1354 self._compare_error(id_, expected, given=response.body)
1355
1355
1356 def test_api_fork_repo_fork_exists(self):
1356 def test_api_fork_repo_fork_exists(self):
1357 fork_name = 'api-repo-fork'
1357 fork_name = 'api-repo-fork'
1358 fixture.create_fork(self.REPO, fork_name)
1358 fixture.create_fork(self.REPO, fork_name)
1359
1359
1360 try:
1360 try:
1361 fork_name = 'api-repo-fork'
1361 fork_name = 'api-repo-fork'
1362
1362
1363 id_, params = _build_data(self.apikey, 'fork_repo',
1363 id_, params = _build_data(self.apikey, 'fork_repo',
1364 repoid=self.REPO,
1364 repoid=self.REPO,
1365 fork_name=fork_name,
1365 fork_name=fork_name,
1366 owner=base.TEST_USER_ADMIN_LOGIN,
1366 owner=base.TEST_USER_ADMIN_LOGIN,
1367 )
1367 )
1368 response = api_call(self, params)
1368 response = api_call(self, params)
1369
1369
1370 expected = "fork `%s` already exist" % fork_name
1370 expected = "fork `%s` already exist" % fork_name
1371 self._compare_error(id_, expected, given=response.body)
1371 self._compare_error(id_, expected, given=response.body)
1372 finally:
1372 finally:
1373 fixture.destroy_repo(fork_name)
1373 fixture.destroy_repo(fork_name)
1374
1374
1375 def test_api_fork_repo_repo_exists(self):
1375 def test_api_fork_repo_repo_exists(self):
1376 fork_name = self.REPO
1376 fork_name = self.REPO
1377
1377
1378 id_, params = _build_data(self.apikey, 'fork_repo',
1378 id_, params = _build_data(self.apikey, 'fork_repo',
1379 repoid=self.REPO,
1379 repoid=self.REPO,
1380 fork_name=fork_name,
1380 fork_name=fork_name,
1381 owner=base.TEST_USER_ADMIN_LOGIN,
1381 owner=base.TEST_USER_ADMIN_LOGIN,
1382 )
1382 )
1383 response = api_call(self, params)
1383 response = api_call(self, params)
1384
1384
1385 expected = "repo `%s` already exist" % fork_name
1385 expected = "repo `%s` already exist" % fork_name
1386 self._compare_error(id_, expected, given=response.body)
1386 self._compare_error(id_, expected, given=response.body)
1387
1387
1388 @mock.patch.object(RepoModel, 'create_fork', raise_exception)
1388 @mock.patch.object(RepoModel, 'create_fork', raise_exception)
1389 def test_api_fork_repo_exception_occurred(self):
1389 def test_api_fork_repo_exception_occurred(self):
1390 fork_name = 'api-repo-fork'
1390 fork_name = 'api-repo-fork'
1391 id_, params = _build_data(self.apikey, 'fork_repo',
1391 id_, params = _build_data(self.apikey, 'fork_repo',
1392 repoid=self.REPO,
1392 repoid=self.REPO,
1393 fork_name=fork_name,
1393 fork_name=fork_name,
1394 owner=base.TEST_USER_ADMIN_LOGIN,
1394 owner=base.TEST_USER_ADMIN_LOGIN,
1395 )
1395 )
1396 response = api_call(self, params)
1396 response = api_call(self, params)
1397
1397
1398 expected = 'failed to fork repository `%s` as `%s`' % (self.REPO,
1398 expected = 'failed to fork repository `%s` as `%s`' % (self.REPO,
1399 fork_name)
1399 fork_name)
1400 self._compare_error(id_, expected, given=response.body)
1400 self._compare_error(id_, expected, given=response.body)
1401
1401
1402 def test_api_get_user_group(self):
1402 def test_api_get_user_group(self):
1403 id_, params = _build_data(self.apikey, 'get_user_group',
1403 id_, params = _build_data(self.apikey, 'get_user_group',
1404 usergroupid=TEST_USER_GROUP)
1404 usergroupid=TEST_USER_GROUP)
1405 response = api_call(self, params)
1405 response = api_call(self, params)
1406
1406
1407 user_group = UserGroupModel().get_group(TEST_USER_GROUP)
1407 user_group = UserGroupModel().get_group(TEST_USER_GROUP)
1408 members = []
1408 members = []
1409 for user in user_group.members:
1409 for user in user_group.members:
1410 user = user.user
1410 user = user.user
1411 members.append(user.get_api_data())
1411 members.append(user.get_api_data())
1412
1412
1413 ret = user_group.get_api_data()
1413 ret = user_group.get_api_data()
1414 ret['members'] = members
1414 ret['members'] = members
1415 expected = ret
1415 expected = ret
1416 self._compare_ok(id_, expected, given=response.body)
1416 self._compare_ok(id_, expected, given=response.body)
1417
1417
1418 def test_api_get_user_groups(self):
1418 def test_api_get_user_groups(self):
1419 gr_name = 'test_user_group2'
1419 gr_name = 'test_user_group2'
1420 make_user_group(gr_name)
1420 make_user_group(gr_name)
1421
1421
1422 try:
1422 try:
1423 id_, params = _build_data(self.apikey, 'get_user_groups', )
1423 id_, params = _build_data(self.apikey, 'get_user_groups', )
1424 response = api_call(self, params)
1424 response = api_call(self, params)
1425
1425
1426 expected = []
1426 expected = []
1427 for gr_name in [TEST_USER_GROUP, 'test_user_group2']:
1427 for gr_name in [TEST_USER_GROUP, 'test_user_group2']:
1428 user_group = UserGroupModel().get_group(gr_name)
1428 user_group = UserGroupModel().get_group(gr_name)
1429 ret = user_group.get_api_data()
1429 ret = user_group.get_api_data()
1430 expected.append(ret)
1430 expected.append(ret)
1431 self._compare_ok(id_, expected, given=response.body)
1431 self._compare_ok(id_, expected, given=response.body)
1432 finally:
1432 finally:
1433 fixture.destroy_user_group(gr_name)
1433 fixture.destroy_user_group(gr_name)
1434
1434
1435 def test_api_create_user_group(self):
1435 def test_api_create_user_group(self):
1436 group_name = 'some_new_group'
1436 group_name = 'some_new_group'
1437 id_, params = _build_data(self.apikey, 'create_user_group',
1437 id_, params = _build_data(self.apikey, 'create_user_group',
1438 group_name=group_name)
1438 group_name=group_name)
1439 response = api_call(self, params)
1439 response = api_call(self, params)
1440
1440
1441 ret = {
1441 ret = {
1442 'msg': 'created new user group `%s`' % group_name,
1442 'msg': 'created new user group `%s`' % group_name,
1443 'user_group': jsonify(UserGroupModel() \
1443 'user_group': jsonify(UserGroupModel() \
1444 .get_by_name(group_name) \
1444 .get_by_name(group_name) \
1445 .get_api_data())
1445 .get_api_data())
1446 }
1446 }
1447 expected = ret
1447 expected = ret
1448 self._compare_ok(id_, expected, given=response.body)
1448 self._compare_ok(id_, expected, given=response.body)
1449
1449
1450 fixture.destroy_user_group(group_name)
1450 fixture.destroy_user_group(group_name)
1451
1451
1452 def test_api_get_user_group_that_exist(self):
1452 def test_api_get_user_group_that_exist(self):
1453 id_, params = _build_data(self.apikey, 'create_user_group',
1453 id_, params = _build_data(self.apikey, 'create_user_group',
1454 group_name=TEST_USER_GROUP)
1454 group_name=TEST_USER_GROUP)
1455 response = api_call(self, params)
1455 response = api_call(self, params)
1456
1456
1457 expected = "user group `%s` already exist" % TEST_USER_GROUP
1457 expected = "user group `%s` already exist" % TEST_USER_GROUP
1458 self._compare_error(id_, expected, given=response.body)
1458 self._compare_error(id_, expected, given=response.body)
1459
1459
1460 @mock.patch.object(UserGroupModel, 'create', raise_exception)
1460 @mock.patch.object(UserGroupModel, 'create', raise_exception)
1461 def test_api_get_user_group_exception_occurred(self):
1461 def test_api_get_user_group_exception_occurred(self):
1462 group_name = 'exception_happens'
1462 group_name = 'exception_happens'
1463 id_, params = _build_data(self.apikey, 'create_user_group',
1463 id_, params = _build_data(self.apikey, 'create_user_group',
1464 group_name=group_name)
1464 group_name=group_name)
1465 response = api_call(self, params)
1465 response = api_call(self, params)
1466
1466
1467 expected = 'failed to create group `%s`' % group_name
1467 expected = 'failed to create group `%s`' % group_name
1468 self._compare_error(id_, expected, given=response.body)
1468 self._compare_error(id_, expected, given=response.body)
1469
1469
1470 @base.parametrize('changing_attr,updates', [
1470 @base.parametrize('changing_attr,updates', [
1471 ('group_name', {'group_name': 'new_group_name'}),
1471 ('group_name', {'group_name': 'new_group_name'}),
1472 ('group_name', {'group_name': 'test_group_for_update'}),
1472 ('group_name', {'group_name': 'test_group_for_update'}),
1473 ('owner', {'owner': base.TEST_USER_REGULAR_LOGIN}),
1473 ('owner', {'owner': base.TEST_USER_REGULAR_LOGIN}),
1474 ('active', {'active': False}),
1474 ('active', {'active': False}),
1475 ('active', {'active': True}),
1475 ('active', {'active': True}),
1476 ])
1476 ])
1477 def test_api_update_user_group(self, changing_attr, updates):
1477 def test_api_update_user_group(self, changing_attr, updates):
1478 gr_name = 'test_group_for_update'
1478 gr_name = 'test_group_for_update'
1479 user_group = fixture.create_user_group(gr_name)
1479 user_group = fixture.create_user_group(gr_name)
1480 try:
1480 try:
1481 id_, params = _build_data(self.apikey, 'update_user_group',
1481 id_, params = _build_data(self.apikey, 'update_user_group',
1482 usergroupid=gr_name, **updates)
1482 usergroupid=gr_name, **updates)
1483 response = api_call(self, params)
1483 response = api_call(self, params)
1484 expected = {
1484 expected = {
1485 'msg': 'updated user group ID:%s %s' % (user_group.users_group_id,
1485 'msg': 'updated user group ID:%s %s' % (user_group.users_group_id,
1486 user_group.users_group_name),
1486 user_group.users_group_name),
1487 'user_group': user_group.get_api_data()
1487 'user_group': user_group.get_api_data()
1488 }
1488 }
1489 self._compare_ok(id_, expected, given=response.body)
1489 self._compare_ok(id_, expected, given=response.body)
1490 finally:
1490 finally:
1491 if changing_attr == 'group_name':
1491 if changing_attr == 'group_name':
1492 # switch to updated name for proper cleanup
1492 # switch to updated name for proper cleanup
1493 gr_name = updates['group_name']
1493 gr_name = updates['group_name']
1494 fixture.destroy_user_group(gr_name)
1494 fixture.destroy_user_group(gr_name)
1495
1495
1496 @mock.patch.object(UserGroupModel, 'update', raise_exception)
1496 @mock.patch.object(UserGroupModel, 'update', raise_exception)
1497 def test_api_update_user_group_exception_occurred(self):
1497 def test_api_update_user_group_exception_occurred(self):
1498 gr_name = 'test_group'
1498 gr_name = 'test_group'
1499 fixture.create_user_group(gr_name)
1499 fixture.create_user_group(gr_name)
1500 try:
1500 try:
1501 id_, params = _build_data(self.apikey, 'update_user_group',
1501 id_, params = _build_data(self.apikey, 'update_user_group',
1502 usergroupid=gr_name)
1502 usergroupid=gr_name)
1503 response = api_call(self, params)
1503 response = api_call(self, params)
1504 expected = 'failed to update user group `%s`' % gr_name
1504 expected = 'failed to update user group `%s`' % gr_name
1505 self._compare_error(id_, expected, given=response.body)
1505 self._compare_error(id_, expected, given=response.body)
1506 finally:
1506 finally:
1507 fixture.destroy_user_group(gr_name)
1507 fixture.destroy_user_group(gr_name)
1508
1508
1509 def test_api_add_user_to_user_group(self):
1509 def test_api_add_user_to_user_group(self):
1510 gr_name = 'test_group'
1510 gr_name = 'test_group'
1511 fixture.create_user_group(gr_name)
1511 fixture.create_user_group(gr_name)
1512 try:
1512 try:
1513 id_, params = _build_data(self.apikey, 'add_user_to_user_group',
1513 id_, params = _build_data(self.apikey, 'add_user_to_user_group',
1514 usergroupid=gr_name,
1514 usergroupid=gr_name,
1515 userid=base.TEST_USER_ADMIN_LOGIN)
1515 userid=base.TEST_USER_ADMIN_LOGIN)
1516 response = api_call(self, params)
1516 response = api_call(self, params)
1517 expected = {
1517 expected = {
1518 'msg': 'added member `%s` to user group `%s`' % (
1518 'msg': 'added member `%s` to user group `%s`' % (
1519 base.TEST_USER_ADMIN_LOGIN, gr_name),
1519 base.TEST_USER_ADMIN_LOGIN, gr_name),
1520 'success': True
1520 'success': True
1521 }
1521 }
1522 self._compare_ok(id_, expected, given=response.body)
1522 self._compare_ok(id_, expected, given=response.body)
1523 finally:
1523 finally:
1524 fixture.destroy_user_group(gr_name)
1524 fixture.destroy_user_group(gr_name)
1525
1525
1526 def test_api_add_user_to_user_group_that_doesnt_exist(self):
1526 def test_api_add_user_to_user_group_that_doesnt_exist(self):
1527 id_, params = _build_data(self.apikey, 'add_user_to_user_group',
1527 id_, params = _build_data(self.apikey, 'add_user_to_user_group',
1528 usergroupid='false-group',
1528 usergroupid='false-group',
1529 userid=base.TEST_USER_ADMIN_LOGIN)
1529 userid=base.TEST_USER_ADMIN_LOGIN)
1530 response = api_call(self, params)
1530 response = api_call(self, params)
1531
1531
1532 expected = 'user group `%s` does not exist' % 'false-group'
1532 expected = 'user group `%s` does not exist' % 'false-group'
1533 self._compare_error(id_, expected, given=response.body)
1533 self._compare_error(id_, expected, given=response.body)
1534
1534
1535 @mock.patch.object(UserGroupModel, 'add_user_to_group', raise_exception)
1535 @mock.patch.object(UserGroupModel, 'add_user_to_group', raise_exception)
1536 def test_api_add_user_to_user_group_exception_occurred(self):
1536 def test_api_add_user_to_user_group_exception_occurred(self):
1537 gr_name = 'test_group'
1537 gr_name = 'test_group'
1538 fixture.create_user_group(gr_name)
1538 fixture.create_user_group(gr_name)
1539 try:
1539 try:
1540 id_, params = _build_data(self.apikey, 'add_user_to_user_group',
1540 id_, params = _build_data(self.apikey, 'add_user_to_user_group',
1541 usergroupid=gr_name,
1541 usergroupid=gr_name,
1542 userid=base.TEST_USER_ADMIN_LOGIN)
1542 userid=base.TEST_USER_ADMIN_LOGIN)
1543 response = api_call(self, params)
1543 response = api_call(self, params)
1544 expected = 'failed to add member to user group `%s`' % gr_name
1544 expected = 'failed to add member to user group `%s`' % gr_name
1545 self._compare_error(id_, expected, given=response.body)
1545 self._compare_error(id_, expected, given=response.body)
1546 finally:
1546 finally:
1547 fixture.destroy_user_group(gr_name)
1547 fixture.destroy_user_group(gr_name)
1548
1548
1549 def test_api_remove_user_from_user_group(self):
1549 def test_api_remove_user_from_user_group(self):
1550 gr_name = 'test_group_3'
1550 gr_name = 'test_group_3'
1551 gr = fixture.create_user_group(gr_name)
1551 gr = fixture.create_user_group(gr_name)
1552 UserGroupModel().add_user_to_group(gr, user=base.TEST_USER_ADMIN_LOGIN)
1552 UserGroupModel().add_user_to_group(gr, user=base.TEST_USER_ADMIN_LOGIN)
1553 meta.Session().commit()
1553 meta.Session().commit()
1554 try:
1554 try:
1555 id_, params = _build_data(self.apikey, 'remove_user_from_user_group',
1555 id_, params = _build_data(self.apikey, 'remove_user_from_user_group',
1556 usergroupid=gr_name,
1556 usergroupid=gr_name,
1557 userid=base.TEST_USER_ADMIN_LOGIN)
1557 userid=base.TEST_USER_ADMIN_LOGIN)
1558 response = api_call(self, params)
1558 response = api_call(self, params)
1559 expected = {
1559 expected = {
1560 'msg': 'removed member `%s` from user group `%s`' % (
1560 'msg': 'removed member `%s` from user group `%s`' % (
1561 base.TEST_USER_ADMIN_LOGIN, gr_name
1561 base.TEST_USER_ADMIN_LOGIN, gr_name
1562 ),
1562 ),
1563 'success': True}
1563 'success': True}
1564 self._compare_ok(id_, expected, given=response.body)
1564 self._compare_ok(id_, expected, given=response.body)
1565 finally:
1565 finally:
1566 fixture.destroy_user_group(gr_name)
1566 fixture.destroy_user_group(gr_name)
1567
1567
1568 @mock.patch.object(UserGroupModel, 'remove_user_from_group', raise_exception)
1568 @mock.patch.object(UserGroupModel, 'remove_user_from_group', raise_exception)
1569 def test_api_remove_user_from_user_group_exception_occurred(self):
1569 def test_api_remove_user_from_user_group_exception_occurred(self):
1570 gr_name = 'test_group_3'
1570 gr_name = 'test_group_3'
1571 gr = fixture.create_user_group(gr_name)
1571 gr = fixture.create_user_group(gr_name)
1572 UserGroupModel().add_user_to_group(gr, user=base.TEST_USER_ADMIN_LOGIN)
1572 UserGroupModel().add_user_to_group(gr, user=base.TEST_USER_ADMIN_LOGIN)
1573 meta.Session().commit()
1573 meta.Session().commit()
1574 try:
1574 try:
1575 id_, params = _build_data(self.apikey, 'remove_user_from_user_group',
1575 id_, params = _build_data(self.apikey, 'remove_user_from_user_group',
1576 usergroupid=gr_name,
1576 usergroupid=gr_name,
1577 userid=base.TEST_USER_ADMIN_LOGIN)
1577 userid=base.TEST_USER_ADMIN_LOGIN)
1578 response = api_call(self, params)
1578 response = api_call(self, params)
1579 expected = 'failed to remove member from user group `%s`' % gr_name
1579 expected = 'failed to remove member from user group `%s`' % gr_name
1580 self._compare_error(id_, expected, given=response.body)
1580 self._compare_error(id_, expected, given=response.body)
1581 finally:
1581 finally:
1582 fixture.destroy_user_group(gr_name)
1582 fixture.destroy_user_group(gr_name)
1583
1583
1584 def test_api_delete_user_group(self):
1584 def test_api_delete_user_group(self):
1585 gr_name = 'test_group'
1585 gr_name = 'test_group'
1586 ugroup = fixture.create_user_group(gr_name)
1586 ugroup = fixture.create_user_group(gr_name)
1587 gr_id = ugroup.users_group_id
1587 gr_id = ugroup.users_group_id
1588 try:
1588 try:
1589 id_, params = _build_data(self.apikey, 'delete_user_group',
1589 id_, params = _build_data(self.apikey, 'delete_user_group',
1590 usergroupid=gr_name)
1590 usergroupid=gr_name)
1591 response = api_call(self, params)
1591 response = api_call(self, params)
1592 expected = {
1592 expected = {
1593 'user_group': None,
1593 'user_group': None,
1594 'msg': 'deleted user group ID:%s %s' % (gr_id, gr_name)
1594 'msg': 'deleted user group ID:%s %s' % (gr_id, gr_name)
1595 }
1595 }
1596 self._compare_ok(id_, expected, given=response.body)
1596 self._compare_ok(id_, expected, given=response.body)
1597 finally:
1597 finally:
1598 if UserGroupModel().get_by_name(gr_name):
1598 if UserGroupModel().get_by_name(gr_name):
1599 fixture.destroy_user_group(gr_name)
1599 fixture.destroy_user_group(gr_name)
1600
1600
1601 def test_api_delete_user_group_that_is_assigned(self):
1601 def test_api_delete_user_group_that_is_assigned(self):
1602 gr_name = 'test_group'
1602 gr_name = 'test_group'
1603 ugroup = fixture.create_user_group(gr_name)
1603 ugroup = fixture.create_user_group(gr_name)
1604 gr_id = ugroup.users_group_id
1604 gr_id = ugroup.users_group_id
1605
1605
1606 ugr_to_perm = RepoModel().grant_user_group_permission(self.REPO, gr_name, 'repository.write')
1606 ugr_to_perm = RepoModel().grant_user_group_permission(self.REPO, gr_name, 'repository.write')
1607 msg = 'User Group assigned to %s' % ugr_to_perm.repository.repo_name
1607 msg = 'User Group assigned to %s' % ugr_to_perm.repository.repo_name
1608
1608
1609 try:
1609 try:
1610 id_, params = _build_data(self.apikey, 'delete_user_group',
1610 id_, params = _build_data(self.apikey, 'delete_user_group',
1611 usergroupid=gr_name)
1611 usergroupid=gr_name)
1612 response = api_call(self, params)
1612 response = api_call(self, params)
1613 expected = msg
1613 expected = msg
1614 self._compare_error(id_, expected, given=response.body)
1614 self._compare_error(id_, expected, given=response.body)
1615 finally:
1615 finally:
1616 if UserGroupModel().get_by_name(gr_name):
1616 if UserGroupModel().get_by_name(gr_name):
1617 fixture.destroy_user_group(gr_name)
1617 fixture.destroy_user_group(gr_name)
1618
1618
1619 def test_api_delete_user_group_exception_occurred(self):
1619 def test_api_delete_user_group_exception_occurred(self):
1620 gr_name = 'test_group'
1620 gr_name = 'test_group'
1621 ugroup = fixture.create_user_group(gr_name)
1621 ugroup = fixture.create_user_group(gr_name)
1622 gr_id = ugroup.users_group_id
1622 gr_id = ugroup.users_group_id
1623 id_, params = _build_data(self.apikey, 'delete_user_group',
1623 id_, params = _build_data(self.apikey, 'delete_user_group',
1624 usergroupid=gr_name)
1624 usergroupid=gr_name)
1625
1625
1626 try:
1626 try:
1627 with mock.patch.object(UserGroupModel, 'delete', raise_exception):
1627 with mock.patch.object(UserGroupModel, 'delete', raise_exception):
1628 response = api_call(self, params)
1628 response = api_call(self, params)
1629 expected = 'failed to delete user group ID:%s %s' % (gr_id, gr_name)
1629 expected = 'failed to delete user group ID:%s %s' % (gr_id, gr_name)
1630 self._compare_error(id_, expected, given=response.body)
1630 self._compare_error(id_, expected, given=response.body)
1631 finally:
1631 finally:
1632 fixture.destroy_user_group(gr_name)
1632 fixture.destroy_user_group(gr_name)
1633
1633
1634 @base.parametrize('name,perm', [
1634 @base.parametrize('name,perm', [
1635 ('none', 'repository.none'),
1635 ('none', 'repository.none'),
1636 ('read', 'repository.read'),
1636 ('read', 'repository.read'),
1637 ('write', 'repository.write'),
1637 ('write', 'repository.write'),
1638 ('admin', 'repository.admin'),
1638 ('admin', 'repository.admin'),
1639 ])
1639 ])
1640 def test_api_grant_user_permission(self, name, perm):
1640 def test_api_grant_user_permission(self, name, perm):
1641 id_, params = _build_data(self.apikey,
1641 id_, params = _build_data(self.apikey,
1642 'grant_user_permission',
1642 'grant_user_permission',
1643 repoid=self.REPO,
1643 repoid=self.REPO,
1644 userid=base.TEST_USER_ADMIN_LOGIN,
1644 userid=base.TEST_USER_ADMIN_LOGIN,
1645 perm=perm)
1645 perm=perm)
1646 response = api_call(self, params)
1646 response = api_call(self, params)
1647
1647
1648 ret = {
1648 ret = {
1649 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1649 'msg': 'Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1650 perm, base.TEST_USER_ADMIN_LOGIN, self.REPO
1650 perm, base.TEST_USER_ADMIN_LOGIN, self.REPO
1651 ),
1651 ),
1652 'success': True
1652 'success': True
1653 }
1653 }
1654 expected = ret
1654 expected = ret
1655 self._compare_ok(id_, expected, given=response.body)
1655 self._compare_ok(id_, expected, given=response.body)
1656
1656
1657 def test_api_grant_user_permission_wrong_permission(self):
1657 def test_api_grant_user_permission_wrong_permission(self):
1658 perm = 'haha.no.permission'
1658 perm = 'haha.no.permission'
1659 id_, params = _build_data(self.apikey,
1659 id_, params = _build_data(self.apikey,
1660 'grant_user_permission',
1660 'grant_user_permission',
1661 repoid=self.REPO,
1661 repoid=self.REPO,
1662 userid=base.TEST_USER_ADMIN_LOGIN,
1662 userid=base.TEST_USER_ADMIN_LOGIN,
1663 perm=perm)
1663 perm=perm)
1664 response = api_call(self, params)
1664 response = api_call(self, params)
1665
1665
1666 expected = 'permission `%s` does not exist' % perm
1666 expected = 'permission `%s` does not exist' % perm
1667 self._compare_error(id_, expected, given=response.body)
1667 self._compare_error(id_, expected, given=response.body)
1668
1668
1669 @mock.patch.object(RepoModel, 'grant_user_permission', raise_exception)
1669 @mock.patch.object(RepoModel, 'grant_user_permission', raise_exception)
1670 def test_api_grant_user_permission_exception_when_adding(self):
1670 def test_api_grant_user_permission_exception_when_adding(self):
1671 perm = 'repository.read'
1671 perm = 'repository.read'
1672 id_, params = _build_data(self.apikey,
1672 id_, params = _build_data(self.apikey,
1673 'grant_user_permission',
1673 'grant_user_permission',
1674 repoid=self.REPO,
1674 repoid=self.REPO,
1675 userid=base.TEST_USER_ADMIN_LOGIN,
1675 userid=base.TEST_USER_ADMIN_LOGIN,
1676 perm=perm)
1676 perm=perm)
1677 response = api_call(self, params)
1677 response = api_call(self, params)
1678
1678
1679 expected = 'failed to edit permission for user: `%s` in repo: `%s`' % (
1679 expected = 'failed to edit permission for user: `%s` in repo: `%s`' % (
1680 base.TEST_USER_ADMIN_LOGIN, self.REPO
1680 base.TEST_USER_ADMIN_LOGIN, self.REPO
1681 )
1681 )
1682 self._compare_error(id_, expected, given=response.body)
1682 self._compare_error(id_, expected, given=response.body)
1683
1683
1684 def test_api_revoke_user_permission(self):
1684 def test_api_revoke_user_permission(self):
1685 id_, params = _build_data(self.apikey,
1685 id_, params = _build_data(self.apikey,
1686 'revoke_user_permission',
1686 'revoke_user_permission',
1687 repoid=self.REPO,
1687 repoid=self.REPO,
1688 userid=base.TEST_USER_ADMIN_LOGIN, )
1688 userid=base.TEST_USER_ADMIN_LOGIN, )
1689 response = api_call(self, params)
1689 response = api_call(self, params)
1690
1690
1691 expected = {
1691 expected = {
1692 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1692 'msg': 'Revoked perm for user: `%s` in repo: `%s`' % (
1693 base.TEST_USER_ADMIN_LOGIN, self.REPO
1693 base.TEST_USER_ADMIN_LOGIN, self.REPO
1694 ),
1694 ),
1695 'success': True
1695 'success': True
1696 }
1696 }
1697 self._compare_ok(id_, expected, given=response.body)
1697 self._compare_ok(id_, expected, given=response.body)
1698
1698
1699 @mock.patch.object(RepoModel, 'revoke_user_permission', raise_exception)
1699 @mock.patch.object(RepoModel, 'revoke_user_permission', raise_exception)
1700 def test_api_revoke_user_permission_exception_when_adding(self):
1700 def test_api_revoke_user_permission_exception_when_adding(self):
1701 id_, params = _build_data(self.apikey,
1701 id_, params = _build_data(self.apikey,
1702 'revoke_user_permission',
1702 'revoke_user_permission',
1703 repoid=self.REPO,
1703 repoid=self.REPO,
1704 userid=base.TEST_USER_ADMIN_LOGIN, )
1704 userid=base.TEST_USER_ADMIN_LOGIN, )
1705 response = api_call(self, params)
1705 response = api_call(self, params)
1706
1706
1707 expected = 'failed to edit permission for user: `%s` in repo: `%s`' % (
1707 expected = 'failed to edit permission for user: `%s` in repo: `%s`' % (
1708 base.TEST_USER_ADMIN_LOGIN, self.REPO
1708 base.TEST_USER_ADMIN_LOGIN, self.REPO
1709 )
1709 )
1710 self._compare_error(id_, expected, given=response.body)
1710 self._compare_error(id_, expected, given=response.body)
1711
1711
1712 @base.parametrize('name,perm', [
1712 @base.parametrize('name,perm', [
1713 ('none', 'repository.none'),
1713 ('none', 'repository.none'),
1714 ('read', 'repository.read'),
1714 ('read', 'repository.read'),
1715 ('write', 'repository.write'),
1715 ('write', 'repository.write'),
1716 ('admin', 'repository.admin'),
1716 ('admin', 'repository.admin'),
1717 ])
1717 ])
1718 def test_api_grant_user_group_permission(self, name, perm):
1718 def test_api_grant_user_group_permission(self, name, perm):
1719 id_, params = _build_data(self.apikey,
1719 id_, params = _build_data(self.apikey,
1720 'grant_user_group_permission',
1720 'grant_user_group_permission',
1721 repoid=self.REPO,
1721 repoid=self.REPO,
1722 usergroupid=TEST_USER_GROUP,
1722 usergroupid=TEST_USER_GROUP,
1723 perm=perm)
1723 perm=perm)
1724 response = api_call(self, params)
1724 response = api_call(self, params)
1725
1725
1726 ret = {
1726 ret = {
1727 'msg': 'Granted perm: `%s` for user group: `%s` in repo: `%s`' % (
1727 'msg': 'Granted perm: `%s` for user group: `%s` in repo: `%s`' % (
1728 perm, TEST_USER_GROUP, self.REPO
1728 perm, TEST_USER_GROUP, self.REPO
1729 ),
1729 ),
1730 'success': True
1730 'success': True
1731 }
1731 }
1732 expected = ret
1732 expected = ret
1733 self._compare_ok(id_, expected, given=response.body)
1733 self._compare_ok(id_, expected, given=response.body)
1734
1734
1735 def test_api_grant_user_group_permission_wrong_permission(self):
1735 def test_api_grant_user_group_permission_wrong_permission(self):
1736 perm = 'haha.no.permission'
1736 perm = 'haha.no.permission'
1737 id_, params = _build_data(self.apikey,
1737 id_, params = _build_data(self.apikey,
1738 'grant_user_group_permission',
1738 'grant_user_group_permission',
1739 repoid=self.REPO,
1739 repoid=self.REPO,
1740 usergroupid=TEST_USER_GROUP,
1740 usergroupid=TEST_USER_GROUP,
1741 perm=perm)
1741 perm=perm)
1742 response = api_call(self, params)
1742 response = api_call(self, params)
1743
1743
1744 expected = 'permission `%s` does not exist' % perm
1744 expected = 'permission `%s` does not exist' % perm
1745 self._compare_error(id_, expected, given=response.body)
1745 self._compare_error(id_, expected, given=response.body)
1746
1746
1747 @mock.patch.object(RepoModel, 'grant_user_group_permission', raise_exception)
1747 @mock.patch.object(RepoModel, 'grant_user_group_permission', raise_exception)
1748 def test_api_grant_user_group_permission_exception_when_adding(self):
1748 def test_api_grant_user_group_permission_exception_when_adding(self):
1749 perm = 'repository.read'
1749 perm = 'repository.read'
1750 id_, params = _build_data(self.apikey,
1750 id_, params = _build_data(self.apikey,
1751 'grant_user_group_permission',
1751 'grant_user_group_permission',
1752 repoid=self.REPO,
1752 repoid=self.REPO,
1753 usergroupid=TEST_USER_GROUP,
1753 usergroupid=TEST_USER_GROUP,
1754 perm=perm)
1754 perm=perm)
1755 response = api_call(self, params)
1755 response = api_call(self, params)
1756
1756
1757 expected = 'failed to edit permission for user group: `%s` in repo: `%s`' % (
1757 expected = 'failed to edit permission for user group: `%s` in repo: `%s`' % (
1758 TEST_USER_GROUP, self.REPO
1758 TEST_USER_GROUP, self.REPO
1759 )
1759 )
1760 self._compare_error(id_, expected, given=response.body)
1760 self._compare_error(id_, expected, given=response.body)
1761
1761
1762 def test_api_revoke_user_group_permission(self):
1762 def test_api_revoke_user_group_permission(self):
1763 RepoModel().grant_user_group_permission(repo=self.REPO,
1763 RepoModel().grant_user_group_permission(repo=self.REPO,
1764 group_name=TEST_USER_GROUP,
1764 group_name=TEST_USER_GROUP,
1765 perm='repository.read')
1765 perm='repository.read')
1766 meta.Session().commit()
1766 meta.Session().commit()
1767 id_, params = _build_data(self.apikey,
1767 id_, params = _build_data(self.apikey,
1768 'revoke_user_group_permission',
1768 'revoke_user_group_permission',
1769 repoid=self.REPO,
1769 repoid=self.REPO,
1770 usergroupid=TEST_USER_GROUP, )
1770 usergroupid=TEST_USER_GROUP, )
1771 response = api_call(self, params)
1771 response = api_call(self, params)
1772
1772
1773 expected = {
1773 expected = {
1774 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1774 'msg': 'Revoked perm for user group: `%s` in repo: `%s`' % (
1775 TEST_USER_GROUP, self.REPO
1775 TEST_USER_GROUP, self.REPO
1776 ),
1776 ),
1777 'success': True
1777 'success': True
1778 }
1778 }
1779 self._compare_ok(id_, expected, given=response.body)
1779 self._compare_ok(id_, expected, given=response.body)
1780
1780
1781 @mock.patch.object(RepoModel, 'revoke_user_group_permission', raise_exception)
1781 @mock.patch.object(RepoModel, 'revoke_user_group_permission', raise_exception)
1782 def test_api_revoke_user_group_permission_exception_when_adding(self):
1782 def test_api_revoke_user_group_permission_exception_when_adding(self):
1783 id_, params = _build_data(self.apikey,
1783 id_, params = _build_data(self.apikey,
1784 'revoke_user_group_permission',
1784 'revoke_user_group_permission',
1785 repoid=self.REPO,
1785 repoid=self.REPO,
1786 usergroupid=TEST_USER_GROUP, )
1786 usergroupid=TEST_USER_GROUP, )
1787 response = api_call(self, params)
1787 response = api_call(self, params)
1788
1788
1789 expected = 'failed to edit permission for user group: `%s` in repo: `%s`' % (
1789 expected = 'failed to edit permission for user group: `%s` in repo: `%s`' % (
1790 TEST_USER_GROUP, self.REPO
1790 TEST_USER_GROUP, self.REPO
1791 )
1791 )
1792 self._compare_error(id_, expected, given=response.body)
1792 self._compare_error(id_, expected, given=response.body)
1793
1793
1794 @base.parametrize('name,perm,apply_to_children', [
1794 @base.parametrize('name,perm,apply_to_children', [
1795 ('none', 'group.none', 'none'),
1795 ('none', 'group.none', 'none'),
1796 ('read', 'group.read', 'none'),
1796 ('read', 'group.read', 'none'),
1797 ('write', 'group.write', 'none'),
1797 ('write', 'group.write', 'none'),
1798 ('admin', 'group.admin', 'none'),
1798 ('admin', 'group.admin', 'none'),
1799
1799
1800 ('none', 'group.none', 'all'),
1800 ('none', 'group.none', 'all'),
1801 ('read', 'group.read', 'all'),
1801 ('read', 'group.read', 'all'),
1802 ('write', 'group.write', 'all'),
1802 ('write', 'group.write', 'all'),
1803 ('admin', 'group.admin', 'all'),
1803 ('admin', 'group.admin', 'all'),
1804
1804
1805 ('none', 'group.none', 'repos'),
1805 ('none', 'group.none', 'repos'),
1806 ('read', 'group.read', 'repos'),
1806 ('read', 'group.read', 'repos'),
1807 ('write', 'group.write', 'repos'),
1807 ('write', 'group.write', 'repos'),
1808 ('admin', 'group.admin', 'repos'),
1808 ('admin', 'group.admin', 'repos'),
1809
1809
1810 ('none', 'group.none', 'groups'),
1810 ('none', 'group.none', 'groups'),
1811 ('read', 'group.read', 'groups'),
1811 ('read', 'group.read', 'groups'),
1812 ('write', 'group.write', 'groups'),
1812 ('write', 'group.write', 'groups'),
1813 ('admin', 'group.admin', 'groups'),
1813 ('admin', 'group.admin', 'groups'),
1814 ])
1814 ])
1815 def test_api_grant_user_permission_to_repo_group(self, name, perm, apply_to_children):
1815 def test_api_grant_user_permission_to_repo_group(self, name, perm, apply_to_children):
1816 id_, params = _build_data(self.apikey,
1816 id_, params = _build_data(self.apikey,
1817 'grant_user_permission_to_repo_group',
1817 'grant_user_permission_to_repo_group',
1818 repogroupid=TEST_REPO_GROUP,
1818 repogroupid=TEST_REPO_GROUP,
1819 userid=base.TEST_USER_ADMIN_LOGIN,
1819 userid=base.TEST_USER_ADMIN_LOGIN,
1820 perm=perm, apply_to_children=apply_to_children)
1820 perm=perm, apply_to_children=apply_to_children)
1821 response = api_call(self, params)
1821 response = api_call(self, params)
1822
1822
1823 ret = {
1823 ret = {
1824 'msg': 'Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
1824 'msg': 'Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
1825 perm, apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1825 perm, apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1826 ),
1826 ),
1827 'success': True
1827 'success': True
1828 }
1828 }
1829 expected = ret
1829 expected = ret
1830 self._compare_ok(id_, expected, given=response.body)
1830 self._compare_ok(id_, expected, given=response.body)
1831
1831
1832 @base.parametrize('name,perm,apply_to_children,grant_admin,access_ok', [
1832 @base.parametrize('name,perm,apply_to_children,grant_admin,access_ok', [
1833 ('none_fails', 'group.none', 'none', False, False),
1833 ('none_fails', 'group.none', 'none', False, False),
1834 ('read_fails', 'group.read', 'none', False, False),
1834 ('read_fails', 'group.read', 'none', False, False),
1835 ('write_fails', 'group.write', 'none', False, False),
1835 ('write_fails', 'group.write', 'none', False, False),
1836 ('admin_fails', 'group.admin', 'none', False, False),
1836 ('admin_fails', 'group.admin', 'none', False, False),
1837
1837
1838 # with granted perms
1838 # with granted perms
1839 ('none_ok', 'group.none', 'none', True, True),
1839 ('none_ok', 'group.none', 'none', True, True),
1840 ('read_ok', 'group.read', 'none', True, True),
1840 ('read_ok', 'group.read', 'none', True, True),
1841 ('write_ok', 'group.write', 'none', True, True),
1841 ('write_ok', 'group.write', 'none', True, True),
1842 ('admin_ok', 'group.admin', 'none', True, True),
1842 ('admin_ok', 'group.admin', 'none', True, True),
1843 ])
1843 ])
1844 def test_api_grant_user_permission_to_repo_group_by_regular_user(
1844 def test_api_grant_user_permission_to_repo_group_by_regular_user(
1845 self, name, perm, apply_to_children, grant_admin, access_ok):
1845 self, name, perm, apply_to_children, grant_admin, access_ok):
1846 if grant_admin:
1846 if grant_admin:
1847 RepoGroupModel().grant_user_permission(TEST_REPO_GROUP,
1847 RepoGroupModel().grant_user_permission(TEST_REPO_GROUP,
1848 self.TEST_USER_LOGIN,
1848 self.TEST_USER_LOGIN,
1849 'group.admin')
1849 'group.admin')
1850 meta.Session().commit()
1850 meta.Session().commit()
1851
1851
1852 id_, params = _build_data(self.apikey_regular,
1852 id_, params = _build_data(self.apikey_regular,
1853 'grant_user_permission_to_repo_group',
1853 'grant_user_permission_to_repo_group',
1854 repogroupid=TEST_REPO_GROUP,
1854 repogroupid=TEST_REPO_GROUP,
1855 userid=base.TEST_USER_ADMIN_LOGIN,
1855 userid=base.TEST_USER_ADMIN_LOGIN,
1856 perm=perm, apply_to_children=apply_to_children)
1856 perm=perm, apply_to_children=apply_to_children)
1857 response = api_call(self, params)
1857 response = api_call(self, params)
1858 if access_ok:
1858 if access_ok:
1859 ret = {
1859 ret = {
1860 'msg': 'Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
1860 'msg': 'Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
1861 perm, apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1861 perm, apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1862 ),
1862 ),
1863 'success': True
1863 'success': True
1864 }
1864 }
1865 expected = ret
1865 expected = ret
1866 self._compare_ok(id_, expected, given=response.body)
1866 self._compare_ok(id_, expected, given=response.body)
1867 else:
1867 else:
1868 expected = 'repository group `%s` does not exist' % TEST_REPO_GROUP
1868 expected = 'repository group `%s` does not exist' % TEST_REPO_GROUP
1869 self._compare_error(id_, expected, given=response.body)
1869 self._compare_error(id_, expected, given=response.body)
1870
1870
1871 def test_api_grant_user_permission_to_repo_group_wrong_permission(self):
1871 def test_api_grant_user_permission_to_repo_group_wrong_permission(self):
1872 perm = 'haha.no.permission'
1872 perm = 'haha.no.permission'
1873 id_, params = _build_data(self.apikey,
1873 id_, params = _build_data(self.apikey,
1874 'grant_user_permission_to_repo_group',
1874 'grant_user_permission_to_repo_group',
1875 repogroupid=TEST_REPO_GROUP,
1875 repogroupid=TEST_REPO_GROUP,
1876 userid=base.TEST_USER_ADMIN_LOGIN,
1876 userid=base.TEST_USER_ADMIN_LOGIN,
1877 perm=perm)
1877 perm=perm)
1878 response = api_call(self, params)
1878 response = api_call(self, params)
1879
1879
1880 expected = 'permission `%s` does not exist' % perm
1880 expected = 'permission `%s` does not exist' % perm
1881 self._compare_error(id_, expected, given=response.body)
1881 self._compare_error(id_, expected, given=response.body)
1882
1882
1883 @mock.patch.object(RepoGroupModel, 'grant_user_permission', raise_exception)
1883 @mock.patch.object(RepoGroupModel, 'grant_user_permission', raise_exception)
1884 def test_api_grant_user_permission_to_repo_group_exception_when_adding(self):
1884 def test_api_grant_user_permission_to_repo_group_exception_when_adding(self):
1885 perm = 'group.read'
1885 perm = 'group.read'
1886 id_, params = _build_data(self.apikey,
1886 id_, params = _build_data(self.apikey,
1887 'grant_user_permission_to_repo_group',
1887 'grant_user_permission_to_repo_group',
1888 repogroupid=TEST_REPO_GROUP,
1888 repogroupid=TEST_REPO_GROUP,
1889 userid=base.TEST_USER_ADMIN_LOGIN,
1889 userid=base.TEST_USER_ADMIN_LOGIN,
1890 perm=perm)
1890 perm=perm)
1891 response = api_call(self, params)
1891 response = api_call(self, params)
1892
1892
1893 expected = 'failed to edit permission for user: `%s` in repo group: `%s`' % (
1893 expected = 'failed to edit permission for user: `%s` in repo group: `%s`' % (
1894 base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1894 base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1895 )
1895 )
1896 self._compare_error(id_, expected, given=response.body)
1896 self._compare_error(id_, expected, given=response.body)
1897
1897
1898 @base.parametrize('name,apply_to_children', [
1898 @base.parametrize('name,apply_to_children', [
1899 ('none', 'none'),
1899 ('none', 'none'),
1900 ('all', 'all'),
1900 ('all', 'all'),
1901 ('repos', 'repos'),
1901 ('repos', 'repos'),
1902 ('groups', 'groups'),
1902 ('groups', 'groups'),
1903 ])
1903 ])
1904 def test_api_revoke_user_permission_from_repo_group(self, name, apply_to_children):
1904 def test_api_revoke_user_permission_from_repo_group(self, name, apply_to_children):
1905 RepoGroupModel().grant_user_permission(repo_group=TEST_REPO_GROUP,
1905 RepoGroupModel().grant_user_permission(repo_group=TEST_REPO_GROUP,
1906 user=base.TEST_USER_ADMIN_LOGIN,
1906 user=base.TEST_USER_ADMIN_LOGIN,
1907 perm='group.read',)
1907 perm='group.read',)
1908 meta.Session().commit()
1908 meta.Session().commit()
1909
1909
1910 id_, params = _build_data(self.apikey,
1910 id_, params = _build_data(self.apikey,
1911 'revoke_user_permission_from_repo_group',
1911 'revoke_user_permission_from_repo_group',
1912 repogroupid=TEST_REPO_GROUP,
1912 repogroupid=TEST_REPO_GROUP,
1913 userid=base.TEST_USER_ADMIN_LOGIN,
1913 userid=base.TEST_USER_ADMIN_LOGIN,
1914 apply_to_children=apply_to_children,)
1914 apply_to_children=apply_to_children,)
1915 response = api_call(self, params)
1915 response = api_call(self, params)
1916
1916
1917 expected = {
1917 expected = {
1918 'msg': 'Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
1918 'msg': 'Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
1919 apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1919 apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1920 ),
1920 ),
1921 'success': True
1921 'success': True
1922 }
1922 }
1923 self._compare_ok(id_, expected, given=response.body)
1923 self._compare_ok(id_, expected, given=response.body)
1924
1924
1925 @base.parametrize('name,apply_to_children,grant_admin,access_ok', [
1925 @base.parametrize('name,apply_to_children,grant_admin,access_ok', [
1926 ('none', 'none', False, False),
1926 ('none', 'none', False, False),
1927 ('all', 'all', False, False),
1927 ('all', 'all', False, False),
1928 ('repos', 'repos', False, False),
1928 ('repos', 'repos', False, False),
1929 ('groups', 'groups', False, False),
1929 ('groups', 'groups', False, False),
1930
1930
1931 # after granting admin rights
1931 # after granting admin rights
1932 ('none', 'none', False, False),
1932 ('none', 'none', False, False),
1933 ('all', 'all', False, False),
1933 ('all', 'all', False, False),
1934 ('repos', 'repos', False, False),
1934 ('repos', 'repos', False, False),
1935 ('groups', 'groups', False, False),
1935 ('groups', 'groups', False, False),
1936 ])
1936 ])
1937 def test_api_revoke_user_permission_from_repo_group_by_regular_user(
1937 def test_api_revoke_user_permission_from_repo_group_by_regular_user(
1938 self, name, apply_to_children, grant_admin, access_ok):
1938 self, name, apply_to_children, grant_admin, access_ok):
1939 RepoGroupModel().grant_user_permission(repo_group=TEST_REPO_GROUP,
1939 RepoGroupModel().grant_user_permission(repo_group=TEST_REPO_GROUP,
1940 user=base.TEST_USER_ADMIN_LOGIN,
1940 user=base.TEST_USER_ADMIN_LOGIN,
1941 perm='group.read',)
1941 perm='group.read',)
1942 meta.Session().commit()
1942 meta.Session().commit()
1943
1943
1944 if grant_admin:
1944 if grant_admin:
1945 RepoGroupModel().grant_user_permission(TEST_REPO_GROUP,
1945 RepoGroupModel().grant_user_permission(TEST_REPO_GROUP,
1946 self.TEST_USER_LOGIN,
1946 self.TEST_USER_LOGIN,
1947 'group.admin')
1947 'group.admin')
1948 meta.Session().commit()
1948 meta.Session().commit()
1949
1949
1950 id_, params = _build_data(self.apikey_regular,
1950 id_, params = _build_data(self.apikey_regular,
1951 'revoke_user_permission_from_repo_group',
1951 'revoke_user_permission_from_repo_group',
1952 repogroupid=TEST_REPO_GROUP,
1952 repogroupid=TEST_REPO_GROUP,
1953 userid=base.TEST_USER_ADMIN_LOGIN,
1953 userid=base.TEST_USER_ADMIN_LOGIN,
1954 apply_to_children=apply_to_children,)
1954 apply_to_children=apply_to_children,)
1955 response = api_call(self, params)
1955 response = api_call(self, params)
1956 if access_ok:
1956 if access_ok:
1957 expected = {
1957 expected = {
1958 'msg': 'Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
1958 'msg': 'Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
1959 apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1959 apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1960 ),
1960 ),
1961 'success': True
1961 'success': True
1962 }
1962 }
1963 self._compare_ok(id_, expected, given=response.body)
1963 self._compare_ok(id_, expected, given=response.body)
1964 else:
1964 else:
1965 expected = 'repository group `%s` does not exist' % TEST_REPO_GROUP
1965 expected = 'repository group `%s` does not exist' % TEST_REPO_GROUP
1966 self._compare_error(id_, expected, given=response.body)
1966 self._compare_error(id_, expected, given=response.body)
1967
1967
1968 @mock.patch.object(RepoGroupModel, 'revoke_user_permission', raise_exception)
1968 @mock.patch.object(RepoGroupModel, 'revoke_user_permission', raise_exception)
1969 def test_api_revoke_user_permission_from_repo_group_exception_when_adding(self):
1969 def test_api_revoke_user_permission_from_repo_group_exception_when_adding(self):
1970 id_, params = _build_data(self.apikey,
1970 id_, params = _build_data(self.apikey,
1971 'revoke_user_permission_from_repo_group',
1971 'revoke_user_permission_from_repo_group',
1972 repogroupid=TEST_REPO_GROUP,
1972 repogroupid=TEST_REPO_GROUP,
1973 userid=base.TEST_USER_ADMIN_LOGIN, )
1973 userid=base.TEST_USER_ADMIN_LOGIN, )
1974 response = api_call(self, params)
1974 response = api_call(self, params)
1975
1975
1976 expected = 'failed to edit permission for user: `%s` in repo group: `%s`' % (
1976 expected = 'failed to edit permission for user: `%s` in repo group: `%s`' % (
1977 base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1977 base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
1978 )
1978 )
1979 self._compare_error(id_, expected, given=response.body)
1979 self._compare_error(id_, expected, given=response.body)
1980
1980
1981 @base.parametrize('name,perm,apply_to_children', [
1981 @base.parametrize('name,perm,apply_to_children', [
1982 ('none', 'group.none', 'none'),
1982 ('none', 'group.none', 'none'),
1983 ('read', 'group.read', 'none'),
1983 ('read', 'group.read', 'none'),
1984 ('write', 'group.write', 'none'),
1984 ('write', 'group.write', 'none'),
1985 ('admin', 'group.admin', 'none'),
1985 ('admin', 'group.admin', 'none'),
1986
1986
1987 ('none', 'group.none', 'all'),
1987 ('none', 'group.none', 'all'),
1988 ('read', 'group.read', 'all'),
1988 ('read', 'group.read', 'all'),
1989 ('write', 'group.write', 'all'),
1989 ('write', 'group.write', 'all'),
1990 ('admin', 'group.admin', 'all'),
1990 ('admin', 'group.admin', 'all'),
1991
1991
1992 ('none', 'group.none', 'repos'),
1992 ('none', 'group.none', 'repos'),
1993 ('read', 'group.read', 'repos'),
1993 ('read', 'group.read', 'repos'),
1994 ('write', 'group.write', 'repos'),
1994 ('write', 'group.write', 'repos'),
1995 ('admin', 'group.admin', 'repos'),
1995 ('admin', 'group.admin', 'repos'),
1996
1996
1997 ('none', 'group.none', 'groups'),
1997 ('none', 'group.none', 'groups'),
1998 ('read', 'group.read', 'groups'),
1998 ('read', 'group.read', 'groups'),
1999 ('write', 'group.write', 'groups'),
1999 ('write', 'group.write', 'groups'),
2000 ('admin', 'group.admin', 'groups'),
2000 ('admin', 'group.admin', 'groups'),
2001 ])
2001 ])
2002 def test_api_grant_user_group_permission_to_repo_group(self, name, perm, apply_to_children):
2002 def test_api_grant_user_group_permission_to_repo_group(self, name, perm, apply_to_children):
2003 id_, params = _build_data(self.apikey,
2003 id_, params = _build_data(self.apikey,
2004 'grant_user_group_permission_to_repo_group',
2004 'grant_user_group_permission_to_repo_group',
2005 repogroupid=TEST_REPO_GROUP,
2005 repogroupid=TEST_REPO_GROUP,
2006 usergroupid=TEST_USER_GROUP,
2006 usergroupid=TEST_USER_GROUP,
2007 perm=perm,
2007 perm=perm,
2008 apply_to_children=apply_to_children,)
2008 apply_to_children=apply_to_children,)
2009 response = api_call(self, params)
2009 response = api_call(self, params)
2010
2010
2011 ret = {
2011 ret = {
2012 'msg': 'Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2012 'msg': 'Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2013 perm, apply_to_children, TEST_USER_GROUP, TEST_REPO_GROUP
2013 perm, apply_to_children, TEST_USER_GROUP, TEST_REPO_GROUP
2014 ),
2014 ),
2015 'success': True
2015 'success': True
2016 }
2016 }
2017 expected = ret
2017 expected = ret
2018 self._compare_ok(id_, expected, given=response.body)
2018 self._compare_ok(id_, expected, given=response.body)
2019
2019
2020 @base.parametrize('name,perm,apply_to_children,grant_admin,access_ok', [
2020 @base.parametrize('name,perm,apply_to_children,grant_admin,access_ok', [
2021 ('none_fails', 'group.none', 'none', False, False),
2021 ('none_fails', 'group.none', 'none', False, False),
2022 ('read_fails', 'group.read', 'none', False, False),
2022 ('read_fails', 'group.read', 'none', False, False),
2023 ('write_fails', 'group.write', 'none', False, False),
2023 ('write_fails', 'group.write', 'none', False, False),
2024 ('admin_fails', 'group.admin', 'none', False, False),
2024 ('admin_fails', 'group.admin', 'none', False, False),
2025
2025
2026 # with granted perms
2026 # with granted perms
2027 ('none_ok', 'group.none', 'none', True, True),
2027 ('none_ok', 'group.none', 'none', True, True),
2028 ('read_ok', 'group.read', 'none', True, True),
2028 ('read_ok', 'group.read', 'none', True, True),
2029 ('write_ok', 'group.write', 'none', True, True),
2029 ('write_ok', 'group.write', 'none', True, True),
2030 ('admin_ok', 'group.admin', 'none', True, True),
2030 ('admin_ok', 'group.admin', 'none', True, True),
2031 ])
2031 ])
2032 def test_api_grant_user_group_permission_to_repo_group_by_regular_user(
2032 def test_api_grant_user_group_permission_to_repo_group_by_regular_user(
2033 self, name, perm, apply_to_children, grant_admin, access_ok):
2033 self, name, perm, apply_to_children, grant_admin, access_ok):
2034 if grant_admin:
2034 if grant_admin:
2035 RepoGroupModel().grant_user_permission(TEST_REPO_GROUP,
2035 RepoGroupModel().grant_user_permission(TEST_REPO_GROUP,
2036 self.TEST_USER_LOGIN,
2036 self.TEST_USER_LOGIN,
2037 'group.admin')
2037 'group.admin')
2038 meta.Session().commit()
2038 meta.Session().commit()
2039
2039
2040 id_, params = _build_data(self.apikey_regular,
2040 id_, params = _build_data(self.apikey_regular,
2041 'grant_user_group_permission_to_repo_group',
2041 'grant_user_group_permission_to_repo_group',
2042 repogroupid=TEST_REPO_GROUP,
2042 repogroupid=TEST_REPO_GROUP,
2043 usergroupid=TEST_USER_GROUP,
2043 usergroupid=TEST_USER_GROUP,
2044 perm=perm,
2044 perm=perm,
2045 apply_to_children=apply_to_children,)
2045 apply_to_children=apply_to_children,)
2046 response = api_call(self, params)
2046 response = api_call(self, params)
2047 if access_ok:
2047 if access_ok:
2048 ret = {
2048 ret = {
2049 'msg': 'Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2049 'msg': 'Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2050 perm, apply_to_children, TEST_USER_GROUP, TEST_REPO_GROUP
2050 perm, apply_to_children, TEST_USER_GROUP, TEST_REPO_GROUP
2051 ),
2051 ),
2052 'success': True
2052 'success': True
2053 }
2053 }
2054 expected = ret
2054 expected = ret
2055 self._compare_ok(id_, expected, given=response.body)
2055 self._compare_ok(id_, expected, given=response.body)
2056 else:
2056 else:
2057 expected = 'repository group `%s` does not exist' % TEST_REPO_GROUP
2057 expected = 'repository group `%s` does not exist' % TEST_REPO_GROUP
2058 self._compare_error(id_, expected, given=response.body)
2058 self._compare_error(id_, expected, given=response.body)
2059
2059
2060 def test_api_grant_user_group_permission_to_repo_group_wrong_permission(self):
2060 def test_api_grant_user_group_permission_to_repo_group_wrong_permission(self):
2061 perm = 'haha.no.permission'
2061 perm = 'haha.no.permission'
2062 id_, params = _build_data(self.apikey,
2062 id_, params = _build_data(self.apikey,
2063 'grant_user_group_permission_to_repo_group',
2063 'grant_user_group_permission_to_repo_group',
2064 repogroupid=TEST_REPO_GROUP,
2064 repogroupid=TEST_REPO_GROUP,
2065 usergroupid=TEST_USER_GROUP,
2065 usergroupid=TEST_USER_GROUP,
2066 perm=perm)
2066 perm=perm)
2067 response = api_call(self, params)
2067 response = api_call(self, params)
2068
2068
2069 expected = 'permission `%s` does not exist' % perm
2069 expected = 'permission `%s` does not exist' % perm
2070 self._compare_error(id_, expected, given=response.body)
2070 self._compare_error(id_, expected, given=response.body)
2071
2071
2072 @mock.patch.object(RepoGroupModel, 'grant_user_group_permission', raise_exception)
2072 @mock.patch.object(RepoGroupModel, 'grant_user_group_permission', raise_exception)
2073 def test_api_grant_user_group_permission_exception_when_adding_to_repo_group(self):
2073 def test_api_grant_user_group_permission_exception_when_adding_to_repo_group(self):
2074 perm = 'group.read'
2074 perm = 'group.read'
2075 id_, params = _build_data(self.apikey,
2075 id_, params = _build_data(self.apikey,
2076 'grant_user_group_permission_to_repo_group',
2076 'grant_user_group_permission_to_repo_group',
2077 repogroupid=TEST_REPO_GROUP,
2077 repogroupid=TEST_REPO_GROUP,
2078 usergroupid=TEST_USER_GROUP,
2078 usergroupid=TEST_USER_GROUP,
2079 perm=perm)
2079 perm=perm)
2080 response = api_call(self, params)
2080 response = api_call(self, params)
2081
2081
2082 expected = 'failed to edit permission for user group: `%s` in repo group: `%s`' % (
2082 expected = 'failed to edit permission for user group: `%s` in repo group: `%s`' % (
2083 TEST_USER_GROUP, TEST_REPO_GROUP
2083 TEST_USER_GROUP, TEST_REPO_GROUP
2084 )
2084 )
2085 self._compare_error(id_, expected, given=response.body)
2085 self._compare_error(id_, expected, given=response.body)
2086
2086
2087 @base.parametrize('name,apply_to_children', [
2087 @base.parametrize('name,apply_to_children', [
2088 ('none', 'none'),
2088 ('none', 'none'),
2089 ('all', 'all'),
2089 ('all', 'all'),
2090 ('repos', 'repos'),
2090 ('repos', 'repos'),
2091 ('groups', 'groups'),
2091 ('groups', 'groups'),
2092 ])
2092 ])
2093 def test_api_revoke_user_group_permission_from_repo_group(self, name, apply_to_children):
2093 def test_api_revoke_user_group_permission_from_repo_group(self, name, apply_to_children):
2094 RepoGroupModel().grant_user_group_permission(repo_group=TEST_REPO_GROUP,
2094 RepoGroupModel().grant_user_group_permission(repo_group=TEST_REPO_GROUP,
2095 group_name=TEST_USER_GROUP,
2095 group_name=TEST_USER_GROUP,
2096 perm='group.read',)
2096 perm='group.read',)
2097 meta.Session().commit()
2097 meta.Session().commit()
2098 id_, params = _build_data(self.apikey,
2098 id_, params = _build_data(self.apikey,
2099 'revoke_user_group_permission_from_repo_group',
2099 'revoke_user_group_permission_from_repo_group',
2100 repogroupid=TEST_REPO_GROUP,
2100 repogroupid=TEST_REPO_GROUP,
2101 usergroupid=TEST_USER_GROUP,
2101 usergroupid=TEST_USER_GROUP,
2102 apply_to_children=apply_to_children,)
2102 apply_to_children=apply_to_children,)
2103 response = api_call(self, params)
2103 response = api_call(self, params)
2104
2104
2105 expected = {
2105 expected = {
2106 'msg': 'Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2106 'msg': 'Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2107 apply_to_children, TEST_USER_GROUP, TEST_REPO_GROUP
2107 apply_to_children, TEST_USER_GROUP, TEST_REPO_GROUP
2108 ),
2108 ),
2109 'success': True
2109 'success': True
2110 }
2110 }
2111 self._compare_ok(id_, expected, given=response.body)
2111 self._compare_ok(id_, expected, given=response.body)
2112
2112
2113 @base.parametrize('name,apply_to_children,grant_admin,access_ok', [
2113 @base.parametrize('name,apply_to_children,grant_admin,access_ok', [
2114 ('none', 'none', False, False),
2114 ('none', 'none', False, False),
2115 ('all', 'all', False, False),
2115 ('all', 'all', False, False),
2116 ('repos', 'repos', False, False),
2116 ('repos', 'repos', False, False),
2117 ('groups', 'groups', False, False),
2117 ('groups', 'groups', False, False),
2118
2118
2119 # after granting admin rights
2119 # after granting admin rights
2120 ('none', 'none', False, False),
2120 ('none', 'none', False, False),
2121 ('all', 'all', False, False),
2121 ('all', 'all', False, False),
2122 ('repos', 'repos', False, False),
2122 ('repos', 'repos', False, False),
2123 ('groups', 'groups', False, False),
2123 ('groups', 'groups', False, False),
2124 ])
2124 ])
2125 def test_api_revoke_user_group_permission_from_repo_group_by_regular_user(
2125 def test_api_revoke_user_group_permission_from_repo_group_by_regular_user(
2126 self, name, apply_to_children, grant_admin, access_ok):
2126 self, name, apply_to_children, grant_admin, access_ok):
2127 RepoGroupModel().grant_user_permission(repo_group=TEST_REPO_GROUP,
2127 RepoGroupModel().grant_user_permission(repo_group=TEST_REPO_GROUP,
2128 user=base.TEST_USER_ADMIN_LOGIN,
2128 user=base.TEST_USER_ADMIN_LOGIN,
2129 perm='group.read',)
2129 perm='group.read',)
2130 meta.Session().commit()
2130 meta.Session().commit()
2131
2131
2132 if grant_admin:
2132 if grant_admin:
2133 RepoGroupModel().grant_user_permission(TEST_REPO_GROUP,
2133 RepoGroupModel().grant_user_permission(TEST_REPO_GROUP,
2134 self.TEST_USER_LOGIN,
2134 self.TEST_USER_LOGIN,
2135 'group.admin')
2135 'group.admin')
2136 meta.Session().commit()
2136 meta.Session().commit()
2137
2137
2138 id_, params = _build_data(self.apikey_regular,
2138 id_, params = _build_data(self.apikey_regular,
2139 'revoke_user_group_permission_from_repo_group',
2139 'revoke_user_group_permission_from_repo_group',
2140 repogroupid=TEST_REPO_GROUP,
2140 repogroupid=TEST_REPO_GROUP,
2141 usergroupid=TEST_USER_GROUP,
2141 usergroupid=TEST_USER_GROUP,
2142 apply_to_children=apply_to_children,)
2142 apply_to_children=apply_to_children,)
2143 response = api_call(self, params)
2143 response = api_call(self, params)
2144 if access_ok:
2144 if access_ok:
2145 expected = {
2145 expected = {
2146 'msg': 'Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2146 'msg': 'Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2147 apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
2147 apply_to_children, base.TEST_USER_ADMIN_LOGIN, TEST_REPO_GROUP
2148 ),
2148 ),
2149 'success': True
2149 'success': True
2150 }
2150 }
2151 self._compare_ok(id_, expected, given=response.body)
2151 self._compare_ok(id_, expected, given=response.body)
2152 else:
2152 else:
2153 expected = 'repository group `%s` does not exist' % TEST_REPO_GROUP
2153 expected = 'repository group `%s` does not exist' % TEST_REPO_GROUP
2154 self._compare_error(id_, expected, given=response.body)
2154 self._compare_error(id_, expected, given=response.body)
2155
2155
2156 @mock.patch.object(RepoGroupModel, 'revoke_user_group_permission', raise_exception)
2156 @mock.patch.object(RepoGroupModel, 'revoke_user_group_permission', raise_exception)
2157 def test_api_revoke_user_group_permission_from_repo_group_exception_when_adding(self):
2157 def test_api_revoke_user_group_permission_from_repo_group_exception_when_adding(self):
2158 id_, params = _build_data(self.apikey, 'revoke_user_group_permission_from_repo_group',
2158 id_, params = _build_data(self.apikey, 'revoke_user_group_permission_from_repo_group',
2159 repogroupid=TEST_REPO_GROUP,
2159 repogroupid=TEST_REPO_GROUP,
2160 usergroupid=TEST_USER_GROUP,)
2160 usergroupid=TEST_USER_GROUP,)
2161 response = api_call(self, params)
2161 response = api_call(self, params)
2162
2162
2163 expected = 'failed to edit permission for user group: `%s` in repo group: `%s`' % (
2163 expected = 'failed to edit permission for user group: `%s` in repo group: `%s`' % (
2164 TEST_USER_GROUP, TEST_REPO_GROUP
2164 TEST_USER_GROUP, TEST_REPO_GROUP
2165 )
2165 )
2166 self._compare_error(id_, expected, given=response.body)
2166 self._compare_error(id_, expected, given=response.body)
2167
2167
2168 def test_api_get_gist(self):
2168 def test_api_get_gist(self):
2169 gist = fixture.create_gist()
2169 gist = fixture.create_gist()
2170 gist_id = gist.gist_access_id
2170 gist_id = gist.gist_access_id
2171 gist_created_on = gist.created_on
2171 gist_created_on = gist.created_on
2172 id_, params = _build_data(self.apikey, 'get_gist',
2172 id_, params = _build_data(self.apikey, 'get_gist',
2173 gistid=gist_id, )
2173 gistid=gist_id, )
2174 response = api_call(self, params)
2174 response = api_call(self, params)
2175
2175
2176 expected = {
2176 expected = {
2177 'access_id': gist_id,
2177 'access_id': gist_id,
2178 'created_on': gist_created_on,
2178 'created_on': gist_created_on,
2179 'description': 'new-gist',
2179 'description': 'new-gist',
2180 'expires': -1.0,
2180 'expires': -1.0,
2181 'gist_id': int(gist_id),
2181 'gist_id': int(gist_id),
2182 'type': 'public',
2182 'type': 'public',
2183 'url': 'http://localhost:80/_admin/gists/%s' % gist_id
2183 'url': 'http://localhost:80/_admin/gists/%s' % gist_id
2184 }
2184 }
2185
2185
2186 self._compare_ok(id_, expected, given=response.body)
2186 self._compare_ok(id_, expected, given=response.body)
2187
2187
2188 def test_api_get_gist_that_does_not_exist(self):
2188 def test_api_get_gist_that_does_not_exist(self):
2189 id_, params = _build_data(self.apikey_regular, 'get_gist',
2189 id_, params = _build_data(self.apikey_regular, 'get_gist',
2190 gistid='12345', )
2190 gistid='12345', )
2191 response = api_call(self, params)
2191 response = api_call(self, params)
2192 expected = 'gist `%s` does not exist' % ('12345',)
2192 expected = 'gist `%s` does not exist' % ('12345',)
2193 self._compare_error(id_, expected, given=response.body)
2193 self._compare_error(id_, expected, given=response.body)
2194
2194
2195 def test_api_get_gist_private_gist_without_permission(self):
2195 def test_api_get_gist_private_gist_without_permission(self):
2196 gist = fixture.create_gist()
2196 gist = fixture.create_gist()
2197 gist_id = gist.gist_access_id
2197 gist_id = gist.gist_access_id
2198 gist_created_on = gist.created_on
2198 gist_created_on = gist.created_on
2199 id_, params = _build_data(self.apikey_regular, 'get_gist',
2199 id_, params = _build_data(self.apikey_regular, 'get_gist',
2200 gistid=gist_id, )
2200 gistid=gist_id, )
2201 response = api_call(self, params)
2201 response = api_call(self, params)
2202
2202
2203 expected = 'gist `%s` does not exist' % gist_id
2203 expected = 'gist `%s` does not exist' % gist_id
2204 self._compare_error(id_, expected, given=response.body)
2204 self._compare_error(id_, expected, given=response.body)
2205
2205
2206 def test_api_get_gists(self):
2206 def test_api_get_gists(self):
2207 fixture.create_gist()
2207 fixture.create_gist()
2208 fixture.create_gist()
2208 fixture.create_gist()
2209
2209
2210 id_, params = _build_data(self.apikey, 'get_gists')
2210 id_, params = _build_data(self.apikey, 'get_gists')
2211 response = api_call(self, params)
2211 response = api_call(self, params)
2212 expected = response.json
2212 expected = response.json
2213 assert len(response.json['result']) == 2
2213 assert len(response.json['result']) == 2
2214 #self._compare_ok(id_, expected, given=response.body)
2214 #self._compare_ok(id_, expected, given=response.body)
2215
2215
2216 def test_api_get_gists_regular_user(self):
2216 def test_api_get_gists_regular_user(self):
2217 # by admin
2217 # by admin
2218 fixture.create_gist()
2218 fixture.create_gist()
2219 fixture.create_gist()
2219 fixture.create_gist()
2220
2220
2221 # by reg user
2221 # by reg user
2222 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2222 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2223 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2223 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2224 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2224 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2225
2225
2226 id_, params = _build_data(self.apikey_regular, 'get_gists')
2226 id_, params = _build_data(self.apikey_regular, 'get_gists')
2227 response = api_call(self, params)
2227 response = api_call(self, params)
2228 expected = response.json
2228 expected = response.json
2229 assert len(response.json['result']) == 3
2229 assert len(response.json['result']) == 3
2230 #self._compare_ok(id_, expected, given=response.body)
2230 #self._compare_ok(id_, expected, given=response.body)
2231
2231
2232 def test_api_get_gists_only_for_regular_user(self):
2232 def test_api_get_gists_only_for_regular_user(self):
2233 # by admin
2233 # by admin
2234 fixture.create_gist()
2234 fixture.create_gist()
2235 fixture.create_gist()
2235 fixture.create_gist()
2236
2236
2237 # by reg user
2237 # by reg user
2238 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2238 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2239 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2239 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2240 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2240 fixture.create_gist(owner=self.TEST_USER_LOGIN)
2241
2241
2242 id_, params = _build_data(self.apikey, 'get_gists',
2242 id_, params = _build_data(self.apikey, 'get_gists',
2243 userid=self.TEST_USER_LOGIN)
2243 userid=self.TEST_USER_LOGIN)
2244 response = api_call(self, params)
2244 response = api_call(self, params)
2245 expected = response.json
2245 expected = response.json
2246 assert len(response.json['result']) == 3
2246 assert len(response.json['result']) == 3
2247 #self._compare_ok(id_, expected, given=response.body)
2247 #self._compare_ok(id_, expected, given=response.body)
2248
2248
2249 def test_api_get_gists_regular_user_with_different_userid(self):
2249 def test_api_get_gists_regular_user_with_different_userid(self):
2250 id_, params = _build_data(self.apikey_regular, 'get_gists',
2250 id_, params = _build_data(self.apikey_regular, 'get_gists',
2251 userid=base.TEST_USER_ADMIN_LOGIN)
2251 userid=base.TEST_USER_ADMIN_LOGIN)
2252 response = api_call(self, params)
2252 response = api_call(self, params)
2253 expected = 'userid is not the same as your user'
2253 expected = 'userid is not the same as your user'
2254 self._compare_error(id_, expected, given=response.body)
2254 self._compare_error(id_, expected, given=response.body)
2255
2255
2256 def test_api_create_gist(self):
2256 def test_api_create_gist(self):
2257 id_, params = _build_data(self.apikey_regular, 'create_gist',
2257 id_, params = _build_data(self.apikey_regular, 'create_gist',
2258 lifetime=10,
2258 lifetime=10,
2259 description='foobar-gist',
2259 description='foobar-gist',
2260 gist_type='public',
2260 gist_type='public',
2261 files={'foobar': {'content': 'foo'}})
2261 files={'foobar': {'content': 'foo'}})
2262 response = api_call(self, params)
2262 response = api_call(self, params)
2263 expected = {
2263 expected = {
2264 'gist': {
2264 'gist': {
2265 'access_id': response.json['result']['gist']['access_id'],
2265 'access_id': response.json['result']['gist']['access_id'],
2266 'created_on': response.json['result']['gist']['created_on'],
2266 'created_on': response.json['result']['gist']['created_on'],
2267 'description': 'foobar-gist',
2267 'description': 'foobar-gist',
2268 'expires': response.json['result']['gist']['expires'],
2268 'expires': response.json['result']['gist']['expires'],
2269 'gist_id': response.json['result']['gist']['gist_id'],
2269 'gist_id': response.json['result']['gist']['gist_id'],
2270 'type': 'public',
2270 'type': 'public',
2271 'url': response.json['result']['gist']['url']
2271 'url': response.json['result']['gist']['url']
2272 },
2272 },
2273 'msg': 'created new gist'
2273 'msg': 'created new gist'
2274 }
2274 }
2275 self._compare_ok(id_, expected, given=response.body)
2275 self._compare_ok(id_, expected, given=response.body)
2276
2276
2277 @mock.patch.object(GistModel, 'create', raise_exception)
2277 @mock.patch.object(GistModel, 'create', raise_exception)
2278 def test_api_create_gist_exception_occurred(self):
2278 def test_api_create_gist_exception_occurred(self):
2279 id_, params = _build_data(self.apikey_regular, 'create_gist',
2279 id_, params = _build_data(self.apikey_regular, 'create_gist',
2280 files={})
2280 files={})
2281 response = api_call(self, params)
2281 response = api_call(self, params)
2282 expected = 'failed to create gist'
2282 expected = 'failed to create gist'
2283 self._compare_error(id_, expected, given=response.body)
2283 self._compare_error(id_, expected, given=response.body)
2284
2284
2285 def test_api_delete_gist(self):
2285 def test_api_delete_gist(self):
2286 gist_id = fixture.create_gist().gist_access_id
2286 gist_id = fixture.create_gist().gist_access_id
2287 id_, params = _build_data(self.apikey, 'delete_gist',
2287 id_, params = _build_data(self.apikey, 'delete_gist',
2288 gistid=gist_id)
2288 gistid=gist_id)
2289 response = api_call(self, params)
2289 response = api_call(self, params)
2290 expected = {'gist': None, 'msg': 'deleted gist ID:%s' % gist_id}
2290 expected = {'gist': None, 'msg': 'deleted gist ID:%s' % gist_id}
2291 self._compare_ok(id_, expected, given=response.body)
2291 self._compare_ok(id_, expected, given=response.body)
2292
2292
2293 def test_api_delete_gist_regular_user(self):
2293 def test_api_delete_gist_regular_user(self):
2294 gist_id = fixture.create_gist(owner=self.TEST_USER_LOGIN).gist_access_id
2294 gist_id = fixture.create_gist(owner=self.TEST_USER_LOGIN).gist_access_id
2295 id_, params = _build_data(self.apikey_regular, 'delete_gist',
2295 id_, params = _build_data(self.apikey_regular, 'delete_gist',
2296 gistid=gist_id)
2296 gistid=gist_id)
2297 response = api_call(self, params)
2297 response = api_call(self, params)
2298 expected = {'gist': None, 'msg': 'deleted gist ID:%s' % gist_id}
2298 expected = {'gist': None, 'msg': 'deleted gist ID:%s' % gist_id}
2299 self._compare_ok(id_, expected, given=response.body)
2299 self._compare_ok(id_, expected, given=response.body)
2300
2300
2301 def test_api_delete_gist_regular_user_no_permission(self):
2301 def test_api_delete_gist_regular_user_no_permission(self):
2302 gist_id = fixture.create_gist().gist_access_id
2302 gist_id = fixture.create_gist().gist_access_id
2303 id_, params = _build_data(self.apikey_regular, 'delete_gist',
2303 id_, params = _build_data(self.apikey_regular, 'delete_gist',
2304 gistid=gist_id)
2304 gistid=gist_id)
2305 response = api_call(self, params)
2305 response = api_call(self, params)
2306 expected = 'gist `%s` does not exist' % (gist_id,)
2306 expected = 'gist `%s` does not exist' % (gist_id,)
2307 self._compare_error(id_, expected, given=response.body)
2307 self._compare_error(id_, expected, given=response.body)
2308
2308
2309 @mock.patch.object(GistModel, 'delete', raise_exception)
2309 @mock.patch.object(GistModel, 'delete', raise_exception)
2310 def test_api_delete_gist_exception_occurred(self):
2310 def test_api_delete_gist_exception_occurred(self):
2311 gist_id = fixture.create_gist().gist_access_id
2311 gist_id = fixture.create_gist().gist_access_id
2312 id_, params = _build_data(self.apikey, 'delete_gist',
2312 id_, params = _build_data(self.apikey, 'delete_gist',
2313 gistid=gist_id)
2313 gistid=gist_id)
2314 response = api_call(self, params)
2314 response = api_call(self, params)
2315 expected = 'failed to delete gist ID:%s' % (gist_id,)
2315 expected = 'failed to delete gist ID:%s' % (gist_id,)
2316 self._compare_error(id_, expected, given=response.body)
2316 self._compare_error(id_, expected, given=response.body)
2317
2317
2318 def test_api_get_ip(self):
2318 def test_api_get_ip(self):
2319 id_, params = _build_data(self.apikey, 'get_ip')
2319 id_, params = _build_data(self.apikey, 'get_ip')
2320 response = api_call(self, params)
2320 response = api_call(self, params)
2321 expected = {
2321 expected = {
2322 'server_ip_addr': '0.0.0.0',
2322 'server_ip_addr': '0.0.0.0',
2323 'user_ips': []
2323 'user_ips': []
2324 }
2324 }
2325 self._compare_ok(id_, expected, given=response.body)
2325 self._compare_ok(id_, expected, given=response.body)
2326
2326
2327 def test_api_get_server_info(self):
2327 def test_api_get_server_info(self):
2328 id_, params = _build_data(self.apikey, 'get_server_info')
2328 id_, params = _build_data(self.apikey, 'get_server_info')
2329 response = api_call(self, params)
2329 response = api_call(self, params)
2330 expected = db.Setting.get_server_info()
2330 expected = db.Setting.get_server_info()
2331 self._compare_ok(id_, expected, given=response.body)
2331 self._compare_ok(id_, expected, given=response.body)
2332
2332
2333 def test_api_get_changesets(self):
2333 def test_api_get_changesets(self):
2334 id_, params = _build_data(self.apikey, 'get_changesets',
2334 id_, params = _build_data(self.apikey, 'get_changesets',
2335 repoid=self.REPO, start=0, end=2)
2335 repoid=self.REPO, start=0, end=2)
2336 response = api_call(self, params)
2336 response = api_call(self, params)
2337 result = ext_json.loads(response.body)["result"]
2337 result = ext_json.loads(response.body)["result"]
2338 assert len(result) == 3
2338 assert len(result) == 3
2339 assert 'message' in result[0]
2339 assert 'message' in result[0]
2340 assert 'added' not in result[0]
2340 assert 'added' not in result[0]
2341
2341
2342 def test_api_get_changesets_with_max_revisions(self):
2342 def test_api_get_changesets_with_max_revisions(self):
2343 id_, params = _build_data(self.apikey, 'get_changesets',
2343 id_, params = _build_data(self.apikey, 'get_changesets',
2344 repoid=self.REPO, start_date="2011-02-24T00:00:00", max_revisions=10)
2344 repoid=self.REPO, start_date="2011-02-24T00:00:00", max_revisions=10)
2345 response = api_call(self, params)
2345 response = api_call(self, params)
2346 result = ext_json.loads(response.body)["result"]
2346 result = ext_json.loads(response.body)["result"]
2347 assert len(result) == 10
2347 assert len(result) == 10
2348 assert 'message' in result[0]
2348 assert 'message' in result[0]
2349 assert 'added' not in result[0]
2349 assert 'added' not in result[0]
2350
2350
2351 def test_api_get_changesets_with_branch(self):
2351 def test_api_get_changesets_with_branch(self):
2352 if self.REPO == 'vcs_test_hg':
2352 if self.REPO == 'vcs_test_hg':
2353 branch = 'stable'
2353 branch = 'stable'
2354 else:
2354 else:
2355 pytest.skip("skipping due to missing branches in git test repo")
2355 pytest.skip("skipping due to missing branches in git test repo")
2356 id_, params = _build_data(self.apikey, 'get_changesets',
2356 id_, params = _build_data(self.apikey, 'get_changesets',
2357 repoid=self.REPO, branch_name=branch, start_date="2011-02-24T00:00:00")
2357 repoid=self.REPO, branch_name=branch, start_date="2011-02-24T00:00:00")
2358 response = api_call(self, params)
2358 response = api_call(self, params)
2359 result = ext_json.loads(response.body)["result"]
2359 result = ext_json.loads(response.body)["result"]
2360 assert len(result) == 5
2360 assert len(result) == 5
2361 assert 'message' in result[0]
2361 assert 'message' in result[0]
2362 assert 'added' not in result[0]
2362 assert 'added' not in result[0]
2363
2363
2364 def test_api_get_changesets_with_file_list(self):
2364 def test_api_get_changesets_with_file_list(self):
2365 id_, params = _build_data(self.apikey, 'get_changesets',
2365 id_, params = _build_data(self.apikey, 'get_changesets',
2366 repoid=self.REPO, start_date="2010-04-07T23:30:30", end_date="2010-04-08T00:31:14", with_file_list=True)
2366 repoid=self.REPO, start_date="2010-04-07T23:30:30", end_date="2010-04-08T00:31:14", with_file_list=True)
2367 response = api_call(self, params)
2367 response = api_call(self, params)
2368 result = ext_json.loads(response.body)["result"]
2368 result = ext_json.loads(response.body)["result"]
2369 assert len(result) == 3
2369 assert len(result) == 3
2370 assert 'message' in result[0]
2370 assert 'message' in result[0]
2371 assert 'added' in result[0]
2371 assert 'added' in result[0]
2372
2372
2373 def test_api_get_changeset(self):
2373 def test_api_get_changeset(self):
2374 review = fixture.review_changeset(self.REPO, self.TEST_REVISION, "approved")
2374 review = fixture.review_changeset(self.REPO, self.TEST_REVISION, "approved")
2375 id_, params = _build_data(self.apikey, 'get_changeset',
2375 id_, params = _build_data(self.apikey, 'get_changeset',
2376 repoid=self.REPO, raw_id=self.TEST_REVISION)
2376 repoid=self.REPO, raw_id=self.TEST_REVISION)
2377 response = api_call(self, params)
2377 response = api_call(self, params)
2378 result = ext_json.loads(response.body)["result"]
2378 result = ext_json.loads(response.body)["result"]
2379 assert result["raw_id"] == self.TEST_REVISION
2379 assert result["raw_id"] == self.TEST_REVISION
2380 assert "reviews" not in result
2380 assert "reviews" not in result
2381
2381
2382 def test_api_get_changeset_with_reviews(self):
2382 def test_api_get_changeset_with_reviews(self):
2383 reviewobjs = fixture.review_changeset(self.REPO, self.TEST_REVISION, "approved")
2383 reviewobjs = fixture.review_changeset(self.REPO, self.TEST_REVISION, "approved")
2384 id_, params = _build_data(self.apikey, 'get_changeset',
2384 id_, params = _build_data(self.apikey, 'get_changeset',
2385 repoid=self.REPO, raw_id=self.TEST_REVISION,
2385 repoid=self.REPO, raw_id=self.TEST_REVISION,
2386 with_reviews=True)
2386 with_reviews=True)
2387 response = api_call(self, params)
2387 response = api_call(self, params)
2388 result = ext_json.loads(response.body)["result"]
2388 result = ext_json.loads(response.body)["result"]
2389 assert result["raw_id"] == self.TEST_REVISION
2389 assert result["raw_id"] == self.TEST_REVISION
2390 assert "reviews" in result
2390 assert "reviews" in result
2391 assert len(result["reviews"]) == 1
2391 assert len(result["reviews"]) == 1
2392 review = result["reviews"][0]
2392 review = result["reviews"][0]
2393 expected = {
2393 expected = {
2394 'status': 'approved',
2394 'status': 'approved',
2395 'modified_at': reviewobjs[0].modified_at.replace(microsecond=0).isoformat(),
2395 'modified_at': reviewobjs[0].modified_at.replace(microsecond=0).isoformat(),
2396 'reviewer': 'test_admin',
2396 'reviewer': 'test_admin',
2397 }
2397 }
2398 assert review == expected
2398 assert review == expected
2399
2399
2400 def test_api_get_changeset_that_does_not_exist(self):
2400 def test_api_get_changeset_that_does_not_exist(self):
2401 """ Fetch changeset status for non-existant changeset.
2401 """ Fetch changeset status for non-existant changeset.
2402 revision id is the above git hash used in the test above with the
2402 revision id is the above git hash used in the test above with the
2403 last 3 nibbles replaced with 0xf. Should not exist for git _or_ hg.
2403 last 3 nibbles replaced with 0xf. Should not exist for git _or_ hg.
2404 """
2404 """
2405 id_, params = _build_data(self.apikey, 'get_changeset',
2405 id_, params = _build_data(self.apikey, 'get_changeset',
2406 repoid=self.REPO, raw_id = '7ab37bc680b4aa72c34d07b230c866c28e9fcfff')
2406 repoid=self.REPO, raw_id = '7ab37bc680b4aa72c34d07b230c866c28e9fcfff')
2407 response = api_call(self, params)
2407 response = api_call(self, params)
2408 expected = 'Changeset %s does not exist' % ('7ab37bc680b4aa72c34d07b230c866c28e9fcfff',)
2408 expected = 'Changeset %s does not exist' % ('7ab37bc680b4aa72c34d07b230c866c28e9fcfff',)
2409 self._compare_error(id_, expected, given=response.body)
2409 self._compare_error(id_, expected, given=response.body)
2410
2410
2411 def test_api_get_changeset_without_permission(self):
2411 def test_api_get_changeset_without_permission(self):
2412 review = fixture.review_changeset(self.REPO, self.TEST_REVISION, "approved")
2412 review = fixture.review_changeset(self.REPO, self.TEST_REVISION, "approved")
2413 RepoModel().revoke_user_permission(repo=self.REPO, user=self.TEST_USER_LOGIN)
2413 RepoModel().revoke_user_permission(repo=self.REPO, user=self.TEST_USER_LOGIN)
2414 RepoModel().revoke_user_permission(repo=self.REPO, user="default")
2414 RepoModel().revoke_user_permission(repo=self.REPO, user="default")
2415 id_, params = _build_data(self.apikey_regular, 'get_changeset',
2415 id_, params = _build_data(self.apikey_regular, 'get_changeset',
2416 repoid=self.REPO, raw_id=self.TEST_REVISION)
2416 repoid=self.REPO, raw_id=self.TEST_REVISION)
2417 response = api_call(self, params)
2417 response = api_call(self, params)
2418 expected = 'Access denied to repo %s' % self.REPO
2418 expected = 'Access denied to repo %s' % self.REPO
2419 self._compare_error(id_, expected, given=response.body)
2419 self._compare_error(id_, expected, given=response.body)
2420
2420
2421 def test_api_get_pullrequest(self):
2421 def test_api_get_pullrequest(self):
2422 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'get test')
2422 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'get test')
2423 random_id = random.randrange(1, 9999)
2423 random_id = random.randrange(1, 9999)
2424 params = ascii_bytes(ext_json.dumps({
2424 params = ascii_bytes(ext_json.dumps({
2425 "id": random_id,
2425 "id": random_id,
2426 "api_key": self.apikey,
2426 "api_key": self.apikey,
2427 "method": 'get_pullrequest',
2427 "method": 'get_pullrequest',
2428 "args": {"pullrequest_id": pull_request_id},
2428 "args": {"pullrequest_id": pull_request_id},
2429 }))
2429 }))
2430 response = api_call(self, params)
2430 response = api_call(self, params)
2431 pullrequest = db.PullRequest().get(pull_request_id)
2431 pullrequest = db.PullRequest().get(pull_request_id)
2432 expected = {
2432 expected = {
2433 "status": "new",
2433 "status": "new",
2434 "pull_request_id": pull_request_id,
2434 "pull_request_id": pull_request_id,
2435 "description": "No description",
2435 "description": "No description",
2436 "url": "/%s/pull-request/%s/_/%s" % (self.REPO, pull_request_id, "stable"),
2436 "url": "/%s/pull-request/%s/_/%s" % (self.REPO, pull_request_id, "stable"),
2437 "reviewers": [{"username": "test_regular"}],
2437 "reviewers": [{"username": "test_regular"}],
2438 "org_repo_url": "http://localhost:80/%s" % self.REPO,
2438 "org_repo_url": "http://localhost:80/%s" % self.REPO,
2439 "org_ref_parts": ["branch", "stable", self.TEST_PR_SRC],
2439 "org_ref_parts": ["branch", "stable", self.TEST_PR_SRC],
2440 "other_ref_parts": ["branch", "default", self.TEST_PR_DST],
2440 "other_ref_parts": ["branch", "default", self.TEST_PR_DST],
2441 "comments": [{"username": base.TEST_USER_ADMIN_LOGIN, "text": "",
2441 "comments": [{"username": base.TEST_USER_ADMIN_LOGIN, "text": "",
2442 "comment_id": pullrequest.comments[0].comment_id}],
2442 "comment_id": pullrequest.comments[0].comment_id}],
2443 "owner": base.TEST_USER_ADMIN_LOGIN,
2443 "owner": base.TEST_USER_ADMIN_LOGIN,
2444 "statuses": [{"status": "under_review", "reviewer": base.TEST_USER_ADMIN_LOGIN, "modified_at": "2000-01-01T00:00:00"} for i in range(0, len(self.TEST_PR_REVISIONS))],
2444 "statuses": [{"status": "under_review", "reviewer": base.TEST_USER_ADMIN_LOGIN, "modified_at": "2000-01-01T00:00:00"} for i in range(0, len(self.TEST_PR_REVISIONS))],
2445 "title": "get test",
2445 "title": "get test",
2446 "revisions": self.TEST_PR_REVISIONS,
2446 "revisions": self.TEST_PR_REVISIONS,
2447 "created_on": "2000-01-01T00:00:00",
2447 "created_on": "2000-01-01T00:00:00",
2448 "updated_on": "2000-01-01T00:00:00",
2448 "updated_on": "2000-01-01T00:00:00",
2449 }
2449 }
2450 self._compare_ok(random_id, expected,
2450 self._compare_ok(random_id, expected,
2451 given=re.sub(br"\d\d\d\d\-\d\d\-\d\dT\d\d\:\d\d\:\d\d",
2451 given=re.sub(br"\d\d\d\d\-\d\d\-\d\dT\d\d\:\d\d\:\d\d",
2452 b"2000-01-01T00:00:00", response.body))
2452 b"2000-01-01T00:00:00", response.body))
2453
2453
2454 def test_api_close_pullrequest(self):
2454 def test_api_close_pullrequest(self):
2455 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'close test')
2455 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'close test')
2456 random_id = random.randrange(1, 9999)
2456 random_id = random.randrange(1, 9999)
2457 params = ascii_bytes(ext_json.dumps({
2457 params = ascii_bytes(ext_json.dumps({
2458 "id": random_id,
2458 "id": random_id,
2459 "api_key": self.apikey,
2459 "api_key": self.apikey,
2460 "method": "comment_pullrequest",
2460 "method": "comment_pullrequest",
2461 "args": {"pull_request_id": pull_request_id, "close_pr": True},
2461 "args": {"pull_request_id": pull_request_id, "close_pr": True},
2462 }))
2462 }))
2463 response = api_call(self, params)
2463 response = api_call(self, params)
2464 self._compare_ok(random_id, True, given=response.body)
2464 self._compare_ok(random_id, True, given=response.body)
2465 pullrequest = db.PullRequest().get(pull_request_id)
2465 pullrequest = db.PullRequest().get(pull_request_id)
2466 assert pullrequest.comments[-1].text == ''
2466 assert pullrequest.comments[-1].text == ''
2467 assert pullrequest.status == db.PullRequest.STATUS_CLOSED
2467 assert pullrequest.status == db.PullRequest.STATUS_CLOSED
2468 assert pullrequest.is_closed() == True
2468 assert pullrequest.is_closed() == True
2469
2469
2470 def test_api_status_pullrequest(self):
2470 def test_api_status_pullrequest(self):
2471 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, "status test")
2471 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, "status test")
2472
2472
2473 random_id = random.randrange(1, 9999)
2473 random_id = random.randrange(1, 9999)
2474 params = ascii_bytes(ext_json.dumps({
2474 params = ascii_bytes(ext_json.dumps({
2475 "id": random_id,
2475 "id": random_id,
2476 "api_key": db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN).api_key,
2476 "api_key": db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN).api_key,
2477 "method": "comment_pullrequest",
2477 "method": "comment_pullrequest",
2478 "args": {"pull_request_id": pull_request_id, "status": db.ChangesetStatus.STATUS_APPROVED},
2478 "args": {"pull_request_id": pull_request_id, "status": db.ChangesetStatus.STATUS_APPROVED},
2479 }))
2479 }))
2480 response = api_call(self, params)
2480 response = api_call(self, params)
2481 pullrequest = db.PullRequest().get(pull_request_id)
2481 pullrequest = db.PullRequest().get(pull_request_id)
2482 self._compare_error(random_id, "No permission to change pull request status. User needs to be admin, owner or reviewer.", given=response.body)
2482 self._compare_error(random_id, "No permission to change pull request status. User needs to be admin, owner or reviewer.", given=response.body)
2483 assert db.ChangesetStatus.STATUS_UNDER_REVIEW == ChangesetStatusModel().calculate_pull_request_result(pullrequest)[2]
2483 assert db.ChangesetStatus.STATUS_UNDER_REVIEW == ChangesetStatusModel().calculate_pull_request_result(pullrequest)[2]
2484 params = ascii_bytes(ext_json.dumps({
2484 params = ascii_bytes(ext_json.dumps({
2485 "id": random_id,
2485 "id": random_id,
2486 "api_key": db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN).api_key,
2486 "api_key": db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN).api_key,
2487 "method": "comment_pullrequest",
2487 "method": "comment_pullrequest",
2488 "args": {"pull_request_id": pull_request_id, "status": db.ChangesetStatus.STATUS_APPROVED},
2488 "args": {"pull_request_id": pull_request_id, "status": db.ChangesetStatus.STATUS_APPROVED},
2489 }))
2489 }))
2490 response = api_call(self, params)
2490 response = api_call(self, params)
2491 self._compare_ok(random_id, True, given=response.body)
2491 self._compare_ok(random_id, True, given=response.body)
2492 pullrequest = db.PullRequest().get(pull_request_id)
2492 pullrequest = db.PullRequest().get(pull_request_id)
2493 assert db.ChangesetStatus.STATUS_APPROVED == ChangesetStatusModel().calculate_pull_request_result(pullrequest)[2]
2493 assert db.ChangesetStatus.STATUS_APPROVED == ChangesetStatusModel().calculate_pull_request_result(pullrequest)[2]
2494
2494
2495 def test_api_comment_pullrequest(self):
2495 def test_api_comment_pullrequest(self):
2496 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, "comment test")
2496 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, "comment test")
2497 random_id = random.randrange(1, 9999)
2497 random_id = random.randrange(1, 9999)
2498 params = ascii_bytes(ext_json.dumps({
2498 params = ascii_bytes(ext_json.dumps({
2499 "id": random_id,
2499 "id": random_id,
2500 "api_key": self.apikey,
2500 "api_key": self.apikey,
2501 "method": "comment_pullrequest",
2501 "method": "comment_pullrequest",
2502 "args": {"pull_request_id": pull_request_id, "comment_msg": "Looks good to me"},
2502 "args": {"pull_request_id": pull_request_id, "comment_msg": "Looks good to me"},
2503 }))
2503 }))
2504 response = api_call(self, params)
2504 response = api_call(self, params)
2505 self._compare_ok(random_id, True, given=response.body)
2505 self._compare_ok(random_id, True, given=response.body)
2506 pullrequest = db.PullRequest().get(pull_request_id)
2506 pullrequest = db.PullRequest().get(pull_request_id)
2507 assert pullrequest.comments[-1].text == 'Looks good to me'
2507 assert pullrequest.comments[-1].text == 'Looks good to me'
2508
2508
2509 def test_api_edit_reviewers_add_single(self):
2509 def test_api_edit_reviewers_add_single(self):
2510 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2510 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2511 pullrequest = db.PullRequest().get(pull_request_id)
2511 pullrequest = db.PullRequest().get(pull_request_id)
2512 pullrequest.owner = self.test_user
2512 pullrequest.owner = self.test_user
2513 random_id = random.randrange(1, 9999)
2513 random_id = random.randrange(1, 9999)
2514 params = ascii_bytes(ext_json.dumps({
2514 params = ascii_bytes(ext_json.dumps({
2515 "id": random_id,
2515 "id": random_id,
2516 "api_key": self.apikey_regular,
2516 "api_key": self.apikey_regular,
2517 "method": "edit_reviewers",
2517 "method": "edit_reviewers",
2518 "args": {"pull_request_id": pull_request_id, "add": base.TEST_USER_REGULAR2_LOGIN},
2518 "args": {"pull_request_id": pull_request_id, "add": base.TEST_USER_REGULAR2_LOGIN},
2519 }))
2519 }))
2520 response = api_call(self, params)
2520 response = api_call(self, params)
2521 expected = { 'added': [base.TEST_USER_REGULAR2_LOGIN], 'already_present': [], 'removed': [] }
2521 expected = { 'added': [base.TEST_USER_REGULAR2_LOGIN], 'already_present': [], 'removed': [] }
2522
2522
2523 self._compare_ok(random_id, expected, given=response.body)
2523 self._compare_ok(random_id, expected, given=response.body)
2524 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2524 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2525
2525
2526 def test_api_edit_reviewers_add_nonexistent(self):
2526 def test_api_edit_reviewers_add_nonexistent(self):
2527 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2527 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2528 pullrequest = db.PullRequest().get(pull_request_id)
2528 pullrequest = db.PullRequest().get(pull_request_id)
2529 pullrequest.owner = self.test_user
2529 pullrequest.owner = self.test_user
2530 random_id = random.randrange(1, 9999)
2530 random_id = random.randrange(1, 9999)
2531 params = ascii_bytes(ext_json.dumps({
2531 params = ascii_bytes(ext_json.dumps({
2532 "id": random_id,
2532 "id": random_id,
2533 "api_key": self.apikey_regular,
2533 "api_key": self.apikey_regular,
2534 "method": "edit_reviewers",
2534 "method": "edit_reviewers",
2535 "args": {"pull_request_id": pull_request_id, "add": 999},
2535 "args": {"pull_request_id": pull_request_id, "add": 999},
2536 }))
2536 }))
2537 response = api_call(self, params)
2537 response = api_call(self, params)
2538
2538
2539 self._compare_error(random_id, "user `999` does not exist", given=response.body)
2539 self._compare_error(random_id, "user `999` does not exist", given=response.body)
2540
2540
2541 def test_api_edit_reviewers_add_multiple(self):
2541 def test_api_edit_reviewers_add_multiple(self):
2542 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2542 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2543 pullrequest = db.PullRequest().get(pull_request_id)
2543 pullrequest = db.PullRequest().get(pull_request_id)
2544 pullrequest.owner = self.test_user
2544 pullrequest.owner = self.test_user
2545 random_id = random.randrange(1, 9999)
2545 random_id = random.randrange(1, 9999)
2546 params = ascii_bytes(ext_json.dumps({
2546 params = ascii_bytes(ext_json.dumps({
2547 "id": random_id,
2547 "id": random_id,
2548 "api_key": self.apikey_regular,
2548 "api_key": self.apikey_regular,
2549 "method": "edit_reviewers",
2549 "method": "edit_reviewers",
2550 "args": {
2550 "args": {
2551 "pull_request_id": pull_request_id,
2551 "pull_request_id": pull_request_id,
2552 "add": [ self.TEST_USER_LOGIN, base.TEST_USER_REGULAR2_LOGIN ]
2552 "add": [ self.TEST_USER_LOGIN, base.TEST_USER_REGULAR2_LOGIN ]
2553 },
2553 },
2554 }))
2554 }))
2555 response = api_call(self, params)
2555 response = api_call(self, params)
2556 # list order depends on python sorting hash, which is randomized
2556 # list order depends on python sorting hash, which is randomized
2557 assert set(ext_json.loads(response.body)['result']['added']) == set([base.TEST_USER_REGULAR2_LOGIN, self.TEST_USER_LOGIN])
2557 assert set(ext_json.loads(response.body)['result']['added']) == set([base.TEST_USER_REGULAR2_LOGIN, self.TEST_USER_LOGIN])
2558 assert set(ext_json.loads(response.body)['result']['already_present']) == set()
2558 assert set(ext_json.loads(response.body)['result']['already_present']) == set()
2559 assert set(ext_json.loads(response.body)['result']['removed']) == set()
2559 assert set(ext_json.loads(response.body)['result']['removed']) == set()
2560
2560
2561 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2561 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2562 assert db.User.get_by_username(self.TEST_USER_LOGIN) in pullrequest.get_reviewer_users()
2562 assert db.User.get_by_username(self.TEST_USER_LOGIN) in pullrequest.get_reviewer_users()
2563
2563
2564 def test_api_edit_reviewers_add_already_present(self):
2564 def test_api_edit_reviewers_add_already_present(self):
2565 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2565 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2566 pullrequest = db.PullRequest().get(pull_request_id)
2566 pullrequest = db.PullRequest().get(pull_request_id)
2567 pullrequest.owner = self.test_user
2567 pullrequest.owner = self.test_user
2568 random_id = random.randrange(1, 9999)
2568 random_id = random.randrange(1, 9999)
2569 params = ascii_bytes(ext_json.dumps({
2569 params = ascii_bytes(ext_json.dumps({
2570 "id": random_id,
2570 "id": random_id,
2571 "api_key": self.apikey_regular,
2571 "api_key": self.apikey_regular,
2572 "method": "edit_reviewers",
2572 "method": "edit_reviewers",
2573 "args": {
2573 "args": {
2574 "pull_request_id": pull_request_id,
2574 "pull_request_id": pull_request_id,
2575 "add": [ base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR2_LOGIN ]
2575 "add": [ base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR2_LOGIN ]
2576 },
2576 },
2577 }))
2577 }))
2578 response = api_call(self, params)
2578 response = api_call(self, params)
2579 expected = { 'added': [base.TEST_USER_REGULAR2_LOGIN],
2579 expected = { 'added': [base.TEST_USER_REGULAR2_LOGIN],
2580 'already_present': [base.TEST_USER_REGULAR_LOGIN],
2580 'already_present': [base.TEST_USER_REGULAR_LOGIN],
2581 'removed': [],
2581 'removed': [],
2582 }
2582 }
2583
2583
2584 self._compare_ok(random_id, expected, given=response.body)
2584 self._compare_ok(random_id, expected, given=response.body)
2585 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2585 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2586 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2586 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2587
2587
2588 def test_api_edit_reviewers_add_closed(self):
2588 def test_api_edit_reviewers_add_closed(self):
2589 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2589 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2590 pullrequest = db.PullRequest().get(pull_request_id)
2590 pullrequest = db.PullRequest().get(pull_request_id)
2591 pullrequest.owner = self.test_user
2591 pullrequest.owner = self.test_user
2592 PullRequestModel().close_pull_request(pull_request_id)
2592 PullRequestModel().close_pull_request(pull_request_id)
2593 random_id = random.randrange(1, 9999)
2593 random_id = random.randrange(1, 9999)
2594 params = ascii_bytes(ext_json.dumps({
2594 params = ascii_bytes(ext_json.dumps({
2595 "id": random_id,
2595 "id": random_id,
2596 "api_key": self.apikey_regular,
2596 "api_key": self.apikey_regular,
2597 "method": "edit_reviewers",
2597 "method": "edit_reviewers",
2598 "args": {"pull_request_id": pull_request_id, "add": base.TEST_USER_REGULAR2_LOGIN},
2598 "args": {"pull_request_id": pull_request_id, "add": base.TEST_USER_REGULAR2_LOGIN},
2599 }))
2599 }))
2600 response = api_call(self, params)
2600 response = api_call(self, params)
2601 self._compare_error(random_id, "Cannot edit reviewers of a closed pull request.", given=response.body)
2601 self._compare_error(random_id, "Cannot edit reviewers of a closed pull request.", given=response.body)
2602
2602
2603 def test_api_edit_reviewers_add_not_owner(self):
2603 def test_api_edit_reviewers_add_not_owner(self):
2604 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2604 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2605 pullrequest = db.PullRequest().get(pull_request_id)
2605 pullrequest = db.PullRequest().get(pull_request_id)
2606 pullrequest.owner = db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
2606 pullrequest.owner = db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
2607 random_id = random.randrange(1, 9999)
2607 random_id = random.randrange(1, 9999)
2608 params = ascii_bytes(ext_json.dumps({
2608 params = ascii_bytes(ext_json.dumps({
2609 "id": random_id,
2609 "id": random_id,
2610 "api_key": self.apikey_regular,
2610 "api_key": self.apikey_regular,
2611 "method": "edit_reviewers",
2611 "method": "edit_reviewers",
2612 "args": {"pull_request_id": pull_request_id, "add": base.TEST_USER_REGULAR2_LOGIN},
2612 "args": {"pull_request_id": pull_request_id, "add": base.TEST_USER_REGULAR2_LOGIN},
2613 }))
2613 }))
2614 response = api_call(self, params)
2614 response = api_call(self, params)
2615 self._compare_error(random_id, "No permission to edit reviewers of this pull request. User needs to be admin or pull request owner.", given=response.body)
2615 self._compare_error(random_id, "No permission to edit reviewers of this pull request. User needs to be admin or pull request owner.", given=response.body)
2616
2616
2617
2617
2618 def test_api_edit_reviewers_remove_single(self):
2618 def test_api_edit_reviewers_remove_single(self):
2619 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2619 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2620 pullrequest = db.PullRequest().get(pull_request_id)
2620 pullrequest = db.PullRequest().get(pull_request_id)
2621 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2621 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2622
2622
2623 pullrequest.owner = self.test_user
2623 pullrequest.owner = self.test_user
2624 random_id = random.randrange(1, 9999)
2624 random_id = random.randrange(1, 9999)
2625 params = ascii_bytes(ext_json.dumps({
2625 params = ascii_bytes(ext_json.dumps({
2626 "id": random_id,
2626 "id": random_id,
2627 "api_key": self.apikey_regular,
2627 "api_key": self.apikey_regular,
2628 "method": "edit_reviewers",
2628 "method": "edit_reviewers",
2629 "args": {"pull_request_id": pull_request_id, "remove": base.TEST_USER_REGULAR_LOGIN},
2629 "args": {"pull_request_id": pull_request_id, "remove": base.TEST_USER_REGULAR_LOGIN},
2630 }))
2630 }))
2631 response = api_call(self, params)
2631 response = api_call(self, params)
2632
2632
2633 expected = { 'added': [],
2633 expected = { 'added': [],
2634 'already_present': [],
2634 'already_present': [],
2635 'removed': [base.TEST_USER_REGULAR_LOGIN],
2635 'removed': [base.TEST_USER_REGULAR_LOGIN],
2636 }
2636 }
2637 self._compare_ok(random_id, expected, given=response.body)
2637 self._compare_ok(random_id, expected, given=response.body)
2638 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) not in pullrequest.get_reviewer_users()
2638 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) not in pullrequest.get_reviewer_users()
2639
2639
2640 def test_api_edit_reviewers_remove_nonexistent(self):
2640 def test_api_edit_reviewers_remove_nonexistent(self):
2641 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2641 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2642 pullrequest = db.PullRequest().get(pull_request_id)
2642 pullrequest = db.PullRequest().get(pull_request_id)
2643 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2643 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2644
2644
2645 pullrequest.owner = self.test_user
2645 pullrequest.owner = self.test_user
2646 random_id = random.randrange(1, 9999)
2646 random_id = random.randrange(1, 9999)
2647 params = ascii_bytes(ext_json.dumps({
2647 params = ascii_bytes(ext_json.dumps({
2648 "id": random_id,
2648 "id": random_id,
2649 "api_key": self.apikey_regular,
2649 "api_key": self.apikey_regular,
2650 "method": "edit_reviewers",
2650 "method": "edit_reviewers",
2651 "args": {"pull_request_id": pull_request_id, "remove": 999},
2651 "args": {"pull_request_id": pull_request_id, "remove": 999},
2652 }))
2652 }))
2653 response = api_call(self, params)
2653 response = api_call(self, params)
2654
2654
2655 self._compare_error(random_id, "user `999` does not exist", given=response.body)
2655 self._compare_error(random_id, "user `999` does not exist", given=response.body)
2656 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2656 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2657
2657
2658 def test_api_edit_reviewers_remove_nonpresent(self):
2658 def test_api_edit_reviewers_remove_nonpresent(self):
2659 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2659 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2660 pullrequest = db.PullRequest().get(pull_request_id)
2660 pullrequest = db.PullRequest().get(pull_request_id)
2661 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2661 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2662 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) not in pullrequest.get_reviewer_users()
2662 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) not in pullrequest.get_reviewer_users()
2663
2663
2664 pullrequest.owner = self.test_user
2664 pullrequest.owner = self.test_user
2665 random_id = random.randrange(1, 9999)
2665 random_id = random.randrange(1, 9999)
2666 params = ascii_bytes(ext_json.dumps({
2666 params = ascii_bytes(ext_json.dumps({
2667 "id": random_id,
2667 "id": random_id,
2668 "api_key": self.apikey_regular,
2668 "api_key": self.apikey_regular,
2669 "method": "edit_reviewers",
2669 "method": "edit_reviewers",
2670 "args": {"pull_request_id": pull_request_id, "remove": base.TEST_USER_REGULAR2_LOGIN},
2670 "args": {"pull_request_id": pull_request_id, "remove": base.TEST_USER_REGULAR2_LOGIN},
2671 }))
2671 }))
2672 response = api_call(self, params)
2672 response = api_call(self, params)
2673
2673
2674 # NOTE: no explicit indication that removed user was not even a reviewer
2674 # NOTE: no explicit indication that removed user was not even a reviewer
2675 expected = { 'added': [],
2675 expected = { 'added': [],
2676 'already_present': [],
2676 'already_present': [],
2677 'removed': [base.TEST_USER_REGULAR2_LOGIN],
2677 'removed': [base.TEST_USER_REGULAR2_LOGIN],
2678 }
2678 }
2679 self._compare_ok(random_id, expected, given=response.body)
2679 self._compare_ok(random_id, expected, given=response.body)
2680 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2680 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2681 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) not in pullrequest.get_reviewer_users()
2681 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) not in pullrequest.get_reviewer_users()
2682
2682
2683 def test_api_edit_reviewers_remove_multiple(self):
2683 def test_api_edit_reviewers_remove_multiple(self):
2684 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2684 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2685 pullrequest = db.PullRequest().get(pull_request_id)
2685 pullrequest = db.PullRequest().get(pull_request_id)
2686 prr = db.PullRequestReviewer(db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN), pullrequest)
2686 prr = db.PullRequestReviewer(db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN), pullrequest)
2687 meta.Session().add(prr)
2687 meta.Session().add(prr)
2688 meta.Session().commit()
2688 meta.Session().commit()
2689
2689
2690 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2690 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2691 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2691 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2692
2692
2693 pullrequest.owner = self.test_user
2693 pullrequest.owner = self.test_user
2694 random_id = random.randrange(1, 9999)
2694 random_id = random.randrange(1, 9999)
2695 params = ascii_bytes(ext_json.dumps({
2695 params = ascii_bytes(ext_json.dumps({
2696 "id": random_id,
2696 "id": random_id,
2697 "api_key": self.apikey_regular,
2697 "api_key": self.apikey_regular,
2698 "method": "edit_reviewers",
2698 "method": "edit_reviewers",
2699 "args": {"pull_request_id": pull_request_id, "remove": [ base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR2_LOGIN ] },
2699 "args": {"pull_request_id": pull_request_id, "remove": [ base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR2_LOGIN ] },
2700 }))
2700 }))
2701 response = api_call(self, params)
2701 response = api_call(self, params)
2702
2702
2703 # list order depends on python sorting hash, which is randomized
2703 # list order depends on python sorting hash, which is randomized
2704 assert set(ext_json.loads(response.body)['result']['added']) == set()
2704 assert set(ext_json.loads(response.body)['result']['added']) == set()
2705 assert set(ext_json.loads(response.body)['result']['already_present']) == set()
2705 assert set(ext_json.loads(response.body)['result']['already_present']) == set()
2706 assert set(ext_json.loads(response.body)['result']['removed']) == set([base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR2_LOGIN])
2706 assert set(ext_json.loads(response.body)['result']['removed']) == set([base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_REGULAR2_LOGIN])
2707 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) not in pullrequest.get_reviewer_users()
2707 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) not in pullrequest.get_reviewer_users()
2708 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) not in pullrequest.get_reviewer_users()
2708 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) not in pullrequest.get_reviewer_users()
2709
2709
2710 def test_api_edit_reviewers_remove_closed(self):
2710 def test_api_edit_reviewers_remove_closed(self):
2711 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2711 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2712 pullrequest = db.PullRequest().get(pull_request_id)
2712 pullrequest = db.PullRequest().get(pull_request_id)
2713 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2713 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2714 PullRequestModel().close_pull_request(pull_request_id)
2714 PullRequestModel().close_pull_request(pull_request_id)
2715
2715
2716 pullrequest.owner = self.test_user
2716 pullrequest.owner = self.test_user
2717 random_id = random.randrange(1, 9999)
2717 random_id = random.randrange(1, 9999)
2718 params = ascii_bytes(ext_json.dumps({
2718 params = ascii_bytes(ext_json.dumps({
2719 "id": random_id,
2719 "id": random_id,
2720 "api_key": self.apikey_regular,
2720 "api_key": self.apikey_regular,
2721 "method": "edit_reviewers",
2721 "method": "edit_reviewers",
2722 "args": {"pull_request_id": pull_request_id, "remove": base.TEST_USER_REGULAR_LOGIN},
2722 "args": {"pull_request_id": pull_request_id, "remove": base.TEST_USER_REGULAR_LOGIN},
2723 }))
2723 }))
2724 response = api_call(self, params)
2724 response = api_call(self, params)
2725
2725
2726 self._compare_error(random_id, "Cannot edit reviewers of a closed pull request.", given=response.body)
2726 self._compare_error(random_id, "Cannot edit reviewers of a closed pull request.", given=response.body)
2727 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2727 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2728
2728
2729 def test_api_edit_reviewers_remove_not_owner(self):
2729 def test_api_edit_reviewers_remove_not_owner(self):
2730 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2730 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2731 pullrequest = db.PullRequest().get(pull_request_id)
2731 pullrequest = db.PullRequest().get(pull_request_id)
2732 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2732 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2733
2733
2734 pullrequest.owner = db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
2734 pullrequest.owner = db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
2735 random_id = random.randrange(1, 9999)
2735 random_id = random.randrange(1, 9999)
2736 params = ascii_bytes(ext_json.dumps({
2736 params = ascii_bytes(ext_json.dumps({
2737 "id": random_id,
2737 "id": random_id,
2738 "api_key": self.apikey_regular,
2738 "api_key": self.apikey_regular,
2739 "method": "edit_reviewers",
2739 "method": "edit_reviewers",
2740 "args": {"pull_request_id": pull_request_id, "remove": base.TEST_USER_REGULAR_LOGIN},
2740 "args": {"pull_request_id": pull_request_id, "remove": base.TEST_USER_REGULAR_LOGIN},
2741 }))
2741 }))
2742 response = api_call(self, params)
2742 response = api_call(self, params)
2743
2743
2744 self._compare_error(random_id, "No permission to edit reviewers of this pull request. User needs to be admin or pull request owner.", given=response.body)
2744 self._compare_error(random_id, "No permission to edit reviewers of this pull request. User needs to be admin or pull request owner.", given=response.body)
2745 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2745 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2746
2746
2747 def test_api_edit_reviewers_add_remove_single(self):
2747 def test_api_edit_reviewers_add_remove_single(self):
2748 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2748 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2749 pullrequest = db.PullRequest().get(pull_request_id)
2749 pullrequest = db.PullRequest().get(pull_request_id)
2750 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2750 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2751 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) not in pullrequest.get_reviewer_users()
2751 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) not in pullrequest.get_reviewer_users()
2752
2752
2753 pullrequest.owner = self.test_user
2753 pullrequest.owner = self.test_user
2754 random_id = random.randrange(1, 9999)
2754 random_id = random.randrange(1, 9999)
2755 params = ascii_bytes(ext_json.dumps({
2755 params = ascii_bytes(ext_json.dumps({
2756 "id": random_id,
2756 "id": random_id,
2757 "api_key": self.apikey_regular,
2757 "api_key": self.apikey_regular,
2758 "method": "edit_reviewers",
2758 "method": "edit_reviewers",
2759 "args": {"pull_request_id": pull_request_id,
2759 "args": {"pull_request_id": pull_request_id,
2760 "add": base.TEST_USER_REGULAR2_LOGIN,
2760 "add": base.TEST_USER_REGULAR2_LOGIN,
2761 "remove": base.TEST_USER_REGULAR_LOGIN
2761 "remove": base.TEST_USER_REGULAR_LOGIN
2762 },
2762 },
2763 }))
2763 }))
2764 response = api_call(self, params)
2764 response = api_call(self, params)
2765
2765
2766 expected = { 'added': [base.TEST_USER_REGULAR2_LOGIN],
2766 expected = { 'added': [base.TEST_USER_REGULAR2_LOGIN],
2767 'already_present': [],
2767 'already_present': [],
2768 'removed': [base.TEST_USER_REGULAR_LOGIN],
2768 'removed': [base.TEST_USER_REGULAR_LOGIN],
2769 }
2769 }
2770 self._compare_ok(random_id, expected, given=response.body)
2770 self._compare_ok(random_id, expected, given=response.body)
2771 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) not in pullrequest.get_reviewer_users()
2771 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) not in pullrequest.get_reviewer_users()
2772 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2772 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2773
2773
2774 def test_api_edit_reviewers_add_remove_multiple(self):
2774 def test_api_edit_reviewers_add_remove_multiple(self):
2775 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2775 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2776 pullrequest = db.PullRequest().get(pull_request_id)
2776 pullrequest = db.PullRequest().get(pull_request_id)
2777 prr = db.PullRequestReviewer(db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN), pullrequest)
2777 prr = db.PullRequestReviewer(db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN), pullrequest)
2778 meta.Session().add(prr)
2778 meta.Session().add(prr)
2779 meta.Session().commit()
2779 meta.Session().commit()
2780 assert db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN) in pullrequest.get_reviewer_users()
2780 assert db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN) in pullrequest.get_reviewer_users()
2781 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2781 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2782 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) not in pullrequest.get_reviewer_users()
2782 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) not in pullrequest.get_reviewer_users()
2783
2783
2784 pullrequest.owner = self.test_user
2784 pullrequest.owner = self.test_user
2785 random_id = random.randrange(1, 9999)
2785 random_id = random.randrange(1, 9999)
2786 params = ascii_bytes(ext_json.dumps({
2786 params = ascii_bytes(ext_json.dumps({
2787 "id": random_id,
2787 "id": random_id,
2788 "api_key": self.apikey_regular,
2788 "api_key": self.apikey_regular,
2789 "method": "edit_reviewers",
2789 "method": "edit_reviewers",
2790 "args": {"pull_request_id": pull_request_id,
2790 "args": {"pull_request_id": pull_request_id,
2791 "add": [ base.TEST_USER_REGULAR2_LOGIN ],
2791 "add": [ base.TEST_USER_REGULAR2_LOGIN ],
2792 "remove": [ base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_ADMIN_LOGIN ],
2792 "remove": [ base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_ADMIN_LOGIN ],
2793 },
2793 },
2794 }))
2794 }))
2795 response = api_call(self, params)
2795 response = api_call(self, params)
2796
2796
2797 # list order depends on python sorting hash, which is randomized
2797 # list order depends on python sorting hash, which is randomized
2798 assert set(ext_json.loads(response.body)['result']['added']) == set([base.TEST_USER_REGULAR2_LOGIN])
2798 assert set(ext_json.loads(response.body)['result']['added']) == set([base.TEST_USER_REGULAR2_LOGIN])
2799 assert set(ext_json.loads(response.body)['result']['already_present']) == set()
2799 assert set(ext_json.loads(response.body)['result']['already_present']) == set()
2800 assert set(ext_json.loads(response.body)['result']['removed']) == set([base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_ADMIN_LOGIN])
2800 assert set(ext_json.loads(response.body)['result']['removed']) == set([base.TEST_USER_REGULAR_LOGIN, base.TEST_USER_ADMIN_LOGIN])
2801 assert db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN) not in pullrequest.get_reviewer_users()
2801 assert db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN) not in pullrequest.get_reviewer_users()
2802 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) not in pullrequest.get_reviewer_users()
2802 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) not in pullrequest.get_reviewer_users()
2803 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2803 assert db.User.get_by_username(base.TEST_USER_REGULAR2_LOGIN) in pullrequest.get_reviewer_users()
2804
2804
2805 def test_api_edit_reviewers_invalid_params(self):
2805 def test_api_edit_reviewers_invalid_params(self):
2806 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2806 pull_request_id = fixture.create_pullrequest(self, self.REPO, self.TEST_PR_SRC, self.TEST_PR_DST, 'edit reviewer test')
2807 pullrequest = db.PullRequest().get(pull_request_id)
2807 pullrequest = db.PullRequest().get(pull_request_id)
2808 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2808 assert db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN) in pullrequest.get_reviewer_users()
2809
2809
2810 pullrequest.owner = db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
2810 pullrequest.owner = db.User.get_by_username(base.TEST_USER_REGULAR_LOGIN)
2811 random_id = random.randrange(1, 9999)
2811 random_id = random.randrange(1, 9999)
2812 params = ascii_bytes(ext_json.dumps({
2812 params = ascii_bytes(ext_json.dumps({
2813 "id": random_id,
2813 "id": random_id,
2814 "api_key": self.apikey_regular,
2814 "api_key": self.apikey_regular,
2815 "method": "edit_reviewers",
2815 "method": "edit_reviewers",
2816 "args": {"pull_request_id": pull_request_id},
2816 "args": {"pull_request_id": pull_request_id},
2817 }))
2817 }))
2818 response = api_call(self, params)
2818 response = api_call(self, params)
2819
2819
2820 self._compare_error(random_id, "Invalid request. Neither 'add' nor 'remove' is specified.", given=response.body)
2820 self._compare_error(random_id, "Invalid request. Neither 'add' nor 'remove' is specified.", given=response.body)
2821 assert ext_json.loads(response.body)['result'] is None
2821 assert ext_json.loads(response.body)['result'] is None
@@ -1,194 +1,194 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
15 import datetime
15 import datetime
16 import logging
16 import logging
17 import os
17 import os
18 import re
18 import re
19 import tempfile
19 import tempfile
20 import time
20 import time
21
21
22 import pytest
22 import pytest
23 from beaker.cache import cache_managers
23 from webtest import TestApp
24 from webtest import TestApp
24
25
25 from kallithea.lib.utils2 import ascii_str
26 from kallithea.lib.utils2 import ascii_str
26 from kallithea.model import db
27 from kallithea.model import db
27
28
28
29
29 log = logging.getLogger(__name__)
30 log = logging.getLogger(__name__)
30
31
31 skipif = pytest.mark.skipif
32 skipif = pytest.mark.skipif
32 parametrize = pytest.mark.parametrize
33 parametrize = pytest.mark.parametrize
33
34
34 # Hack: These module global values MUST be set to actual values before running any tests. This is currently done by conftest.py.
35 # Hack: These module global values MUST be set to actual values before running any tests. This is currently done by conftest.py.
35 url = None
36 url = None
36 testapp = None
37 testapp = None
37
38
38 __all__ = [
39 __all__ = [
39 'skipif', 'parametrize', 'url', 'TestController',
40 'skipif', 'parametrize', 'url', 'TestController',
40 'ldap_lib_installed', 'pam_lib_installed', 'invalidate_all_caches',
41 'ldap_lib_installed', 'pam_lib_installed', 'invalidate_all_caches',
41 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO',
42 'TESTS_TMP_PATH', 'HG_REPO', 'GIT_REPO', 'NEW_HG_REPO', 'NEW_GIT_REPO',
42 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS',
43 'HG_FORK', 'GIT_FORK', 'TEST_USER_ADMIN_LOGIN', 'TEST_USER_ADMIN_PASS',
43 'TEST_USER_ADMIN_EMAIL', 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
44 'TEST_USER_ADMIN_EMAIL', 'TEST_USER_REGULAR_LOGIN', 'TEST_USER_REGULAR_PASS',
44 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
45 'TEST_USER_REGULAR_EMAIL', 'TEST_USER_REGULAR2_LOGIN',
45 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL', 'IP_ADDR',
46 'TEST_USER_REGULAR2_PASS', 'TEST_USER_REGULAR2_EMAIL', 'IP_ADDR',
46 'TEST_HG_REPO', 'TEST_HG_REPO_CLONE', 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO',
47 'TEST_HG_REPO', 'TEST_HG_REPO_CLONE', 'TEST_HG_REPO_PULL', 'TEST_GIT_REPO',
47 'TEST_GIT_REPO_CLONE', 'TEST_GIT_REPO_PULL', 'HG_REMOTE_REPO',
48 'TEST_GIT_REPO_CLONE', 'TEST_GIT_REPO_PULL', 'HG_REMOTE_REPO',
48 'GIT_REMOTE_REPO', 'HG_TEST_REVISION', 'GIT_TEST_REVISION',
49 'GIT_REMOTE_REPO', 'HG_TEST_REVISION', 'GIT_TEST_REVISION',
49 ]
50 ]
50
51
51 ## SOME GLOBALS FOR TESTS
52 ## SOME GLOBALS FOR TESTS
52
53
53 TESTS_TMP_PATH = os.environ.get('KALLITHEA_TESTS_TMP_PATH', tempfile.mkdtemp(prefix='kallithea-test-'))
54 TESTS_TMP_PATH = os.environ.get('KALLITHEA_TESTS_TMP_PATH', tempfile.mkdtemp(prefix='kallithea-test-'))
54
55
55 TEST_USER_ADMIN_LOGIN = 'test_admin'
56 TEST_USER_ADMIN_LOGIN = 'test_admin'
56 TEST_USER_ADMIN_PASS = 'test12'
57 TEST_USER_ADMIN_PASS = 'test12'
57 TEST_USER_ADMIN_EMAIL = 'test_admin@example.com'
58 TEST_USER_ADMIN_EMAIL = 'test_admin@example.com'
58
59
59 TEST_USER_REGULAR_LOGIN = 'test_regular'
60 TEST_USER_REGULAR_LOGIN = 'test_regular'
60 TEST_USER_REGULAR_PASS = 'test12'
61 TEST_USER_REGULAR_PASS = 'test12'
61 TEST_USER_REGULAR_EMAIL = 'test_regular@example.com'
62 TEST_USER_REGULAR_EMAIL = 'test_regular@example.com'
62
63
63 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
64 TEST_USER_REGULAR2_LOGIN = 'test_regular2'
64 TEST_USER_REGULAR2_PASS = 'test12'
65 TEST_USER_REGULAR2_PASS = 'test12'
65 TEST_USER_REGULAR2_EMAIL = 'test_regular2@example.com'
66 TEST_USER_REGULAR2_EMAIL = 'test_regular2@example.com'
66
67
67 IP_ADDR = '127.0.0.127'
68 IP_ADDR = '127.0.0.127'
68
69
69 HG_REPO = 'vcs_test_hg'
70 HG_REPO = 'vcs_test_hg'
70 GIT_REPO = 'vcs_test_git'
71 GIT_REPO = 'vcs_test_git'
71
72
72 NEW_HG_REPO = 'vcs_test_hg_new'
73 NEW_HG_REPO = 'vcs_test_hg_new'
73 NEW_GIT_REPO = 'vcs_test_git_new'
74 NEW_GIT_REPO = 'vcs_test_git_new'
74
75
75 HG_FORK = 'vcs_test_hg_fork'
76 HG_FORK = 'vcs_test_hg_fork'
76 GIT_FORK = 'vcs_test_git_fork'
77 GIT_FORK = 'vcs_test_git_fork'
77
78
78 HG_TEST_REVISION = "a53d9201d4bc278910d416d94941b7ea007ecd52"
79 HG_TEST_REVISION = "a53d9201d4bc278910d416d94941b7ea007ecd52"
79 GIT_TEST_REVISION = "7ab37bc680b4aa72c34d07b230c866c28e9fc204"
80 GIT_TEST_REVISION = "7ab37bc680b4aa72c34d07b230c866c28e9fc204"
80
81
81
82
82 ## VCS
83 ## VCS
83 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
84 uniq_suffix = str(int(time.mktime(datetime.datetime.now().timetuple())))
84
85
85 GIT_REMOTE_REPO = os.path.join(TESTS_TMP_PATH, GIT_REPO)
86 GIT_REMOTE_REPO = os.path.join(TESTS_TMP_PATH, GIT_REPO)
86
87
87 TEST_GIT_REPO = os.path.join(TESTS_TMP_PATH, GIT_REPO)
88 TEST_GIT_REPO = os.path.join(TESTS_TMP_PATH, GIT_REPO)
88 TEST_GIT_REPO_CLONE = os.path.join(TESTS_TMP_PATH, 'vcs-git-clone-%s' % uniq_suffix)
89 TEST_GIT_REPO_CLONE = os.path.join(TESTS_TMP_PATH, 'vcs-git-clone-%s' % uniq_suffix)
89 TEST_GIT_REPO_PULL = os.path.join(TESTS_TMP_PATH, 'vcs-git-pull-%s' % uniq_suffix)
90 TEST_GIT_REPO_PULL = os.path.join(TESTS_TMP_PATH, 'vcs-git-pull-%s' % uniq_suffix)
90
91
91 HG_REMOTE_REPO = os.path.join(TESTS_TMP_PATH, HG_REPO)
92 HG_REMOTE_REPO = os.path.join(TESTS_TMP_PATH, HG_REPO)
92
93
93 TEST_HG_REPO = os.path.join(TESTS_TMP_PATH, HG_REPO)
94 TEST_HG_REPO = os.path.join(TESTS_TMP_PATH, HG_REPO)
94 TEST_HG_REPO_CLONE = os.path.join(TESTS_TMP_PATH, 'vcs-hg-clone-%s' % uniq_suffix)
95 TEST_HG_REPO_CLONE = os.path.join(TESTS_TMP_PATH, 'vcs-hg-clone-%s' % uniq_suffix)
95 TEST_HG_REPO_PULL = os.path.join(TESTS_TMP_PATH, 'vcs-hg-pull-%s' % uniq_suffix)
96 TEST_HG_REPO_PULL = os.path.join(TESTS_TMP_PATH, 'vcs-hg-pull-%s' % uniq_suffix)
96
97
97 # By default, some of the tests will utilise locally available
98 # By default, some of the tests will utilise locally available
98 # repositories stored within tar.gz archives as source for
99 # repositories stored within tar.gz archives as source for
99 # cloning. Should you wish to use some other, remote archive, simply
100 # cloning. Should you wish to use some other, remote archive, simply
100 # uncomment these entries and/or update the URLs to use.
101 # uncomment these entries and/or update the URLs to use.
101 #
102 #
102 # GIT_REMOTE_REPO = 'git://github.com/codeinn/vcs.git'
103 # GIT_REMOTE_REPO = 'git://github.com/codeinn/vcs.git'
103 # HG_REMOTE_REPO = 'http://bitbucket.org/marcinkuzminski/vcs'
104 # HG_REMOTE_REPO = 'http://bitbucket.org/marcinkuzminski/vcs'
104
105
105 # skip ldap tests if LDAP lib is not installed
106 # skip ldap tests if LDAP lib is not installed
106 ldap_lib_installed = False
107 ldap_lib_installed = False
107 try:
108 try:
108 import ldap
109 import ldap
109 ldap.API_VERSION
110 ldap.API_VERSION
110 ldap_lib_installed = True
111 ldap_lib_installed = True
111 except ImportError:
112 except ImportError:
112 # means that python-ldap is not installed
113 # means that python-ldap is not installed
113 pass
114 pass
114
115
115 try:
116 try:
116 import pam
117 import pam
117 pam.PAM_TEXT_INFO
118 pam.PAM_TEXT_INFO
118 pam_lib_installed = True
119 pam_lib_installed = True
119 except ImportError:
120 except ImportError:
120 pam_lib_installed = False
121 pam_lib_installed = False
121
122
122
123
123 def invalidate_all_caches():
124 def invalidate_all_caches():
124 """Invalidate all beaker caches currently configured.
125 """Invalidate all beaker caches currently configured.
125 Useful when manipulating IP permissions in a test and changes need to take
126 Useful when manipulating IP permissions in a test and changes need to take
126 effect immediately.
127 effect immediately.
127 Note: Any use of this function is probably a workaround - it should be
128 Note: Any use of this function is probably a workaround - it should be
128 replaced with a more specific cache invalidation in code or test."""
129 replaced with a more specific cache invalidation in code or test."""
129 from beaker.cache import cache_managers
130 for cache in cache_managers.values():
130 for cache in cache_managers.values():
131 cache.clear()
131 cache.clear()
132
132
133
133
134 class NullHandler(logging.Handler):
134 class NullHandler(logging.Handler):
135 def emit(self, record):
135 def emit(self, record):
136 pass
136 pass
137
137
138
138
139 class TestController(object):
139 class TestController(object):
140 """Pytest-style test controller"""
140 """Pytest-style test controller"""
141
141
142 # Note: pytest base classes cannot have an __init__ method
142 # Note: pytest base classes cannot have an __init__ method
143
143
144 @pytest.fixture(autouse=True)
144 @pytest.fixture(autouse=True)
145 def app_fixture(self):
145 def app_fixture(self):
146 h = NullHandler()
146 h = NullHandler()
147 logging.getLogger("kallithea").addHandler(h)
147 logging.getLogger("kallithea").addHandler(h)
148 self.app = TestApp(testapp)
148 self.app = TestApp(testapp)
149 return self.app
149 return self.app
150
150
151 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
151 def log_user(self, username=TEST_USER_ADMIN_LOGIN,
152 password=TEST_USER_ADMIN_PASS):
152 password=TEST_USER_ADMIN_PASS):
153 self._logged_username = username
153 self._logged_username = username
154 response = self.app.post(url(controller='login', action='index'),
154 response = self.app.post(url(controller='login', action='index'),
155 {'username': username,
155 {'username': username,
156 'password': password,
156 'password': password,
157 '_session_csrf_secret_token': self.session_csrf_secret_token()})
157 '_session_csrf_secret_token': self.session_csrf_secret_token()})
158
158
159 if b'Invalid username or password' in response.body:
159 if b'Invalid username or password' in response.body:
160 pytest.fail('could not login using %s %s' % (username, password))
160 pytest.fail('could not login using %s %s' % (username, password))
161
161
162 assert response.status == '302 Found'
162 assert response.status == '302 Found'
163 self.assert_authenticated_user(response, username)
163 self.assert_authenticated_user(response, username)
164
164
165 response = response.follow()
165 response = response.follow()
166 return response.session['authuser']
166 return response.session['authuser']
167
167
168 def _get_logged_user(self):
168 def _get_logged_user(self):
169 return db.User.get_by_username(self._logged_username)
169 return db.User.get_by_username(self._logged_username)
170
170
171 def assert_authenticated_user(self, response, expected_username):
171 def assert_authenticated_user(self, response, expected_username):
172 cookie = response.session.get('authuser')
172 cookie = response.session.get('authuser')
173 user = cookie and cookie.get('user_id')
173 user = cookie and cookie.get('user_id')
174 user = user and db.User.get(user)
174 user = user and db.User.get(user)
175 user = user and user.username
175 user = user and user.username
176 assert user == expected_username
176 assert user == expected_username
177
177
178 def session_csrf_secret_token(self):
178 def session_csrf_secret_token(self):
179 return ascii_str(self.app.get(url('session_csrf_secret_token')).body)
179 return ascii_str(self.app.get(url('session_csrf_secret_token')).body)
180
180
181 def checkSessionFlash(self, response, msg=None, skip=0, _matcher=lambda msg, m: msg in m):
181 def checkSessionFlash(self, response, msg=None, skip=0, _matcher=lambda msg, m: msg in m):
182 if 'flash' not in response.session:
182 if 'flash' not in response.session:
183 pytest.fail('msg `%s` not found - session has no flash:\n%s' % (msg, response))
183 pytest.fail('msg `%s` not found - session has no flash:\n%s' % (msg, response))
184 try:
184 try:
185 level, m = response.session['flash'][-1 - skip]
185 level, m = response.session['flash'][-1 - skip]
186 if _matcher(msg, m):
186 if _matcher(msg, m):
187 return
187 return
188 except IndexError:
188 except IndexError:
189 pass
189 pass
190 pytest.fail('msg `%s` not found in session flash (skipping %s): %s' %
190 pytest.fail('msg `%s` not found in session flash (skipping %s): %s' %
191 (msg, skip, ', '.join('`%s`' % m for level, m in response.session['flash'])))
191 (msg, skip, ', '.join('`%s`' % m for level, m in response.session['flash'])))
192
192
193 def checkSessionFlashRegex(self, response, regex, skip=0):
193 def checkSessionFlashRegex(self, response, regex, skip=0):
194 self.checkSessionFlash(response, regex, skip=skip, _matcher=re.search)
194 self.checkSessionFlash(response, regex, skip=skip, _matcher=re.search)
@@ -1,216 +1,216 b''
1 import logging
1 import logging
2 import os
2 import os
3 import sys
3 import sys
4 import time
4 import time
5
5
6 import formencode
6 import formencode
7 import pkg_resources
7 import pkg_resources
8 import pytest
8 import pytest
9 from paste.deploy import loadwsgi
9 from paste.deploy import loadwsgi
10 from pytest_localserver.http import WSGIServer
10 from pytest_localserver.http import WSGIServer
11 from routes.util import URLGenerator
11 from routes.util import URLGenerator
12 from tg.util.webtest import test_context
12 from tg.util.webtest import test_context
13
13
14 import kallithea.tests.base # FIXME: needed for setting testapp instance!!!
14 import kallithea.tests.base # FIXME: needed for setting testapp instance!!!
15 from kallithea.controllers.root import RootController
15 from kallithea.controllers.root import RootController
16 from kallithea.lib import inifile
16 from kallithea.lib import inifile
17 from kallithea.lib.utils import repo2db_mapper
17 from kallithea.lib.utils import repo2db_mapper
18 from kallithea.model import db, meta
18 from kallithea.model import db, meta
19 from kallithea.model.scm import ScmModel
19 from kallithea.model.scm import ScmModel
20 from kallithea.model.user import UserModel
20 from kallithea.model.user import UserModel
21 from kallithea.tests.base import TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH, invalidate_all_caches
21 from kallithea.tests.base import TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH, invalidate_all_caches
22 from kallithea.tests.fixture import create_test_env, create_test_index
22
23
23
24
24 def pytest_configure():
25 def pytest_configure():
25 os.environ['TZ'] = 'UTC'
26 os.environ['TZ'] = 'UTC'
26 if not kallithea.is_windows:
27 if not kallithea.is_windows:
27 time.tzset() # only available on Unix
28 time.tzset() # only available on Unix
28
29
29 path = os.getcwd()
30 path = os.getcwd()
30 sys.path.insert(0, path)
31 sys.path.insert(0, path)
31 pkg_resources.working_set.add_entry(path)
32 pkg_resources.working_set.add_entry(path)
32
33
33 # Disable INFO logging of test database creation, restore with NOTSET
34 # Disable INFO logging of test database creation, restore with NOTSET
34 logging.disable(logging.INFO)
35 logging.disable(logging.INFO)
35
36
36 ini_settings = {
37 ini_settings = {
37 '[server:main]': {
38 '[server:main]': {
38 'port': '4999',
39 'port': '4999',
39 },
40 },
40 '[app:main]': {
41 '[app:main]': {
41 'ssh_enabled': 'true',
42 'ssh_enabled': 'true',
42 # Mainly to safeguard against accidentally overwriting the real one:
43 # Mainly to safeguard against accidentally overwriting the real one:
43 'ssh_authorized_keys': os.path.join(TESTS_TMP_PATH, 'authorized_keys'),
44 'ssh_authorized_keys': os.path.join(TESTS_TMP_PATH, 'authorized_keys'),
44 #'ssh_locale': 'C',
45 #'ssh_locale': 'C',
45 'app_instance_uuid': 'test',
46 'app_instance_uuid': 'test',
46 'show_revision_number': 'true',
47 'show_revision_number': 'true',
47 'session.secret': '{74e0cd75-b339-478b-b129-07dd221def1f}',
48 'session.secret': '{74e0cd75-b339-478b-b129-07dd221def1f}',
48 #'i18n.lang': '',
49 #'i18n.lang': '',
49 },
50 },
50 '[handler_console]': {
51 '[handler_console]': {
51 'formatter': 'color_formatter',
52 'formatter': 'color_formatter',
52 },
53 },
53 # The 'handler_console_sql' block is very similar to the one in
54 # The 'handler_console_sql' block is very similar to the one in
54 # development.ini, but without the explicit 'level=DEBUG' setting:
55 # development.ini, but without the explicit 'level=DEBUG' setting:
55 # it causes duplicate sqlalchemy debug logs, one through
56 # it causes duplicate sqlalchemy debug logs, one through
56 # handler_console_sql and another through another path.
57 # handler_console_sql and another through another path.
57 '[handler_console_sql]': {
58 '[handler_console_sql]': {
58 'formatter': 'color_formatter_sql',
59 'formatter': 'color_formatter_sql',
59 },
60 },
60 }
61 }
61 create_database = os.environ.get('TEST_DB') # TODO: rename to 'CREATE_TEST_DB'
62 create_database = os.environ.get('TEST_DB') # TODO: rename to 'CREATE_TEST_DB'
62 if create_database:
63 if create_database:
63 ini_settings['[app:main]']['sqlalchemy.url'] = create_database
64 ini_settings['[app:main]']['sqlalchemy.url'] = create_database
64 reuse_database = os.environ.get('REUSE_TEST_DB')
65 reuse_database = os.environ.get('REUSE_TEST_DB')
65 if reuse_database:
66 if reuse_database:
66 ini_settings['[app:main]']['sqlalchemy.url'] = reuse_database
67 ini_settings['[app:main]']['sqlalchemy.url'] = reuse_database
67
68
68 test_ini_file = os.path.join(TESTS_TMP_PATH, 'test.ini')
69 test_ini_file = os.path.join(TESTS_TMP_PATH, 'test.ini')
69 inifile.create(test_ini_file, None, ini_settings)
70 inifile.create(test_ini_file, None, ini_settings)
70
71
71 context = loadwsgi.loadcontext(loadwsgi.APP, 'config:%s' % test_ini_file)
72 context = loadwsgi.loadcontext(loadwsgi.APP, 'config:%s' % test_ini_file)
72 from kallithea.tests.fixture import create_test_env, create_test_index
73
73
74 # set KALLITHEA_NO_TMP_PATH=1 to disable re-creating the database and test repos
74 # set KALLITHEA_NO_TMP_PATH=1 to disable re-creating the database and test repos
75 if not int(os.environ.get('KALLITHEA_NO_TMP_PATH', 0)):
75 if not int(os.environ.get('KALLITHEA_NO_TMP_PATH', 0)):
76 create_test_env(TESTS_TMP_PATH, context.config(), reuse_database=bool(reuse_database))
76 create_test_env(TESTS_TMP_PATH, context.config(), reuse_database=bool(reuse_database))
77
77
78 # set KALLITHEA_WHOOSH_TEST_DISABLE=1 to disable whoosh index during tests
78 # set KALLITHEA_WHOOSH_TEST_DISABLE=1 to disable whoosh index during tests
79 if not int(os.environ.get('KALLITHEA_WHOOSH_TEST_DISABLE', 0)):
79 if not int(os.environ.get('KALLITHEA_WHOOSH_TEST_DISABLE', 0)):
80 create_test_index(TESTS_TMP_PATH, context.config(), True)
80 create_test_index(TESTS_TMP_PATH, context.config(), True)
81
81
82 kallithea.tests.base.testapp = context.create()
82 kallithea.tests.base.testapp = context.create()
83 # do initial repo scan
83 # do initial repo scan
84 repo2db_mapper(ScmModel().repo_scan(TESTS_TMP_PATH))
84 repo2db_mapper(ScmModel().repo_scan(TESTS_TMP_PATH))
85
85
86 logging.disable(logging.NOTSET)
86 logging.disable(logging.NOTSET)
87
87
88 kallithea.tests.base.url = URLGenerator(RootController().mapper, {'HTTP_HOST': 'example.com'})
88 kallithea.tests.base.url = URLGenerator(RootController().mapper, {'HTTP_HOST': 'example.com'})
89
89
90 # set fixed language for form messages, regardless of environment settings
90 # set fixed language for form messages, regardless of environment settings
91 formencode.api.set_stdtranslation(languages=[])
91 formencode.api.set_stdtranslation(languages=[])
92
92
93
93
94 @pytest.fixture
94 @pytest.fixture
95 def create_test_user():
95 def create_test_user():
96 """Provide users that automatically disappear after test is over."""
96 """Provide users that automatically disappear after test is over."""
97 test_user_ids = []
97 test_user_ids = []
98
98
99 def _create_test_user(user_form):
99 def _create_test_user(user_form):
100 user = UserModel().create(user_form)
100 user = UserModel().create(user_form)
101 test_user_ids.append(user.user_id)
101 test_user_ids.append(user.user_id)
102 return user
102 return user
103 yield _create_test_user
103 yield _create_test_user
104 for user_id in test_user_ids:
104 for user_id in test_user_ids:
105 UserModel().delete(user_id)
105 UserModel().delete(user_id)
106 meta.Session().commit()
106 meta.Session().commit()
107
107
108
108
109 def _set_settings(*kvtseq):
109 def _set_settings(*kvtseq):
110 session = meta.Session()
110 session = meta.Session()
111 for kvt in kvtseq:
111 for kvt in kvtseq:
112 assert len(kvt) in (2, 3)
112 assert len(kvt) in (2, 3)
113 k = kvt[0]
113 k = kvt[0]
114 v = kvt[1]
114 v = kvt[1]
115 t = kvt[2] if len(kvt) == 3 else 'unicode'
115 t = kvt[2] if len(kvt) == 3 else 'unicode'
116 db.Setting.create_or_update(k, v, t)
116 db.Setting.create_or_update(k, v, t)
117 session.commit()
117 session.commit()
118
118
119
119
120 @pytest.fixture
120 @pytest.fixture
121 def set_test_settings():
121 def set_test_settings():
122 """Restore settings after test is over."""
122 """Restore settings after test is over."""
123 # Save settings.
123 # Save settings.
124 settings_snapshot = [
124 settings_snapshot = [
125 (s.app_settings_name, s.app_settings_value, s.app_settings_type)
125 (s.app_settings_name, s.app_settings_value, s.app_settings_type)
126 for s in db.Setting.query().all()]
126 for s in db.Setting.query().all()]
127 yield _set_settings
127 yield _set_settings
128 # Restore settings.
128 # Restore settings.
129 session = meta.Session()
129 session = meta.Session()
130 keys = frozenset(k for (k, v, t) in settings_snapshot)
130 keys = frozenset(k for (k, v, t) in settings_snapshot)
131 for s in db.Setting.query().all():
131 for s in db.Setting.query().all():
132 if s.app_settings_name not in keys:
132 if s.app_settings_name not in keys:
133 session.delete(s)
133 session.delete(s)
134 for k, v, t in settings_snapshot:
134 for k, v, t in settings_snapshot:
135 if t == 'list' and hasattr(v, '__iter__'):
135 if t == 'list' and hasattr(v, '__iter__'):
136 v = ','.join(v) # Quirk: must format list value manually.
136 v = ','.join(v) # Quirk: must format list value manually.
137 db.Setting.create_or_update(k, v, t)
137 db.Setting.create_or_update(k, v, t)
138 session.commit()
138 session.commit()
139
139
140
140
141 @pytest.fixture
141 @pytest.fixture
142 def auto_clear_ip_permissions():
142 def auto_clear_ip_permissions():
143 """Fixture that provides nothing but clearing IP permissions upon test
143 """Fixture that provides nothing but clearing IP permissions upon test
144 exit. This clearing is needed to avoid other test failing to make fake http
144 exit. This clearing is needed to avoid other test failing to make fake http
145 accesses."""
145 accesses."""
146 yield
146 yield
147 # cleanup
147 # cleanup
148 user_model = UserModel()
148 user_model = UserModel()
149
149
150 user_ids = []
150 user_ids = []
151 user_ids.append(kallithea.DEFAULT_USER_ID)
151 user_ids.append(kallithea.DEFAULT_USER_ID)
152 user_ids.append(db.User.get_by_username(TEST_USER_REGULAR_LOGIN).user_id)
152 user_ids.append(db.User.get_by_username(TEST_USER_REGULAR_LOGIN).user_id)
153
153
154 for user_id in user_ids:
154 for user_id in user_ids:
155 for ip in db.UserIpMap.query().filter(db.UserIpMap.user_id == user_id):
155 for ip in db.UserIpMap.query().filter(db.UserIpMap.user_id == user_id):
156 user_model.delete_extra_ip(user_id, ip.ip_id)
156 user_model.delete_extra_ip(user_id, ip.ip_id)
157
157
158 # IP permissions are cached, need to invalidate this cache explicitly
158 # IP permissions are cached, need to invalidate this cache explicitly
159 invalidate_all_caches()
159 invalidate_all_caches()
160 session = meta.Session()
160 session = meta.Session()
161 session.commit()
161 session.commit()
162
162
163
163
164 @pytest.fixture
164 @pytest.fixture
165 def test_context_fixture(app_fixture):
165 def test_context_fixture(app_fixture):
166 """
166 """
167 Encompass the entire test using this fixture in a test_context,
167 Encompass the entire test using this fixture in a test_context,
168 making sure that certain functionality still works even if no call to
168 making sure that certain functionality still works even if no call to
169 self.app.get/post has been made.
169 self.app.get/post has been made.
170 The typical error message indicating you need a test_context is:
170 The typical error message indicating you need a test_context is:
171 TypeError: No object (name: context) has been registered for this thread
171 TypeError: No object (name: context) has been registered for this thread
172
172
173 The standard way to fix this is simply using the test_context context
173 The standard way to fix this is simply using the test_context context
174 manager directly inside your test:
174 manager directly inside your test:
175 with test_context(self.app):
175 with test_context(self.app):
176 <actions>
176 <actions>
177 but if test setup code (xUnit-style or pytest fixtures) also needs to be
177 but if test setup code (xUnit-style or pytest fixtures) also needs to be
178 executed inside the test context, that method is not possible.
178 executed inside the test context, that method is not possible.
179 Even if there is no such setup code, the fixture may reduce code complexity
179 Even if there is no such setup code, the fixture may reduce code complexity
180 if the entire test needs to run inside a test context.
180 if the entire test needs to run inside a test context.
181
181
182 To apply this fixture (like any other fixture) to all test methods of a
182 To apply this fixture (like any other fixture) to all test methods of a
183 class, use the following class decorator:
183 class, use the following class decorator:
184 @pytest.mark.usefixtures("test_context_fixture")
184 @pytest.mark.usefixtures("test_context_fixture")
185 class TestFoo(TestController):
185 class TestFoo(TestController):
186 ...
186 ...
187 """
187 """
188 with test_context(app_fixture):
188 with test_context(app_fixture):
189 yield
189 yield
190
190
191
191
192 class MyWSGIServer(WSGIServer):
192 class MyWSGIServer(WSGIServer):
193 def repo_url(self, repo_name, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS):
193 def repo_url(self, repo_name, username=TEST_USER_ADMIN_LOGIN, password=TEST_USER_ADMIN_PASS):
194 """Return URL to repo on this web server."""
194 """Return URL to repo on this web server."""
195 host, port = self.server_address
195 host, port = self.server_address
196 proto = 'http' if self._server.ssl_context is None else 'https'
196 proto = 'http' if self._server.ssl_context is None else 'https'
197 auth = ''
197 auth = ''
198 if username is not None:
198 if username is not None:
199 auth = username
199 auth = username
200 if password is not None:
200 if password is not None:
201 auth += ':' + password
201 auth += ':' + password
202 if auth:
202 if auth:
203 auth += '@'
203 auth += '@'
204 return '%s://%s%s:%s/%s' % (proto, auth, host, port, repo_name)
204 return '%s://%s%s:%s/%s' % (proto, auth, host, port, repo_name)
205
205
206
206
207 @pytest.yield_fixture(scope="session")
207 @pytest.yield_fixture(scope="session")
208 def webserver():
208 def webserver():
209 """Start web server while tests are running.
209 """Start web server while tests are running.
210 Useful for debugging and necessary for vcs operation tests."""
210 Useful for debugging and necessary for vcs operation tests."""
211 server = MyWSGIServer(application=kallithea.tests.base.testapp)
211 server = MyWSGIServer(application=kallithea.tests.base.testapp)
212 server.start()
212 server.start()
213
213
214 yield server
214 yield server
215
215
216 server.stop()
216 server.stop()
@@ -1,441 +1,440 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
15 """
15 """
16 Helpers for fixture generation
16 Helpers for fixture generation
17 """
17 """
18
18
19 import logging
19 import logging
20 import os
20 import os
21 import shutil
21 import shutil
22 import tarfile
22 import tarfile
23 from os.path import dirname
23 from os.path import dirname
24
24
25 from tg import request
25 from tg import request
26 from tg.util.webtest import test_context
26 from tg.util.webtest import test_context
27
27
28 from kallithea.lib.auth import AuthUser
28 from kallithea.lib.auth import AuthUser
29 from kallithea.lib.db_manage import DbManage
29 from kallithea.lib.db_manage import DbManage
30 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
31 from kallithea.lib.pidlock import DaemonLock
30 from kallithea.lib.vcs.backends.base import EmptyChangeset
32 from kallithea.lib.vcs.backends.base import EmptyChangeset
31 from kallithea.model import db, meta
33 from kallithea.model import db, meta
32 from kallithea.model.changeset_status import ChangesetStatusModel
34 from kallithea.model.changeset_status import ChangesetStatusModel
33 from kallithea.model.comment import ChangesetCommentsModel
35 from kallithea.model.comment import ChangesetCommentsModel
34 from kallithea.model.gist import GistModel
36 from kallithea.model.gist import GistModel
35 from kallithea.model.pull_request import CreatePullRequestAction # , CreatePullRequestIterationAction, PullRequestModel
37 from kallithea.model.pull_request import CreatePullRequestAction # , CreatePullRequestIterationAction, PullRequestModel
36 from kallithea.model.repo import RepoModel
38 from kallithea.model.repo import RepoModel
37 from kallithea.model.repo_group import RepoGroupModel
39 from kallithea.model.repo_group import RepoGroupModel
38 from kallithea.model.scm import ScmModel
40 from kallithea.model.scm import ScmModel
39 from kallithea.model.user import UserModel
41 from kallithea.model.user import UserModel
40 from kallithea.model.user_group import UserGroupModel
42 from kallithea.model.user_group import UserGroupModel
41 from kallithea.tests.base import (GIT_REPO, HG_REPO, IP_ADDR, TEST_USER_ADMIN_EMAIL, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS, TEST_USER_REGULAR2_EMAIL,
43 from kallithea.tests.base import (GIT_REPO, HG_REPO, IP_ADDR, TEST_USER_ADMIN_EMAIL, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS, TEST_USER_REGULAR2_EMAIL,
42 TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
44 TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
43 TESTS_TMP_PATH, invalidate_all_caches)
45 TESTS_TMP_PATH, invalidate_all_caches)
46 from kallithea.tests.vcs import setup_package
44
47
45
48
46 log = logging.getLogger(__name__)
49 log = logging.getLogger(__name__)
47
50
48 FIXTURES = os.path.join(dirname(dirname(os.path.abspath(__file__))), 'tests', 'fixtures')
51 FIXTURES = os.path.join(dirname(dirname(os.path.abspath(__file__))), 'tests', 'fixtures')
49
52
50
53
51 def raise_exception(*args, **kwargs):
54 def raise_exception(*args, **kwargs):
52 raise Exception('raise_exception raised exception')
55 raise Exception('raise_exception raised exception')
53
56
54
57
55 class Fixture(object):
58 class Fixture(object):
56
59
57 def __init__(self):
60 def __init__(self):
58 pass
61 pass
59
62
60 def anon_access(self, status):
63 def anon_access(self, status):
61 """
64 """
62 Context manager for controlling anonymous access.
65 Context manager for controlling anonymous access.
63 Anon access will be set and committed, but restored again when exiting the block.
66 Anon access will be set and committed, but restored again when exiting the block.
64
67
65 Usage:
68 Usage:
66
69
67 fixture = Fixture()
70 fixture = Fixture()
68 with fixture.anon_access(False):
71 with fixture.anon_access(False):
69 stuff
72 stuff
70 """
73 """
71
74
72 class context(object):
75 class context(object):
73 def __enter__(self):
76 def __enter__(self):
74 anon = db.User.get_default_user()
77 anon = db.User.get_default_user()
75 self._before = anon.active
78 self._before = anon.active
76 anon.active = status
79 anon.active = status
77 meta.Session().commit()
80 meta.Session().commit()
78 invalidate_all_caches()
81 invalidate_all_caches()
79
82
80 def __exit__(self, exc_type, exc_val, exc_tb):
83 def __exit__(self, exc_type, exc_val, exc_tb):
81 anon = db.User.get_default_user()
84 anon = db.User.get_default_user()
82 anon.active = self._before
85 anon.active = self._before
83 meta.Session().commit()
86 meta.Session().commit()
84
87
85 return context()
88 return context()
86
89
87 def _get_repo_create_params(self, **custom):
90 def _get_repo_create_params(self, **custom):
88 """Return form values to be validated through RepoForm"""
91 """Return form values to be validated through RepoForm"""
89 defs = dict(
92 defs = dict(
90 repo_name=None,
93 repo_name=None,
91 repo_type='hg',
94 repo_type='hg',
92 clone_uri='',
95 clone_uri='',
93 repo_group='-1',
96 repo_group='-1',
94 repo_description='DESC',
97 repo_description='DESC',
95 repo_private=False,
98 repo_private=False,
96 repo_landing_rev='rev:tip',
99 repo_landing_rev='rev:tip',
97 repo_copy_permissions=False,
100 repo_copy_permissions=False,
98 repo_state=db.Repository.STATE_CREATED,
101 repo_state=db.Repository.STATE_CREATED,
99 )
102 )
100 defs.update(custom)
103 defs.update(custom)
101 if 'repo_name_full' not in custom:
104 if 'repo_name_full' not in custom:
102 defs.update({'repo_name_full': defs['repo_name']})
105 defs.update({'repo_name_full': defs['repo_name']})
103
106
104 # fix the repo name if passed as repo_name_full
107 # fix the repo name if passed as repo_name_full
105 if defs['repo_name']:
108 if defs['repo_name']:
106 defs['repo_name'] = defs['repo_name'].split('/')[-1]
109 defs['repo_name'] = defs['repo_name'].split('/')[-1]
107
110
108 return defs
111 return defs
109
112
110 def _get_repo_group_create_params(self, **custom):
113 def _get_repo_group_create_params(self, **custom):
111 """Return form values to be validated through RepoGroupForm"""
114 """Return form values to be validated through RepoGroupForm"""
112 defs = dict(
115 defs = dict(
113 group_name=None,
116 group_name=None,
114 group_description='DESC',
117 group_description='DESC',
115 parent_group_id='-1',
118 parent_group_id='-1',
116 perms_updates=[],
119 perms_updates=[],
117 perms_new=[],
120 perms_new=[],
118 recursive=False
121 recursive=False
119 )
122 )
120 defs.update(custom)
123 defs.update(custom)
121
124
122 return defs
125 return defs
123
126
124 def _get_user_create_params(self, name, **custom):
127 def _get_user_create_params(self, name, **custom):
125 defs = dict(
128 defs = dict(
126 username=name,
129 username=name,
127 password='qweqwe',
130 password='qweqwe',
128 email='%s+test@example.com' % name,
131 email='%s+test@example.com' % name,
129 firstname='TestUser',
132 firstname='TestUser',
130 lastname='Test',
133 lastname='Test',
131 active=True,
134 active=True,
132 admin=False,
135 admin=False,
133 extern_type='internal',
136 extern_type='internal',
134 extern_name=None
137 extern_name=None
135 )
138 )
136 defs.update(custom)
139 defs.update(custom)
137
140
138 return defs
141 return defs
139
142
140 def _get_user_group_create_params(self, name, **custom):
143 def _get_user_group_create_params(self, name, **custom):
141 defs = dict(
144 defs = dict(
142 users_group_name=name,
145 users_group_name=name,
143 user_group_description='DESC',
146 user_group_description='DESC',
144 users_group_active=True,
147 users_group_active=True,
145 user_group_data={},
148 user_group_data={},
146 )
149 )
147 defs.update(custom)
150 defs.update(custom)
148
151
149 return defs
152 return defs
150
153
151 def create_repo(self, name, repo_group=None, **kwargs):
154 def create_repo(self, name, repo_group=None, **kwargs):
152 if 'skip_if_exists' in kwargs:
155 if 'skip_if_exists' in kwargs:
153 del kwargs['skip_if_exists']
156 del kwargs['skip_if_exists']
154 r = db.Repository.get_by_repo_name(name)
157 r = db.Repository.get_by_repo_name(name)
155 if r:
158 if r:
156 return r
159 return r
157
160
158 if isinstance(repo_group, db.RepoGroup):
161 if isinstance(repo_group, db.RepoGroup):
159 repo_group = repo_group.group_id
162 repo_group = repo_group.group_id
160
163
161 form_data = self._get_repo_create_params(repo_name=name, **kwargs)
164 form_data = self._get_repo_create_params(repo_name=name, **kwargs)
162 form_data['repo_group'] = repo_group # patch form dict so it can be used directly by model
165 form_data['repo_group'] = repo_group # patch form dict so it can be used directly by model
163 cur_user = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
166 cur_user = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
164 RepoModel().create(form_data, cur_user)
167 RepoModel().create(form_data, cur_user)
165 meta.Session().commit()
168 meta.Session().commit()
166 ScmModel().mark_for_invalidation(name)
169 ScmModel().mark_for_invalidation(name)
167 return db.Repository.get_by_repo_name(name)
170 return db.Repository.get_by_repo_name(name)
168
171
169 def create_fork(self, repo_to_fork, fork_name, **kwargs):
172 def create_fork(self, repo_to_fork, fork_name, **kwargs):
170 repo_to_fork = db.Repository.get_by_repo_name(repo_to_fork)
173 repo_to_fork = db.Repository.get_by_repo_name(repo_to_fork)
171
174
172 form_data = self._get_repo_create_params(repo_name=fork_name,
175 form_data = self._get_repo_create_params(repo_name=fork_name,
173 fork_parent_id=repo_to_fork,
176 fork_parent_id=repo_to_fork,
174 repo_type=repo_to_fork.repo_type,
177 repo_type=repo_to_fork.repo_type,
175 **kwargs)
178 **kwargs)
176 # patch form dict so it can be used directly by model
179 # patch form dict so it can be used directly by model
177 form_data['description'] = form_data['repo_description']
180 form_data['description'] = form_data['repo_description']
178 form_data['private'] = form_data['repo_private']
181 form_data['private'] = form_data['repo_private']
179 form_data['landing_rev'] = form_data['repo_landing_rev']
182 form_data['landing_rev'] = form_data['repo_landing_rev']
180
183
181 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
184 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
182 RepoModel().create_fork(form_data, cur_user=owner)
185 RepoModel().create_fork(form_data, cur_user=owner)
183 meta.Session().commit()
186 meta.Session().commit()
184 ScmModel().mark_for_invalidation(fork_name)
187 ScmModel().mark_for_invalidation(fork_name)
185 r = db.Repository.get_by_repo_name(fork_name)
188 r = db.Repository.get_by_repo_name(fork_name)
186 assert r
189 assert r
187 return r
190 return r
188
191
189 def destroy_repo(self, repo_name, **kwargs):
192 def destroy_repo(self, repo_name, **kwargs):
190 RepoModel().delete(repo_name, **kwargs)
193 RepoModel().delete(repo_name, **kwargs)
191 meta.Session().commit()
194 meta.Session().commit()
192
195
193 def create_repo_group(self, name, parent_group_id=None, **kwargs):
196 def create_repo_group(self, name, parent_group_id=None, **kwargs):
194 assert '/' not in name, (name, kwargs) # use group_parent_id to make nested groups
197 assert '/' not in name, (name, kwargs) # use group_parent_id to make nested groups
195 if 'skip_if_exists' in kwargs:
198 if 'skip_if_exists' in kwargs:
196 del kwargs['skip_if_exists']
199 del kwargs['skip_if_exists']
197 gr = db.RepoGroup.get_by_group_name(group_name=name)
200 gr = db.RepoGroup.get_by_group_name(group_name=name)
198 if gr:
201 if gr:
199 return gr
202 return gr
200 form_data = self._get_repo_group_create_params(group_name=name, **kwargs)
203 form_data = self._get_repo_group_create_params(group_name=name, **kwargs)
201 gr = RepoGroupModel().create(
204 gr = RepoGroupModel().create(
202 group_name=form_data['group_name'],
205 group_name=form_data['group_name'],
203 group_description=form_data['group_name'],
206 group_description=form_data['group_name'],
204 parent=parent_group_id,
207 parent=parent_group_id,
205 owner=kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN),
208 owner=kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN),
206 )
209 )
207 meta.Session().commit()
210 meta.Session().commit()
208 gr = db.RepoGroup.get_by_group_name(gr.group_name)
211 gr = db.RepoGroup.get_by_group_name(gr.group_name)
209 return gr
212 return gr
210
213
211 def destroy_repo_group(self, repogroupid):
214 def destroy_repo_group(self, repogroupid):
212 RepoGroupModel().delete(repogroupid)
215 RepoGroupModel().delete(repogroupid)
213 meta.Session().commit()
216 meta.Session().commit()
214
217
215 def create_user(self, name, **kwargs):
218 def create_user(self, name, **kwargs):
216 if 'skip_if_exists' in kwargs:
219 if 'skip_if_exists' in kwargs:
217 del kwargs['skip_if_exists']
220 del kwargs['skip_if_exists']
218 user = db.User.get_by_username(name)
221 user = db.User.get_by_username(name)
219 if user:
222 if user:
220 return user
223 return user
221 form_data = self._get_user_create_params(name, **kwargs)
224 form_data = self._get_user_create_params(name, **kwargs)
222 user = UserModel().create(form_data)
225 user = UserModel().create(form_data)
223 meta.Session().commit()
226 meta.Session().commit()
224 user = db.User.get_by_username(user.username)
227 user = db.User.get_by_username(user.username)
225 return user
228 return user
226
229
227 def destroy_user(self, userid):
230 def destroy_user(self, userid):
228 UserModel().delete(userid)
231 UserModel().delete(userid)
229 meta.Session().commit()
232 meta.Session().commit()
230
233
231 def create_user_group(self, name, **kwargs):
234 def create_user_group(self, name, **kwargs):
232 if 'skip_if_exists' in kwargs:
235 if 'skip_if_exists' in kwargs:
233 del kwargs['skip_if_exists']
236 del kwargs['skip_if_exists']
234 gr = db.UserGroup.get_by_group_name(group_name=name)
237 gr = db.UserGroup.get_by_group_name(group_name=name)
235 if gr:
238 if gr:
236 return gr
239 return gr
237 form_data = self._get_user_group_create_params(name, **kwargs)
240 form_data = self._get_user_group_create_params(name, **kwargs)
238 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
241 owner = kwargs.get('cur_user', TEST_USER_ADMIN_LOGIN)
239 user_group = UserGroupModel().create(
242 user_group = UserGroupModel().create(
240 name=form_data['users_group_name'],
243 name=form_data['users_group_name'],
241 description=form_data['user_group_description'],
244 description=form_data['user_group_description'],
242 owner=owner, active=form_data['users_group_active'],
245 owner=owner, active=form_data['users_group_active'],
243 group_data=form_data['user_group_data'])
246 group_data=form_data['user_group_data'])
244 meta.Session().commit()
247 meta.Session().commit()
245 user_group = db.UserGroup.get_by_group_name(user_group.users_group_name)
248 user_group = db.UserGroup.get_by_group_name(user_group.users_group_name)
246 return user_group
249 return user_group
247
250
248 def destroy_user_group(self, usergroupid):
251 def destroy_user_group(self, usergroupid):
249 UserGroupModel().delete(user_group=usergroupid, force=True)
252 UserGroupModel().delete(user_group=usergroupid, force=True)
250 meta.Session().commit()
253 meta.Session().commit()
251
254
252 def create_gist(self, **kwargs):
255 def create_gist(self, **kwargs):
253 form_data = {
256 form_data = {
254 'description': 'new-gist',
257 'description': 'new-gist',
255 'owner': TEST_USER_ADMIN_LOGIN,
258 'owner': TEST_USER_ADMIN_LOGIN,
256 'gist_type': db.Gist.GIST_PUBLIC,
259 'gist_type': db.Gist.GIST_PUBLIC,
257 'lifetime': -1,
260 'lifetime': -1,
258 'gist_mapping': {'filename1.txt': {'content': 'hello world'}}
261 'gist_mapping': {'filename1.txt': {'content': 'hello world'}}
259 }
262 }
260 form_data.update(kwargs)
263 form_data.update(kwargs)
261 gist = GistModel().create(
264 gist = GistModel().create(
262 description=form_data['description'], owner=form_data['owner'], ip_addr=IP_ADDR,
265 description=form_data['description'], owner=form_data['owner'], ip_addr=IP_ADDR,
263 gist_mapping=form_data['gist_mapping'], gist_type=form_data['gist_type'],
266 gist_mapping=form_data['gist_mapping'], gist_type=form_data['gist_type'],
264 lifetime=form_data['lifetime']
267 lifetime=form_data['lifetime']
265 )
268 )
266 meta.Session().commit()
269 meta.Session().commit()
267
270
268 return gist
271 return gist
269
272
270 def destroy_gists(self, gistid=None):
273 def destroy_gists(self, gistid=None):
271 for g in db.Gist.query():
274 for g in db.Gist.query():
272 if gistid:
275 if gistid:
273 if gistid == g.gist_access_id:
276 if gistid == g.gist_access_id:
274 GistModel().delete(g)
277 GistModel().delete(g)
275 else:
278 else:
276 GistModel().delete(g)
279 GistModel().delete(g)
277 meta.Session().commit()
280 meta.Session().commit()
278
281
279 def load_resource(self, resource_name, strip=True):
282 def load_resource(self, resource_name, strip=True):
280 with open(os.path.join(FIXTURES, resource_name), 'rb') as f:
283 with open(os.path.join(FIXTURES, resource_name), 'rb') as f:
281 source = f.read()
284 source = f.read()
282 if strip:
285 if strip:
283 source = source.strip()
286 source = source.strip()
284
287
285 return source
288 return source
286
289
287 def commit_change(self, repo, filename, content, message, vcs_type,
290 def commit_change(self, repo, filename, content, message, vcs_type,
288 parent=None, newfile=False, author=None):
291 parent=None, newfile=False, author=None):
289 repo = db.Repository.get_by_repo_name(repo)
292 repo = db.Repository.get_by_repo_name(repo)
290 _cs = parent
293 _cs = parent
291 if parent is None:
294 if parent is None:
292 _cs = EmptyChangeset(alias=vcs_type)
295 _cs = EmptyChangeset(alias=vcs_type)
293 if author is None:
296 if author is None:
294 author = '%s <%s>' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_EMAIL)
297 author = '%s <%s>' % (TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_EMAIL)
295
298
296 if newfile:
299 if newfile:
297 nodes = {
300 nodes = {
298 filename: {
301 filename: {
299 'content': content
302 'content': content
300 }
303 }
301 }
304 }
302 cs = ScmModel().create_nodes(
305 cs = ScmModel().create_nodes(
303 user=TEST_USER_ADMIN_LOGIN,
306 user=TEST_USER_ADMIN_LOGIN,
304 ip_addr=IP_ADDR,
307 ip_addr=IP_ADDR,
305 repo=repo,
308 repo=repo,
306 message=message,
309 message=message,
307 nodes=nodes,
310 nodes=nodes,
308 parent_cs=_cs,
311 parent_cs=_cs,
309 author=author,
312 author=author,
310 )
313 )
311 else:
314 else:
312 cs = ScmModel().commit_change(
315 cs = ScmModel().commit_change(
313 repo=repo.scm_instance, repo_name=repo.repo_name,
316 repo=repo.scm_instance, repo_name=repo.repo_name,
314 cs=parent,
317 cs=parent,
315 user=TEST_USER_ADMIN_LOGIN,
318 user=TEST_USER_ADMIN_LOGIN,
316 ip_addr=IP_ADDR,
319 ip_addr=IP_ADDR,
317 author=author,
320 author=author,
318 message=message,
321 message=message,
319 content=content,
322 content=content,
320 f_path=filename
323 f_path=filename
321 )
324 )
322 return cs
325 return cs
323
326
324 def review_changeset(self, repo, revision, status, author=TEST_USER_ADMIN_LOGIN):
327 def review_changeset(self, repo, revision, status, author=TEST_USER_ADMIN_LOGIN):
325 comment = ChangesetCommentsModel().create("review comment", repo, author, revision=revision, send_email=False)
328 comment = ChangesetCommentsModel().create("review comment", repo, author, revision=revision, send_email=False)
326 csm = ChangesetStatusModel().set_status(repo, db.ChangesetStatus.STATUS_APPROVED, author, comment, revision=revision)
329 csm = ChangesetStatusModel().set_status(repo, db.ChangesetStatus.STATUS_APPROVED, author, comment, revision=revision)
327 meta.Session().commit()
330 meta.Session().commit()
328 return csm
331 return csm
329
332
330 def create_pullrequest(self, testcontroller, repo_name, pr_src_rev, pr_dst_rev, title='title'):
333 def create_pullrequest(self, testcontroller, repo_name, pr_src_rev, pr_dst_rev, title='title'):
331 org_ref = 'branch:stable:%s' % pr_src_rev
334 org_ref = 'branch:stable:%s' % pr_src_rev
332 other_ref = 'branch:default:%s' % pr_dst_rev
335 other_ref = 'branch:default:%s' % pr_dst_rev
333 with test_context(testcontroller.app): # needed to be able to mock request user and routes.url
336 with test_context(testcontroller.app): # needed to be able to mock request user and routes.url
334 org_repo = other_repo = db.Repository.get_by_repo_name(repo_name)
337 org_repo = other_repo = db.Repository.get_by_repo_name(repo_name)
335 owner_user = db.User.get_by_username(TEST_USER_ADMIN_LOGIN)
338 owner_user = db.User.get_by_username(TEST_USER_ADMIN_LOGIN)
336 reviewers = [db.User.get_by_username(TEST_USER_REGULAR_LOGIN)]
339 reviewers = [db.User.get_by_username(TEST_USER_REGULAR_LOGIN)]
337 request.authuser = AuthUser(dbuser=owner_user)
340 request.authuser = AuthUser(dbuser=owner_user)
338 # creating a PR sends a message with an absolute URL - without routing that requires mocking
341 # creating a PR sends a message with an absolute URL - without routing that requires mocking
339 request.environ['routes.url'] = lambda arg, qualified=False, **kwargs: ('https://localhost' if qualified else '') + '/fake/' + arg
342 request.environ['routes.url'] = lambda arg, qualified=False, **kwargs: ('https://localhost' if qualified else '') + '/fake/' + arg
340 cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, 'No description', owner_user, reviewers)
343 cmd = CreatePullRequestAction(org_repo, other_repo, org_ref, other_ref, title, 'No description', owner_user, reviewers)
341 pull_request = cmd.execute()
344 pull_request = cmd.execute()
342 meta.Session().commit()
345 meta.Session().commit()
343 return pull_request.pull_request_id
346 return pull_request.pull_request_id
344
347
345
348
346 #==============================================================================
349 #==============================================================================
347 # Global test environment setup
350 # Global test environment setup
348 #==============================================================================
351 #==============================================================================
349
352
350 def create_test_env(repos_test_path, config, reuse_database):
353 def create_test_env(repos_test_path, config, reuse_database):
351 """
354 """
352 Makes a fresh database and
355 Makes a fresh database and
353 install test repository into tmp dir
356 install test repository into tmp dir
354 """
357 """
355
358
356 # PART ONE create db
359 # PART ONE create db
357 dbconf = config['sqlalchemy.url']
360 dbconf = config['sqlalchemy.url']
358 log.debug('making test db %s', dbconf)
361 log.debug('making test db %s', dbconf)
359
362
360 # create test dir if it doesn't exist
363 # create test dir if it doesn't exist
361 if not os.path.isdir(repos_test_path):
364 if not os.path.isdir(repos_test_path):
362 log.debug('Creating testdir %s', repos_test_path)
365 log.debug('Creating testdir %s', repos_test_path)
363 os.makedirs(repos_test_path)
366 os.makedirs(repos_test_path)
364
367
365 dbmanage = DbManage(dbconf=dbconf, root=config['here'],
368 dbmanage = DbManage(dbconf=dbconf, root=config['here'],
366 cli_args={
369 cli_args={
367 'force_ask': True,
370 'force_ask': True,
368 'username': TEST_USER_ADMIN_LOGIN,
371 'username': TEST_USER_ADMIN_LOGIN,
369 'password': TEST_USER_ADMIN_PASS,
372 'password': TEST_USER_ADMIN_PASS,
370 'email': TEST_USER_ADMIN_EMAIL,
373 'email': TEST_USER_ADMIN_EMAIL,
371 })
374 })
372 dbmanage.create_tables(reuse_database=reuse_database)
375 dbmanage.create_tables(reuse_database=reuse_database)
373 # for tests dynamically set new root paths based on generated content
376 # for tests dynamically set new root paths based on generated content
374 dbmanage.create_settings(dbmanage.prompt_repo_root_path(repos_test_path))
377 dbmanage.create_settings(dbmanage.prompt_repo_root_path(repos_test_path))
375 dbmanage.create_default_user()
378 dbmanage.create_default_user()
376 dbmanage.create_admin_user()
379 dbmanage.create_admin_user()
377 dbmanage.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, TEST_USER_REGULAR_EMAIL, False)
380 dbmanage.create_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, TEST_USER_REGULAR_EMAIL, False)
378 dbmanage.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL, False)
381 dbmanage.create_user(TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL, False)
379 dbmanage.create_permissions()
382 dbmanage.create_permissions()
380 dbmanage.populate_default_permissions()
383 dbmanage.populate_default_permissions()
381 meta.Session().commit()
384 meta.Session().commit()
382 # PART TWO make test repo
385 # PART TWO make test repo
383 log.debug('making test vcs repositories')
386 log.debug('making test vcs repositories')
384
387
385 idx_path = config['index_dir']
388 idx_path = config['index_dir']
386 data_path = config['cache_dir']
389 data_path = config['cache_dir']
387
390
388 # clean index and data
391 # clean index and data
389 if idx_path and os.path.exists(idx_path):
392 if idx_path and os.path.exists(idx_path):
390 log.debug('remove %s', idx_path)
393 log.debug('remove %s', idx_path)
391 shutil.rmtree(idx_path)
394 shutil.rmtree(idx_path)
392
395
393 if data_path and os.path.exists(data_path):
396 if data_path and os.path.exists(data_path):
394 log.debug('remove %s', data_path)
397 log.debug('remove %s', data_path)
395 shutil.rmtree(data_path)
398 shutil.rmtree(data_path)
396
399
397 # CREATE DEFAULT TEST REPOS
400 # CREATE DEFAULT TEST REPOS
398 tar = tarfile.open(os.path.join(FIXTURES, 'vcs_test_hg.tar.gz'))
401 tar = tarfile.open(os.path.join(FIXTURES, 'vcs_test_hg.tar.gz'))
399 tar.extractall(os.path.join(TESTS_TMP_PATH, HG_REPO))
402 tar.extractall(os.path.join(TESTS_TMP_PATH, HG_REPO))
400 tar.close()
403 tar.close()
401
404
402 tar = tarfile.open(os.path.join(FIXTURES, 'vcs_test_git.tar.gz'))
405 tar = tarfile.open(os.path.join(FIXTURES, 'vcs_test_git.tar.gz'))
403 tar.extractall(os.path.join(TESTS_TMP_PATH, GIT_REPO))
406 tar.extractall(os.path.join(TESTS_TMP_PATH, GIT_REPO))
404 tar.close()
407 tar.close()
405
408
406 # LOAD VCS test stuff
409 # LOAD VCS test stuff
407 from kallithea.tests.vcs import setup_package
408 setup_package()
410 setup_package()
409
411
410
412
411 def create_test_index(repo_location, config, full_index):
413 def create_test_index(repo_location, config, full_index):
412 """
414 """
413 Makes default test index
415 Makes default test index
414 """
416 """
415
417
416 from kallithea.lib.indexers.daemon import WhooshIndexingDaemon
417 from kallithea.lib.pidlock import DaemonLock
418
419 index_location = os.path.join(config['index_dir'])
418 index_location = os.path.join(config['index_dir'])
420 if not os.path.exists(index_location):
419 if not os.path.exists(index_location):
421 os.makedirs(index_location)
420 os.makedirs(index_location)
422
421
423 l = DaemonLock(os.path.join(index_location, 'make_index.lock'))
422 l = DaemonLock(os.path.join(index_location, 'make_index.lock'))
424 WhooshIndexingDaemon(index_location=index_location,
423 WhooshIndexingDaemon(index_location=index_location,
425 repo_location=repo_location) \
424 repo_location=repo_location) \
426 .run(full_index=full_index)
425 .run(full_index=full_index)
427 l.release()
426 l.release()
428
427
429
428
430 def failing_test_hook(ui, repo, **kwargs):
429 def failing_test_hook(ui, repo, **kwargs):
431 ui.write(b"failing_test_hook failed\n")
430 ui.write(b"failing_test_hook failed\n")
432 return 1
431 return 1
433
432
434
433
435 def exception_test_hook(ui, repo, **kwargs):
434 def exception_test_hook(ui, repo, **kwargs):
436 raise Exception("exception_test_hook threw an exception")
435 raise Exception("exception_test_hook threw an exception")
437
436
438
437
439 def passing_test_hook(ui, repo, **kwargs):
438 def passing_test_hook(ui, repo, **kwargs):
440 ui.write(b"passing_test_hook succeeded\n")
439 ui.write(b"passing_test_hook succeeded\n")
441 return 0
440 return 0
@@ -1,294 +1,293 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 from tg.util.webtest import test_context
3 from tg.util.webtest import test_context
4
4
5 from kallithea.lib import webutils
5 from kallithea.lib import webutils
6 from kallithea.model import db, meta
6 from kallithea.model import db, meta, validators
7 from kallithea.model.user import UserModel
7 from kallithea.model.user import UserModel
8 from kallithea.tests import base
8 from kallithea.tests import base
9 from kallithea.tests.fixture import Fixture
9 from kallithea.tests.fixture import Fixture
10
10
11
11
12 fixture = Fixture()
12 fixture = Fixture()
13
13
14
14
15 class TestMyAccountController(base.TestController):
15 class TestMyAccountController(base.TestController):
16 test_user_1 = 'testme'
16 test_user_1 = 'testme'
17
17
18 @classmethod
18 @classmethod
19 def teardown_class(cls):
19 def teardown_class(cls):
20 if db.User.get_by_username(cls.test_user_1):
20 if db.User.get_by_username(cls.test_user_1):
21 UserModel().delete(cls.test_user_1)
21 UserModel().delete(cls.test_user_1)
22 meta.Session().commit()
22 meta.Session().commit()
23
23
24 def test_my_account(self):
24 def test_my_account(self):
25 self.log_user()
25 self.log_user()
26 response = self.app.get(base.url('my_account'))
26 response = self.app.get(base.url('my_account'))
27
27
28 response.mustcontain('value="%s' % base.TEST_USER_ADMIN_LOGIN)
28 response.mustcontain('value="%s' % base.TEST_USER_ADMIN_LOGIN)
29
29
30 def test_my_account_my_repos(self):
30 def test_my_account_my_repos(self):
31 self.log_user()
31 self.log_user()
32 response = self.app.get(base.url('my_account_repos'))
32 response = self.app.get(base.url('my_account_repos'))
33 cnt = db.Repository.query().filter(db.Repository.owner ==
33 cnt = db.Repository.query().filter(db.Repository.owner ==
34 db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)).count()
34 db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)).count()
35 response.mustcontain('"raw_name": "%s"' % base.HG_REPO)
35 response.mustcontain('"raw_name": "%s"' % base.HG_REPO)
36 response.mustcontain('"just_name": "%s"' % base.GIT_REPO)
36 response.mustcontain('"just_name": "%s"' % base.GIT_REPO)
37
37
38 def test_my_account_my_watched(self):
38 def test_my_account_my_watched(self):
39 self.log_user()
39 self.log_user()
40 response = self.app.get(base.url('my_account_watched'))
40 response = self.app.get(base.url('my_account_watched'))
41
41
42 cnt = db.UserFollowing.query().filter(db.UserFollowing.user ==
42 cnt = db.UserFollowing.query().filter(db.UserFollowing.user ==
43 db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)).count()
43 db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)).count()
44 response.mustcontain('"raw_name": "%s"' % base.HG_REPO)
44 response.mustcontain('"raw_name": "%s"' % base.HG_REPO)
45 response.mustcontain('"just_name": "%s"' % base.GIT_REPO)
45 response.mustcontain('"just_name": "%s"' % base.GIT_REPO)
46
46
47 def test_my_account_my_emails(self):
47 def test_my_account_my_emails(self):
48 self.log_user()
48 self.log_user()
49 response = self.app.get(base.url('my_account_emails'))
49 response = self.app.get(base.url('my_account_emails'))
50 response.mustcontain('No additional emails specified')
50 response.mustcontain('No additional emails specified')
51
51
52 def test_my_account_my_emails_add_existing_email(self):
52 def test_my_account_my_emails_add_existing_email(self):
53 self.log_user()
53 self.log_user()
54 response = self.app.get(base.url('my_account_emails'))
54 response = self.app.get(base.url('my_account_emails'))
55 response.mustcontain('No additional emails specified')
55 response.mustcontain('No additional emails specified')
56 response = self.app.post(base.url('my_account_emails'),
56 response = self.app.post(base.url('my_account_emails'),
57 {'new_email': base.TEST_USER_REGULAR_EMAIL, '_session_csrf_secret_token': self.session_csrf_secret_token()})
57 {'new_email': base.TEST_USER_REGULAR_EMAIL, '_session_csrf_secret_token': self.session_csrf_secret_token()})
58 self.checkSessionFlash(response, 'This email address is already in use')
58 self.checkSessionFlash(response, 'This email address is already in use')
59
59
60 def test_my_account_my_emails_add_missing_email_in_form(self):
60 def test_my_account_my_emails_add_missing_email_in_form(self):
61 self.log_user()
61 self.log_user()
62 response = self.app.get(base.url('my_account_emails'))
62 response = self.app.get(base.url('my_account_emails'))
63 response.mustcontain('No additional emails specified')
63 response.mustcontain('No additional emails specified')
64 response = self.app.post(base.url('my_account_emails'),
64 response = self.app.post(base.url('my_account_emails'),
65 {'_session_csrf_secret_token': self.session_csrf_secret_token()})
65 {'_session_csrf_secret_token': self.session_csrf_secret_token()})
66 self.checkSessionFlash(response, 'Please enter an email address')
66 self.checkSessionFlash(response, 'Please enter an email address')
67
67
68 def test_my_account_my_emails_add_remove(self):
68 def test_my_account_my_emails_add_remove(self):
69 self.log_user()
69 self.log_user()
70 response = self.app.get(base.url('my_account_emails'))
70 response = self.app.get(base.url('my_account_emails'))
71 response.mustcontain('No additional emails specified')
71 response.mustcontain('No additional emails specified')
72
72
73 response = self.app.post(base.url('my_account_emails'),
73 response = self.app.post(base.url('my_account_emails'),
74 {'new_email': 'barz@example.com', '_session_csrf_secret_token': self.session_csrf_secret_token()})
74 {'new_email': 'barz@example.com', '_session_csrf_secret_token': self.session_csrf_secret_token()})
75
75
76 response = self.app.get(base.url('my_account_emails'))
76 response = self.app.get(base.url('my_account_emails'))
77
77
78 email_id = db.UserEmailMap.query() \
78 email_id = db.UserEmailMap.query() \
79 .filter(db.UserEmailMap.user == db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)) \
79 .filter(db.UserEmailMap.user == db.User.get_by_username(base.TEST_USER_ADMIN_LOGIN)) \
80 .filter(db.UserEmailMap.email == 'barz@example.com').one().email_id
80 .filter(db.UserEmailMap.email == 'barz@example.com').one().email_id
81
81
82 response.mustcontain('barz@example.com')
82 response.mustcontain('barz@example.com')
83 response.mustcontain('<input id="del_email_id" name="del_email_id" type="hidden" value="%s" />' % email_id)
83 response.mustcontain('<input id="del_email_id" name="del_email_id" type="hidden" value="%s" />' % email_id)
84
84
85 response = self.app.post(base.url('my_account_emails_delete'),
85 response = self.app.post(base.url('my_account_emails_delete'),
86 {'del_email_id': email_id, '_session_csrf_secret_token': self.session_csrf_secret_token()})
86 {'del_email_id': email_id, '_session_csrf_secret_token': self.session_csrf_secret_token()})
87 self.checkSessionFlash(response, 'Removed email from user')
87 self.checkSessionFlash(response, 'Removed email from user')
88 response = self.app.get(base.url('my_account_emails'))
88 response = self.app.get(base.url('my_account_emails'))
89 response.mustcontain('No additional emails specified')
89 response.mustcontain('No additional emails specified')
90
90
91
91
92 @base.parametrize('name,attrs',
92 @base.parametrize('name,attrs',
93 [('firstname', {'firstname': 'new_username'}),
93 [('firstname', {'firstname': 'new_username'}),
94 ('lastname', {'lastname': 'new_username'}),
94 ('lastname', {'lastname': 'new_username'}),
95 ('admin', {'admin': True}),
95 ('admin', {'admin': True}),
96 ('admin', {'admin': False}),
96 ('admin', {'admin': False}),
97 ('extern_type', {'extern_type': 'ldap'}),
97 ('extern_type', {'extern_type': 'ldap'}),
98 ('extern_type', {'extern_type': None}),
98 ('extern_type', {'extern_type': None}),
99 #('extern_name', {'extern_name': 'test'}),
99 #('extern_name', {'extern_name': 'test'}),
100 #('extern_name', {'extern_name': None}),
100 #('extern_name', {'extern_name': None}),
101 ('active', {'active': False}),
101 ('active', {'active': False}),
102 ('active', {'active': True}),
102 ('active', {'active': True}),
103 ('email', {'email': 'someemail@example.com'}),
103 ('email', {'email': 'someemail@example.com'}),
104 # ('new_password', {'new_password': 'foobar123',
104 # ('new_password', {'new_password': 'foobar123',
105 # 'password_confirmation': 'foobar123'})
105 # 'password_confirmation': 'foobar123'})
106 ])
106 ])
107 def test_my_account_update(self, name, attrs):
107 def test_my_account_update(self, name, attrs):
108 usr = fixture.create_user(self.test_user_1, password='qweqwe',
108 usr = fixture.create_user(self.test_user_1, password='qweqwe',
109 email='testme@example.com',
109 email='testme@example.com',
110 extern_type='internal',
110 extern_type='internal',
111 extern_name=self.test_user_1,
111 extern_name=self.test_user_1,
112 skip_if_exists=True)
112 skip_if_exists=True)
113 params = usr.get_api_data(True) # current user data
113 params = usr.get_api_data(True) # current user data
114 user_id = usr.user_id
114 user_id = usr.user_id
115 self.log_user(username=self.test_user_1, password='qweqwe')
115 self.log_user(username=self.test_user_1, password='qweqwe')
116
116
117 params.update({'password_confirmation': ''})
117 params.update({'password_confirmation': ''})
118 params.update({'new_password': ''})
118 params.update({'new_password': ''})
119 params.update({'extern_type': 'internal'})
119 params.update({'extern_type': 'internal'})
120 params.update({'extern_name': self.test_user_1})
120 params.update({'extern_name': self.test_user_1})
121 params.update({'_session_csrf_secret_token': self.session_csrf_secret_token()})
121 params.update({'_session_csrf_secret_token': self.session_csrf_secret_token()})
122
122
123 params.update(attrs)
123 params.update(attrs)
124 response = self.app.post(base.url('my_account'), params)
124 response = self.app.post(base.url('my_account'), params)
125
125
126 self.checkSessionFlash(response,
126 self.checkSessionFlash(response,
127 'Your account was updated successfully')
127 'Your account was updated successfully')
128
128
129 updated_user = db.User.get_by_username(self.test_user_1)
129 updated_user = db.User.get_by_username(self.test_user_1)
130 updated_params = updated_user.get_api_data(True)
130 updated_params = updated_user.get_api_data(True)
131 updated_params.update({'password_confirmation': ''})
131 updated_params.update({'password_confirmation': ''})
132 updated_params.update({'new_password': ''})
132 updated_params.update({'new_password': ''})
133
133
134 params['last_login'] = updated_params['last_login']
134 params['last_login'] = updated_params['last_login']
135 if name == 'email':
135 if name == 'email':
136 params['emails'] = [attrs['email']]
136 params['emails'] = [attrs['email']]
137 if name == 'extern_type':
137 if name == 'extern_type':
138 # cannot update this via form, expected value is original one
138 # cannot update this via form, expected value is original one
139 params['extern_type'] = "internal"
139 params['extern_type'] = "internal"
140 if name == 'extern_name':
140 if name == 'extern_name':
141 # cannot update this via form, expected value is original one
141 # cannot update this via form, expected value is original one
142 params['extern_name'] = str(user_id)
142 params['extern_name'] = str(user_id)
143 if name == 'active':
143 if name == 'active':
144 # my account cannot deactivate account
144 # my account cannot deactivate account
145 params['active'] = True
145 params['active'] = True
146 if name == 'admin':
146 if name == 'admin':
147 # my account cannot make you an admin !
147 # my account cannot make you an admin !
148 params['admin'] = False
148 params['admin'] = False
149
149
150 params.pop('_session_csrf_secret_token')
150 params.pop('_session_csrf_secret_token')
151 assert params == updated_params
151 assert params == updated_params
152
152
153 def test_my_account_update_err_email_exists(self):
153 def test_my_account_update_err_email_exists(self):
154 self.log_user()
154 self.log_user()
155
155
156 new_email = base.TEST_USER_REGULAR_EMAIL # already existing email
156 new_email = base.TEST_USER_REGULAR_EMAIL # already existing email
157 response = self.app.post(base.url('my_account'),
157 response = self.app.post(base.url('my_account'),
158 params=dict(
158 params=dict(
159 username=base.TEST_USER_ADMIN_LOGIN,
159 username=base.TEST_USER_ADMIN_LOGIN,
160 new_password=base.TEST_USER_ADMIN_PASS,
160 new_password=base.TEST_USER_ADMIN_PASS,
161 password_confirmation='test122',
161 password_confirmation='test122',
162 firstname='NewName',
162 firstname='NewName',
163 lastname='NewLastname',
163 lastname='NewLastname',
164 email=new_email,
164 email=new_email,
165 _session_csrf_secret_token=self.session_csrf_secret_token())
165 _session_csrf_secret_token=self.session_csrf_secret_token())
166 )
166 )
167
167
168 response.mustcontain('This email address is already in use')
168 response.mustcontain('This email address is already in use')
169
169
170 def test_my_account_update_err(self):
170 def test_my_account_update_err(self):
171 self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
171 self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
172
172
173 new_email = 'newmail.pl'
173 new_email = 'newmail.pl'
174 response = self.app.post(base.url('my_account'),
174 response = self.app.post(base.url('my_account'),
175 params=dict(
175 params=dict(
176 username=base.TEST_USER_ADMIN_LOGIN,
176 username=base.TEST_USER_ADMIN_LOGIN,
177 new_password=base.TEST_USER_ADMIN_PASS,
177 new_password=base.TEST_USER_ADMIN_PASS,
178 password_confirmation='test122',
178 password_confirmation='test122',
179 firstname='NewName',
179 firstname='NewName',
180 lastname='NewLastname',
180 lastname='NewLastname',
181 email=new_email,
181 email=new_email,
182 _session_csrf_secret_token=self.session_csrf_secret_token()))
182 _session_csrf_secret_token=self.session_csrf_secret_token()))
183
183
184 response.mustcontain('An email address must contain a single @')
184 response.mustcontain('An email address must contain a single @')
185 from kallithea.model import validators
186 with test_context(self.app):
185 with test_context(self.app):
187 msg = validators.ValidUsername(edit=False, old_data={}) \
186 msg = validators.ValidUsername(edit=False, old_data={}) \
188 ._messages['username_exists']
187 ._messages['username_exists']
189 msg = webutils.html_escape(msg % {'username': base.TEST_USER_ADMIN_LOGIN})
188 msg = webutils.html_escape(msg % {'username': base.TEST_USER_ADMIN_LOGIN})
190 response.mustcontain(msg)
189 response.mustcontain(msg)
191
190
192 def test_my_account_api_keys(self):
191 def test_my_account_api_keys(self):
193 usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
192 usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
194 user = db.User.get(usr['user_id'])
193 user = db.User.get(usr['user_id'])
195 response = self.app.get(base.url('my_account_api_keys'))
194 response = self.app.get(base.url('my_account_api_keys'))
196 response.mustcontain(user.api_key)
195 response.mustcontain(user.api_key)
197 response.mustcontain('Expires: Never')
196 response.mustcontain('Expires: Never')
198
197
199 @base.parametrize('desc,lifetime', [
198 @base.parametrize('desc,lifetime', [
200 ('forever', -1),
199 ('forever', -1),
201 ('5mins', 60*5),
200 ('5mins', 60*5),
202 ('30days', 60*60*24*30),
201 ('30days', 60*60*24*30),
203 ])
202 ])
204 def test_my_account_add_api_keys(self, desc, lifetime):
203 def test_my_account_add_api_keys(self, desc, lifetime):
205 usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
204 usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
206 user = db.User.get(usr['user_id'])
205 user = db.User.get(usr['user_id'])
207 response = self.app.post(base.url('my_account_api_keys'),
206 response = self.app.post(base.url('my_account_api_keys'),
208 {'description': desc, 'lifetime': lifetime, '_session_csrf_secret_token': self.session_csrf_secret_token()})
207 {'description': desc, 'lifetime': lifetime, '_session_csrf_secret_token': self.session_csrf_secret_token()})
209 self.checkSessionFlash(response, 'API key successfully created')
208 self.checkSessionFlash(response, 'API key successfully created')
210 try:
209 try:
211 response = response.follow()
210 response = response.follow()
212 user = db.User.get(usr['user_id'])
211 user = db.User.get(usr['user_id'])
213 for api_key in user.api_keys:
212 for api_key in user.api_keys:
214 response.mustcontain(api_key)
213 response.mustcontain(api_key)
215 finally:
214 finally:
216 for api_key in db.UserApiKeys.query().all():
215 for api_key in db.UserApiKeys.query().all():
217 meta.Session().delete(api_key)
216 meta.Session().delete(api_key)
218 meta.Session().commit()
217 meta.Session().commit()
219
218
220 def test_my_account_remove_api_key(self):
219 def test_my_account_remove_api_key(self):
221 usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
220 usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
222 user = db.User.get(usr['user_id'])
221 user = db.User.get(usr['user_id'])
223 response = self.app.post(base.url('my_account_api_keys'),
222 response = self.app.post(base.url('my_account_api_keys'),
224 {'description': 'desc', 'lifetime': -1, '_session_csrf_secret_token': self.session_csrf_secret_token()})
223 {'description': 'desc', 'lifetime': -1, '_session_csrf_secret_token': self.session_csrf_secret_token()})
225 self.checkSessionFlash(response, 'API key successfully created')
224 self.checkSessionFlash(response, 'API key successfully created')
226 response = response.follow()
225 response = response.follow()
227
226
228 # now delete our key
227 # now delete our key
229 keys = db.UserApiKeys.query().all()
228 keys = db.UserApiKeys.query().all()
230 assert 1 == len(keys)
229 assert 1 == len(keys)
231
230
232 response = self.app.post(base.url('my_account_api_keys_delete'),
231 response = self.app.post(base.url('my_account_api_keys_delete'),
233 {'del_api_key': keys[0].api_key, '_session_csrf_secret_token': self.session_csrf_secret_token()})
232 {'del_api_key': keys[0].api_key, '_session_csrf_secret_token': self.session_csrf_secret_token()})
234 self.checkSessionFlash(response, 'API key successfully deleted')
233 self.checkSessionFlash(response, 'API key successfully deleted')
235 keys = db.UserApiKeys.query().all()
234 keys = db.UserApiKeys.query().all()
236 assert 0 == len(keys)
235 assert 0 == len(keys)
237
236
238 def test_my_account_reset_main_api_key(self):
237 def test_my_account_reset_main_api_key(self):
239 usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
238 usr = self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
240 user = db.User.get(usr['user_id'])
239 user = db.User.get(usr['user_id'])
241 api_key = user.api_key
240 api_key = user.api_key
242 response = self.app.get(base.url('my_account_api_keys'))
241 response = self.app.get(base.url('my_account_api_keys'))
243 response.mustcontain(api_key)
242 response.mustcontain(api_key)
244 response.mustcontain('Expires: Never')
243 response.mustcontain('Expires: Never')
245
244
246 response = self.app.post(base.url('my_account_api_keys_delete'),
245 response = self.app.post(base.url('my_account_api_keys_delete'),
247 {'del_api_key_builtin': api_key, '_session_csrf_secret_token': self.session_csrf_secret_token()})
246 {'del_api_key_builtin': api_key, '_session_csrf_secret_token': self.session_csrf_secret_token()})
248 self.checkSessionFlash(response, 'API key successfully reset')
247 self.checkSessionFlash(response, 'API key successfully reset')
249 response = response.follow()
248 response = response.follow()
250 response.mustcontain(no=[api_key])
249 response.mustcontain(no=[api_key])
251
250
252 def test_my_account_add_ssh_key(self):
251 def test_my_account_add_ssh_key(self):
253 description = 'something'
252 description = 'something'
254 public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
253 public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
255 fingerprint = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
254 fingerprint = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
256
255
257 self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
256 self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
258 response = self.app.post(base.url('my_account_ssh_keys'),
257 response = self.app.post(base.url('my_account_ssh_keys'),
259 {'description': description,
258 {'description': description,
260 'public_key': public_key,
259 'public_key': public_key,
261 '_session_csrf_secret_token': self.session_csrf_secret_token()})
260 '_session_csrf_secret_token': self.session_csrf_secret_token()})
262 self.checkSessionFlash(response, 'SSH key %s successfully added' % fingerprint)
261 self.checkSessionFlash(response, 'SSH key %s successfully added' % fingerprint)
263
262
264 response = response.follow()
263 response = response.follow()
265 response.mustcontain(fingerprint)
264 response.mustcontain(fingerprint)
266 user_id = response.session['authuser']['user_id']
265 user_id = response.session['authuser']['user_id']
267 ssh_key = db.UserSshKeys.query().filter(db.UserSshKeys.user_id == user_id).one()
266 ssh_key = db.UserSshKeys.query().filter(db.UserSshKeys.user_id == user_id).one()
268 assert ssh_key.fingerprint == fingerprint
267 assert ssh_key.fingerprint == fingerprint
269 assert ssh_key.description == description
268 assert ssh_key.description == description
270 meta.Session().delete(ssh_key)
269 meta.Session().delete(ssh_key)
271 meta.Session().commit()
270 meta.Session().commit()
272
271
273 def test_my_account_remove_ssh_key(self):
272 def test_my_account_remove_ssh_key(self):
274 description = ''
273 description = ''
275 public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
274 public_key = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAAgQC6Ycnc2oUZHQnQwuqgZqTTdMDZD7ataf3JM7oG2Fw8JR6cdmz4QZLe5mfDwaFwG2pWHLRpVqzfrD/Pn3rIO++bgCJH5ydczrl1WScfryV1hYMJ/4EzLGM657J1/q5EI+b9SntKjf4ax+KP322L0TNQGbZUHLbfG2MwHMrYBQpHUQ== me@localhost'
276 fingerprint = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
275 fingerprint = 'Ke3oUCNJM87P0jJTb3D+e3shjceP2CqMpQKVd75E9I8'
277
276
278 self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
277 self.log_user(base.TEST_USER_REGULAR2_LOGIN, base.TEST_USER_REGULAR2_PASS)
279 response = self.app.post(base.url('my_account_ssh_keys'),
278 response = self.app.post(base.url('my_account_ssh_keys'),
280 {'description': description,
279 {'description': description,
281 'public_key': public_key,
280 'public_key': public_key,
282 '_session_csrf_secret_token': self.session_csrf_secret_token()})
281 '_session_csrf_secret_token': self.session_csrf_secret_token()})
283 self.checkSessionFlash(response, 'SSH key %s successfully added' % fingerprint)
282 self.checkSessionFlash(response, 'SSH key %s successfully added' % fingerprint)
284 response.follow()
283 response.follow()
285 user_id = response.session['authuser']['user_id']
284 user_id = response.session['authuser']['user_id']
286 ssh_key = db.UserSshKeys.query().filter(db.UserSshKeys.user_id == user_id).one()
285 ssh_key = db.UserSshKeys.query().filter(db.UserSshKeys.user_id == user_id).one()
287 assert ssh_key.description == 'me@localhost'
286 assert ssh_key.description == 'me@localhost'
288
287
289 response = self.app.post(base.url('my_account_ssh_keys_delete'),
288 response = self.app.post(base.url('my_account_ssh_keys_delete'),
290 {'del_public_key_fingerprint': ssh_key.fingerprint,
289 {'del_public_key_fingerprint': ssh_key.fingerprint,
291 '_session_csrf_secret_token': self.session_csrf_secret_token()})
290 '_session_csrf_secret_token': self.session_csrf_secret_token()})
292 self.checkSessionFlash(response, 'SSH key successfully deleted')
291 self.checkSessionFlash(response, 'SSH key successfully deleted')
293 keys = db.UserSshKeys.query().all()
292 keys = db.UserSshKeys.query().all()
294 assert 0 == len(keys)
293 assert 0 == len(keys)
@@ -1,592 +1,580 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.tests.other.test_libs
15 kallithea.tests.other.test_libs
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Package for testing various lib/helper functions in kallithea
18 Package for testing various lib/helper functions in kallithea
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Jun 9, 2011
22 :created_on: Jun 9, 2011
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26 """
26 """
27
27
28 import datetime
28 import datetime
29 import hashlib
29 import hashlib
30 import re
30
31
31 import mock
32 import mock
33 import routes
34 from dateutil import relativedelta
35 from tg import request
32 from tg.util.webtest import test_context
36 from tg.util.webtest import test_context
33
37
34 import kallithea.lib.helpers as h
38 import kallithea.lib.helpers as h
35 from kallithea.lib import webutils
39 from kallithea.lib import webutils
36 from kallithea.lib.utils2 import AttributeDict, safe_bytes
40 from kallithea.lib.utils2 import AttributeDict, get_clone_url, safe_bytes
37 from kallithea.model import db
41 from kallithea.model import db
38 from kallithea.tests import base
42 from kallithea.tests import base
39
43
40
44
41 proto = 'http'
45 proto = 'http'
42 TEST_URLS = [
46 TEST_URLS = [
43 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
47 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
44 '%s://127.0.0.1' % proto),
48 '%s://127.0.0.1' % proto),
45 ('%s://username@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
49 ('%s://username@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
46 '%s://127.0.0.1' % proto),
50 '%s://127.0.0.1' % proto),
47 ('%s://username:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
51 ('%s://username:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
48 '%s://127.0.0.1' % proto),
52 '%s://127.0.0.1' % proto),
49 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
53 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
50 '%s://127.0.0.1:8080' % proto),
54 '%s://127.0.0.1:8080' % proto),
51 ('%s://example.com' % proto, ['%s://' % proto, 'example.com'],
55 ('%s://example.com' % proto, ['%s://' % proto, 'example.com'],
52 '%s://example.com' % proto),
56 '%s://example.com' % proto),
53 ('%s://user:pass@example.com:8080' % proto, ['%s://' % proto, 'example.com',
57 ('%s://user:pass@example.com:8080' % proto, ['%s://' % proto, 'example.com',
54 '8080'],
58 '8080'],
55 '%s://example.com:8080' % proto),
59 '%s://example.com:8080' % proto),
56 ]
60 ]
57
61
58 proto = 'https'
62 proto = 'https'
59 TEST_URLS += [
63 TEST_URLS += [
60 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
64 ('%s://127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
61 '%s://127.0.0.1' % proto),
65 '%s://127.0.0.1' % proto),
62 ('%s://username@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
66 ('%s://username@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
63 '%s://127.0.0.1' % proto),
67 '%s://127.0.0.1' % proto),
64 ('%s://username:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
68 ('%s://username:pass@127.0.0.1' % proto, ['%s://' % proto, '127.0.0.1'],
65 '%s://127.0.0.1' % proto),
69 '%s://127.0.0.1' % proto),
66 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
70 ('%s://127.0.0.1:8080' % proto, ['%s://' % proto, '127.0.0.1', '8080'],
67 '%s://127.0.0.1:8080' % proto),
71 '%s://127.0.0.1:8080' % proto),
68 ('%s://example.com' % proto, ['%s://' % proto, 'example.com'],
72 ('%s://example.com' % proto, ['%s://' % proto, 'example.com'],
69 '%s://example.com' % proto),
73 '%s://example.com' % proto),
70 ('%s://user:pass@example.com:8080' % proto, ['%s://' % proto, 'example.com',
74 ('%s://user:pass@example.com:8080' % proto, ['%s://' % proto, 'example.com',
71 '8080'],
75 '8080'],
72 '%s://example.com:8080' % proto),
76 '%s://example.com:8080' % proto),
73 ]
77 ]
74
78
75
79
76 class TestLibs(base.TestController):
80 class TestLibs(base.TestController):
77
81
78 @base.parametrize('test_url,expected,expected_creds', TEST_URLS)
82 @base.parametrize('test_url,expected,expected_creds', TEST_URLS)
79 def test_uri_filter(self, test_url, expected, expected_creds):
83 def test_uri_filter(self, test_url, expected, expected_creds):
80 from kallithea.lib.utils2 import uri_filter
84 from kallithea.lib.utils2 import uri_filter
81 assert uri_filter(test_url) == expected
85 assert uri_filter(test_url) == expected
82
86
83 @base.parametrize('test_url,expected,expected_creds', TEST_URLS)
87 @base.parametrize('test_url,expected,expected_creds', TEST_URLS)
84 def test_credentials_filter(self, test_url, expected, expected_creds):
88 def test_credentials_filter(self, test_url, expected, expected_creds):
85 from kallithea.lib.utils2 import credentials_filter
89 from kallithea.lib.utils2 import credentials_filter
86 assert credentials_filter(test_url) == expected_creds
90 assert credentials_filter(test_url) == expected_creds
87
91
88 @base.parametrize('str_bool,expected', [
92 @base.parametrize('str_bool,expected', [
89 ('t', True),
93 ('t', True),
90 ('true', True),
94 ('true', True),
91 ('y', True),
95 ('y', True),
92 ('yes', True),
96 ('yes', True),
93 ('on', True),
97 ('on', True),
94 ('1', True),
98 ('1', True),
95 ('Y', True),
99 ('Y', True),
96 ('yeS', True),
100 ('yeS', True),
97 ('Y', True),
101 ('Y', True),
98 ('TRUE', True),
102 ('TRUE', True),
99 ('T', True),
103 ('T', True),
100 ('False', False),
104 ('False', False),
101 ('F', False),
105 ('F', False),
102 ('FALSE', False),
106 ('FALSE', False),
103 ('0', False),
107 ('0', False),
104 ])
108 ])
105 def test_asbool(self, str_bool, expected):
109 def test_asbool(self, str_bool, expected):
106 from kallithea.lib.utils2 import asbool
110 from kallithea.lib.utils2 import asbool
107 assert asbool(str_bool) == expected
111 assert asbool(str_bool) == expected
108
112
109 def test_mention_extractor(self):
113 def test_mention_extractor(self):
110 from kallithea.lib.utils2 import extract_mentioned_usernames
114 from kallithea.lib.utils2 import extract_mentioned_usernames
111 sample = (
115 sample = (
112 "@first hi there @world here's my email username@example.com "
116 "@first hi there @world here's my email username@example.com "
113 "@lukaszb check @one_more22 it pls @ ttwelve @D[] @one@two@three "
117 "@lukaszb check @one_more22 it pls @ ttwelve @D[] @one@two@three "
114 "@UPPER @cAmEL @2one_more22 @john please see this http://org.pl "
118 "@UPPER @cAmEL @2one_more22 @john please see this http://org.pl "
115 "@marian.user just do it @marco-polo and next extract @marco_polo "
119 "@marian.user just do it @marco-polo and next extract @marco_polo "
116 "user.dot hej ! not-needed maril@example.com"
120 "user.dot hej ! not-needed maril@example.com"
117 )
121 )
118
122
119 expected = set([
123 expected = set([
120 '2one_more22', 'first', 'lukaszb', 'one', 'one_more22', 'UPPER', 'cAmEL', 'john',
124 '2one_more22', 'first', 'lukaszb', 'one', 'one_more22', 'UPPER', 'cAmEL', 'john',
121 'marian.user', 'marco-polo', 'marco_polo', 'world'])
125 'marian.user', 'marco-polo', 'marco_polo', 'world'])
122 assert expected == set(extract_mentioned_usernames(sample))
126 assert expected == set(extract_mentioned_usernames(sample))
123
127
124 @base.parametrize('age_args,expected', [
128 @base.parametrize('age_args,expected', [
125 (dict(), 'just now'),
129 (dict(), 'just now'),
126 (dict(seconds= -1), '1 second ago'),
130 (dict(seconds= -1), '1 second ago'),
127 (dict(seconds= -60 * 2), '2 minutes ago'),
131 (dict(seconds= -60 * 2), '2 minutes ago'),
128 (dict(hours= -1), '1 hour ago'),
132 (dict(hours= -1), '1 hour ago'),
129 (dict(hours= -24), '1 day ago'),
133 (dict(hours= -24), '1 day ago'),
130 (dict(hours= -24 * 5), '5 days ago'),
134 (dict(hours= -24 * 5), '5 days ago'),
131 (dict(months= -1), '1 month ago'),
135 (dict(months= -1), '1 month ago'),
132 (dict(months= -1, days= -2), '1 month and 2 days ago'),
136 (dict(months= -1, days= -2), '1 month and 2 days ago'),
133 (dict(months= -1, days= -20), '1 month and 19 days ago'),
137 (dict(months= -1, days= -20), '1 month and 19 days ago'),
134 (dict(years= -1, months= -1), '1 year and 1 month ago'),
138 (dict(years= -1, months= -1), '1 year and 1 month ago'),
135 (dict(years= -1, months= -10), '1 year and 10 months ago'),
139 (dict(years= -1, months= -10), '1 year and 10 months ago'),
136 (dict(years= -2, months= -4), '2 years and 4 months ago'),
140 (dict(years= -2, months= -4), '2 years and 4 months ago'),
137 (dict(years= -2, months= -11), '2 years and 11 months ago'),
141 (dict(years= -2, months= -11), '2 years and 11 months ago'),
138 (dict(years= -3, months= -2), '3 years and 2 months ago'),
142 (dict(years= -3, months= -2), '3 years and 2 months ago'),
139 ])
143 ])
140 def test_age(self, age_args, expected):
144 def test_age(self, age_args, expected):
141 from dateutil import relativedelta
142
143 from kallithea.lib.utils2 import age
145 from kallithea.lib.utils2 import age
144 with test_context(self.app):
146 with test_context(self.app):
145 n = datetime.datetime(year=2012, month=5, day=17)
147 n = datetime.datetime(year=2012, month=5, day=17)
146 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
148 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
147 assert age(n + delt(**age_args), now=n) == expected
149 assert age(n + delt(**age_args), now=n) == expected
148
150
149 @base.parametrize('age_args,expected', [
151 @base.parametrize('age_args,expected', [
150 (dict(), 'just now'),
152 (dict(), 'just now'),
151 (dict(seconds= -1), '1 second ago'),
153 (dict(seconds= -1), '1 second ago'),
152 (dict(seconds= -60 * 2), '2 minutes ago'),
154 (dict(seconds= -60 * 2), '2 minutes ago'),
153 (dict(hours= -1), '1 hour ago'),
155 (dict(hours= -1), '1 hour ago'),
154 (dict(hours= -24), '1 day ago'),
156 (dict(hours= -24), '1 day ago'),
155 (dict(hours= -24 * 5), '5 days ago'),
157 (dict(hours= -24 * 5), '5 days ago'),
156 (dict(months= -1), '1 month ago'),
158 (dict(months= -1), '1 month ago'),
157 (dict(months= -1, days= -2), '1 month ago'),
159 (dict(months= -1, days= -2), '1 month ago'),
158 (dict(months= -1, days= -20), '1 month ago'),
160 (dict(months= -1, days= -20), '1 month ago'),
159 (dict(years= -1, months= -1), '13 months ago'),
161 (dict(years= -1, months= -1), '13 months ago'),
160 (dict(years= -1, months= -10), '22 months ago'),
162 (dict(years= -1, months= -10), '22 months ago'),
161 (dict(years= -2, months= -4), '2 years ago'),
163 (dict(years= -2, months= -4), '2 years ago'),
162 (dict(years= -2, months= -11), '3 years ago'),
164 (dict(years= -2, months= -11), '3 years ago'),
163 (dict(years= -3, months= -2), '3 years ago'),
165 (dict(years= -3, months= -2), '3 years ago'),
164 (dict(years= -4, months= -8), '5 years ago'),
166 (dict(years= -4, months= -8), '5 years ago'),
165 ])
167 ])
166 def test_age_short(self, age_args, expected):
168 def test_age_short(self, age_args, expected):
167 from dateutil import relativedelta
168
169 from kallithea.lib.utils2 import age
169 from kallithea.lib.utils2 import age
170 with test_context(self.app):
170 with test_context(self.app):
171 n = datetime.datetime(year=2012, month=5, day=17)
171 n = datetime.datetime(year=2012, month=5, day=17)
172 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
172 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
173 assert age(n + delt(**age_args), show_short_version=True, now=n) == expected
173 assert age(n + delt(**age_args), show_short_version=True, now=n) == expected
174
174
175 @base.parametrize('age_args,expected', [
175 @base.parametrize('age_args,expected', [
176 (dict(), 'just now'),
176 (dict(), 'just now'),
177 (dict(seconds=1), 'in 1 second'),
177 (dict(seconds=1), 'in 1 second'),
178 (dict(seconds=60 * 2), 'in 2 minutes'),
178 (dict(seconds=60 * 2), 'in 2 minutes'),
179 (dict(hours=1), 'in 1 hour'),
179 (dict(hours=1), 'in 1 hour'),
180 (dict(hours=24), 'in 1 day'),
180 (dict(hours=24), 'in 1 day'),
181 (dict(hours=24 * 5), 'in 5 days'),
181 (dict(hours=24 * 5), 'in 5 days'),
182 (dict(months=1), 'in 1 month'),
182 (dict(months=1), 'in 1 month'),
183 (dict(months=1, days=1), 'in 1 month and 1 day'),
183 (dict(months=1, days=1), 'in 1 month and 1 day'),
184 (dict(years=1, months=1), 'in 1 year and 1 month')
184 (dict(years=1, months=1), 'in 1 year and 1 month')
185 ])
185 ])
186 def test_age_in_future(self, age_args, expected):
186 def test_age_in_future(self, age_args, expected):
187 from dateutil import relativedelta
188
189 from kallithea.lib.utils2 import age
187 from kallithea.lib.utils2 import age
190 with test_context(self.app):
188 with test_context(self.app):
191 n = datetime.datetime(year=2012, month=5, day=17)
189 n = datetime.datetime(year=2012, month=5, day=17)
192 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
190 delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
193 assert age(n + delt(**age_args), now=n) == expected
191 assert age(n + delt(**age_args), now=n) == expected
194
192
195 def test_tag_extractor(self):
193 def test_tag_extractor(self):
196 sample = (
194 sample = (
197 "hello pta[tag] gog [[]] [[] sda ero[or]d [me =>>< sa]"
195 "hello pta[tag] gog [[]] [[] sda ero[or]d [me =>>< sa]"
198 "[requires] [stale] [see<>=>] [see => http://example.com]"
196 "[requires] [stale] [see<>=>] [see => http://example.com]"
199 "[requires => url] [lang => python] [just a tag]"
197 "[requires => url] [lang => python] [just a tag]"
200 "[,d] [ => ULR ] [obsolete] [desc]]"
198 "[,d] [ => ULR ] [obsolete] [desc]]"
201 )
199 )
202 res = h.urlify_text(sample, stylize=True)
200 res = h.urlify_text(sample, stylize=True)
203 assert '<div class="label label-meta" data-tag="tag">tag</div>' in res
201 assert '<div class="label label-meta" data-tag="tag">tag</div>' in res
204 assert '<div class="label label-meta" data-tag="obsolete">obsolete</div>' in res
202 assert '<div class="label label-meta" data-tag="obsolete">obsolete</div>' in res
205 assert '<div class="label label-meta" data-tag="stale">stale</div>' in res
203 assert '<div class="label label-meta" data-tag="stale">stale</div>' in res
206 assert '<div class="label label-meta" data-tag="lang">python</div>' in res
204 assert '<div class="label label-meta" data-tag="lang">python</div>' in res
207 assert '<div class="label label-meta" data-tag="requires">requires =&gt; <a href="/url">url</a></div>' in res
205 assert '<div class="label label-meta" data-tag="requires">requires =&gt; <a href="/url">url</a></div>' in res
208 assert '<div class="label label-meta" data-tag="tag">tag</div>' in res
206 assert '<div class="label label-meta" data-tag="tag">tag</div>' in res
209
207
210 def test_alternative_gravatar(self):
208 def test_alternative_gravatar(self):
211 _md5 = lambda s: hashlib.md5(safe_bytes(s)).hexdigest()
209 _md5 = lambda s: hashlib.md5(safe_bytes(s)).hexdigest()
212
210
213 # mock tg.tmpl_context
211 # mock tg.tmpl_context
214 def fake_tmpl_context(_url):
212 def fake_tmpl_context(_url):
215 _c = AttributeDict()
213 _c = AttributeDict()
216 _c.visual = AttributeDict()
214 _c.visual = AttributeDict()
217 _c.visual.use_gravatar = True
215 _c.visual.use_gravatar = True
218 _c.visual.gravatar_url = _url
216 _c.visual.gravatar_url = _url
219
217
220 return _c
218 return _c
221
219
222 with mock.patch('kallithea.lib.webutils.url.current', lambda *a, **b: 'https://example.com'):
220 with mock.patch('kallithea.lib.webutils.url.current', lambda *a, **b: 'https://example.com'):
223 fake = fake_tmpl_context(_url='http://example.com/{email}')
221 fake = fake_tmpl_context(_url='http://example.com/{email}')
224 with mock.patch('tg.tmpl_context', fake):
222 with mock.patch('kallithea.lib.helpers.c', fake):
225 from kallithea.lib.webutils import url
223 assert webutils.url.current() == 'https://example.com'
226 assert url.current() == 'https://example.com'
227 grav = h.gravatar_url(email_address='test@example.com', size=24)
224 grav = h.gravatar_url(email_address='test@example.com', size=24)
228 assert grav == 'http://example.com/test@example.com'
225 assert grav == 'http://example.com/test@example.com'
229
226
230 fake = fake_tmpl_context(_url='http://example.com/{email}')
227 fake = fake_tmpl_context(_url='http://example.com/{email}')
231 with mock.patch('tg.tmpl_context', fake):
228 with mock.patch('kallithea.lib.helpers.c', fake):
232 grav = h.gravatar_url(email_address='test@example.com', size=24)
229 grav = h.gravatar_url(email_address='test@example.com', size=24)
233 assert grav == 'http://example.com/test@example.com'
230 assert grav == 'http://example.com/test@example.com'
234
231
235 fake = fake_tmpl_context(_url='http://example.com/{md5email}')
232 fake = fake_tmpl_context(_url='http://example.com/{md5email}')
236 with mock.patch('tg.tmpl_context', fake):
233 with mock.patch('kallithea.lib.helpers.c', fake):
237 em = 'test@example.com'
234 em = 'test@example.com'
238 grav = h.gravatar_url(email_address=em, size=24)
235 grav = h.gravatar_url(email_address=em, size=24)
239 assert grav == 'http://example.com/%s' % (_md5(em))
236 assert grav == 'http://example.com/%s' % (_md5(em))
240
237
241 fake = fake_tmpl_context(_url='http://example.com/{md5email}/{size}')
238 fake = fake_tmpl_context(_url='http://example.com/{md5email}/{size}')
242 with mock.patch('tg.tmpl_context', fake):
239 with mock.patch('kallithea.lib.helpers.c', fake):
243 em = 'test@example.com'
240 em = 'test@example.com'
244 grav = h.gravatar_url(email_address=em, size=24)
241 grav = h.gravatar_url(email_address=em, size=24)
245 assert grav == 'http://example.com/%s/%s' % (_md5(em), 24)
242 assert grav == 'http://example.com/%s/%s' % (_md5(em), 24)
246
243
247 fake = fake_tmpl_context(_url='{scheme}://{netloc}/{md5email}/{size}')
244 fake = fake_tmpl_context(_url='{scheme}://{netloc}/{md5email}/{size}')
248 with mock.patch('tg.tmpl_context', fake):
245 with mock.patch('kallithea.lib.helpers.c', fake):
249 em = 'test@example.com'
246 em = 'test@example.com'
250 grav = h.gravatar_url(email_address=em, size=24)
247 grav = h.gravatar_url(email_address=em, size=24)
251 assert grav == 'https://example.com/%s/%s' % (_md5(em), 24)
248 assert grav == 'https://example.com/%s/%s' % (_md5(em), 24)
252
249
253 @base.parametrize('clone_uri_tmpl,repo_name,username,prefix,expected', [
250 @base.parametrize('clone_uri_tmpl,repo_name,username,prefix,expected', [
254 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', None, '', 'http://vps1:8000/group/repo1'),
251 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', None, '', 'http://vps1:8000/group/repo1'),
255 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', 'username', '', 'http://username@vps1:8000/group/repo1'),
252 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', 'username', '', 'http://username@vps1:8000/group/repo1'),
256 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', None, '/prefix', 'http://vps1:8000/prefix/group/repo1'),
253 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', None, '/prefix', 'http://vps1:8000/prefix/group/repo1'),
257 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', 'user', '/prefix', 'http://user@vps1:8000/prefix/group/repo1'),
254 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', 'user', '/prefix', 'http://user@vps1:8000/prefix/group/repo1'),
258 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', 'username', '/prefix', 'http://username@vps1:8000/prefix/group/repo1'),
255 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', 'username', '/prefix', 'http://username@vps1:8000/prefix/group/repo1'),
259 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', 'user', '/prefix/', 'http://user@vps1:8000/prefix/group/repo1'),
256 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', 'user', '/prefix/', 'http://user@vps1:8000/prefix/group/repo1'),
260 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', 'username', '/prefix/', 'http://username@vps1:8000/prefix/group/repo1'),
257 (db.Repository.DEFAULT_CLONE_URI, 'group/repo1', 'username', '/prefix/', 'http://username@vps1:8000/prefix/group/repo1'),
261 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', None, '', 'http://vps1:8000/_23'),
258 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', None, '', 'http://vps1:8000/_23'),
262 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', 'username', '', 'http://username@vps1:8000/_23'),
259 ('{scheme}://{user}@{netloc}/_{repoid}', 'group/repo1', 'username', '', 'http://username@vps1:8000/_23'),
263 ('http://{user}@{netloc}/_{repoid}', 'group/repo1', 'username', '', 'http://username@vps1:8000/_23'),
260 ('http://{user}@{netloc}/_{repoid}', 'group/repo1', 'username', '', 'http://username@vps1:8000/_23'),
264 ('http://{netloc}/_{repoid}', 'group/repo1', 'username', '', 'http://vps1:8000/_23'),
261 ('http://{netloc}/_{repoid}', 'group/repo1', 'username', '', 'http://vps1:8000/_23'),
265 ('https://{user}@proxy1.example.com/{repo}', 'group/repo1', 'username', '', 'https://username@proxy1.example.com/group/repo1'),
262 ('https://{user}@proxy1.example.com/{repo}', 'group/repo1', 'username', '', 'https://username@proxy1.example.com/group/repo1'),
266 ('https://{user}@proxy1.example.com/{repo}', 'group/repo1', None, '', 'https://proxy1.example.com/group/repo1'),
263 ('https://{user}@proxy1.example.com/{repo}', 'group/repo1', None, '', 'https://proxy1.example.com/group/repo1'),
267 ('https://proxy1.example.com/{user}/{repo}', 'group/repo1', 'username', '', 'https://proxy1.example.com/username/group/repo1'),
264 ('https://proxy1.example.com/{user}/{repo}', 'group/repo1', 'username', '', 'https://proxy1.example.com/username/group/repo1'),
268 ])
265 ])
269 def test_clone_url_generator(self, clone_uri_tmpl, repo_name, username, prefix, expected):
266 def test_clone_url_generator(self, clone_uri_tmpl, repo_name, username, prefix, expected):
270 from kallithea.lib.utils2 import get_clone_url
271 clone_url = get_clone_url(clone_uri_tmpl=clone_uri_tmpl, prefix_url='http://vps1:8000' + prefix,
267 clone_url = get_clone_url(clone_uri_tmpl=clone_uri_tmpl, prefix_url='http://vps1:8000' + prefix,
272 repo_name=repo_name, repo_id=23, username=username)
268 repo_name=repo_name, repo_id=23, username=username)
273 assert clone_url == expected
269 assert clone_url == expected
274
270
275 def _quick_url(self, text, tmpl="""<a class="changeset_hash" href="%s">%s</a>""", url_=None):
271 def _quick_url(self, text, tmpl="""<a class="changeset_hash" href="%s">%s</a>""", url_=None):
276 """
272 """
277 Changes `some text url[foo]` => `some text <a href="/">foo</a>
273 Changes `some text url[foo]` => `some text <a href="/">foo</a>
278
274
279 :param text:
275 :param text:
280 """
276 """
281 import re
282
283 # quickly change expected url[] into a link
277 # quickly change expected url[] into a link
284 url_pattern = re.compile(r'(?:url\[)(.+?)(?:\])')
278 url_pattern = re.compile(r'(?:url\[)(.+?)(?:\])')
285
279
286 def url_func(match_obj):
280 def url_func(match_obj):
287 _url = match_obj.groups()[0]
281 _url = match_obj.groups()[0]
288 return tmpl % (url_ or '/repo_name/changeset/%s' % _url, _url)
282 return tmpl % (url_ or '/repo_name/changeset/%s' % _url, _url)
289 return url_pattern.sub(url_func, text)
283 return url_pattern.sub(url_func, text)
290
284
291 @base.parametrize('sample,expected', [
285 @base.parametrize('sample,expected', [
292 ("",
286 ("",
293 ""),
287 ""),
294 ("git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
288 ("git-svn-id: https://svn.apache.org/repos/asf/libcloud/trunk@1441655 13f79535-47bb-0310-9956-ffa450edef68",
295 """git-svn-id: <a href="https://svn.apache.org/repos/asf/libcloud/trunk@1441655">https://svn.apache.org/repos/asf/libcloud/trunk@1441655</a> 13f79535-47bb-0310-9956-ffa450edef68"""),
289 """git-svn-id: <a href="https://svn.apache.org/repos/asf/libcloud/trunk@1441655">https://svn.apache.org/repos/asf/libcloud/trunk@1441655</a> 13f79535-47bb-0310-9956-ffa450edef68"""),
296 ("from rev 000000000000",
290 ("from rev 000000000000",
297 """from rev url[000000000000]"""),
291 """from rev url[000000000000]"""),
298 ("from rev 000000000000123123 also rev 000000000000",
292 ("from rev 000000000000123123 also rev 000000000000",
299 """from rev url[000000000000123123] also rev url[000000000000]"""),
293 """from rev url[000000000000123123] also rev url[000000000000]"""),
300 ("this should-000 00",
294 ("this should-000 00",
301 """this should-000 00"""),
295 """this should-000 00"""),
302 ("longtextffffffffff rev 123123123123",
296 ("longtextffffffffff rev 123123123123",
303 """longtextffffffffff rev url[123123123123]"""),
297 """longtextffffffffff rev url[123123123123]"""),
304 ("rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
298 ("rev ffffffffffffffffffffffffffffffffffffffffffffffffff",
305 """rev ffffffffffffffffffffffffffffffffffffffffffffffffff"""),
299 """rev ffffffffffffffffffffffffffffffffffffffffffffffffff"""),
306 ("ffffffffffff some text traalaa",
300 ("ffffffffffff some text traalaa",
307 """url[ffffffffffff] some text traalaa"""),
301 """url[ffffffffffff] some text traalaa"""),
308 ("""Multi line
302 ("""Multi line
309 123123123123
303 123123123123
310 some text 123123123123
304 some text 123123123123
311 sometimes !
305 sometimes !
312 """,
306 """,
313 """Multi line<br/>"""
307 """Multi line<br/>"""
314 """ url[123123123123]<br/>"""
308 """ url[123123123123]<br/>"""
315 """ some text url[123123123123]<br/>"""
309 """ some text url[123123123123]<br/>"""
316 """ sometimes !"""),
310 """ sometimes !"""),
317 ])
311 ])
318 def test_urlify_text(self, sample, expected):
312 def test_urlify_text(self, sample, expected):
319 expected = self._quick_url(expected)
313 expected = self._quick_url(expected)
320
314
321 with mock.patch('kallithea.lib.webutils.UrlGenerator.__call__',
315 with mock.patch('kallithea.lib.webutils.UrlGenerator.__call__',
322 lambda self, name, **kwargs: dict(changeset_home='/%(repo_name)s/changeset/%(revision)s')[name] % kwargs,
316 lambda self, name, **kwargs: dict(changeset_home='/%(repo_name)s/changeset/%(revision)s')[name] % kwargs,
323 ):
317 ):
324 assert h.urlify_text(sample, 'repo_name') == expected
318 assert h.urlify_text(sample, 'repo_name') == expected
325
319
326 @base.parametrize('sample,expected,url_', [
320 @base.parametrize('sample,expected,url_', [
327 ("",
321 ("",
328 "",
322 "",
329 ""),
323 ""),
330 ("https://svn.apache.org/repos",
324 ("https://svn.apache.org/repos",
331 """url[https://svn.apache.org/repos]""",
325 """url[https://svn.apache.org/repos]""",
332 "https://svn.apache.org/repos"),
326 "https://svn.apache.org/repos"),
333 ("http://svn.apache.org/repos",
327 ("http://svn.apache.org/repos",
334 """url[http://svn.apache.org/repos]""",
328 """url[http://svn.apache.org/repos]""",
335 "http://svn.apache.org/repos"),
329 "http://svn.apache.org/repos"),
336 ("from rev a also rev http://google.com",
330 ("from rev a also rev http://google.com",
337 """from rev a also rev url[http://google.com]""",
331 """from rev a also rev url[http://google.com]""",
338 "http://google.com"),
332 "http://google.com"),
339 ("http://imgur.com/foo.gif inline http://imgur.com/foo.gif ending http://imgur.com/foo.gif",
333 ("http://imgur.com/foo.gif inline http://imgur.com/foo.gif ending http://imgur.com/foo.gif",
340 """url[http://imgur.com/foo.gif] inline url[http://imgur.com/foo.gif] ending url[http://imgur.com/foo.gif]""",
334 """url[http://imgur.com/foo.gif] inline url[http://imgur.com/foo.gif] ending url[http://imgur.com/foo.gif]""",
341 "http://imgur.com/foo.gif"),
335 "http://imgur.com/foo.gif"),
342 ("""Multi line
336 ("""Multi line
343 https://foo.bar.example.com
337 https://foo.bar.example.com
344 some text lalala""",
338 some text lalala""",
345 """Multi line<br/>"""
339 """Multi line<br/>"""
346 """ url[https://foo.bar.example.com]<br/>"""
340 """ url[https://foo.bar.example.com]<br/>"""
347 """ some text lalala""",
341 """ some text lalala""",
348 "https://foo.bar.example.com"),
342 "https://foo.bar.example.com"),
349 ("@mention @someone",
343 ("@mention @someone",
350 """<b>@mention</b> <b>@someone</b>""",
344 """<b>@mention</b> <b>@someone</b>""",
351 ""),
345 ""),
352 ("deadbeefcafe 123412341234",
346 ("deadbeefcafe 123412341234",
353 """<a class="changeset_hash" href="/repo_name/changeset/deadbeefcafe">deadbeefcafe</a> <a class="changeset_hash" href="/repo_name/changeset/123412341234">123412341234</a>""",
347 """<a class="changeset_hash" href="/repo_name/changeset/deadbeefcafe">deadbeefcafe</a> <a class="changeset_hash" href="/repo_name/changeset/123412341234">123412341234</a>""",
354 ""),
348 ""),
355 ("We support * markup for *bold* markup of *single or multiple* words, "
349 ("We support * markup for *bold* markup of *single or multiple* words, "
356 "*a bit @like http://slack.com*. "
350 "*a bit @like http://slack.com*. "
357 "The first * must come after whitespace and not be followed by whitespace, "
351 "The first * must come after whitespace and not be followed by whitespace, "
358 "contain anything but * and newline until the next *, "
352 "contain anything but * and newline until the next *, "
359 "which must not come after whitespace "
353 "which must not come after whitespace "
360 "and not be followed by * or alphanumerical *characters*.",
354 "and not be followed by * or alphanumerical *characters*.",
361 """We support * markup for <b>*bold*</b> markup of <b>*single or multiple*</b> words, """
355 """We support * markup for <b>*bold*</b> markup of <b>*single or multiple*</b> words, """
362 """<b>*a bit <b>@like</b> <a href="http://slack.com">http://slack.com</a>*</b>. """
356 """<b>*a bit <b>@like</b> <a href="http://slack.com">http://slack.com</a>*</b>. """
363 """The first * must come after whitespace and not be followed by whitespace, """
357 """The first * must come after whitespace and not be followed by whitespace, """
364 """contain anything but * and newline until the next *, """
358 """contain anything but * and newline until the next *, """
365 """which must not come after whitespace """
359 """which must not come after whitespace """
366 """and not be followed by * or alphanumerical <b>*characters*</b>.""",
360 """and not be followed by * or alphanumerical <b>*characters*</b>.""",
367 "-"),
361 "-"),
368 ("HTML escaping: <abc> 'single' \"double\" &pointer",
362 ("HTML escaping: <abc> 'single' \"double\" &pointer",
369 "HTML escaping: &lt;abc&gt; &#39;single&#39; &quot;double&quot; &amp;pointer",
363 "HTML escaping: &lt;abc&gt; &#39;single&#39; &quot;double&quot; &amp;pointer",
370 "-"),
364 "-"),
371 # tags are covered by test_tag_extractor
365 # tags are covered by test_tag_extractor
372 ])
366 ])
373 def test_urlify_test(self, sample, expected, url_):
367 def test_urlify_test(self, sample, expected, url_):
374 expected = self._quick_url(expected,
368 expected = self._quick_url(expected,
375 tmpl="""<a href="%s">%s</a>""", url_=url_)
369 tmpl="""<a href="%s">%s</a>""", url_=url_)
376 with mock.patch('kallithea.lib.webutils.UrlGenerator.__call__',
370 with mock.patch('kallithea.lib.webutils.UrlGenerator.__call__',
377 lambda self, name, **kwargs: dict(changeset_home='/%(repo_name)s/changeset/%(revision)s')[name] % kwargs,
371 lambda self, name, **kwargs: dict(changeset_home='/%(repo_name)s/changeset/%(revision)s')[name] % kwargs,
378 ):
372 ):
379 assert h.urlify_text(sample, 'repo_name', stylize=True) == expected
373 assert h.urlify_text(sample, 'repo_name', stylize=True) == expected
380
374
381 @base.parametrize('sample,expected', [
375 @base.parametrize('sample,expected', [
382 ("deadbeefcafe @mention, and http://foo.bar/ yo",
376 ("deadbeefcafe @mention, and http://foo.bar/ yo",
383 """<a class="changeset_hash" href="/repo_name/changeset/deadbeefcafe">deadbeefcafe</a>"""
377 """<a class="changeset_hash" href="/repo_name/changeset/deadbeefcafe">deadbeefcafe</a>"""
384 """<a class="message-link" href="#the-link"> <b>@mention</b>, and </a>"""
378 """<a class="message-link" href="#the-link"> <b>@mention</b>, and </a>"""
385 """<a href="http://foo.bar/">http://foo.bar/</a>"""
379 """<a href="http://foo.bar/">http://foo.bar/</a>"""
386 """<a class="message-link" href="#the-link"> yo</a>"""),
380 """<a class="message-link" href="#the-link"> yo</a>"""),
387 ])
381 ])
388 def test_urlify_link(self, sample, expected):
382 def test_urlify_link(self, sample, expected):
389 with mock.patch('kallithea.lib.webutils.UrlGenerator.__call__',
383 with mock.patch('kallithea.lib.webutils.UrlGenerator.__call__',
390 lambda self, name, **kwargs: dict(changeset_home='/%(repo_name)s/changeset/%(revision)s')[name] % kwargs,
384 lambda self, name, **kwargs: dict(changeset_home='/%(repo_name)s/changeset/%(revision)s')[name] % kwargs,
391 ):
385 ):
392 assert h.urlify_text(sample, 'repo_name', link_='#the-link') == expected
386 assert h.urlify_text(sample, 'repo_name', link_='#the-link') == expected
393
387
394 @base.parametrize('issue_pat,issue_server,issue_sub,sample,expected', [
388 @base.parametrize('issue_pat,issue_server,issue_sub,sample,expected', [
395 (r'#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
389 (r'#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
396 'issue #123 and issue#456',
390 'issue #123 and issue#456',
397 """issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
391 """issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
398 """issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/456">#456</a>"""),
392 """issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/456">#456</a>"""),
399 (r'(?:\s*#)(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
393 (r'(?:\s*#)(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
400 'issue #123 and issue#456',
394 'issue #123 and issue#456',
401 """issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
395 """issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
402 """issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/456">#456</a>"""),
396 """issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/456">#456</a>"""),
403 # to require whitespace before the issue reference, one may be tempted to use \b...
397 # to require whitespace before the issue reference, one may be tempted to use \b...
404 (r'\bPR(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
398 (r'\bPR(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
405 'issue PR123 and issuePR456',
399 'issue PR123 and issuePR456',
406 """issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
400 """issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
407 """issuePR456"""),
401 """issuePR456"""),
408 # ... but it turns out that \b does not work well in combination with '#': the expectations
402 # ... but it turns out that \b does not work well in combination with '#': the expectations
409 # are reversed from what is actually happening.
403 # are reversed from what is actually happening.
410 (r'\b#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
404 (r'\b#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
411 'issue #123 and issue#456',
405 'issue #123 and issue#456',
412 """issue #123 and """
406 """issue #123 and """
413 """issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/456">#456</a>"""),
407 """issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/456">#456</a>"""),
414 # ... so maybe try to be explicit? Unfortunately the whitespace before the issue
408 # ... so maybe try to be explicit? Unfortunately the whitespace before the issue
415 # reference is not retained, again, because it is part of the pattern.
409 # reference is not retained, again, because it is part of the pattern.
416 (r'(?:^|\s)#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
410 (r'(?:^|\s)#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
417 '#15 and issue #123 and issue#456',
411 '#15 and issue #123 and issue#456',
418 """<a class="issue-tracker-link" href="http://foo/repo_name/issue/15">#15</a> and """
412 """<a class="issue-tracker-link" href="http://foo/repo_name/issue/15">#15</a> and """
419 """issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
413 """issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
420 """issue#456"""),
414 """issue#456"""),
421 # ... instead, use lookbehind assertions.
415 # ... instead, use lookbehind assertions.
422 (r'(?:^|(?<=\s))#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
416 (r'(?:^|(?<=\s))#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
423 '#15 and issue #123 and issue#456',
417 '#15 and issue #123 and issue#456',
424 """<a class="issue-tracker-link" href="http://foo/repo_name/issue/15">#15</a> and """
418 """<a class="issue-tracker-link" href="http://foo/repo_name/issue/15">#15</a> and """
425 """issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
419 """issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> and """
426 """issue#456"""),
420 """issue#456"""),
427 (r'(?:pullrequest|pull request|PR|pr) ?#?(\d+)', 'http://foo/{repo}/issue/\\1', 'PR#\\1',
421 (r'(?:pullrequest|pull request|PR|pr) ?#?(\d+)', 'http://foo/{repo}/issue/\\1', 'PR#\\1',
428 'fixed with pullrequest #1, pull request#2, PR 3, pr4',
422 'fixed with pullrequest #1, pull request#2, PR 3, pr4',
429 """fixed with <a class="issue-tracker-link" href="http://foo/repo_name/issue/1">PR#1</a>, """
423 """fixed with <a class="issue-tracker-link" href="http://foo/repo_name/issue/1">PR#1</a>, """
430 """<a class="issue-tracker-link" href="http://foo/repo_name/issue/2">PR#2</a>, """
424 """<a class="issue-tracker-link" href="http://foo/repo_name/issue/2">PR#2</a>, """
431 """<a class="issue-tracker-link" href="http://foo/repo_name/issue/3">PR#3</a>, """
425 """<a class="issue-tracker-link" href="http://foo/repo_name/issue/3">PR#3</a>, """
432 """<a class="issue-tracker-link" href="http://foo/repo_name/issue/4">PR#4</a>"""),
426 """<a class="issue-tracker-link" href="http://foo/repo_name/issue/4">PR#4</a>"""),
433 (r'#(\d+)', 'http://foo/{repo}/issue/\\1', 'PR\\1',
427 (r'#(\d+)', 'http://foo/{repo}/issue/\\1', 'PR\\1',
434 'interesting issue #123',
428 'interesting issue #123',
435 """interesting issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">PR123</a>"""),
429 """interesting issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">PR123</a>"""),
436 (r'BUG\d{5}', 'https://bar/{repo}/\\1', '\\1',
430 (r'BUG\d{5}', 'https://bar/{repo}/\\1', '\\1',
437 'silly me, I did not parenthesize the id, BUG12345.',
431 'silly me, I did not parenthesize the id, BUG12345.',
438 """silly me, I did not parenthesize the id, <a class="issue-tracker-link" href="https://bar/repo_name/\\1">BUG12345</a>."""),
432 """silly me, I did not parenthesize the id, <a class="issue-tracker-link" href="https://bar/repo_name/\\1">BUG12345</a>."""),
439 (r'BUG(\d{5})', 'https://bar/{repo}/', 'BUG\\1',
433 (r'BUG(\d{5})', 'https://bar/{repo}/', 'BUG\\1',
440 'silly me, the URL does not contain id, BUG12345.',
434 'silly me, the URL does not contain id, BUG12345.',
441 """silly me, the URL does not contain id, <a class="issue-tracker-link" href="https://bar/repo_name/">BUG12345</a>."""),
435 """silly me, the URL does not contain id, <a class="issue-tracker-link" href="https://bar/repo_name/">BUG12345</a>."""),
442 (r'(PR-\d+)', 'http://foo/{repo}/issue/\\1', '',
436 (r'(PR-\d+)', 'http://foo/{repo}/issue/\\1', '',
443 'interesting issue #123, err PR-56',
437 'interesting issue #123, err PR-56',
444 """interesting issue #123, err <a class="issue-tracker-link" href="http://foo/repo_name/issue/PR-56">PR-56</a>"""),
438 """interesting issue #123, err <a class="issue-tracker-link" href="http://foo/repo_name/issue/PR-56">PR-56</a>"""),
445 (r'#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
439 (r'#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
446 "some 'standard' text with apostrophes",
440 "some 'standard' text with apostrophes",
447 """some &#39;standard&#39; text with apostrophes"""),
441 """some &#39;standard&#39; text with apostrophes"""),
448 (r'#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
442 (r'#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
449 "some 'standard' issue #123",
443 "some 'standard' issue #123",
450 """some &#39;standard&#39; issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a>"""),
444 """some &#39;standard&#39; issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a>"""),
451 (r'#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
445 (r'#(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
452 'an issue #123 with extra whitespace',
446 'an issue #123 with extra whitespace',
453 """an issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> with extra whitespace"""),
447 """an issue <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> with extra whitespace"""),
454 (r'(?:\s*#)(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
448 (r'(?:\s*#)(\d+)', 'http://foo/{repo}/issue/\\1', '#\\1',
455 'an issue #123 with extra whitespace',
449 'an issue #123 with extra whitespace',
456 """an issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> with extra whitespace"""),
450 """an issue<a class="issue-tracker-link" href="http://foo/repo_name/issue/123">#123</a> with extra whitespace"""),
457 # invalid issue pattern
451 # invalid issue pattern
458 (r'(PR\d+', 'http://foo/{repo}/issue/{id}', '',
452 (r'(PR\d+', 'http://foo/{repo}/issue/{id}', '',
459 'PR135',
453 'PR135',
460 """PR135"""),
454 """PR135"""),
461 # other character than #
455 # other character than #
462 (r'(?:^|(?<=\s))\$(\d+)', 'http://foo/{repo}/issue/\\1', '',
456 (r'(?:^|(?<=\s))\$(\d+)', 'http://foo/{repo}/issue/\\1', '',
463 'empty issue_sub $123 and issue$456',
457 'empty issue_sub $123 and issue$456',
464 """empty issue_sub <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">$123</a> and """
458 """empty issue_sub <a class="issue-tracker-link" href="http://foo/repo_name/issue/123">$123</a> and """
465 """issue$456"""),
459 """issue$456"""),
466 # named groups
460 # named groups
467 (r'(PR|pullrequest|pull request) ?(?P<sitecode>BRU|CPH|BER)-(?P<id>\d+)', r'http://foo/\g<sitecode>/pullrequest/\g<id>/', r'PR-\g<sitecode>-\g<id>',
461 (r'(PR|pullrequest|pull request) ?(?P<sitecode>BRU|CPH|BER)-(?P<id>\d+)', r'http://foo/\g<sitecode>/pullrequest/\g<id>/', r'PR-\g<sitecode>-\g<id>',
468 'pullrequest CPH-789 is similar to PRBRU-747',
462 'pullrequest CPH-789 is similar to PRBRU-747',
469 """<a class="issue-tracker-link" href="http://foo/CPH/pullrequest/789/">PR-CPH-789</a> is similar to """
463 """<a class="issue-tracker-link" href="http://foo/CPH/pullrequest/789/">PR-CPH-789</a> is similar to """
470 """<a class="issue-tracker-link" href="http://foo/BRU/pullrequest/747/">PR-BRU-747</a>"""),
464 """<a class="issue-tracker-link" href="http://foo/BRU/pullrequest/747/">PR-BRU-747</a>"""),
471 ])
465 ])
472 def test_urlify_issues(self, issue_pat, issue_server, issue_sub, sample, expected):
466 def test_urlify_issues(self, issue_pat, issue_server, issue_sub, sample, expected):
473 config_stub = {
467 config_stub = {
474 'sqlalchemy.url': 'foo',
468 'sqlalchemy.url': 'foo',
475 'issue_pat': issue_pat,
469 'issue_pat': issue_pat,
476 'issue_server_link': issue_server,
470 'issue_server_link': issue_server,
477 'issue_sub': issue_sub,
471 'issue_sub': issue_sub,
478 }
472 }
479 # force recreation of lazy function
473 # force recreation of lazy function
480 with mock.patch('kallithea.lib.helpers._urlify_issues_f', None):
474 with mock.patch('kallithea.lib.helpers._urlify_issues_f', None):
481 with mock.patch('kallithea.CONFIG', config_stub):
475 with mock.patch('kallithea.CONFIG', config_stub):
482 assert h.urlify_text(sample, 'repo_name') == expected
476 assert h.urlify_text(sample, 'repo_name') == expected
483
477
484 @base.parametrize('sample,expected', [
478 @base.parametrize('sample,expected', [
485 ('abc X5', 'abc <a class="issue-tracker-link" href="http://main/repo_name/main/5/">#5</a>'),
479 ('abc X5', 'abc <a class="issue-tracker-link" href="http://main/repo_name/main/5/">#5</a>'),
486 ('abc pullrequest #6 xyz', 'abc <a class="issue-tracker-link" href="http://pr/repo_name/pr/6">PR#6</a> xyz'),
480 ('abc pullrequest #6 xyz', 'abc <a class="issue-tracker-link" href="http://pr/repo_name/pr/6">PR#6</a> xyz'),
487 ('pull request7 #', '<a class="issue-tracker-link" href="http://pr/repo_name/pr/7">PR#7</a> #'),
481 ('pull request7 #', '<a class="issue-tracker-link" href="http://pr/repo_name/pr/7">PR#7</a> #'),
488 ('look PR9 and pr #11', 'look <a class="issue-tracker-link" href="http://pr/repo_name/pr/9">PR#9</a> and <a class="issue-tracker-link" href="http://pr/repo_name/pr/11">PR#11</a>'),
482 ('look PR9 and pr #11', 'look <a class="issue-tracker-link" href="http://pr/repo_name/pr/9">PR#9</a> and <a class="issue-tracker-link" href="http://pr/repo_name/pr/11">PR#11</a>'),
489 ('pullrequest#10 solves issue 9', '<a class="issue-tracker-link" href="http://pr/repo_name/pr/10">PR#10</a> solves <a class="issue-tracker-link" href="http://bug/repo_name/bug/9">bug#9</a>'),
483 ('pullrequest#10 solves issue 9', '<a class="issue-tracker-link" href="http://pr/repo_name/pr/10">PR#10</a> solves <a class="issue-tracker-link" href="http://bug/repo_name/bug/9">bug#9</a>'),
490 ('issue FAIL67', 'issue <a class="issue-tracker-link" href="http://fail/repo_name/67">FAIL67</a>'),
484 ('issue FAIL67', 'issue <a class="issue-tracker-link" href="http://fail/repo_name/67">FAIL67</a>'),
491 ('issue FAILMORE89', 'issue FAILMORE89'), # no match because absent prefix
485 ('issue FAILMORE89', 'issue FAILMORE89'), # no match because absent prefix
492 ])
486 ])
493 def test_urlify_issues_multiple_issue_patterns(self, sample, expected):
487 def test_urlify_issues_multiple_issue_patterns(self, sample, expected):
494 config_stub = {
488 config_stub = {
495 'sqlalchemy.url': r'foo',
489 'sqlalchemy.url': r'foo',
496 'issue_pat': r'X(\d+)',
490 'issue_pat': r'X(\d+)',
497 'issue_server_link': r'http://main/{repo}/main/\1/',
491 'issue_server_link': r'http://main/{repo}/main/\1/',
498 'issue_sub': r'#\1',
492 'issue_sub': r'#\1',
499 'issue_pat_pr': r'(?:pullrequest|pull request|PR|pr) ?#?(\d+)',
493 'issue_pat_pr': r'(?:pullrequest|pull request|PR|pr) ?#?(\d+)',
500 'issue_server_link_pr': r'http://pr/{repo}/pr/\1',
494 'issue_server_link_pr': r'http://pr/{repo}/pr/\1',
501 'issue_sub_pr': r'PR#\1',
495 'issue_sub_pr': r'PR#\1',
502 'issue_pat_bug': r'(?:BUG|bug|issue) ?#?(\d+)',
496 'issue_pat_bug': r'(?:BUG|bug|issue) ?#?(\d+)',
503 'issue_server_link_bug': r'http://bug/{repo}/bug/\1',
497 'issue_server_link_bug': r'http://bug/{repo}/bug/\1',
504 'issue_sub_bug': r'bug#\1',
498 'issue_sub_bug': r'bug#\1',
505 'issue_pat_empty_prefix': r'FAIL(\d+)',
499 'issue_pat_empty_prefix': r'FAIL(\d+)',
506 'issue_server_link_empty_prefix': r'http://fail/{repo}/\1',
500 'issue_server_link_empty_prefix': r'http://fail/{repo}/\1',
507 'issue_sub_empty_prefix': r'',
501 'issue_sub_empty_prefix': r'',
508 'issue_pat_absent_prefix': r'FAILMORE(\d+)',
502 'issue_pat_absent_prefix': r'FAILMORE(\d+)',
509 'issue_server_link_absent_prefix': r'http://failmore/{repo}/\1',
503 'issue_server_link_absent_prefix': r'http://failmore/{repo}/\1',
510 }
504 }
511 # force recreation of lazy function
505 # force recreation of lazy function
512 with mock.patch('kallithea.lib.helpers._urlify_issues_f', None):
506 with mock.patch('kallithea.lib.helpers._urlify_issues_f', None):
513 with mock.patch('kallithea.CONFIG', config_stub):
507 with mock.patch('kallithea.CONFIG', config_stub):
514 assert h.urlify_text(sample, 'repo_name') == expected
508 assert h.urlify_text(sample, 'repo_name') == expected
515
509
516 @base.parametrize('test,expected', [
510 @base.parametrize('test,expected', [
517 ("", None),
511 ("", None),
518 ("/_2", None),
512 ("/_2", None),
519 ("_2", 2),
513 ("_2", 2),
520 ("_2/", None),
514 ("_2/", None),
521 ])
515 ])
522 def test_get_permanent_id(self, test, expected):
516 def test_get_permanent_id(self, test, expected):
523 from kallithea.lib.utils import _get_permanent_id
517 from kallithea.lib.utils import _get_permanent_id
524 extracted = _get_permanent_id(test)
518 extracted = _get_permanent_id(test)
525 assert extracted == expected, 'url:%s, got:`%s` expected: `%s`' % (test, base._test, expected)
519 assert extracted == expected, 'url:%s, got:`%s` expected: `%s`' % (test, base._test, expected)
526
520
527 @base.parametrize('test,expected', [
521 @base.parametrize('test,expected', [
528 ("", ""),
522 ("", ""),
529 ("/", "/"),
523 ("/", "/"),
530 ("/_ID", '/_ID'),
524 ("/_ID", '/_ID'),
531 ("ID", "ID"),
525 ("ID", "ID"),
532 ("_ID", 'NAME'),
526 ("_ID", 'NAME'),
533 ("_ID/", 'NAME/'),
527 ("_ID/", 'NAME/'),
534 ("_ID/1/2", 'NAME/1/2'),
528 ("_ID/1/2", 'NAME/1/2'),
535 ("_IDa", '_IDa'),
529 ("_IDa", '_IDa'),
536 ])
530 ])
537 def test_fix_repo_id_name(self, test, expected):
531 def test_fix_repo_id_name(self, test, expected):
538 repo = db.Repository.get_by_repo_name(base.HG_REPO)
532 repo = db.Repository.get_by_repo_name(base.HG_REPO)
539 test = test.replace('ID', str(repo.repo_id))
533 test = test.replace('ID', str(repo.repo_id))
540 expected = expected.replace('NAME', repo.repo_name).replace('ID', str(repo.repo_id))
534 expected = expected.replace('NAME', repo.repo_name).replace('ID', str(repo.repo_id))
541 from kallithea.lib.utils import fix_repo_id_name
535 from kallithea.lib.utils import fix_repo_id_name
542 replaced = fix_repo_id_name(test)
536 replaced = fix_repo_id_name(test)
543 assert replaced == expected, 'url:%s, got:`%s` expected: `%s`' % (test, replaced, expected)
537 assert replaced == expected, 'url:%s, got:`%s` expected: `%s`' % (test, replaced, expected)
544
538
545 @base.parametrize('canonical,test,expected', [
539 @base.parametrize('canonical,test,expected', [
546 ('http://www.example.org/', '/abc/xyz', 'http://www.example.org/abc/xyz'),
540 ('http://www.example.org/', '/abc/xyz', 'http://www.example.org/abc/xyz'),
547 ('http://www.example.org', '/abc/xyz', 'http://www.example.org/abc/xyz'),
541 ('http://www.example.org', '/abc/xyz', 'http://www.example.org/abc/xyz'),
548 ('http://www.example.org', '/abc/xyz/', 'http://www.example.org/abc/xyz/'),
542 ('http://www.example.org', '/abc/xyz/', 'http://www.example.org/abc/xyz/'),
549 ('http://www.example.org', 'abc/xyz/', 'http://www.example.org/abc/xyz/'),
543 ('http://www.example.org', 'abc/xyz/', 'http://www.example.org/abc/xyz/'),
550 ('http://www.example.org', 'about', 'http://www.example.org/about-page'),
544 ('http://www.example.org', 'about', 'http://www.example.org/about-page'),
551 ('http://www.example.org/repos/', 'abc/xyz/', 'http://www.example.org/repos/abc/xyz/'),
545 ('http://www.example.org/repos/', 'abc/xyz/', 'http://www.example.org/repos/abc/xyz/'),
552 ('http://www.example.org/kallithea/repos/', 'abc/xyz/', 'http://www.example.org/kallithea/repos/abc/xyz/'),
546 ('http://www.example.org/kallithea/repos/', 'abc/xyz/', 'http://www.example.org/kallithea/repos/abc/xyz/'),
553 ])
547 ])
554 def test_canonical_url(self, canonical, test, expected):
548 def test_canonical_url(self, canonical, test, expected):
555 # setup url(), used by canonical_url
549 # setup url(), used by canonical_url
556 import routes
557 from tg import request
558
559 m = routes.Mapper()
550 m = routes.Mapper()
560 m.connect('about', '/about-page')
551 m.connect('about', '/about-page')
561 url = routes.URLGenerator(m, {'HTTP_HOST': 'http_host.example.org'})
552 url = routes.URLGenerator(m, {'HTTP_HOST': 'http_host.example.org'})
562
553
563 config_mock = {
554 config_mock = {
564 'canonical_url': canonical,
555 'canonical_url': canonical,
565 }
556 }
566
557
567 with test_context(self.app):
558 with test_context(self.app):
568 request.environ['routes.url'] = url
559 request.environ['routes.url'] = url
569 with mock.patch('kallithea.CONFIG', config_mock):
560 with mock.patch('kallithea.CONFIG', config_mock):
570 assert webutils.canonical_url(test) == expected
561 assert webutils.canonical_url(test) == expected
571
562
572 @base.parametrize('canonical,expected', [
563 @base.parametrize('canonical,expected', [
573 ('http://www.example.org', 'www.example.org'),
564 ('http://www.example.org', 'www.example.org'),
574 ('http://www.example.org/repos/', 'www.example.org'),
565 ('http://www.example.org/repos/', 'www.example.org'),
575 ('http://www.example.org/kallithea/repos/', 'www.example.org'),
566 ('http://www.example.org/kallithea/repos/', 'www.example.org'),
576 ])
567 ])
577 def test_canonical_hostname(self, canonical, expected):
568 def test_canonical_hostname(self, canonical, expected):
578 import routes
579 from tg import request
580
581 # setup url(), used by canonical_hostname
569 # setup url(), used by canonical_hostname
582 m = routes.Mapper()
570 m = routes.Mapper()
583 url = routes.URLGenerator(m, {'HTTP_HOST': 'http_host.example.org'})
571 url = routes.URLGenerator(m, {'HTTP_HOST': 'http_host.example.org'})
584
572
585 config_mock = {
573 config_mock = {
586 'canonical_url': canonical,
574 'canonical_url': canonical,
587 }
575 }
588
576
589 with test_context(self.app):
577 with test_context(self.app):
590 request.environ['routes.url'] = url
578 request.environ['routes.url'] = url
591 with mock.patch('kallithea.CONFIG', config_mock):
579 with mock.patch('kallithea.CONFIG', config_mock):
592 assert webutils.canonical_hostname() == expected
580 assert webutils.canonical_hostname() == expected
@@ -1,37 +1,37 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14
14
15 import pytest
15 import pytest
16
16
17 from kallithea.lib.graphmod import graph_data
17 from kallithea.model import db
18 from kallithea.model import db
18 from kallithea.tests import base
19 from kallithea.tests import base
19
20
20
21
21 @pytest.mark.skipif("'TEST_PERFORMANCE' not in os.environ", reason="skipping performance tests, set TEST_PERFORMANCE in environment if desired")
22 @pytest.mark.skipif("'TEST_PERFORMANCE' not in os.environ", reason="skipping performance tests, set TEST_PERFORMANCE in environment if desired")
22 class TestVCSPerformance(base.TestController):
23 class TestVCSPerformance(base.TestController):
23
24
24 def graphmod(self, repo):
25 def graphmod(self, repo):
25 """ Simple test for running the graph_data function for profiling/testing performance. """
26 """ Simple test for running the graph_data function for profiling/testing performance. """
26 from kallithea.lib.graphmod import graph_data
27 dbr = db.Repository.get_by_repo_name(repo)
27 dbr = db.Repository.get_by_repo_name(repo)
28 scm_inst = dbr.scm_instance
28 scm_inst = dbr.scm_instance
29 collection = scm_inst.get_changesets(start=0, end=None, branch_name=None)
29 collection = scm_inst.get_changesets(start=0, end=None, branch_name=None)
30 revs = [x.revision for x in collection]
30 revs = [x.revision for x in collection]
31 jsdata = graph_data(scm_inst, revs)
31 jsdata = graph_data(scm_inst, revs)
32
32
33 def test_graphmod_hg(self, benchmark):
33 def test_graphmod_hg(self, benchmark):
34 benchmark(self.graphmod, base.HG_REPO)
34 benchmark(self.graphmod, base.HG_REPO)
35
35
36 def test_graphmod_git(self, benchmark):
36 def test_graphmod_git(self, benchmark):
37 benchmark(self.graphmod, base.GIT_REPO)
37 benchmark(self.graphmod, base.GIT_REPO)
@@ -1,212 +1,212 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2 # This program is free software: you can redistribute it and/or modify
2 # This program is free software: you can redistribute it and/or modify
3 # it under the terms of the GNU General Public License as published by
3 # it under the terms of the GNU General Public License as published by
4 # the Free Software Foundation, either version 3 of the License, or
4 # the Free Software Foundation, either version 3 of the License, or
5 # (at your option) any later version.
5 # (at your option) any later version.
6 #
6 #
7 # This program is distributed in the hope that it will be useful,
7 # This program is distributed in the hope that it will be useful,
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 # GNU General Public License for more details.
10 # GNU General Public License for more details.
11 #
11 #
12 # You should have received a copy of the GNU General Public License
12 # You should have received a copy of the GNU General Public License
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 """
14 """
15 kallithea.tests.scripts.manual_test_concurrency
15 kallithea.tests.scripts.manual_test_concurrency
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Test suite for making push/pull operations
18 Test suite for making push/pull operations
19
19
20 This file was forked by the Kallithea project in July 2014.
20 This file was forked by the Kallithea project in July 2014.
21 Original author and date, and relevant copyright and licensing information is below:
21 Original author and date, and relevant copyright and licensing information is below:
22 :created_on: Dec 30, 2010
22 :created_on: Dec 30, 2010
23 :author: marcink
23 :author: marcink
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
24 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :license: GPLv3, see LICENSE.md for more details.
25 :license: GPLv3, see LICENSE.md for more details.
26
26
27 """
27 """
28
28
29 import logging
29 import logging
30 import os
30 import os
31 import shutil
31 import shutil
32 import sys
32 import sys
33 import tempfile
33 import tempfile
34 from os.path import dirname
34 from os.path import dirname
35 from subprocess import PIPE, Popen
35 from subprocess import PIPE, Popen
36
36
37 from paste.deploy import appconfig
37 from paste.deploy import appconfig
38 from sqlalchemy import engine_from_config
38 from sqlalchemy import engine_from_config
39
39
40 import kallithea.config.application
40 import kallithea.config.application
41 from kallithea.lib.auth import get_crypt_password
41 from kallithea.lib.auth import get_crypt_password
42 from kallithea.model import db, meta
42 from kallithea.model import db, meta
43 from kallithea.model.base import init_model
43 from kallithea.model.base import init_model
44 from kallithea.model.repo import RepoModel
44 from kallithea.tests.base import HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS
45 from kallithea.tests.base import HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS
45
46
46
47
47 rel_path = dirname(dirname(dirname(dirname(os.path.abspath(__file__)))))
48 rel_path = dirname(dirname(dirname(dirname(os.path.abspath(__file__)))))
48 conf = appconfig('config:development.ini', relative_to=rel_path)
49 conf = appconfig('config:development.ini', relative_to=rel_path)
49 kallithea.config.application.make_app(conf.global_conf, **conf.local_conf)
50 kallithea.config.application.make_app(conf.global_conf, **conf.local_conf)
50
51
51 USER = TEST_USER_ADMIN_LOGIN
52 USER = TEST_USER_ADMIN_LOGIN
52 PASS = TEST_USER_ADMIN_PASS
53 PASS = TEST_USER_ADMIN_PASS
53 HOST = 'server.local'
54 HOST = 'server.local'
54 METHOD = 'pull'
55 METHOD = 'pull'
55 DEBUG = True
56 DEBUG = True
56 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
57
58
58
59
59 class Command(object):
60 class Command(object):
60
61
61 def __init__(self, cwd):
62 def __init__(self, cwd):
62 self.cwd = cwd
63 self.cwd = cwd
63
64
64 def execute(self, cmd, *args):
65 def execute(self, cmd, *args):
65 """Runs command on the system with given ``args``.
66 """Runs command on the system with given ``args``.
66 """
67 """
67
68
68 command = cmd + ' ' + ' '.join(args)
69 command = cmd + ' ' + ' '.join(args)
69 log.debug('Executing %s', command)
70 log.debug('Executing %s', command)
70 if DEBUG:
71 if DEBUG:
71 print(command)
72 print(command)
72 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, cwd=self.cwd)
73 p = Popen(command, shell=True, stdout=PIPE, stderr=PIPE, cwd=self.cwd)
73 stdout, stderr = p.communicate()
74 stdout, stderr = p.communicate()
74 if DEBUG:
75 if DEBUG:
75 print(stdout, stderr)
76 print(stdout, stderr)
76 return stdout, stderr
77 return stdout, stderr
77
78
78
79
79 def get_session():
80 def get_session():
80 engine = engine_from_config(conf, 'sqlalchemy.')
81 engine = engine_from_config(conf, 'sqlalchemy.')
81 init_model(engine)
82 init_model(engine)
82 sa = meta.Session
83 sa = meta.Session
83 return sa
84 return sa
84
85
85
86
86 def create_test_user(force=True):
87 def create_test_user(force=True):
87 print('creating test user')
88 print('creating test user')
88 sa = get_session()
89 sa = get_session()
89
90
90 user = sa.query(db.User).filter(db.User.username == USER).scalar()
91 user = sa.query(db.User).filter(db.User.username == USER).scalar()
91
92
92 if force and user is not None:
93 if force and user is not None:
93 print('removing current user')
94 print('removing current user')
94 for repo in sa.query(db.Repository).filter(db.Repository.user == user).all():
95 for repo in sa.query(db.Repository).filter(db.Repository.user == user).all():
95 sa.delete(repo)
96 sa.delete(repo)
96 sa.delete(user)
97 sa.delete(user)
97 sa.commit()
98 sa.commit()
98
99
99 if user is None or force:
100 if user is None or force:
100 print('creating new one')
101 print('creating new one')
101 new_usr = db.User()
102 new_usr = db.User()
102 new_usr.username = USER
103 new_usr.username = USER
103 new_usr.password = get_crypt_password(PASS)
104 new_usr.password = get_crypt_password(PASS)
104 new_usr.email = 'mail@example.com'
105 new_usr.email = 'mail@example.com'
105 new_usr.name = 'test'
106 new_usr.name = 'test'
106 new_usr.lastname = 'lasttestname'
107 new_usr.lastname = 'lasttestname'
107 new_usr.active = True
108 new_usr.active = True
108 new_usr.admin = True
109 new_usr.admin = True
109 sa.add(new_usr)
110 sa.add(new_usr)
110 sa.commit()
111 sa.commit()
111
112
112 print('done')
113 print('done')
113
114
114
115
115 def create_test_repo(force=True):
116 def create_test_repo(force=True):
116 print('creating test repo')
117 print('creating test repo')
117 from kallithea.model.repo import RepoModel
118 sa = get_session()
118 sa = get_session()
119
119
120 user = sa.query(db.User).filter(db.User.username == USER).scalar()
120 user = sa.query(db.User).filter(db.User.username == USER).scalar()
121 if user is None:
121 if user is None:
122 raise Exception('user not found')
122 raise Exception('user not found')
123
123
124 repo = sa.query(db.Repository).filter(db.Repository.repo_name == HG_REPO).scalar()
124 repo = sa.query(db.Repository).filter(db.Repository.repo_name == HG_REPO).scalar()
125
125
126 if repo is None:
126 if repo is None:
127 print('repo not found creating')
127 print('repo not found creating')
128
128
129 form_data = {'repo_name': HG_REPO,
129 form_data = {'repo_name': HG_REPO,
130 'repo_type': 'hg',
130 'repo_type': 'hg',
131 'private': False,
131 'private': False,
132 'clone_uri': ''}
132 'clone_uri': ''}
133 rm = RepoModel()
133 rm = RepoModel()
134 rm.base_path = '/home/hg'
134 rm.base_path = '/home/hg'
135 rm.create(form_data, user)
135 rm.create(form_data, user)
136
136
137 print('done')
137 print('done')
138
138
139
139
140 def set_anonymous_access(enable=True):
140 def set_anonymous_access(enable=True):
141 sa = get_session()
141 sa = get_session()
142 user = sa.query(db.User).filter(db.User.username == 'default').one()
142 user = sa.query(db.User).filter(db.User.username == 'default').one()
143 user.active = enable
143 user.active = enable
144 sa.add(user)
144 sa.add(user)
145 sa.commit()
145 sa.commit()
146
146
147
147
148 def get_anonymous_access():
148 def get_anonymous_access():
149 sa = get_session()
149 sa = get_session()
150 return sa.query(db.User).filter(db.User.username == 'default').one().active
150 return sa.query(db.User).filter(db.User.username == 'default').one().active
151
151
152
152
153 #==============================================================================
153 #==============================================================================
154 # TESTS
154 # TESTS
155 #==============================================================================
155 #==============================================================================
156 def test_clone_with_credentials(no_errors=False, repo=HG_REPO, method=METHOD,
156 def test_clone_with_credentials(no_errors=False, repo=HG_REPO, method=METHOD,
157 backend='hg'):
157 backend='hg'):
158 cwd = path = os.path.join(db.Ui.get_by_key('paths', '/').ui_value, repo)
158 cwd = path = os.path.join(db.Ui.get_by_key('paths', '/').ui_value, repo)
159
159
160 try:
160 try:
161 shutil.rmtree(path, ignore_errors=True)
161 shutil.rmtree(path, ignore_errors=True)
162 os.makedirs(path)
162 os.makedirs(path)
163 #print 'made dirs %s' % os.path.join(path)
163 #print 'made dirs %s' % os.path.join(path)
164 except OSError:
164 except OSError:
165 raise
165 raise
166
166
167 clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s' % \
167 clone_url = 'http://%(user)s:%(pass)s@%(host)s/%(cloned_repo)s' % \
168 {'user': USER,
168 {'user': USER,
169 'pass': PASS,
169 'pass': PASS,
170 'host': HOST,
170 'host': HOST,
171 'cloned_repo': repo, }
171 'cloned_repo': repo, }
172
172
173 dest = tempfile.mktemp(dir=path, prefix='dest-')
173 dest = tempfile.mktemp(dir=path, prefix='dest-')
174 if method == 'pull':
174 if method == 'pull':
175 stdout, stderr = Command(cwd).execute(backend, method, '--cwd', dest, clone_url)
175 stdout, stderr = Command(cwd).execute(backend, method, '--cwd', dest, clone_url)
176 else:
176 else:
177 stdout, stderr = Command(cwd).execute(backend, method, clone_url, dest)
177 stdout, stderr = Command(cwd).execute(backend, method, clone_url, dest)
178 if not no_errors:
178 if not no_errors:
179 if backend == 'hg':
179 if backend == 'hg':
180 assert """adding file changes""" in stdout, 'no messages about cloning'
180 assert """adding file changes""" in stdout, 'no messages about cloning'
181 assert """abort""" not in stderr, 'got error from clone'
181 assert """abort""" not in stderr, 'got error from clone'
182 elif backend == 'git':
182 elif backend == 'git':
183 assert """Cloning into""" in stdout, 'no messages about cloning'
183 assert """Cloning into""" in stdout, 'no messages about cloning'
184
184
185
185
186 if __name__ == '__main__':
186 if __name__ == '__main__':
187 try:
187 try:
188 create_test_user(force=False)
188 create_test_user(force=False)
189 import time
189 import time
190
190
191 try:
191 try:
192 METHOD = sys.argv[3]
192 METHOD = sys.argv[3]
193 except IndexError:
193 except IndexError:
194 pass
194 pass
195
195
196 try:
196 try:
197 backend = sys.argv[4]
197 backend = sys.argv[4]
198 except IndexError:
198 except IndexError:
199 backend = 'hg'
199 backend = 'hg'
200
200
201 if METHOD == 'pull':
201 if METHOD == 'pull':
202 seq = next(tempfile._RandomNameSequence())
202 seq = next(tempfile._RandomNameSequence())
203 test_clone_with_credentials(repo=sys.argv[1], method='clone',
203 test_clone_with_credentials(repo=sys.argv[1], method='clone',
204 backend=backend)
204 backend=backend)
205 s = time.time()
205 s = time.time()
206 for i in range(1, int(sys.argv[2]) + 1):
206 for i in range(1, int(sys.argv[2]) + 1):
207 print('take', i)
207 print('take', i)
208 test_clone_with_credentials(repo=sys.argv[1], method=METHOD,
208 test_clone_with_credentials(repo=sys.argv[1], method=METHOD,
209 backend=backend)
209 backend=backend)
210 print('time taken %.3f' % (time.time() - s))
210 print('time taken %.3f' % (time.time() - s))
211 except Exception as e:
211 except Exception as e:
212 sys.exit('stop on %s' % e)
212 sys.exit('stop on %s' % e)
@@ -1,256 +1,256 b''
1 import copy
1 import datetime
2 import datetime
2
3
3 import pytest
4 import pytest
4
5
5 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
6 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
6 from kallithea.lib.vcs.nodes import FileNode
7 from kallithea.lib.vcs.nodes import FileNode
7 from kallithea.tests.vcs import TEST_USER_CONFIG_FILE
8 from kallithea.tests.vcs import TEST_USER_CONFIG_FILE
8 from kallithea.tests.vcs.base import _BackendTestMixin
9 from kallithea.tests.vcs.base import _BackendTestMixin
9
10
10
11
11 class RepositoryBaseTest(_BackendTestMixin):
12 class RepositoryBaseTest(_BackendTestMixin):
12 recreate_repo_per_test = False
13 recreate_repo_per_test = False
13
14
14 @classmethod
15 @classmethod
15 def _get_commits(cls):
16 def _get_commits(cls):
16 return super(RepositoryBaseTest, cls)._get_commits()[:1]
17 return super(RepositoryBaseTest, cls)._get_commits()[:1]
17
18
18 def test_get_config_value(self):
19 def test_get_config_value(self):
19 assert self.repo.get_config_value('universal', 'foo', TEST_USER_CONFIG_FILE) == 'bar'
20 assert self.repo.get_config_value('universal', 'foo', TEST_USER_CONFIG_FILE) == 'bar'
20
21
21 def test_get_config_value_defaults_to_None(self):
22 def test_get_config_value_defaults_to_None(self):
22 assert self.repo.get_config_value('universal', 'nonexist', TEST_USER_CONFIG_FILE) == None
23 assert self.repo.get_config_value('universal', 'nonexist', TEST_USER_CONFIG_FILE) == None
23
24
24 def test_get_user_name(self):
25 def test_get_user_name(self):
25 assert self.repo.get_user_name(TEST_USER_CONFIG_FILE) == 'Foo Bar'
26 assert self.repo.get_user_name(TEST_USER_CONFIG_FILE) == 'Foo Bar'
26
27
27 def test_get_user_email(self):
28 def test_get_user_email(self):
28 assert self.repo.get_user_email(TEST_USER_CONFIG_FILE) == 'foo.bar@example.com'
29 assert self.repo.get_user_email(TEST_USER_CONFIG_FILE) == 'foo.bar@example.com'
29
30
30 def test_repo_equality(self):
31 def test_repo_equality(self):
31 assert self.repo == self.repo
32 assert self.repo == self.repo
32
33
33 def test_repo_equality_broken_object(self):
34 def test_repo_equality_broken_object(self):
34 import copy
35 _repo = copy.copy(self.repo)
35 _repo = copy.copy(self.repo)
36 delattr(_repo, 'path')
36 delattr(_repo, 'path')
37 assert self.repo != _repo
37 assert self.repo != _repo
38
38
39 def test_repo_equality_other_object(self):
39 def test_repo_equality_other_object(self):
40 class dummy(object):
40 class dummy(object):
41 path = self.repo.path
41 path = self.repo.path
42 assert self.repo != dummy()
42 assert self.repo != dummy()
43
43
44
44
45 class TestGitRepositoryBase(RepositoryBaseTest):
45 class TestGitRepositoryBase(RepositoryBaseTest):
46 backend_alias = 'git'
46 backend_alias = 'git'
47
47
48
48
49 class TestHgRepositoryBase(RepositoryBaseTest):
49 class TestHgRepositoryBase(RepositoryBaseTest):
50 backend_alias = 'hg'
50 backend_alias = 'hg'
51
51
52
52
53 class RepositoryGetDiffTest(_BackendTestMixin):
53 class RepositoryGetDiffTest(_BackendTestMixin):
54
54
55 @classmethod
55 @classmethod
56 def _get_commits(cls):
56 def _get_commits(cls):
57 commits = [
57 commits = [
58 {
58 {
59 'message': 'Initial commit',
59 'message': 'Initial commit',
60 'author': 'Joe Doe <joe.doe@example.com>',
60 'author': 'Joe Doe <joe.doe@example.com>',
61 'date': datetime.datetime(2010, 1, 1, 20),
61 'date': datetime.datetime(2010, 1, 1, 20),
62 'added': [
62 'added': [
63 FileNode('foobar', content='foobar'),
63 FileNode('foobar', content='foobar'),
64 FileNode('foobar2', content='foobar2'),
64 FileNode('foobar2', content='foobar2'),
65 ],
65 ],
66 },
66 },
67 {
67 {
68 'message': 'Changed foobar, added foobar3',
68 'message': 'Changed foobar, added foobar3',
69 'author': 'Jane Doe <jane.doe@example.com>',
69 'author': 'Jane Doe <jane.doe@example.com>',
70 'date': datetime.datetime(2010, 1, 1, 21),
70 'date': datetime.datetime(2010, 1, 1, 21),
71 'added': [
71 'added': [
72 FileNode('foobar3', content='foobar3'),
72 FileNode('foobar3', content='foobar3'),
73 ],
73 ],
74 'changed': [
74 'changed': [
75 FileNode('foobar', 'FOOBAR'),
75 FileNode('foobar', 'FOOBAR'),
76 ],
76 ],
77 },
77 },
78 {
78 {
79 'message': 'Removed foobar, changed foobar3',
79 'message': 'Removed foobar, changed foobar3',
80 'author': 'Jane Doe <jane.doe@example.com>',
80 'author': 'Jane Doe <jane.doe@example.com>',
81 'date': datetime.datetime(2010, 1, 1, 22),
81 'date': datetime.datetime(2010, 1, 1, 22),
82 'changed': [
82 'changed': [
83 FileNode('foobar3', content='FOOBAR\nFOOBAR\nFOOBAR\n'),
83 FileNode('foobar3', content='FOOBAR\nFOOBAR\nFOOBAR\n'),
84 ],
84 ],
85 'removed': [FileNode('foobar')],
85 'removed': [FileNode('foobar')],
86 },
86 },
87 {
87 {
88 'message': 'Commit that contains glob pattern in filename',
88 'message': 'Commit that contains glob pattern in filename',
89 'author': 'Jane Doe <jane.doe@example.com>',
89 'author': 'Jane Doe <jane.doe@example.com>',
90 'date': datetime.datetime(2010, 1, 1, 22),
90 'date': datetime.datetime(2010, 1, 1, 22),
91 'added': [
91 'added': [
92 FileNode('README{', content='Strangely-named README file'),
92 FileNode('README{', content='Strangely-named README file'),
93 ],
93 ],
94 },
94 },
95 ]
95 ]
96 return commits
96 return commits
97
97
98 def test_raise_for_wrong(self):
98 def test_raise_for_wrong(self):
99 with pytest.raises(ChangesetDoesNotExistError):
99 with pytest.raises(ChangesetDoesNotExistError):
100 self.repo.get_diff('a' * 40, 'b' * 40)
100 self.repo.get_diff('a' * 40, 'b' * 40)
101
101
102 def test_glob_patterns_in_filename_do_not_raise_exception(self):
102 def test_glob_patterns_in_filename_do_not_raise_exception(self):
103 revs = self.repo.revisions
103 revs = self.repo.revisions
104
104
105 diff = self.repo.get_diff(revs[2], revs[3], path='README{') # should not raise
105 diff = self.repo.get_diff(revs[2], revs[3], path='README{') # should not raise
106
106
107
107
108 class TestGitRepositoryGetDiff(RepositoryGetDiffTest):
108 class TestGitRepositoryGetDiff(RepositoryGetDiffTest):
109 backend_alias = 'git'
109 backend_alias = 'git'
110
110
111 def test_initial_commit_diff(self):
111 def test_initial_commit_diff(self):
112 initial_rev = self.repo.revisions[0]
112 initial_rev = self.repo.revisions[0]
113 assert self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev) == br'''diff --git a/foobar b/foobar
113 assert self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev) == br'''diff --git a/foobar b/foobar
114 new file mode 100644
114 new file mode 100644
115 index 0000000000000000000000000000000000000000..f6ea0495187600e7b2288c8ac19c5886383a4632
115 index 0000000000000000000000000000000000000000..f6ea0495187600e7b2288c8ac19c5886383a4632
116 --- /dev/null
116 --- /dev/null
117 +++ b/foobar
117 +++ b/foobar
118 @@ -0,0 +1 @@
118 @@ -0,0 +1 @@
119 +foobar
119 +foobar
120 \ No newline at end of file
120 \ No newline at end of file
121 diff --git a/foobar2 b/foobar2
121 diff --git a/foobar2 b/foobar2
122 new file mode 100644
122 new file mode 100644
123 index 0000000000000000000000000000000000000000..e8c9d6b98e3dce993a464935e1a53f50b56a3783
123 index 0000000000000000000000000000000000000000..e8c9d6b98e3dce993a464935e1a53f50b56a3783
124 --- /dev/null
124 --- /dev/null
125 +++ b/foobar2
125 +++ b/foobar2
126 @@ -0,0 +1 @@
126 @@ -0,0 +1 @@
127 +foobar2
127 +foobar2
128 \ No newline at end of file
128 \ No newline at end of file
129 '''
129 '''
130
130
131 def test_second_changeset_diff(self):
131 def test_second_changeset_diff(self):
132 revs = self.repo.revisions
132 revs = self.repo.revisions
133 assert self.repo.get_diff(revs[0], revs[1]) == br'''diff --git a/foobar b/foobar
133 assert self.repo.get_diff(revs[0], revs[1]) == br'''diff --git a/foobar b/foobar
134 index f6ea0495187600e7b2288c8ac19c5886383a4632..389865bb681b358c9b102d79abd8d5f941e96551 100644
134 index f6ea0495187600e7b2288c8ac19c5886383a4632..389865bb681b358c9b102d79abd8d5f941e96551 100644
135 --- a/foobar
135 --- a/foobar
136 +++ b/foobar
136 +++ b/foobar
137 @@ -1 +1 @@
137 @@ -1 +1 @@
138 -foobar
138 -foobar
139 \ No newline at end of file
139 \ No newline at end of file
140 +FOOBAR
140 +FOOBAR
141 \ No newline at end of file
141 \ No newline at end of file
142 diff --git a/foobar3 b/foobar3
142 diff --git a/foobar3 b/foobar3
143 new file mode 100644
143 new file mode 100644
144 index 0000000000000000000000000000000000000000..c11c37d41d33fb47741cff93fa5f9d798c1535b0
144 index 0000000000000000000000000000000000000000..c11c37d41d33fb47741cff93fa5f9d798c1535b0
145 --- /dev/null
145 --- /dev/null
146 +++ b/foobar3
146 +++ b/foobar3
147 @@ -0,0 +1 @@
147 @@ -0,0 +1 @@
148 +foobar3
148 +foobar3
149 \ No newline at end of file
149 \ No newline at end of file
150 '''
150 '''
151
151
152 def test_third_changeset_diff(self):
152 def test_third_changeset_diff(self):
153 revs = self.repo.revisions
153 revs = self.repo.revisions
154 assert self.repo.get_diff(revs[1], revs[2]) == br'''diff --git a/foobar b/foobar
154 assert self.repo.get_diff(revs[1], revs[2]) == br'''diff --git a/foobar b/foobar
155 deleted file mode 100644
155 deleted file mode 100644
156 index 389865bb681b358c9b102d79abd8d5f941e96551..0000000000000000000000000000000000000000
156 index 389865bb681b358c9b102d79abd8d5f941e96551..0000000000000000000000000000000000000000
157 --- a/foobar
157 --- a/foobar
158 +++ /dev/null
158 +++ /dev/null
159 @@ -1 +0,0 @@
159 @@ -1 +0,0 @@
160 -FOOBAR
160 -FOOBAR
161 \ No newline at end of file
161 \ No newline at end of file
162 diff --git a/foobar3 b/foobar3
162 diff --git a/foobar3 b/foobar3
163 index c11c37d41d33fb47741cff93fa5f9d798c1535b0..f9324477362684ff692aaf5b9a81e01b9e9a671c 100644
163 index c11c37d41d33fb47741cff93fa5f9d798c1535b0..f9324477362684ff692aaf5b9a81e01b9e9a671c 100644
164 --- a/foobar3
164 --- a/foobar3
165 +++ b/foobar3
165 +++ b/foobar3
166 @@ -1 +1,3 @@
166 @@ -1 +1,3 @@
167 -foobar3
167 -foobar3
168 \ No newline at end of file
168 \ No newline at end of file
169 +FOOBAR
169 +FOOBAR
170 +FOOBAR
170 +FOOBAR
171 +FOOBAR
171 +FOOBAR
172 '''
172 '''
173
173
174 def test_fourth_changeset_diff(self):
174 def test_fourth_changeset_diff(self):
175 revs = self.repo.revisions
175 revs = self.repo.revisions
176 assert self.repo.get_diff(revs[2], revs[3]) == br'''diff --git a/README{ b/README{
176 assert self.repo.get_diff(revs[2], revs[3]) == br'''diff --git a/README{ b/README{
177 new file mode 100644
177 new file mode 100644
178 index 0000000000000000000000000000000000000000..cdc0c1b5d234feedb37bbac19cd1b6442061102d
178 index 0000000000000000000000000000000000000000..cdc0c1b5d234feedb37bbac19cd1b6442061102d
179 --- /dev/null
179 --- /dev/null
180 +++ b/README{
180 +++ b/README{
181 @@ -0,0 +1 @@
181 @@ -0,0 +1 @@
182 +Strangely-named README file
182 +Strangely-named README file
183 \ No newline at end of file
183 \ No newline at end of file
184 '''
184 '''
185
185
186
186
187 class TestHgRepositoryGetDiff(RepositoryGetDiffTest):
187 class TestHgRepositoryGetDiff(RepositoryGetDiffTest):
188 backend_alias = 'hg'
188 backend_alias = 'hg'
189
189
190 def test_initial_commit_diff(self):
190 def test_initial_commit_diff(self):
191 initial_rev = self.repo.revisions[0]
191 initial_rev = self.repo.revisions[0]
192 assert self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev) == br'''diff --git a/foobar b/foobar
192 assert self.repo.get_diff(self.repo.EMPTY_CHANGESET, initial_rev) == br'''diff --git a/foobar b/foobar
193 new file mode 100644
193 new file mode 100644
194 --- /dev/null
194 --- /dev/null
195 +++ b/foobar
195 +++ b/foobar
196 @@ -0,0 +1,1 @@
196 @@ -0,0 +1,1 @@
197 +foobar
197 +foobar
198 \ No newline at end of file
198 \ No newline at end of file
199 diff --git a/foobar2 b/foobar2
199 diff --git a/foobar2 b/foobar2
200 new file mode 100644
200 new file mode 100644
201 --- /dev/null
201 --- /dev/null
202 +++ b/foobar2
202 +++ b/foobar2
203 @@ -0,0 +1,1 @@
203 @@ -0,0 +1,1 @@
204 +foobar2
204 +foobar2
205 \ No newline at end of file
205 \ No newline at end of file
206 '''
206 '''
207
207
208 def test_second_changeset_diff(self):
208 def test_second_changeset_diff(self):
209 revs = self.repo.revisions
209 revs = self.repo.revisions
210 assert self.repo.get_diff(revs[0], revs[1]) == br'''diff --git a/foobar b/foobar
210 assert self.repo.get_diff(revs[0], revs[1]) == br'''diff --git a/foobar b/foobar
211 --- a/foobar
211 --- a/foobar
212 +++ b/foobar
212 +++ b/foobar
213 @@ -1,1 +1,1 @@
213 @@ -1,1 +1,1 @@
214 -foobar
214 -foobar
215 \ No newline at end of file
215 \ No newline at end of file
216 +FOOBAR
216 +FOOBAR
217 \ No newline at end of file
217 \ No newline at end of file
218 diff --git a/foobar3 b/foobar3
218 diff --git a/foobar3 b/foobar3
219 new file mode 100644
219 new file mode 100644
220 --- /dev/null
220 --- /dev/null
221 +++ b/foobar3
221 +++ b/foobar3
222 @@ -0,0 +1,1 @@
222 @@ -0,0 +1,1 @@
223 +foobar3
223 +foobar3
224 \ No newline at end of file
224 \ No newline at end of file
225 '''
225 '''
226
226
227 def test_third_changeset_diff(self):
227 def test_third_changeset_diff(self):
228 revs = self.repo.revisions
228 revs = self.repo.revisions
229 assert self.repo.get_diff(revs[1], revs[2]) == br'''diff --git a/foobar b/foobar
229 assert self.repo.get_diff(revs[1], revs[2]) == br'''diff --git a/foobar b/foobar
230 deleted file mode 100644
230 deleted file mode 100644
231 --- a/foobar
231 --- a/foobar
232 +++ /dev/null
232 +++ /dev/null
233 @@ -1,1 +0,0 @@
233 @@ -1,1 +0,0 @@
234 -FOOBAR
234 -FOOBAR
235 \ No newline at end of file
235 \ No newline at end of file
236 diff --git a/foobar3 b/foobar3
236 diff --git a/foobar3 b/foobar3
237 --- a/foobar3
237 --- a/foobar3
238 +++ b/foobar3
238 +++ b/foobar3
239 @@ -1,1 +1,3 @@
239 @@ -1,1 +1,3 @@
240 -foobar3
240 -foobar3
241 \ No newline at end of file
241 \ No newline at end of file
242 +FOOBAR
242 +FOOBAR
243 +FOOBAR
243 +FOOBAR
244 +FOOBAR
244 +FOOBAR
245 '''
245 '''
246
246
247 def test_fourth_changeset_diff(self):
247 def test_fourth_changeset_diff(self):
248 revs = self.repo.revisions
248 revs = self.repo.revisions
249 assert self.repo.get_diff(revs[2], revs[3]) == br'''diff --git a/README{ b/README{
249 assert self.repo.get_diff(revs[2], revs[3]) == br'''diff --git a/README{ b/README{
250 new file mode 100644
250 new file mode 100644
251 --- /dev/null
251 --- /dev/null
252 +++ b/README{
252 +++ b/README{
253 @@ -0,0 +1,1 @@
253 @@ -0,0 +1,1 @@
254 +Strangely-named README file
254 +Strangely-named README file
255 \ No newline at end of file
255 \ No newline at end of file
256 '''
256 '''
@@ -1,91 +1,90 b''
1 import datetime
1 import datetime
2
2
3 import pytest
3 import pytest
4
4
5 from kallithea.lib.vcs.exceptions import BranchDoesNotExistError
5 from kallithea.lib.vcs.nodes import FileNode
6 from kallithea.lib.vcs.nodes import FileNode
6 from kallithea.tests.vcs.base import _BackendTestMixin
7 from kallithea.tests.vcs.base import _BackendTestMixin
7
8
8
9
9 class WorkdirTestCaseMixin(_BackendTestMixin):
10 class WorkdirTestCaseMixin(_BackendTestMixin):
10
11
11 @classmethod
12 @classmethod
12 def _get_commits(cls):
13 def _get_commits(cls):
13 commits = [
14 commits = [
14 {
15 {
15 'message': 'Initial commit',
16 'message': 'Initial commit',
16 'author': 'Joe Doe <joe.doe@example.com>',
17 'author': 'Joe Doe <joe.doe@example.com>',
17 'date': datetime.datetime(2010, 1, 1, 20),
18 'date': datetime.datetime(2010, 1, 1, 20),
18 'added': [
19 'added': [
19 FileNode('foobar', content='Foobar'),
20 FileNode('foobar', content='Foobar'),
20 FileNode('foobar2', content='Foobar II'),
21 FileNode('foobar2', content='Foobar II'),
21 FileNode('foo/bar/baz', content='baz here!'),
22 FileNode('foo/bar/baz', content='baz here!'),
22 ],
23 ],
23 },
24 },
24 {
25 {
25 'message': 'Changes...',
26 'message': 'Changes...',
26 'author': 'Jane Doe <jane.doe@example.com>',
27 'author': 'Jane Doe <jane.doe@example.com>',
27 'date': datetime.datetime(2010, 1, 1, 21),
28 'date': datetime.datetime(2010, 1, 1, 21),
28 'added': [
29 'added': [
29 FileNode('some/new.txt', content='news...'),
30 FileNode('some/new.txt', content='news...'),
30 ],
31 ],
31 'changed': [
32 'changed': [
32 FileNode('foobar', 'Foobar I'),
33 FileNode('foobar', 'Foobar I'),
33 ],
34 ],
34 'removed': [],
35 'removed': [],
35 },
36 },
36 ]
37 ]
37 return commits
38 return commits
38
39
39 def test_get_branch_for_default_branch(self):
40 def test_get_branch_for_default_branch(self):
40 assert self.repo.workdir.get_branch() == self.repo.DEFAULT_BRANCH_NAME
41 assert self.repo.workdir.get_branch() == self.repo.DEFAULT_BRANCH_NAME
41
42
42 def test_get_branch_after_adding_one(self):
43 def test_get_branch_after_adding_one(self):
43 self.imc.add(FileNode('docs/index.txt',
44 self.imc.add(FileNode('docs/index.txt',
44 content='Documentation\n'))
45 content='Documentation\n'))
45 self.imc.commit(
46 self.imc.commit(
46 message='New branch: foobar',
47 message='New branch: foobar',
47 author='joe',
48 author='joe',
48 branch='foobar',
49 branch='foobar',
49 )
50 )
50 assert self.repo.workdir.get_branch() == self.default_branch
51 assert self.repo.workdir.get_branch() == self.default_branch
51
52
52 def test_get_changeset(self):
53 def test_get_changeset(self):
53 old_head = self.repo.get_changeset()
54 old_head = self.repo.get_changeset()
54 self.imc.add(FileNode('docs/index.txt',
55 self.imc.add(FileNode('docs/index.txt',
55 content='Documentation\n'))
56 content='Documentation\n'))
56 head = self.imc.commit(
57 head = self.imc.commit(
57 message='New branch: foobar',
58 message='New branch: foobar',
58 author='joe',
59 author='joe',
59 branch='foobar',
60 branch='foobar',
60 )
61 )
61 assert self.repo.workdir.get_branch() == self.default_branch
62 assert self.repo.workdir.get_branch() == self.default_branch
62 self.repo.workdir.checkout_branch('foobar')
63 self.repo.workdir.checkout_branch('foobar')
63 assert self.repo.workdir.get_changeset() == head
64 assert self.repo.workdir.get_changeset() == head
64
65
65 # Make sure that old head is still there after update to default branch
66 # Make sure that old head is still there after update to default branch
66 self.repo.workdir.checkout_branch(self.default_branch)
67 self.repo.workdir.checkout_branch(self.default_branch)
67 assert self.repo.workdir.get_changeset() == old_head
68 assert self.repo.workdir.get_changeset() == old_head
68
69
69 def test_checkout_branch(self):
70 def test_checkout_branch(self):
70 from kallithea.lib.vcs.exceptions import BranchDoesNotExistError
71
72 # first, 'foobranch' does not exist.
71 # first, 'foobranch' does not exist.
73 with pytest.raises(BranchDoesNotExistError):
72 with pytest.raises(BranchDoesNotExistError):
74 self.repo.workdir.checkout_branch(branch='foobranch')
73 self.repo.workdir.checkout_branch(branch='foobranch')
75 # create new branch 'foobranch'.
74 # create new branch 'foobranch'.
76 self.imc.add(FileNode('file1', content='blah'))
75 self.imc.add(FileNode('file1', content='blah'))
77 self.imc.commit(message='asd', author='john', branch='foobranch')
76 self.imc.commit(message='asd', author='john', branch='foobranch')
78 # go back to the default branch
77 # go back to the default branch
79 self.repo.workdir.checkout_branch()
78 self.repo.workdir.checkout_branch()
80 assert self.repo.workdir.get_branch() == self.backend_class.DEFAULT_BRANCH_NAME
79 assert self.repo.workdir.get_branch() == self.backend_class.DEFAULT_BRANCH_NAME
81 # checkout 'foobranch'
80 # checkout 'foobranch'
82 self.repo.workdir.checkout_branch('foobranch')
81 self.repo.workdir.checkout_branch('foobranch')
83 assert self.repo.workdir.get_branch() == 'foobranch'
82 assert self.repo.workdir.get_branch() == 'foobranch'
84
83
85
84
86 class TestGitBranch(WorkdirTestCaseMixin):
85 class TestGitBranch(WorkdirTestCaseMixin):
87 backend_alias = 'git'
86 backend_alias = 'git'
88
87
89
88
90 class TestHgBranch(WorkdirTestCaseMixin):
89 class TestHgBranch(WorkdirTestCaseMixin):
91 backend_alias = 'hg'
90 backend_alias = 'hg'
@@ -1,161 +1,161 b''
1 #!/usr/bin/env python3
1 #!/usr/bin/env python3
2 # -*- coding: utf-8 -*-
2 # -*- coding: utf-8 -*-
3 import os
3 import os
4 import platform
4 import platform
5 import re
5 import sys
6 import sys
6
7
7 import setuptools
8 import setuptools
8 # monkey patch setuptools to use distutils owner/group functionality
9 # monkey patch setuptools to use distutils owner/group functionality
9 from setuptools.command import sdist
10 from setuptools.command import sdist
10
11
11
12
12 if sys.version_info < (3, 6):
13 if sys.version_info < (3, 6):
13 raise Exception('Kallithea requires Python 3.6 or later')
14 raise Exception('Kallithea requires Python 3.6 or later')
14
15
15
16
16 here = os.path.abspath(os.path.dirname(__file__))
17 here = os.path.abspath(os.path.dirname(__file__))
17
18
18
19
19 def _get_meta_var(name, data, callback_handler=None):
20 def _get_meta_var(name, data, callback_handler=None):
20 import re
21 matches = re.compile(r'(?:%s)\s*=\s*(.*)' % name).search(data)
21 matches = re.compile(r'(?:%s)\s*=\s*(.*)' % name).search(data)
22 if matches:
22 if matches:
23 s = eval(matches.groups()[0])
23 s = eval(matches.groups()[0])
24 if callable(callback_handler):
24 if callable(callback_handler):
25 return callback_handler(s)
25 return callback_handler(s)
26 return s
26 return s
27
27
28 _meta = open(os.path.join(here, 'kallithea', '__init__.py'), 'r')
28 _meta = open(os.path.join(here, 'kallithea', '__init__.py'), 'r')
29 _metadata = _meta.read()
29 _metadata = _meta.read()
30 _meta.close()
30 _meta.close()
31
31
32 def callback(V):
32 def callback(V):
33 return '.'.join(map(str, V[:3])) + '.'.join(V[3:])
33 return '.'.join(map(str, V[:3])) + '.'.join(V[3:])
34 __version__ = _get_meta_var('VERSION', _metadata, callback)
34 __version__ = _get_meta_var('VERSION', _metadata, callback)
35 __license__ = _get_meta_var('__license__', _metadata)
35 __license__ = _get_meta_var('__license__', _metadata)
36 __author__ = _get_meta_var('__author__', _metadata)
36 __author__ = _get_meta_var('__author__', _metadata)
37 __url__ = _get_meta_var('__url__', _metadata)
37 __url__ = _get_meta_var('__url__', _metadata)
38 # defines current platform
38 # defines current platform
39 __platform__ = platform.system()
39 __platform__ = platform.system()
40
40
41 is_windows = __platform__ in ['Windows']
41 is_windows = __platform__ in ['Windows']
42
42
43 requirements = [
43 requirements = [
44 "alembic >= 1.0.10, < 1.5",
44 "alembic >= 1.0.10, < 1.5",
45 "gearbox >= 0.1.0, < 1",
45 "gearbox >= 0.1.0, < 1",
46 "waitress >= 0.8.8, < 1.5",
46 "waitress >= 0.8.8, < 1.5",
47 "WebOb >= 1.8, < 1.9",
47 "WebOb >= 1.8, < 1.9",
48 "backlash >= 0.1.2, < 1",
48 "backlash >= 0.1.2, < 1",
49 "TurboGears2 >= 2.4, < 2.5",
49 "TurboGears2 >= 2.4, < 2.5",
50 "tgext.routes >= 0.2.0, < 1",
50 "tgext.routes >= 0.2.0, < 1",
51 "Beaker >= 1.10.1, < 2",
51 "Beaker >= 1.10.1, < 2",
52 "WebHelpers2 >= 2.0, < 2.1",
52 "WebHelpers2 >= 2.0, < 2.1",
53 "FormEncode >= 1.3.1, < 1.4",
53 "FormEncode >= 1.3.1, < 1.4",
54 "SQLAlchemy >= 1.2.9, < 1.4",
54 "SQLAlchemy >= 1.2.9, < 1.4",
55 "Mako >= 0.9.1, < 1.2",
55 "Mako >= 0.9.1, < 1.2",
56 "Pygments >= 2.2.0, < 2.7",
56 "Pygments >= 2.2.0, < 2.7",
57 "Whoosh >= 2.7.1, < 2.8",
57 "Whoosh >= 2.7.1, < 2.8",
58 "celery >= 4.3, < 4.5, != 4.4.4", # 4.4.4 is broken due to unexpressed dependency on 'future', see https://github.com/celery/celery/pull/6146
58 "celery >= 4.3, < 4.5, != 4.4.4", # 4.4.4 is broken due to unexpressed dependency on 'future', see https://github.com/celery/celery/pull/6146
59 "Babel >= 1.3, < 2.9",
59 "Babel >= 1.3, < 2.9",
60 "python-dateutil >= 2.1.0, < 2.9",
60 "python-dateutil >= 2.1.0, < 2.9",
61 "Markdown >= 2.2.1, < 3.2",
61 "Markdown >= 2.2.1, < 3.2",
62 "docutils >= 0.11, < 0.17",
62 "docutils >= 0.11, < 0.17",
63 "URLObject >= 2.3.4, < 2.5",
63 "URLObject >= 2.3.4, < 2.5",
64 "Routes >= 2.0, < 2.5",
64 "Routes >= 2.0, < 2.5",
65 "dulwich >= 0.19.0, < 0.20",
65 "dulwich >= 0.19.0, < 0.20",
66 "mercurial >= 5.2, < 5.7",
66 "mercurial >= 5.2, < 5.7",
67 "decorator >= 4.2.1, < 4.5",
67 "decorator >= 4.2.1, < 4.5",
68 "Paste >= 2.0.3, < 3.5",
68 "Paste >= 2.0.3, < 3.5",
69 "bleach >= 3.0, < 3.1.4",
69 "bleach >= 3.0, < 3.1.4",
70 "Click >= 7.0, < 8",
70 "Click >= 7.0, < 8",
71 "ipaddr >= 2.2.0, < 2.3",
71 "ipaddr >= 2.2.0, < 2.3",
72 "paginate >= 0.5, < 0.6",
72 "paginate >= 0.5, < 0.6",
73 "paginate_sqlalchemy >= 0.3.0, < 0.4",
73 "paginate_sqlalchemy >= 0.3.0, < 0.4",
74 "bcrypt >= 3.1.0, < 3.2",
74 "bcrypt >= 3.1.0, < 3.2",
75 "pip >= 20.0, < 999",
75 "pip >= 20.0, < 999",
76 ]
76 ]
77
77
78 dependency_links = [
78 dependency_links = [
79 ]
79 ]
80
80
81 classifiers = [
81 classifiers = [
82 'Development Status :: 4 - Beta',
82 'Development Status :: 4 - Beta',
83 'Environment :: Web Environment',
83 'Environment :: Web Environment',
84 'Framework :: Pylons',
84 'Framework :: Pylons',
85 'Intended Audience :: Developers',
85 'Intended Audience :: Developers',
86 'License :: OSI Approved :: GNU General Public License (GPL)',
86 'License :: OSI Approved :: GNU General Public License (GPL)',
87 'Operating System :: OS Independent',
87 'Operating System :: OS Independent',
88 'Programming Language :: Python :: 3.6',
88 'Programming Language :: Python :: 3.6',
89 'Programming Language :: Python :: 3.7',
89 'Programming Language :: Python :: 3.7',
90 'Programming Language :: Python :: 3.8',
90 'Programming Language :: Python :: 3.8',
91 'Topic :: Software Development :: Version Control',
91 'Topic :: Software Development :: Version Control',
92 ]
92 ]
93
93
94
94
95 # additional files from project that goes somewhere in the filesystem
95 # additional files from project that goes somewhere in the filesystem
96 # relative to sys.prefix
96 # relative to sys.prefix
97 data_files = []
97 data_files = []
98
98
99 description = ('Kallithea is a fast and powerful management tool '
99 description = ('Kallithea is a fast and powerful management tool '
100 'for Mercurial and Git with a built in push/pull server, '
100 'for Mercurial and Git with a built in push/pull server, '
101 'full text search and code-review.')
101 'full text search and code-review.')
102
102
103 keywords = ' '.join([
103 keywords = ' '.join([
104 'kallithea', 'mercurial', 'git', 'code review',
104 'kallithea', 'mercurial', 'git', 'code review',
105 'repo groups', 'ldap', 'repository management', 'hgweb replacement',
105 'repo groups', 'ldap', 'repository management', 'hgweb replacement',
106 'hgwebdir', 'gitweb replacement', 'serving hgweb',
106 'hgwebdir', 'gitweb replacement', 'serving hgweb',
107 ])
107 ])
108
108
109 # long description
109 # long description
110 README_FILE = 'README.rst'
110 README_FILE = 'README.rst'
111 try:
111 try:
112 long_description = open(README_FILE).read()
112 long_description = open(README_FILE).read()
113 except IOError as err:
113 except IOError as err:
114 sys.stderr.write(
114 sys.stderr.write(
115 "[WARNING] Cannot find file specified as long_description (%s): %s\n"
115 "[WARNING] Cannot find file specified as long_description (%s): %s\n"
116 % (README_FILE, err)
116 % (README_FILE, err)
117 )
117 )
118 long_description = description
118 long_description = description
119
119
120
120
121 sdist_org = sdist.sdist
121 sdist_org = sdist.sdist
122 class sdist_new(sdist_org):
122 class sdist_new(sdist_org):
123 def initialize_options(self):
123 def initialize_options(self):
124 sdist_org.initialize_options(self)
124 sdist_org.initialize_options(self)
125 self.owner = self.group = 'root'
125 self.owner = self.group = 'root'
126 sdist.sdist = sdist_new
126 sdist.sdist = sdist_new
127
127
128 packages = setuptools.find_packages(exclude=['ez_setup'])
128 packages = setuptools.find_packages(exclude=['ez_setup'])
129
129
130 setuptools.setup(
130 setuptools.setup(
131 name='Kallithea',
131 name='Kallithea',
132 version=__version__,
132 version=__version__,
133 description=description,
133 description=description,
134 long_description=long_description,
134 long_description=long_description,
135 keywords=keywords,
135 keywords=keywords,
136 license=__license__,
136 license=__license__,
137 author=__author__,
137 author=__author__,
138 author_email='kallithea@sfconservancy.org',
138 author_email='kallithea@sfconservancy.org',
139 dependency_links=dependency_links,
139 dependency_links=dependency_links,
140 url=__url__,
140 url=__url__,
141 install_requires=requirements,
141 install_requires=requirements,
142 classifiers=classifiers,
142 classifiers=classifiers,
143 data_files=data_files,
143 data_files=data_files,
144 packages=packages,
144 packages=packages,
145 include_package_data=True,
145 include_package_data=True,
146 message_extractors={'kallithea': [
146 message_extractors={'kallithea': [
147 ('**.py', 'python', None),
147 ('**.py', 'python', None),
148 ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
148 ('templates/**.mako', 'mako', {'input_encoding': 'utf-8'}),
149 ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}),
149 ('templates/**.html', 'mako', {'input_encoding': 'utf-8'}),
150 ('public/**', 'ignore', None)]},
150 ('public/**', 'ignore', None)]},
151 zip_safe=False,
151 zip_safe=False,
152 entry_points="""
152 entry_points="""
153 [console_scripts]
153 [console_scripts]
154 kallithea-api = kallithea.bin.kallithea_api:main
154 kallithea-api = kallithea.bin.kallithea_api:main
155 kallithea-gist = kallithea.bin.kallithea_gist:main
155 kallithea-gist = kallithea.bin.kallithea_gist:main
156 kallithea-cli = kallithea.bin.kallithea_cli:cli
156 kallithea-cli = kallithea.bin.kallithea_cli:cli
157
157
158 [paste.app_factory]
158 [paste.app_factory]
159 main = kallithea.config.application:make_app
159 main = kallithea.config.application:make_app
160 """,
160 """,
161 )
161 )
General Comments 0
You need to be logged in to leave comments. Login now