##// END OF EJS Templates
auth: simplify repository permission checks...
Søren Løvborg -
r6471:a17c8e5f default
parent child Browse files
Show More
@@ -1,589 +1,589 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.repos
15 kallithea.controllers.admin.repos
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Repositories controller for Kallithea
18 Repositories 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 7, 2010
22 :created_on: Apr 7, 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 import formencode
30 import formencode
31 from formencode import htmlfill
31 from formencode import htmlfill
32 from pylons import request, tmpl_context as c
32 from pylons import request, tmpl_context as c
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34 from sqlalchemy.sql.expression import func
34 from sqlalchemy.sql.expression import func
35 from webob.exc import HTTPFound, HTTPInternalServerError, HTTPForbidden, HTTPNotFound
35 from webob.exc import HTTPFound, HTTPInternalServerError, HTTPForbidden, HTTPNotFound
36
36
37 from kallithea.config.routing import url
37 from kallithea.config.routing import url
38 from kallithea.lib import helpers as h
38 from kallithea.lib import helpers as h
39 from kallithea.lib.auth import LoginRequired, \
39 from kallithea.lib.auth import LoginRequired, \
40 HasRepoPermissionAnyDecorator, NotAnonymous, HasPermissionAny
40 HasRepoPermissionLevelDecorator, NotAnonymous, HasPermissionAny
41 from kallithea.lib.base import BaseRepoController, render, jsonify
41 from kallithea.lib.base import BaseRepoController, render, jsonify
42 from kallithea.lib.utils import action_logger
42 from kallithea.lib.utils import action_logger
43 from kallithea.lib.vcs import RepositoryError
43 from kallithea.lib.vcs import RepositoryError
44 from kallithea.model.meta import Session
44 from kallithea.model.meta import Session
45 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup, \
45 from kallithea.model.db import User, Repository, UserFollowing, RepoGroup, \
46 Setting, RepositoryField
46 Setting, RepositoryField
47 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
47 from kallithea.model.forms import RepoForm, RepoFieldForm, RepoPermsForm
48 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices, RepoList
48 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices, RepoList
49 from kallithea.model.repo import RepoModel
49 from kallithea.model.repo import RepoModel
50 from kallithea.lib.compat import json
50 from kallithea.lib.compat import json
51 from kallithea.lib.exceptions import AttachedForksError
51 from kallithea.lib.exceptions import AttachedForksError
52 from kallithea.lib.utils2 import safe_int
52 from kallithea.lib.utils2 import safe_int
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class ReposController(BaseRepoController):
57 class ReposController(BaseRepoController):
58 """
58 """
59 REST Controller styled on the Atom Publishing Protocol"""
59 REST Controller styled on the Atom Publishing Protocol"""
60 # To properly map this controller, ensure your config/routing.py
60 # To properly map this controller, ensure your config/routing.py
61 # file has a resource setup:
61 # file has a resource setup:
62 # map.resource('repo', 'repos')
62 # map.resource('repo', 'repos')
63
63
64 @LoginRequired()
64 @LoginRequired()
65 def __before__(self):
65 def __before__(self):
66 super(ReposController, self).__before__()
66 super(ReposController, self).__before__()
67
67
68 def _load_repo(self):
68 def _load_repo(self):
69 repo_obj = c.db_repo
69 repo_obj = c.db_repo
70
70
71 if repo_obj is None:
71 if repo_obj is None:
72 h.not_mapped_error(c.repo_name)
72 h.not_mapped_error(c.repo_name)
73 raise HTTPFound(location=url('repos'))
73 raise HTTPFound(location=url('repos'))
74
74
75 return repo_obj
75 return repo_obj
76
76
77 def __load_defaults(self, repo=None):
77 def __load_defaults(self, repo=None):
78 top_perms = ['hg.create.repository']
78 top_perms = ['hg.create.repository']
79 repo_group_perms = ['group.admin']
79 repo_group_perms = ['group.admin']
80 if HasPermissionAny('hg.create.write_on_repogroup.true')():
80 if HasPermissionAny('hg.create.write_on_repogroup.true')():
81 repo_group_perms.append('group.write')
81 repo_group_perms.append('group.write')
82 extras = [] if repo is None else [repo.group]
82 extras = [] if repo is None else [repo.group]
83
83
84 c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perms, extras)
84 c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perms, extras)
85
85
86 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
86 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
87
87
88 def __load_data(self):
88 def __load_data(self):
89 """
89 """
90 Load defaults settings for edit, and update
90 Load defaults settings for edit, and update
91 """
91 """
92 c.repo_info = self._load_repo()
92 c.repo_info = self._load_repo()
93 self.__load_defaults(c.repo_info)
93 self.__load_defaults(c.repo_info)
94
94
95 defaults = RepoModel()._get_defaults(c.repo_name)
95 defaults = RepoModel()._get_defaults(c.repo_name)
96 defaults['clone_uri'] = c.repo_info.clone_uri_hidden # don't show password
96 defaults['clone_uri'] = c.repo_info.clone_uri_hidden # don't show password
97
97
98 return defaults
98 return defaults
99
99
100 def index(self, format='html'):
100 def index(self, format='html'):
101 _list = Repository.query(sorted=True).all()
101 _list = Repository.query(sorted=True).all()
102
102
103 c.repos_list = RepoList(_list, perm_set=['repository.admin'])
103 c.repos_list = RepoList(_list, perm_level='admin')
104 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
104 repos_data = RepoModel().get_repos_as_dict(repos_list=c.repos_list,
105 admin=True,
105 admin=True,
106 super_user_actions=True)
106 super_user_actions=True)
107 #json used to render the grid
107 #json used to render the grid
108 c.data = json.dumps(repos_data)
108 c.data = json.dumps(repos_data)
109
109
110 return render('admin/repos/repos.html')
110 return render('admin/repos/repos.html')
111
111
112 @NotAnonymous()
112 @NotAnonymous()
113 def create(self):
113 def create(self):
114 self.__load_defaults()
114 self.__load_defaults()
115 form_result = {}
115 form_result = {}
116 try:
116 try:
117 # CanWriteGroup validators checks permissions of this POST
117 # CanWriteGroup validators checks permissions of this POST
118 form_result = RepoForm(repo_groups=c.repo_groups,
118 form_result = RepoForm(repo_groups=c.repo_groups,
119 landing_revs=c.landing_revs_choices)() \
119 landing_revs=c.landing_revs_choices)() \
120 .to_python(dict(request.POST))
120 .to_python(dict(request.POST))
121
121
122 # create is done sometimes async on celery, db transaction
122 # create is done sometimes async on celery, db transaction
123 # management is handled there.
123 # management is handled there.
124 task = RepoModel().create(form_result, request.authuser.user_id)
124 task = RepoModel().create(form_result, request.authuser.user_id)
125 task_id = task.task_id
125 task_id = task.task_id
126 except formencode.Invalid as errors:
126 except formencode.Invalid as errors:
127 log.info(errors)
127 log.info(errors)
128 return htmlfill.render(
128 return htmlfill.render(
129 render('admin/repos/repo_add.html'),
129 render('admin/repos/repo_add.html'),
130 defaults=errors.value,
130 defaults=errors.value,
131 errors=errors.error_dict or {},
131 errors=errors.error_dict or {},
132 prefix_error=False,
132 prefix_error=False,
133 force_defaults=False,
133 force_defaults=False,
134 encoding="UTF-8")
134 encoding="UTF-8")
135
135
136 except Exception:
136 except Exception:
137 log.error(traceback.format_exc())
137 log.error(traceback.format_exc())
138 msg = (_('Error creating repository %s')
138 msg = (_('Error creating repository %s')
139 % form_result.get('repo_name'))
139 % form_result.get('repo_name'))
140 h.flash(msg, category='error')
140 h.flash(msg, category='error')
141 raise HTTPFound(location=url('home'))
141 raise HTTPFound(location=url('home'))
142
142
143 raise HTTPFound(location=h.url('repo_creating_home',
143 raise HTTPFound(location=h.url('repo_creating_home',
144 repo_name=form_result['repo_name_full'],
144 repo_name=form_result['repo_name_full'],
145 task_id=task_id))
145 task_id=task_id))
146
146
147 @NotAnonymous()
147 @NotAnonymous()
148 def create_repository(self):
148 def create_repository(self):
149 self.__load_defaults()
149 self.__load_defaults()
150 if not c.repo_groups:
150 if not c.repo_groups:
151 raise HTTPForbidden
151 raise HTTPForbidden
152 parent_group = request.GET.get('parent_group')
152 parent_group = request.GET.get('parent_group')
153
153
154 ## apply the defaults from defaults page
154 ## apply the defaults from defaults page
155 defaults = Setting.get_default_repo_settings(strip_prefix=True)
155 defaults = Setting.get_default_repo_settings(strip_prefix=True)
156 if parent_group:
156 if parent_group:
157 prg = RepoGroup.get(parent_group)
157 prg = RepoGroup.get(parent_group)
158 if prg is None or not any(rgc[0] == prg.group_id
158 if prg is None or not any(rgc[0] == prg.group_id
159 for rgc in c.repo_groups):
159 for rgc in c.repo_groups):
160 raise HTTPForbidden
160 raise HTTPForbidden
161 defaults.update({'repo_group': parent_group})
161 defaults.update({'repo_group': parent_group})
162
162
163 return htmlfill.render(
163 return htmlfill.render(
164 render('admin/repos/repo_add.html'),
164 render('admin/repos/repo_add.html'),
165 defaults=defaults,
165 defaults=defaults,
166 errors={},
166 errors={},
167 prefix_error=False,
167 prefix_error=False,
168 encoding="UTF-8",
168 encoding="UTF-8",
169 force_defaults=False)
169 force_defaults=False)
170
170
171 @LoginRequired()
171 @LoginRequired()
172 @NotAnonymous()
172 @NotAnonymous()
173 def repo_creating(self, repo_name):
173 def repo_creating(self, repo_name):
174 c.repo = repo_name
174 c.repo = repo_name
175 c.task_id = request.GET.get('task_id')
175 c.task_id = request.GET.get('task_id')
176 if not c.repo:
176 if not c.repo:
177 raise HTTPNotFound()
177 raise HTTPNotFound()
178 return render('admin/repos/repo_creating.html')
178 return render('admin/repos/repo_creating.html')
179
179
180 @LoginRequired()
180 @LoginRequired()
181 @NotAnonymous()
181 @NotAnonymous()
182 @jsonify
182 @jsonify
183 def repo_check(self, repo_name):
183 def repo_check(self, repo_name):
184 c.repo = repo_name
184 c.repo = repo_name
185 task_id = request.GET.get('task_id')
185 task_id = request.GET.get('task_id')
186
186
187 if task_id and task_id not in ['None']:
187 if task_id and task_id not in ['None']:
188 from kallithea import CELERY_ON
188 from kallithea import CELERY_ON
189 from kallithea.lib import celerypylons
189 from kallithea.lib import celerypylons
190 if CELERY_ON:
190 if CELERY_ON:
191 task = celerypylons.result.AsyncResult(task_id)
191 task = celerypylons.result.AsyncResult(task_id)
192 if task.failed():
192 if task.failed():
193 raise HTTPInternalServerError(task.traceback)
193 raise HTTPInternalServerError(task.traceback)
194
194
195 repo = Repository.get_by_repo_name(repo_name)
195 repo = Repository.get_by_repo_name(repo_name)
196 if repo and repo.repo_state == Repository.STATE_CREATED:
196 if repo and repo.repo_state == Repository.STATE_CREATED:
197 if repo.clone_uri:
197 if repo.clone_uri:
198 h.flash(_('Created repository %s from %s')
198 h.flash(_('Created repository %s from %s')
199 % (repo.repo_name, repo.clone_uri_hidden), category='success')
199 % (repo.repo_name, repo.clone_uri_hidden), category='success')
200 else:
200 else:
201 repo_url = h.link_to(repo.repo_name,
201 repo_url = h.link_to(repo.repo_name,
202 h.url('summary_home',
202 h.url('summary_home',
203 repo_name=repo.repo_name))
203 repo_name=repo.repo_name))
204 fork = repo.fork
204 fork = repo.fork
205 if fork is not None:
205 if fork is not None:
206 fork_name = fork.repo_name
206 fork_name = fork.repo_name
207 h.flash(h.literal(_('Forked repository %s as %s')
207 h.flash(h.literal(_('Forked repository %s as %s')
208 % (fork_name, repo_url)), category='success')
208 % (fork_name, repo_url)), category='success')
209 else:
209 else:
210 h.flash(h.literal(_('Created repository %s') % repo_url),
210 h.flash(h.literal(_('Created repository %s') % repo_url),
211 category='success')
211 category='success')
212 return {'result': True}
212 return {'result': True}
213 return {'result': False}
213 return {'result': False}
214
214
215 @HasRepoPermissionAnyDecorator('repository.admin')
215 @HasRepoPermissionLevelDecorator('admin')
216 def update(self, repo_name):
216 def update(self, repo_name):
217 c.repo_info = self._load_repo()
217 c.repo_info = self._load_repo()
218 self.__load_defaults(c.repo_info)
218 self.__load_defaults(c.repo_info)
219 c.active = 'settings'
219 c.active = 'settings'
220 c.repo_fields = RepositoryField.query() \
220 c.repo_fields = RepositoryField.query() \
221 .filter(RepositoryField.repository == c.repo_info).all()
221 .filter(RepositoryField.repository == c.repo_info).all()
222
222
223 repo_model = RepoModel()
223 repo_model = RepoModel()
224 changed_name = repo_name
224 changed_name = repo_name
225 repo = Repository.get_by_repo_name(repo_name)
225 repo = Repository.get_by_repo_name(repo_name)
226 old_data = {
226 old_data = {
227 'repo_name': repo_name,
227 'repo_name': repo_name,
228 'repo_group': repo.group.get_dict() if repo.group else {},
228 'repo_group': repo.group.get_dict() if repo.group else {},
229 'repo_type': repo.repo_type,
229 'repo_type': repo.repo_type,
230 }
230 }
231 _form = RepoForm(edit=True, old_data=old_data,
231 _form = RepoForm(edit=True, old_data=old_data,
232 repo_groups=c.repo_groups,
232 repo_groups=c.repo_groups,
233 landing_revs=c.landing_revs_choices)()
233 landing_revs=c.landing_revs_choices)()
234
234
235 try:
235 try:
236 form_result = _form.to_python(dict(request.POST))
236 form_result = _form.to_python(dict(request.POST))
237 repo = repo_model.update(repo_name, **form_result)
237 repo = repo_model.update(repo_name, **form_result)
238 ScmModel().mark_for_invalidation(repo_name)
238 ScmModel().mark_for_invalidation(repo_name)
239 h.flash(_('Repository %s updated successfully') % repo_name,
239 h.flash(_('Repository %s updated successfully') % repo_name,
240 category='success')
240 category='success')
241 changed_name = repo.repo_name
241 changed_name = repo.repo_name
242 action_logger(request.authuser, 'admin_updated_repo',
242 action_logger(request.authuser, 'admin_updated_repo',
243 changed_name, request.ip_addr, self.sa)
243 changed_name, request.ip_addr, self.sa)
244 Session().commit()
244 Session().commit()
245 except formencode.Invalid as errors:
245 except formencode.Invalid as errors:
246 log.info(errors)
246 log.info(errors)
247 defaults = self.__load_data()
247 defaults = self.__load_data()
248 defaults.update(errors.value)
248 defaults.update(errors.value)
249 c.users_array = repo_model.get_users_js()
249 c.users_array = repo_model.get_users_js()
250 return htmlfill.render(
250 return htmlfill.render(
251 render('admin/repos/repo_edit.html'),
251 render('admin/repos/repo_edit.html'),
252 defaults=defaults,
252 defaults=defaults,
253 errors=errors.error_dict or {},
253 errors=errors.error_dict or {},
254 prefix_error=False,
254 prefix_error=False,
255 encoding="UTF-8",
255 encoding="UTF-8",
256 force_defaults=False)
256 force_defaults=False)
257
257
258 except Exception:
258 except Exception:
259 log.error(traceback.format_exc())
259 log.error(traceback.format_exc())
260 h.flash(_('Error occurred during update of repository %s') \
260 h.flash(_('Error occurred during update of repository %s') \
261 % repo_name, category='error')
261 % repo_name, category='error')
262 raise HTTPFound(location=url('edit_repo', repo_name=changed_name))
262 raise HTTPFound(location=url('edit_repo', repo_name=changed_name))
263
263
264 @HasRepoPermissionAnyDecorator('repository.admin')
264 @HasRepoPermissionLevelDecorator('admin')
265 def delete(self, repo_name):
265 def delete(self, repo_name):
266 repo_model = RepoModel()
266 repo_model = RepoModel()
267 repo = repo_model.get_by_repo_name(repo_name)
267 repo = repo_model.get_by_repo_name(repo_name)
268 if not repo:
268 if not repo:
269 h.not_mapped_error(repo_name)
269 h.not_mapped_error(repo_name)
270 raise HTTPFound(location=url('repos'))
270 raise HTTPFound(location=url('repos'))
271 try:
271 try:
272 _forks = repo.forks.count()
272 _forks = repo.forks.count()
273 handle_forks = None
273 handle_forks = None
274 if _forks and request.POST.get('forks'):
274 if _forks and request.POST.get('forks'):
275 do = request.POST['forks']
275 do = request.POST['forks']
276 if do == 'detach_forks':
276 if do == 'detach_forks':
277 handle_forks = 'detach'
277 handle_forks = 'detach'
278 h.flash(_('Detached %s forks') % _forks, category='success')
278 h.flash(_('Detached %s forks') % _forks, category='success')
279 elif do == 'delete_forks':
279 elif do == 'delete_forks':
280 handle_forks = 'delete'
280 handle_forks = 'delete'
281 h.flash(_('Deleted %s forks') % _forks, category='success')
281 h.flash(_('Deleted %s forks') % _forks, category='success')
282 repo_model.delete(repo, forks=handle_forks)
282 repo_model.delete(repo, forks=handle_forks)
283 action_logger(request.authuser, 'admin_deleted_repo',
283 action_logger(request.authuser, 'admin_deleted_repo',
284 repo_name, request.ip_addr, self.sa)
284 repo_name, request.ip_addr, self.sa)
285 ScmModel().mark_for_invalidation(repo_name)
285 ScmModel().mark_for_invalidation(repo_name)
286 h.flash(_('Deleted repository %s') % repo_name, category='success')
286 h.flash(_('Deleted repository %s') % repo_name, category='success')
287 Session().commit()
287 Session().commit()
288 except AttachedForksError:
288 except AttachedForksError:
289 h.flash(_('Cannot delete repository %s which still has forks')
289 h.flash(_('Cannot delete repository %s which still has forks')
290 % repo_name, category='warning')
290 % repo_name, category='warning')
291
291
292 except Exception:
292 except Exception:
293 log.error(traceback.format_exc())
293 log.error(traceback.format_exc())
294 h.flash(_('An error occurred during deletion of %s') % repo_name,
294 h.flash(_('An error occurred during deletion of %s') % repo_name,
295 category='error')
295 category='error')
296
296
297 if repo.group:
297 if repo.group:
298 raise HTTPFound(location=url('repos_group_home', group_name=repo.group.group_name))
298 raise HTTPFound(location=url('repos_group_home', group_name=repo.group.group_name))
299 raise HTTPFound(location=url('repos'))
299 raise HTTPFound(location=url('repos'))
300
300
301 @HasRepoPermissionAnyDecorator('repository.admin')
301 @HasRepoPermissionLevelDecorator('admin')
302 def edit(self, repo_name):
302 def edit(self, repo_name):
303 defaults = self.__load_data()
303 defaults = self.__load_data()
304 c.repo_fields = RepositoryField.query() \
304 c.repo_fields = RepositoryField.query() \
305 .filter(RepositoryField.repository == c.repo_info).all()
305 .filter(RepositoryField.repository == c.repo_info).all()
306 repo_model = RepoModel()
306 repo_model = RepoModel()
307 c.users_array = repo_model.get_users_js()
307 c.users_array = repo_model.get_users_js()
308 c.active = 'settings'
308 c.active = 'settings'
309 return htmlfill.render(
309 return htmlfill.render(
310 render('admin/repos/repo_edit.html'),
310 render('admin/repos/repo_edit.html'),
311 defaults=defaults,
311 defaults=defaults,
312 encoding="UTF-8",
312 encoding="UTF-8",
313 force_defaults=False)
313 force_defaults=False)
314
314
315 @HasRepoPermissionAnyDecorator('repository.admin')
315 @HasRepoPermissionLevelDecorator('admin')
316 def edit_permissions(self, repo_name):
316 def edit_permissions(self, repo_name):
317 c.repo_info = self._load_repo()
317 c.repo_info = self._load_repo()
318 repo_model = RepoModel()
318 repo_model = RepoModel()
319 c.users_array = repo_model.get_users_js()
319 c.users_array = repo_model.get_users_js()
320 c.user_groups_array = repo_model.get_user_groups_js()
320 c.user_groups_array = repo_model.get_user_groups_js()
321 c.active = 'permissions'
321 c.active = 'permissions'
322 defaults = RepoModel()._get_defaults(repo_name)
322 defaults = RepoModel()._get_defaults(repo_name)
323
323
324 return htmlfill.render(
324 return htmlfill.render(
325 render('admin/repos/repo_edit.html'),
325 render('admin/repos/repo_edit.html'),
326 defaults=defaults,
326 defaults=defaults,
327 encoding="UTF-8",
327 encoding="UTF-8",
328 force_defaults=False)
328 force_defaults=False)
329
329
330 def edit_permissions_update(self, repo_name):
330 def edit_permissions_update(self, repo_name):
331 form = RepoPermsForm()().to_python(request.POST)
331 form = RepoPermsForm()().to_python(request.POST)
332 RepoModel()._update_permissions(repo_name, form['perms_new'],
332 RepoModel()._update_permissions(repo_name, form['perms_new'],
333 form['perms_updates'])
333 form['perms_updates'])
334 #TODO: implement this
334 #TODO: implement this
335 #action_logger(request.authuser, 'admin_changed_repo_permissions',
335 #action_logger(request.authuser, 'admin_changed_repo_permissions',
336 # repo_name, request.ip_addr, self.sa)
336 # repo_name, request.ip_addr, self.sa)
337 Session().commit()
337 Session().commit()
338 h.flash(_('Repository permissions updated'), category='success')
338 h.flash(_('Repository permissions updated'), category='success')
339 raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name))
339 raise HTTPFound(location=url('edit_repo_perms', repo_name=repo_name))
340
340
341 def edit_permissions_revoke(self, repo_name):
341 def edit_permissions_revoke(self, repo_name):
342 try:
342 try:
343 obj_type = request.POST.get('obj_type')
343 obj_type = request.POST.get('obj_type')
344 obj_id = None
344 obj_id = None
345 if obj_type == 'user':
345 if obj_type == 'user':
346 obj_id = safe_int(request.POST.get('user_id'))
346 obj_id = safe_int(request.POST.get('user_id'))
347 elif obj_type == 'user_group':
347 elif obj_type == 'user_group':
348 obj_id = safe_int(request.POST.get('user_group_id'))
348 obj_id = safe_int(request.POST.get('user_group_id'))
349
349
350 if obj_type == 'user':
350 if obj_type == 'user':
351 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
351 RepoModel().revoke_user_permission(repo=repo_name, user=obj_id)
352 elif obj_type == 'user_group':
352 elif obj_type == 'user_group':
353 RepoModel().revoke_user_group_permission(
353 RepoModel().revoke_user_group_permission(
354 repo=repo_name, group_name=obj_id
354 repo=repo_name, group_name=obj_id
355 )
355 )
356 #TODO: implement this
356 #TODO: implement this
357 #action_logger(request.authuser, 'admin_revoked_repo_permissions',
357 #action_logger(request.authuser, 'admin_revoked_repo_permissions',
358 # repo_name, request.ip_addr, self.sa)
358 # repo_name, request.ip_addr, self.sa)
359 Session().commit()
359 Session().commit()
360 except Exception:
360 except Exception:
361 log.error(traceback.format_exc())
361 log.error(traceback.format_exc())
362 h.flash(_('An error occurred during revoking of permission'),
362 h.flash(_('An error occurred during revoking of permission'),
363 category='error')
363 category='error')
364 raise HTTPInternalServerError()
364 raise HTTPInternalServerError()
365
365
366 @HasRepoPermissionAnyDecorator('repository.admin')
366 @HasRepoPermissionLevelDecorator('admin')
367 def edit_fields(self, repo_name):
367 def edit_fields(self, repo_name):
368 c.repo_info = self._load_repo()
368 c.repo_info = self._load_repo()
369 c.repo_fields = RepositoryField.query() \
369 c.repo_fields = RepositoryField.query() \
370 .filter(RepositoryField.repository == c.repo_info).all()
370 .filter(RepositoryField.repository == c.repo_info).all()
371 c.active = 'fields'
371 c.active = 'fields'
372 if request.POST:
372 if request.POST:
373
373
374 raise HTTPFound(location=url('repo_edit_fields'))
374 raise HTTPFound(location=url('repo_edit_fields'))
375 return render('admin/repos/repo_edit.html')
375 return render('admin/repos/repo_edit.html')
376
376
377 @HasRepoPermissionAnyDecorator('repository.admin')
377 @HasRepoPermissionLevelDecorator('admin')
378 def create_repo_field(self, repo_name):
378 def create_repo_field(self, repo_name):
379 try:
379 try:
380 form_result = RepoFieldForm()().to_python(dict(request.POST))
380 form_result = RepoFieldForm()().to_python(dict(request.POST))
381 new_field = RepositoryField()
381 new_field = RepositoryField()
382 new_field.repository = Repository.get_by_repo_name(repo_name)
382 new_field.repository = Repository.get_by_repo_name(repo_name)
383 new_field.field_key = form_result['new_field_key']
383 new_field.field_key = form_result['new_field_key']
384 new_field.field_type = form_result['new_field_type'] # python type
384 new_field.field_type = form_result['new_field_type'] # python type
385 new_field.field_value = form_result['new_field_value'] # set initial blank value
385 new_field.field_value = form_result['new_field_value'] # set initial blank value
386 new_field.field_desc = form_result['new_field_desc']
386 new_field.field_desc = form_result['new_field_desc']
387 new_field.field_label = form_result['new_field_label']
387 new_field.field_label = form_result['new_field_label']
388 Session().add(new_field)
388 Session().add(new_field)
389 Session().commit()
389 Session().commit()
390 except Exception as e:
390 except Exception as e:
391 log.error(traceback.format_exc())
391 log.error(traceback.format_exc())
392 msg = _('An error occurred during creation of field')
392 msg = _('An error occurred during creation of field')
393 if isinstance(e, formencode.Invalid):
393 if isinstance(e, formencode.Invalid):
394 msg += ". " + e.msg
394 msg += ". " + e.msg
395 h.flash(msg, category='error')
395 h.flash(msg, category='error')
396 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
396 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
397
397
398 @HasRepoPermissionAnyDecorator('repository.admin')
398 @HasRepoPermissionLevelDecorator('admin')
399 def delete_repo_field(self, repo_name, field_id):
399 def delete_repo_field(self, repo_name, field_id):
400 field = RepositoryField.get_or_404(field_id)
400 field = RepositoryField.get_or_404(field_id)
401 try:
401 try:
402 Session().delete(field)
402 Session().delete(field)
403 Session().commit()
403 Session().commit()
404 except Exception as e:
404 except Exception as e:
405 log.error(traceback.format_exc())
405 log.error(traceback.format_exc())
406 msg = _('An error occurred during removal of field')
406 msg = _('An error occurred during removal of field')
407 h.flash(msg, category='error')
407 h.flash(msg, category='error')
408 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
408 raise HTTPFound(location=url('edit_repo_fields', repo_name=repo_name))
409
409
410 @HasRepoPermissionAnyDecorator('repository.admin')
410 @HasRepoPermissionLevelDecorator('admin')
411 def edit_advanced(self, repo_name):
411 def edit_advanced(self, repo_name):
412 c.repo_info = self._load_repo()
412 c.repo_info = self._load_repo()
413 c.default_user_id = User.get_default_user().user_id
413 c.default_user_id = User.get_default_user().user_id
414 c.in_public_journal = UserFollowing.query() \
414 c.in_public_journal = UserFollowing.query() \
415 .filter(UserFollowing.user_id == c.default_user_id) \
415 .filter(UserFollowing.user_id == c.default_user_id) \
416 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
416 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
417
417
418 _repos = Repository.query(sorted=True).all()
418 _repos = Repository.query(sorted=True).all()
419 read_access_repos = RepoList(_repos)
419 read_access_repos = RepoList(_repos, perm_level='read')
420 c.repos_list = [(None, _('-- Not a fork --'))]
420 c.repos_list = [(None, _('-- Not a fork --'))]
421 c.repos_list += [(x.repo_id, x.repo_name)
421 c.repos_list += [(x.repo_id, x.repo_name)
422 for x in read_access_repos
422 for x in read_access_repos
423 if x.repo_id != c.repo_info.repo_id]
423 if x.repo_id != c.repo_info.repo_id]
424
424
425 defaults = {
425 defaults = {
426 'id_fork_of': c.repo_info.fork_id if c.repo_info.fork_id else ''
426 'id_fork_of': c.repo_info.fork_id if c.repo_info.fork_id else ''
427 }
427 }
428
428
429 c.active = 'advanced'
429 c.active = 'advanced'
430 if request.POST:
430 if request.POST:
431 raise HTTPFound(location=url('repo_edit_advanced'))
431 raise HTTPFound(location=url('repo_edit_advanced'))
432 return htmlfill.render(
432 return htmlfill.render(
433 render('admin/repos/repo_edit.html'),
433 render('admin/repos/repo_edit.html'),
434 defaults=defaults,
434 defaults=defaults,
435 encoding="UTF-8",
435 encoding="UTF-8",
436 force_defaults=False)
436 force_defaults=False)
437
437
438 @HasRepoPermissionAnyDecorator('repository.admin')
438 @HasRepoPermissionLevelDecorator('admin')
439 def edit_advanced_journal(self, repo_name):
439 def edit_advanced_journal(self, repo_name):
440 """
440 """
441 Sets this repository to be visible in public journal,
441 Sets this repository to be visible in public journal,
442 in other words asking default user to follow this repo
442 in other words asking default user to follow this repo
443
443
444 :param repo_name:
444 :param repo_name:
445 """
445 """
446
446
447 try:
447 try:
448 repo_id = Repository.get_by_repo_name(repo_name).repo_id
448 repo_id = Repository.get_by_repo_name(repo_name).repo_id
449 user_id = User.get_default_user().user_id
449 user_id = User.get_default_user().user_id
450 self.scm_model.toggle_following_repo(repo_id, user_id)
450 self.scm_model.toggle_following_repo(repo_id, user_id)
451 h.flash(_('Updated repository visibility in public journal'),
451 h.flash(_('Updated repository visibility in public journal'),
452 category='success')
452 category='success')
453 Session().commit()
453 Session().commit()
454 except Exception:
454 except Exception:
455 h.flash(_('An error occurred during setting this'
455 h.flash(_('An error occurred during setting this'
456 ' repository in public journal'),
456 ' repository in public journal'),
457 category='error')
457 category='error')
458 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
458 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
459
459
460
460
461 @HasRepoPermissionAnyDecorator('repository.admin')
461 @HasRepoPermissionLevelDecorator('admin')
462 def edit_advanced_fork(self, repo_name):
462 def edit_advanced_fork(self, repo_name):
463 """
463 """
464 Mark given repository as a fork of another
464 Mark given repository as a fork of another
465
465
466 :param repo_name:
466 :param repo_name:
467 """
467 """
468 try:
468 try:
469 fork_id = request.POST.get('id_fork_of')
469 fork_id = request.POST.get('id_fork_of')
470 repo = ScmModel().mark_as_fork(repo_name, fork_id,
470 repo = ScmModel().mark_as_fork(repo_name, fork_id,
471 request.authuser.username)
471 request.authuser.username)
472 fork = repo.fork.repo_name if repo.fork else _('Nothing')
472 fork = repo.fork.repo_name if repo.fork else _('Nothing')
473 Session().commit()
473 Session().commit()
474 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
474 h.flash(_('Marked repository %s as fork of %s') % (repo_name, fork),
475 category='success')
475 category='success')
476 except RepositoryError as e:
476 except RepositoryError as e:
477 log.error(traceback.format_exc())
477 log.error(traceback.format_exc())
478 h.flash(str(e), category='error')
478 h.flash(str(e), category='error')
479 except Exception as e:
479 except Exception as e:
480 log.error(traceback.format_exc())
480 log.error(traceback.format_exc())
481 h.flash(_('An error occurred during this operation'),
481 h.flash(_('An error occurred during this operation'),
482 category='error')
482 category='error')
483
483
484 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
484 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
485
485
486 @HasRepoPermissionAnyDecorator('repository.admin')
486 @HasRepoPermissionLevelDecorator('admin')
487 def edit_advanced_locking(self, repo_name):
487 def edit_advanced_locking(self, repo_name):
488 """
488 """
489 Unlock repository when it is locked !
489 Unlock repository when it is locked !
490
490
491 :param repo_name:
491 :param repo_name:
492 """
492 """
493 try:
493 try:
494 repo = Repository.get_by_repo_name(repo_name)
494 repo = Repository.get_by_repo_name(repo_name)
495 if request.POST.get('set_lock'):
495 if request.POST.get('set_lock'):
496 Repository.lock(repo, request.authuser.user_id)
496 Repository.lock(repo, request.authuser.user_id)
497 h.flash(_('Repository has been locked'), category='success')
497 h.flash(_('Repository has been locked'), category='success')
498 elif request.POST.get('set_unlock'):
498 elif request.POST.get('set_unlock'):
499 Repository.unlock(repo)
499 Repository.unlock(repo)
500 h.flash(_('Repository has been unlocked'), category='success')
500 h.flash(_('Repository has been unlocked'), category='success')
501 except Exception as e:
501 except Exception as e:
502 log.error(traceback.format_exc())
502 log.error(traceback.format_exc())
503 h.flash(_('An error occurred during unlocking'),
503 h.flash(_('An error occurred during unlocking'),
504 category='error')
504 category='error')
505 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
505 raise HTTPFound(location=url('edit_repo_advanced', repo_name=repo_name))
506
506
507 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
507 @HasRepoPermissionLevelDecorator('write')
508 def toggle_locking(self, repo_name):
508 def toggle_locking(self, repo_name):
509 try:
509 try:
510 repo = Repository.get_by_repo_name(repo_name)
510 repo = Repository.get_by_repo_name(repo_name)
511
511
512 if repo.enable_locking:
512 if repo.enable_locking:
513 if repo.locked[0]:
513 if repo.locked[0]:
514 Repository.unlock(repo)
514 Repository.unlock(repo)
515 h.flash(_('Repository has been unlocked'), category='success')
515 h.flash(_('Repository has been unlocked'), category='success')
516 else:
516 else:
517 Repository.lock(repo, request.authuser.user_id)
517 Repository.lock(repo, request.authuser.user_id)
518 h.flash(_('Repository has been locked'), category='success')
518 h.flash(_('Repository has been locked'), category='success')
519
519
520 except Exception as e:
520 except Exception as e:
521 log.error(traceback.format_exc())
521 log.error(traceback.format_exc())
522 h.flash(_('An error occurred during unlocking'),
522 h.flash(_('An error occurred during unlocking'),
523 category='error')
523 category='error')
524 raise HTTPFound(location=url('summary_home', repo_name=repo_name))
524 raise HTTPFound(location=url('summary_home', repo_name=repo_name))
525
525
526 @HasRepoPermissionAnyDecorator('repository.admin')
526 @HasRepoPermissionLevelDecorator('admin')
527 def edit_caches(self, repo_name):
527 def edit_caches(self, repo_name):
528 c.repo_info = self._load_repo()
528 c.repo_info = self._load_repo()
529 c.active = 'caches'
529 c.active = 'caches'
530 if request.POST:
530 if request.POST:
531 try:
531 try:
532 ScmModel().mark_for_invalidation(repo_name)
532 ScmModel().mark_for_invalidation(repo_name)
533 Session().commit()
533 Session().commit()
534 h.flash(_('Cache invalidation successful'),
534 h.flash(_('Cache invalidation successful'),
535 category='success')
535 category='success')
536 except Exception as e:
536 except Exception as e:
537 log.error(traceback.format_exc())
537 log.error(traceback.format_exc())
538 h.flash(_('An error occurred during cache invalidation'),
538 h.flash(_('An error occurred during cache invalidation'),
539 category='error')
539 category='error')
540
540
541 raise HTTPFound(location=url('edit_repo_caches', repo_name=c.repo_name))
541 raise HTTPFound(location=url('edit_repo_caches', repo_name=c.repo_name))
542 return render('admin/repos/repo_edit.html')
542 return render('admin/repos/repo_edit.html')
543
543
544 @HasRepoPermissionAnyDecorator('repository.admin')
544 @HasRepoPermissionLevelDecorator('admin')
545 def edit_remote(self, repo_name):
545 def edit_remote(self, repo_name):
546 c.repo_info = self._load_repo()
546 c.repo_info = self._load_repo()
547 c.active = 'remote'
547 c.active = 'remote'
548 if request.POST:
548 if request.POST:
549 try:
549 try:
550 ScmModel().pull_changes(repo_name, request.authuser.username)
550 ScmModel().pull_changes(repo_name, request.authuser.username)
551 h.flash(_('Pulled from remote location'), category='success')
551 h.flash(_('Pulled from remote location'), category='success')
552 except Exception as e:
552 except Exception as e:
553 log.error(traceback.format_exc())
553 log.error(traceback.format_exc())
554 h.flash(_('An error occurred during pull from remote location'),
554 h.flash(_('An error occurred during pull from remote location'),
555 category='error')
555 category='error')
556 raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name))
556 raise HTTPFound(location=url('edit_repo_remote', repo_name=c.repo_name))
557 return render('admin/repos/repo_edit.html')
557 return render('admin/repos/repo_edit.html')
558
558
559 @HasRepoPermissionAnyDecorator('repository.admin')
559 @HasRepoPermissionLevelDecorator('admin')
560 def edit_statistics(self, repo_name):
560 def edit_statistics(self, repo_name):
561 c.repo_info = self._load_repo()
561 c.repo_info = self._load_repo()
562 repo = c.repo_info.scm_instance
562 repo = c.repo_info.scm_instance
563
563
564 if c.repo_info.stats:
564 if c.repo_info.stats:
565 # this is on what revision we ended up so we add +1 for count
565 # this is on what revision we ended up so we add +1 for count
566 last_rev = c.repo_info.stats.stat_on_revision + 1
566 last_rev = c.repo_info.stats.stat_on_revision + 1
567 else:
567 else:
568 last_rev = 0
568 last_rev = 0
569 c.stats_revision = last_rev
569 c.stats_revision = last_rev
570
570
571 c.repo_last_rev = repo.count() if repo.revisions else 0
571 c.repo_last_rev = repo.count() if repo.revisions else 0
572
572
573 if last_rev == 0 or c.repo_last_rev == 0:
573 if last_rev == 0 or c.repo_last_rev == 0:
574 c.stats_percentage = 0
574 c.stats_percentage = 0
575 else:
575 else:
576 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
576 c.stats_percentage = '%.2f' % ((float((last_rev)) / c.repo_last_rev) * 100)
577
577
578 c.active = 'statistics'
578 c.active = 'statistics'
579 if request.POST:
579 if request.POST:
580 try:
580 try:
581 RepoModel().delete_stats(repo_name)
581 RepoModel().delete_stats(repo_name)
582 Session().commit()
582 Session().commit()
583 except Exception as e:
583 except Exception as e:
584 log.error(traceback.format_exc())
584 log.error(traceback.format_exc())
585 h.flash(_('An error occurred during deletion of repository stats'),
585 h.flash(_('An error occurred during deletion of repository stats'),
586 category='error')
586 category='error')
587 raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name))
587 raise HTTPFound(location=url('edit_repo_statistics', repo_name=c.repo_name))
588
588
589 return render('admin/repos/repo_edit.html')
589 return render('admin/repos/repo_edit.html')
@@ -1,2527 +1,2509 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.api.api
15 kallithea.controllers.api.api
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 API controller for Kallithea
18 API 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: Aug 20, 2011
22 :created_on: Aug 20, 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 time
28 import time
29 import traceback
29 import traceback
30 import logging
30 import logging
31 from sqlalchemy import or_
31 from sqlalchemy import or_
32
32
33 from pylons import request
33 from pylons import request
34
34
35 from kallithea.controllers.api import JSONRPCController, JSONRPCError
35 from kallithea.controllers.api import JSONRPCController, JSONRPCError
36 from kallithea.lib.auth import (
36 from kallithea.lib.auth import (
37 PasswordGenerator, AuthUser, HasPermissionAnyDecorator,
37 PasswordGenerator, AuthUser, HasPermissionAnyDecorator,
38 HasPermissionAnyDecorator, HasPermissionAny, HasRepoPermissionAny,
38 HasPermissionAnyDecorator, HasPermissionAny, HasRepoPermissionLevel,
39 HasRepoGroupPermissionAny, HasUserGroupPermissionAny)
39 HasRepoGroupPermissionAny, HasUserGroupPermissionAny)
40 from kallithea.lib.utils import map_groups, repo2db_mapper
40 from kallithea.lib.utils import map_groups, repo2db_mapper
41 from kallithea.lib.utils2 import (
41 from kallithea.lib.utils2 import (
42 str2bool, time_to_datetime, safe_int, Optional, OAttr)
42 str2bool, time_to_datetime, safe_int, Optional, OAttr)
43 from kallithea.model.meta import Session
43 from kallithea.model.meta import Session
44 from kallithea.model.repo_group import RepoGroupModel
44 from kallithea.model.repo_group import RepoGroupModel
45 from kallithea.model.scm import ScmModel, UserGroupList
45 from kallithea.model.scm import ScmModel, UserGroupList
46 from kallithea.model.repo import RepoModel
46 from kallithea.model.repo import RepoModel
47 from kallithea.model.user import UserModel
47 from kallithea.model.user import UserModel
48 from kallithea.model.user_group import UserGroupModel
48 from kallithea.model.user_group import UserGroupModel
49 from kallithea.model.gist import GistModel
49 from kallithea.model.gist import GistModel
50 from kallithea.model.db import (
50 from kallithea.model.db import (
51 Repository, Setting, UserIpMap, Permission, User, Gist,
51 Repository, Setting, UserIpMap, Permission, User, Gist,
52 RepoGroup, UserGroup)
52 RepoGroup, UserGroup)
53 from kallithea.lib.compat import json
53 from kallithea.lib.compat import json
54 from kallithea.lib.exceptions import (
54 from kallithea.lib.exceptions import (
55 DefaultUserException, UserGroupsAssignedException)
55 DefaultUserException, UserGroupsAssignedException)
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 def store_update(updates, attr, name):
60 def store_update(updates, attr, name):
61 """
61 """
62 Stores param in updates dict if it's not instance of Optional
62 Stores param in updates dict if it's not instance of Optional
63 allows easy updates of passed in params
63 allows easy updates of passed in params
64 """
64 """
65 if not isinstance(attr, Optional):
65 if not isinstance(attr, Optional):
66 updates[name] = attr
66 updates[name] = attr
67
67
68
68
69 def get_user_or_error(userid):
69 def get_user_or_error(userid):
70 """
70 """
71 Get user by id or name or return JsonRPCError if not found
71 Get user by id or name or return JsonRPCError if not found
72
72
73 :param userid:
73 :param userid:
74 """
74 """
75 user = UserModel().get_user(userid)
75 user = UserModel().get_user(userid)
76 if user is None:
76 if user is None:
77 raise JSONRPCError("user `%s` does not exist" % (userid,))
77 raise JSONRPCError("user `%s` does not exist" % (userid,))
78 return user
78 return user
79
79
80
80
81 def get_repo_or_error(repoid):
81 def get_repo_or_error(repoid):
82 """
82 """
83 Get repo by id or name or return JsonRPCError if not found
83 Get repo by id or name or return JsonRPCError if not found
84
84
85 :param repoid:
85 :param repoid:
86 """
86 """
87 repo = RepoModel().get_repo(repoid)
87 repo = RepoModel().get_repo(repoid)
88 if repo is None:
88 if repo is None:
89 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
89 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
90 return repo
90 return repo
91
91
92
92
93 def get_repo_group_or_error(repogroupid):
93 def get_repo_group_or_error(repogroupid):
94 """
94 """
95 Get repo group by id or name or return JsonRPCError if not found
95 Get repo group by id or name or return JsonRPCError if not found
96
96
97 :param repogroupid:
97 :param repogroupid:
98 """
98 """
99 repo_group = RepoGroup.guess_instance(repogroupid)
99 repo_group = RepoGroup.guess_instance(repogroupid)
100 if repo_group is None:
100 if repo_group is None:
101 raise JSONRPCError(
101 raise JSONRPCError(
102 'repository group `%s` does not exist' % (repogroupid,))
102 'repository group `%s` does not exist' % (repogroupid,))
103 return repo_group
103 return repo_group
104
104
105
105
106 def get_user_group_or_error(usergroupid):
106 def get_user_group_or_error(usergroupid):
107 """
107 """
108 Get user group by id or name or return JsonRPCError if not found
108 Get user group by id or name or return JsonRPCError if not found
109
109
110 :param usergroupid:
110 :param usergroupid:
111 """
111 """
112 user_group = UserGroupModel().get_group(usergroupid)
112 user_group = UserGroupModel().get_group(usergroupid)
113 if user_group is None:
113 if user_group is None:
114 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
114 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
115 return user_group
115 return user_group
116
116
117
117
118 def get_perm_or_error(permid, prefix=None):
118 def get_perm_or_error(permid, prefix=None):
119 """
119 """
120 Get permission by id or name or return JsonRPCError if not found
120 Get permission by id or name or return JsonRPCError if not found
121
121
122 :param permid:
122 :param permid:
123 """
123 """
124 perm = Permission.get_by_key(permid)
124 perm = Permission.get_by_key(permid)
125 if perm is None:
125 if perm is None:
126 raise JSONRPCError('permission `%s` does not exist' % (permid,))
126 raise JSONRPCError('permission `%s` does not exist' % (permid,))
127 if prefix:
127 if prefix:
128 if not perm.permission_name.startswith(prefix):
128 if not perm.permission_name.startswith(prefix):
129 raise JSONRPCError('permission `%s` is invalid, '
129 raise JSONRPCError('permission `%s` is invalid, '
130 'should start with %s' % (permid, prefix))
130 'should start with %s' % (permid, prefix))
131 return perm
131 return perm
132
132
133
133
134 def get_gist_or_error(gistid):
134 def get_gist_or_error(gistid):
135 """
135 """
136 Get gist by id or gist_access_id or return JsonRPCError if not found
136 Get gist by id or gist_access_id or return JsonRPCError if not found
137
137
138 :param gistid:
138 :param gistid:
139 """
139 """
140 gist = GistModel().get_gist(gistid)
140 gist = GistModel().get_gist(gistid)
141 if gist is None:
141 if gist is None:
142 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
142 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
143 return gist
143 return gist
144
144
145
145
146 class ApiController(JSONRPCController):
146 class ApiController(JSONRPCController):
147 """
147 """
148 API Controller
148 API Controller
149
149
150 The authenticated user can be found as request.authuser.
150 The authenticated user can be found as request.authuser.
151
151
152 Example function::
152 Example function::
153
153
154 def func(arg1, arg2,...):
154 def func(arg1, arg2,...):
155 pass
155 pass
156
156
157 Each function should also **raise** JSONRPCError for any
157 Each function should also **raise** JSONRPCError for any
158 errors that happens.
158 errors that happens.
159 """
159 """
160
160
161 @HasPermissionAnyDecorator('hg.admin')
161 @HasPermissionAnyDecorator('hg.admin')
162 def test(self, args):
162 def test(self, args):
163 return args
163 return args
164
164
165 @HasPermissionAnyDecorator('hg.admin')
165 @HasPermissionAnyDecorator('hg.admin')
166 def pull(self, repoid):
166 def pull(self, repoid):
167 """
167 """
168 Triggers a pull from remote location on given repo. Can be used to
168 Triggers a pull from remote location on given repo. Can be used to
169 automatically keep remote repos up to date. This command can be executed
169 automatically keep remote repos up to date. This command can be executed
170 only using api_key belonging to user with admin rights
170 only using api_key belonging to user with admin rights
171
171
172 :param repoid: repository name or repository id
172 :param repoid: repository name or repository id
173 :type repoid: str or int
173 :type repoid: str or int
174
174
175 OUTPUT::
175 OUTPUT::
176
176
177 id : <id_given_in_input>
177 id : <id_given_in_input>
178 result : {
178 result : {
179 "msg": "Pulled from `<repository name>`"
179 "msg": "Pulled from `<repository name>`"
180 "repository": "<repository name>"
180 "repository": "<repository name>"
181 }
181 }
182 error : null
182 error : null
183
183
184 ERROR OUTPUT::
184 ERROR OUTPUT::
185
185
186 id : <id_given_in_input>
186 id : <id_given_in_input>
187 result : null
187 result : null
188 error : {
188 error : {
189 "Unable to pull changes from `<reponame>`"
189 "Unable to pull changes from `<reponame>`"
190 }
190 }
191
191
192 """
192 """
193
193
194 repo = get_repo_or_error(repoid)
194 repo = get_repo_or_error(repoid)
195
195
196 try:
196 try:
197 ScmModel().pull_changes(repo.repo_name,
197 ScmModel().pull_changes(repo.repo_name,
198 request.authuser.username)
198 request.authuser.username)
199 return dict(
199 return dict(
200 msg='Pulled from `%s`' % repo.repo_name,
200 msg='Pulled from `%s`' % repo.repo_name,
201 repository=repo.repo_name
201 repository=repo.repo_name
202 )
202 )
203 except Exception:
203 except Exception:
204 log.error(traceback.format_exc())
204 log.error(traceback.format_exc())
205 raise JSONRPCError(
205 raise JSONRPCError(
206 'Unable to pull changes from `%s`' % repo.repo_name
206 'Unable to pull changes from `%s`' % repo.repo_name
207 )
207 )
208
208
209 @HasPermissionAnyDecorator('hg.admin')
209 @HasPermissionAnyDecorator('hg.admin')
210 def rescan_repos(self, remove_obsolete=Optional(False)):
210 def rescan_repos(self, remove_obsolete=Optional(False)):
211 """
211 """
212 Triggers rescan repositories action. If remove_obsolete is set
212 Triggers rescan repositories action. If remove_obsolete is set
213 than also delete repos that are in database but not in the filesystem.
213 than also delete repos that are in database but not in the filesystem.
214 aka "clean zombies". This command can be executed only using api_key
214 aka "clean zombies". This command can be executed only using api_key
215 belonging to user with admin rights.
215 belonging to user with admin rights.
216
216
217 :param remove_obsolete: deletes repositories from
217 :param remove_obsolete: deletes repositories from
218 database that are not found on the filesystem
218 database that are not found on the filesystem
219 :type remove_obsolete: Optional(bool)
219 :type remove_obsolete: Optional(bool)
220
220
221 OUTPUT::
221 OUTPUT::
222
222
223 id : <id_given_in_input>
223 id : <id_given_in_input>
224 result : {
224 result : {
225 'added': [<added repository name>,...]
225 'added': [<added repository name>,...]
226 'removed': [<removed repository name>,...]
226 'removed': [<removed repository name>,...]
227 }
227 }
228 error : null
228 error : null
229
229
230 ERROR OUTPUT::
230 ERROR OUTPUT::
231
231
232 id : <id_given_in_input>
232 id : <id_given_in_input>
233 result : null
233 result : null
234 error : {
234 error : {
235 'Error occurred during rescan repositories action'
235 'Error occurred during rescan repositories action'
236 }
236 }
237
237
238 """
238 """
239
239
240 try:
240 try:
241 rm_obsolete = Optional.extract(remove_obsolete)
241 rm_obsolete = Optional.extract(remove_obsolete)
242 added, removed = repo2db_mapper(ScmModel().repo_scan(),
242 added, removed = repo2db_mapper(ScmModel().repo_scan(),
243 remove_obsolete=rm_obsolete)
243 remove_obsolete=rm_obsolete)
244 return {'added': added, 'removed': removed}
244 return {'added': added, 'removed': removed}
245 except Exception:
245 except Exception:
246 log.error(traceback.format_exc())
246 log.error(traceback.format_exc())
247 raise JSONRPCError(
247 raise JSONRPCError(
248 'Error occurred during rescan repositories action'
248 'Error occurred during rescan repositories action'
249 )
249 )
250
250
251 def invalidate_cache(self, repoid):
251 def invalidate_cache(self, repoid):
252 """
252 """
253 Invalidate cache for repository.
253 Invalidate cache for repository.
254 This command can be executed only using api_key belonging to user with admin
254 This command can be executed only using api_key belonging to user with admin
255 rights or regular user that have write or admin or write access to repository.
255 rights or regular user that have write or admin or write access to repository.
256
256
257 :param repoid: repository name or repository id
257 :param repoid: repository name or repository id
258 :type repoid: str or int
258 :type repoid: str or int
259
259
260 OUTPUT::
260 OUTPUT::
261
261
262 id : <id_given_in_input>
262 id : <id_given_in_input>
263 result : {
263 result : {
264 'msg': Cache for repository `<repository name>` was invalidated,
264 'msg': Cache for repository `<repository name>` was invalidated,
265 'repository': <repository name>
265 'repository': <repository name>
266 }
266 }
267 error : null
267 error : null
268
268
269 ERROR OUTPUT::
269 ERROR OUTPUT::
270
270
271 id : <id_given_in_input>
271 id : <id_given_in_input>
272 result : null
272 result : null
273 error : {
273 error : {
274 'Error occurred during cache invalidation action'
274 'Error occurred during cache invalidation action'
275 }
275 }
276
276
277 """
277 """
278 repo = get_repo_or_error(repoid)
278 repo = get_repo_or_error(repoid)
279 if not HasPermissionAny('hg.admin')():
279 if not HasPermissionAny('hg.admin')():
280 # check if we have admin permission for this repo !
280 if not HasRepoPermissionLevel('write')(repo.repo_name):
281 if not HasRepoPermissionAny('repository.admin',
282 'repository.write')(
283 repo_name=repo.repo_name):
284 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
281 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
285
282
286 try:
283 try:
287 ScmModel().mark_for_invalidation(repo.repo_name)
284 ScmModel().mark_for_invalidation(repo.repo_name)
288 return dict(
285 return dict(
289 msg='Cache for repository `%s` was invalidated' % (repoid,),
286 msg='Cache for repository `%s` was invalidated' % (repoid,),
290 repository=repo.repo_name
287 repository=repo.repo_name
291 )
288 )
292 except Exception:
289 except Exception:
293 log.error(traceback.format_exc())
290 log.error(traceback.format_exc())
294 raise JSONRPCError(
291 raise JSONRPCError(
295 'Error occurred during cache invalidation action'
292 'Error occurred during cache invalidation action'
296 )
293 )
297
294
298 # permission check inside
295 # permission check inside
299 def lock(self, repoid, locked=Optional(None),
296 def lock(self, repoid, locked=Optional(None),
300 userid=Optional(OAttr('apiuser'))):
297 userid=Optional(OAttr('apiuser'))):
301 """
298 """
302 Set locking state on given repository by given user. If userid param
299 Set locking state on given repository by given user. If userid param
303 is skipped, then it is set to id of user who is calling this method.
300 is skipped, then it is set to id of user who is calling this method.
304 If locked param is skipped then function shows current lock state of
301 If locked param is skipped then function shows current lock state of
305 given repo. This command can be executed only using api_key belonging
302 given repo. This command can be executed only using api_key belonging
306 to user with admin rights or regular user that have admin or write
303 to user with admin rights or regular user that have admin or write
307 access to repository.
304 access to repository.
308
305
309 :param repoid: repository name or repository id
306 :param repoid: repository name or repository id
310 :type repoid: str or int
307 :type repoid: str or int
311 :param locked: lock state to be set
308 :param locked: lock state to be set
312 :type locked: Optional(bool)
309 :type locked: Optional(bool)
313 :param userid: set lock as user
310 :param userid: set lock as user
314 :type userid: Optional(str or int)
311 :type userid: Optional(str or int)
315
312
316 OUTPUT::
313 OUTPUT::
317
314
318 id : <id_given_in_input>
315 id : <id_given_in_input>
319 result : {
316 result : {
320 'repo': '<reponame>',
317 'repo': '<reponame>',
321 'locked': <bool: lock state>,
318 'locked': <bool: lock state>,
322 'locked_since': <int: lock timestamp>,
319 'locked_since': <int: lock timestamp>,
323 'locked_by': <username of person who made the lock>,
320 'locked_by': <username of person who made the lock>,
324 'lock_state_changed': <bool: True if lock state has been changed in this request>,
321 'lock_state_changed': <bool: True if lock state has been changed in this request>,
325 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
322 'msg': 'Repo `<reponame>` locked by `<username>` on <timestamp>.'
326 or
323 or
327 'msg': 'Repo `<repository name>` not locked.'
324 'msg': 'Repo `<repository name>` not locked.'
328 or
325 or
329 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
326 'msg': 'User `<user name>` set lock state for repo `<repository name>` to `<new lock state>`'
330 }
327 }
331 error : null
328 error : null
332
329
333 ERROR OUTPUT::
330 ERROR OUTPUT::
334
331
335 id : <id_given_in_input>
332 id : <id_given_in_input>
336 result : null
333 result : null
337 error : {
334 error : {
338 'Error occurred locking repository `<reponame>`
335 'Error occurred locking repository `<reponame>`
339 }
336 }
340
337
341 """
338 """
342 repo = get_repo_or_error(repoid)
339 repo = get_repo_or_error(repoid)
343 if HasPermissionAny('hg.admin')():
340 if HasPermissionAny('hg.admin')():
344 pass
341 pass
345 elif HasRepoPermissionAny('repository.admin',
342 elif HasRepoPermissionLevel('write')(repo.repo_name):
346 'repository.write')(repo_name=repo.repo_name):
347 # make sure normal user does not pass someone else userid,
343 # make sure normal user does not pass someone else userid,
348 # he is not allowed to do that
344 # he is not allowed to do that
349 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
345 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
350 raise JSONRPCError(
346 raise JSONRPCError(
351 'userid is not the same as your user'
347 'userid is not the same as your user'
352 )
348 )
353 else:
349 else:
354 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
350 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
355
351
356 if isinstance(userid, Optional):
352 if isinstance(userid, Optional):
357 userid = request.authuser.user_id
353 userid = request.authuser.user_id
358
354
359 user = get_user_or_error(userid)
355 user = get_user_or_error(userid)
360
356
361 if isinstance(locked, Optional):
357 if isinstance(locked, Optional):
362 lockobj = Repository.getlock(repo)
358 lockobj = Repository.getlock(repo)
363
359
364 if lockobj[0] is None:
360 if lockobj[0] is None:
365 _d = {
361 _d = {
366 'repo': repo.repo_name,
362 'repo': repo.repo_name,
367 'locked': False,
363 'locked': False,
368 'locked_since': None,
364 'locked_since': None,
369 'locked_by': None,
365 'locked_by': None,
370 'lock_state_changed': False,
366 'lock_state_changed': False,
371 'msg': 'Repo `%s` not locked.' % repo.repo_name
367 'msg': 'Repo `%s` not locked.' % repo.repo_name
372 }
368 }
373 return _d
369 return _d
374 else:
370 else:
375 userid, time_ = lockobj
371 userid, time_ = lockobj
376 lock_user = get_user_or_error(userid)
372 lock_user = get_user_or_error(userid)
377 _d = {
373 _d = {
378 'repo': repo.repo_name,
374 'repo': repo.repo_name,
379 'locked': True,
375 'locked': True,
380 'locked_since': time_,
376 'locked_since': time_,
381 'locked_by': lock_user.username,
377 'locked_by': lock_user.username,
382 'lock_state_changed': False,
378 'lock_state_changed': False,
383 'msg': ('Repo `%s` locked by `%s` on `%s`.'
379 'msg': ('Repo `%s` locked by `%s` on `%s`.'
384 % (repo.repo_name, lock_user.username,
380 % (repo.repo_name, lock_user.username,
385 json.dumps(time_to_datetime(time_))))
381 json.dumps(time_to_datetime(time_))))
386 }
382 }
387 return _d
383 return _d
388
384
389 # force locked state through a flag
385 # force locked state through a flag
390 else:
386 else:
391 locked = str2bool(locked)
387 locked = str2bool(locked)
392 try:
388 try:
393 if locked:
389 if locked:
394 lock_time = time.time()
390 lock_time = time.time()
395 Repository.lock(repo, user.user_id, lock_time)
391 Repository.lock(repo, user.user_id, lock_time)
396 else:
392 else:
397 lock_time = None
393 lock_time = None
398 Repository.unlock(repo)
394 Repository.unlock(repo)
399 _d = {
395 _d = {
400 'repo': repo.repo_name,
396 'repo': repo.repo_name,
401 'locked': locked,
397 'locked': locked,
402 'locked_since': lock_time,
398 'locked_since': lock_time,
403 'locked_by': user.username,
399 'locked_by': user.username,
404 'lock_state_changed': True,
400 'lock_state_changed': True,
405 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
401 'msg': ('User `%s` set lock state for repo `%s` to `%s`'
406 % (user.username, repo.repo_name, locked))
402 % (user.username, repo.repo_name, locked))
407 }
403 }
408 return _d
404 return _d
409 except Exception:
405 except Exception:
410 log.error(traceback.format_exc())
406 log.error(traceback.format_exc())
411 raise JSONRPCError(
407 raise JSONRPCError(
412 'Error occurred locking repository `%s`' % repo.repo_name
408 'Error occurred locking repository `%s`' % repo.repo_name
413 )
409 )
414
410
415 def get_locks(self, userid=Optional(OAttr('apiuser'))):
411 def get_locks(self, userid=Optional(OAttr('apiuser'))):
416 """
412 """
417 Get all repositories with locks for given userid, if
413 Get all repositories with locks for given userid, if
418 this command is run by non-admin account userid is set to user
414 this command is run by non-admin account userid is set to user
419 who is calling this method, thus returning locks for himself.
415 who is calling this method, thus returning locks for himself.
420
416
421 :param userid: User to get locks for
417 :param userid: User to get locks for
422 :type userid: Optional(str or int)
418 :type userid: Optional(str or int)
423
419
424 OUTPUT::
420 OUTPUT::
425
421
426 id : <id_given_in_input>
422 id : <id_given_in_input>
427 result : {
423 result : {
428 [repo_object, repo_object,...]
424 [repo_object, repo_object,...]
429 }
425 }
430 error : null
426 error : null
431 """
427 """
432
428
433 if not HasPermissionAny('hg.admin')():
429 if not HasPermissionAny('hg.admin')():
434 # make sure normal user does not pass someone else userid,
430 # make sure normal user does not pass someone else userid,
435 # he is not allowed to do that
431 # he is not allowed to do that
436 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
432 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
437 raise JSONRPCError(
433 raise JSONRPCError(
438 'userid is not the same as your user'
434 'userid is not the same as your user'
439 )
435 )
440
436
441 ret = []
437 ret = []
442 if isinstance(userid, Optional):
438 if isinstance(userid, Optional):
443 user = None
439 user = None
444 else:
440 else:
445 user = get_user_or_error(userid)
441 user = get_user_or_error(userid)
446
442
447 # show all locks
443 # show all locks
448 for r in Repository.query():
444 for r in Repository.query():
449 userid, time_ = r.locked
445 userid, time_ = r.locked
450 if time_:
446 if time_:
451 _api_data = r.get_api_data()
447 _api_data = r.get_api_data()
452 # if we use userfilter just show the locks for this user
448 # if we use userfilter just show the locks for this user
453 if user is not None:
449 if user is not None:
454 if safe_int(userid) == user.user_id:
450 if safe_int(userid) == user.user_id:
455 ret.append(_api_data)
451 ret.append(_api_data)
456 else:
452 else:
457 ret.append(_api_data)
453 ret.append(_api_data)
458
454
459 return ret
455 return ret
460
456
461 @HasPermissionAnyDecorator('hg.admin')
457 @HasPermissionAnyDecorator('hg.admin')
462 def get_ip(self, userid=Optional(OAttr('apiuser'))):
458 def get_ip(self, userid=Optional(OAttr('apiuser'))):
463 """
459 """
464 Shows IP address as seen from Kallithea server, together with all
460 Shows IP address as seen from Kallithea server, together with all
465 defined IP addresses for given user. If userid is not passed data is
461 defined IP addresses for given user. If userid is not passed data is
466 returned for user who's calling this function.
462 returned for user who's calling this function.
467 This command can be executed only using api_key belonging to user with
463 This command can be executed only using api_key belonging to user with
468 admin rights.
464 admin rights.
469
465
470 :param userid: username to show ips for
466 :param userid: username to show ips for
471 :type userid: Optional(str or int)
467 :type userid: Optional(str or int)
472
468
473 OUTPUT::
469 OUTPUT::
474
470
475 id : <id_given_in_input>
471 id : <id_given_in_input>
476 result : {
472 result : {
477 "server_ip_addr": "<ip_from_clien>",
473 "server_ip_addr": "<ip_from_clien>",
478 "user_ips": [
474 "user_ips": [
479 {
475 {
480 "ip_addr": "<ip_with_mask>",
476 "ip_addr": "<ip_with_mask>",
481 "ip_range": ["<start_ip>", "<end_ip>"],
477 "ip_range": ["<start_ip>", "<end_ip>"],
482 },
478 },
483 ...
479 ...
484 ]
480 ]
485 }
481 }
486
482
487 """
483 """
488 if isinstance(userid, Optional):
484 if isinstance(userid, Optional):
489 userid = request.authuser.user_id
485 userid = request.authuser.user_id
490 user = get_user_or_error(userid)
486 user = get_user_or_error(userid)
491 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
487 ips = UserIpMap.query().filter(UserIpMap.user == user).all()
492 return dict(
488 return dict(
493 server_ip_addr=request.ip_addr,
489 server_ip_addr=request.ip_addr,
494 user_ips=ips
490 user_ips=ips
495 )
491 )
496
492
497 # alias for old
493 # alias for old
498 show_ip = get_ip
494 show_ip = get_ip
499
495
500 @HasPermissionAnyDecorator('hg.admin')
496 @HasPermissionAnyDecorator('hg.admin')
501 def get_server_info(self):
497 def get_server_info(self):
502 """
498 """
503 return server info, including Kallithea version and installed packages
499 return server info, including Kallithea version and installed packages
504
500
505
501
506 OUTPUT::
502 OUTPUT::
507
503
508 id : <id_given_in_input>
504 id : <id_given_in_input>
509 result : {
505 result : {
510 'modules': [<module name>,...]
506 'modules': [<module name>,...]
511 'py_version': <python version>,
507 'py_version': <python version>,
512 'platform': <platform type>,
508 'platform': <platform type>,
513 'kallithea_version': <kallithea version>
509 'kallithea_version': <kallithea version>
514 }
510 }
515 error : null
511 error : null
516 """
512 """
517 return Setting.get_server_info()
513 return Setting.get_server_info()
518
514
519 def get_user(self, userid=Optional(OAttr('apiuser'))):
515 def get_user(self, userid=Optional(OAttr('apiuser'))):
520 """
516 """
521 Gets a user by username or user_id, Returns empty result if user is
517 Gets a user by username or user_id, Returns empty result if user is
522 not found. If userid param is skipped it is set to id of user who is
518 not found. If userid param is skipped it is set to id of user who is
523 calling this method. This command can be executed only using api_key
519 calling this method. This command can be executed only using api_key
524 belonging to user with admin rights, or regular users that cannot
520 belonging to user with admin rights, or regular users that cannot
525 specify different userid than theirs
521 specify different userid than theirs
526
522
527 :param userid: user to get data for
523 :param userid: user to get data for
528 :type userid: Optional(str or int)
524 :type userid: Optional(str or int)
529
525
530 OUTPUT::
526 OUTPUT::
531
527
532 id : <id_given_in_input>
528 id : <id_given_in_input>
533 result: None if user does not exist or
529 result: None if user does not exist or
534 {
530 {
535 "user_id" : "<user_id>",
531 "user_id" : "<user_id>",
536 "api_key" : "<api_key>",
532 "api_key" : "<api_key>",
537 "api_keys": "[<list of all API keys including additional ones>]"
533 "api_keys": "[<list of all API keys including additional ones>]"
538 "username" : "<username>",
534 "username" : "<username>",
539 "firstname": "<firstname>",
535 "firstname": "<firstname>",
540 "lastname" : "<lastname>",
536 "lastname" : "<lastname>",
541 "email" : "<email>",
537 "email" : "<email>",
542 "emails": "[<list of all emails including additional ones>]",
538 "emails": "[<list of all emails including additional ones>]",
543 "ip_addresses": "[<ip_address_for_user>,...]",
539 "ip_addresses": "[<ip_address_for_user>,...]",
544 "active" : "<bool: user active>",
540 "active" : "<bool: user active>",
545 "admin" :  "<bool: user is admin>",
541 "admin" :  "<bool: user is admin>",
546 "extern_name" : "<extern_name>",
542 "extern_name" : "<extern_name>",
547 "extern_type" : "<extern type>
543 "extern_type" : "<extern type>
548 "last_login": "<last_login>",
544 "last_login": "<last_login>",
549 "permissions": {
545 "permissions": {
550 "global": ["hg.create.repository",
546 "global": ["hg.create.repository",
551 "repository.read",
547 "repository.read",
552 "hg.register.manual_activate"],
548 "hg.register.manual_activate"],
553 "repositories": {"repo1": "repository.none"},
549 "repositories": {"repo1": "repository.none"},
554 "repositories_groups": {"Group1": "group.read"}
550 "repositories_groups": {"Group1": "group.read"}
555 },
551 },
556 }
552 }
557
553
558 error: null
554 error: null
559
555
560 """
556 """
561 if not HasPermissionAny('hg.admin')():
557 if not HasPermissionAny('hg.admin')():
562 # make sure normal user does not pass someone else userid,
558 # make sure normal user does not pass someone else userid,
563 # he is not allowed to do that
559 # he is not allowed to do that
564 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
560 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
565 raise JSONRPCError(
561 raise JSONRPCError(
566 'userid is not the same as your user'
562 'userid is not the same as your user'
567 )
563 )
568
564
569 if isinstance(userid, Optional):
565 if isinstance(userid, Optional):
570 userid = request.authuser.user_id
566 userid = request.authuser.user_id
571
567
572 user = get_user_or_error(userid)
568 user = get_user_or_error(userid)
573 data = user.get_api_data()
569 data = user.get_api_data()
574 data['permissions'] = AuthUser(user_id=user.user_id).permissions
570 data['permissions'] = AuthUser(user_id=user.user_id).permissions
575 return data
571 return data
576
572
577 @HasPermissionAnyDecorator('hg.admin')
573 @HasPermissionAnyDecorator('hg.admin')
578 def get_users(self):
574 def get_users(self):
579 """
575 """
580 Lists all existing users. This command can be executed only using api_key
576 Lists all existing users. This command can be executed only using api_key
581 belonging to user with admin rights.
577 belonging to user with admin rights.
582
578
583
579
584 OUTPUT::
580 OUTPUT::
585
581
586 id : <id_given_in_input>
582 id : <id_given_in_input>
587 result: [<user_object>, ...]
583 result: [<user_object>, ...]
588 error: null
584 error: null
589 """
585 """
590
586
591 result = []
587 result = []
592 users_list = User.query().order_by(User.username) \
588 users_list = User.query().order_by(User.username) \
593 .filter(User.username != User.DEFAULT_USER) \
589 .filter(User.username != User.DEFAULT_USER) \
594 .all()
590 .all()
595 for user in users_list:
591 for user in users_list:
596 result.append(user.get_api_data())
592 result.append(user.get_api_data())
597 return result
593 return result
598
594
599 @HasPermissionAnyDecorator('hg.admin')
595 @HasPermissionAnyDecorator('hg.admin')
600 def create_user(self, username, email, password=Optional(''),
596 def create_user(self, username, email, password=Optional(''),
601 firstname=Optional(''), lastname=Optional(''),
597 firstname=Optional(''), lastname=Optional(''),
602 active=Optional(True), admin=Optional(False),
598 active=Optional(True), admin=Optional(False),
603 extern_type=Optional(User.DEFAULT_AUTH_TYPE),
599 extern_type=Optional(User.DEFAULT_AUTH_TYPE),
604 extern_name=Optional('')):
600 extern_name=Optional('')):
605 """
601 """
606 Creates new user. Returns new user object. This command can
602 Creates new user. Returns new user object. This command can
607 be executed only using api_key belonging to user with admin rights.
603 be executed only using api_key belonging to user with admin rights.
608
604
609 :param username: new username
605 :param username: new username
610 :type username: str or int
606 :type username: str or int
611 :param email: email
607 :param email: email
612 :type email: str
608 :type email: str
613 :param password: password
609 :param password: password
614 :type password: Optional(str)
610 :type password: Optional(str)
615 :param firstname: firstname
611 :param firstname: firstname
616 :type firstname: Optional(str)
612 :type firstname: Optional(str)
617 :param lastname: lastname
613 :param lastname: lastname
618 :type lastname: Optional(str)
614 :type lastname: Optional(str)
619 :param active: active
615 :param active: active
620 :type active: Optional(bool)
616 :type active: Optional(bool)
621 :param admin: admin
617 :param admin: admin
622 :type admin: Optional(bool)
618 :type admin: Optional(bool)
623 :param extern_name: name of extern
619 :param extern_name: name of extern
624 :type extern_name: Optional(str)
620 :type extern_name: Optional(str)
625 :param extern_type: extern_type
621 :param extern_type: extern_type
626 :type extern_type: Optional(str)
622 :type extern_type: Optional(str)
627
623
628
624
629 OUTPUT::
625 OUTPUT::
630
626
631 id : <id_given_in_input>
627 id : <id_given_in_input>
632 result: {
628 result: {
633 "msg" : "created new user `<username>`",
629 "msg" : "created new user `<username>`",
634 "user": <user_obj>
630 "user": <user_obj>
635 }
631 }
636 error: null
632 error: null
637
633
638 ERROR OUTPUT::
634 ERROR OUTPUT::
639
635
640 id : <id_given_in_input>
636 id : <id_given_in_input>
641 result : null
637 result : null
642 error : {
638 error : {
643 "user `<username>` already exist"
639 "user `<username>` already exist"
644 or
640 or
645 "email `<email>` already exist"
641 "email `<email>` already exist"
646 or
642 or
647 "failed to create user `<username>`"
643 "failed to create user `<username>`"
648 }
644 }
649
645
650 """
646 """
651
647
652 if User.get_by_username(username):
648 if User.get_by_username(username):
653 raise JSONRPCError("user `%s` already exist" % (username,))
649 raise JSONRPCError("user `%s` already exist" % (username,))
654
650
655 if User.get_by_email(email):
651 if User.get_by_email(email):
656 raise JSONRPCError("email `%s` already exist" % (email,))
652 raise JSONRPCError("email `%s` already exist" % (email,))
657
653
658 try:
654 try:
659 user = UserModel().create_or_update(
655 user = UserModel().create_or_update(
660 username=Optional.extract(username),
656 username=Optional.extract(username),
661 password=Optional.extract(password),
657 password=Optional.extract(password),
662 email=Optional.extract(email),
658 email=Optional.extract(email),
663 firstname=Optional.extract(firstname),
659 firstname=Optional.extract(firstname),
664 lastname=Optional.extract(lastname),
660 lastname=Optional.extract(lastname),
665 active=Optional.extract(active),
661 active=Optional.extract(active),
666 admin=Optional.extract(admin),
662 admin=Optional.extract(admin),
667 extern_type=Optional.extract(extern_type),
663 extern_type=Optional.extract(extern_type),
668 extern_name=Optional.extract(extern_name)
664 extern_name=Optional.extract(extern_name)
669 )
665 )
670 Session().commit()
666 Session().commit()
671 return dict(
667 return dict(
672 msg='created new user `%s`' % username,
668 msg='created new user `%s`' % username,
673 user=user.get_api_data()
669 user=user.get_api_data()
674 )
670 )
675 except Exception:
671 except Exception:
676 log.error(traceback.format_exc())
672 log.error(traceback.format_exc())
677 raise JSONRPCError('failed to create user `%s`' % (username,))
673 raise JSONRPCError('failed to create user `%s`' % (username,))
678
674
679 @HasPermissionAnyDecorator('hg.admin')
675 @HasPermissionAnyDecorator('hg.admin')
680 def update_user(self, userid, username=Optional(None),
676 def update_user(self, userid, username=Optional(None),
681 email=Optional(None), password=Optional(None),
677 email=Optional(None), password=Optional(None),
682 firstname=Optional(None), lastname=Optional(None),
678 firstname=Optional(None), lastname=Optional(None),
683 active=Optional(None), admin=Optional(None),
679 active=Optional(None), admin=Optional(None),
684 extern_type=Optional(None), extern_name=Optional(None)):
680 extern_type=Optional(None), extern_name=Optional(None)):
685 """
681 """
686 updates given user if such user exists. This command can
682 updates given user if such user exists. This command can
687 be executed only using api_key belonging to user with admin rights.
683 be executed only using api_key belonging to user with admin rights.
688
684
689 :param userid: userid to update
685 :param userid: userid to update
690 :type userid: str or int
686 :type userid: str or int
691 :param username: new username
687 :param username: new username
692 :type username: str or int
688 :type username: str or int
693 :param email: email
689 :param email: email
694 :type email: str
690 :type email: str
695 :param password: password
691 :param password: password
696 :type password: Optional(str)
692 :type password: Optional(str)
697 :param firstname: firstname
693 :param firstname: firstname
698 :type firstname: Optional(str)
694 :type firstname: Optional(str)
699 :param lastname: lastname
695 :param lastname: lastname
700 :type lastname: Optional(str)
696 :type lastname: Optional(str)
701 :param active: active
697 :param active: active
702 :type active: Optional(bool)
698 :type active: Optional(bool)
703 :param admin: admin
699 :param admin: admin
704 :type admin: Optional(bool)
700 :type admin: Optional(bool)
705 :param extern_name:
701 :param extern_name:
706 :type extern_name: Optional(str)
702 :type extern_name: Optional(str)
707 :param extern_type:
703 :param extern_type:
708 :type extern_type: Optional(str)
704 :type extern_type: Optional(str)
709
705
710
706
711 OUTPUT::
707 OUTPUT::
712
708
713 id : <id_given_in_input>
709 id : <id_given_in_input>
714 result: {
710 result: {
715 "msg" : "updated user ID:<userid> <username>",
711 "msg" : "updated user ID:<userid> <username>",
716 "user": <user_object>,
712 "user": <user_object>,
717 }
713 }
718 error: null
714 error: null
719
715
720 ERROR OUTPUT::
716 ERROR OUTPUT::
721
717
722 id : <id_given_in_input>
718 id : <id_given_in_input>
723 result : null
719 result : null
724 error : {
720 error : {
725 "failed to update user `<username>`"
721 "failed to update user `<username>`"
726 }
722 }
727
723
728 """
724 """
729
725
730 user = get_user_or_error(userid)
726 user = get_user_or_error(userid)
731
727
732 # only non optional arguments will be stored in updates
728 # only non optional arguments will be stored in updates
733 updates = {}
729 updates = {}
734
730
735 try:
731 try:
736
732
737 store_update(updates, username, 'username')
733 store_update(updates, username, 'username')
738 store_update(updates, password, 'password')
734 store_update(updates, password, 'password')
739 store_update(updates, email, 'email')
735 store_update(updates, email, 'email')
740 store_update(updates, firstname, 'name')
736 store_update(updates, firstname, 'name')
741 store_update(updates, lastname, 'lastname')
737 store_update(updates, lastname, 'lastname')
742 store_update(updates, active, 'active')
738 store_update(updates, active, 'active')
743 store_update(updates, admin, 'admin')
739 store_update(updates, admin, 'admin')
744 store_update(updates, extern_name, 'extern_name')
740 store_update(updates, extern_name, 'extern_name')
745 store_update(updates, extern_type, 'extern_type')
741 store_update(updates, extern_type, 'extern_type')
746
742
747 user = UserModel().update_user(user, **updates)
743 user = UserModel().update_user(user, **updates)
748 Session().commit()
744 Session().commit()
749 return dict(
745 return dict(
750 msg='updated user ID:%s %s' % (user.user_id, user.username),
746 msg='updated user ID:%s %s' % (user.user_id, user.username),
751 user=user.get_api_data()
747 user=user.get_api_data()
752 )
748 )
753 except DefaultUserException:
749 except DefaultUserException:
754 log.error(traceback.format_exc())
750 log.error(traceback.format_exc())
755 raise JSONRPCError('editing default user is forbidden')
751 raise JSONRPCError('editing default user is forbidden')
756 except Exception:
752 except Exception:
757 log.error(traceback.format_exc())
753 log.error(traceback.format_exc())
758 raise JSONRPCError('failed to update user `%s`' % (userid,))
754 raise JSONRPCError('failed to update user `%s`' % (userid,))
759
755
760 @HasPermissionAnyDecorator('hg.admin')
756 @HasPermissionAnyDecorator('hg.admin')
761 def delete_user(self, userid):
757 def delete_user(self, userid):
762 """
758 """
763 deletes given user if such user exists. This command can
759 deletes given user if such user exists. This command can
764 be executed only using api_key belonging to user with admin rights.
760 be executed only using api_key belonging to user with admin rights.
765
761
766 :param userid: user to delete
762 :param userid: user to delete
767 :type userid: str or int
763 :type userid: str or int
768
764
769 OUTPUT::
765 OUTPUT::
770
766
771 id : <id_given_in_input>
767 id : <id_given_in_input>
772 result: {
768 result: {
773 "msg" : "deleted user ID:<userid> <username>",
769 "msg" : "deleted user ID:<userid> <username>",
774 "user": null
770 "user": null
775 }
771 }
776 error: null
772 error: null
777
773
778 ERROR OUTPUT::
774 ERROR OUTPUT::
779
775
780 id : <id_given_in_input>
776 id : <id_given_in_input>
781 result : null
777 result : null
782 error : {
778 error : {
783 "failed to delete user ID:<userid> <username>"
779 "failed to delete user ID:<userid> <username>"
784 }
780 }
785
781
786 """
782 """
787 user = get_user_or_error(userid)
783 user = get_user_or_error(userid)
788
784
789 try:
785 try:
790 UserModel().delete(userid)
786 UserModel().delete(userid)
791 Session().commit()
787 Session().commit()
792 return dict(
788 return dict(
793 msg='deleted user ID:%s %s' % (user.user_id, user.username),
789 msg='deleted user ID:%s %s' % (user.user_id, user.username),
794 user=None
790 user=None
795 )
791 )
796 except Exception:
792 except Exception:
797
793
798 log.error(traceback.format_exc())
794 log.error(traceback.format_exc())
799 raise JSONRPCError('failed to delete user ID:%s %s'
795 raise JSONRPCError('failed to delete user ID:%s %s'
800 % (user.user_id, user.username))
796 % (user.user_id, user.username))
801
797
802 # permission check inside
798 # permission check inside
803 def get_user_group(self, usergroupid):
799 def get_user_group(self, usergroupid):
804 """
800 """
805 Gets an existing user group. This command can be executed only using api_key
801 Gets an existing user group. This command can be executed only using api_key
806 belonging to user with admin rights or user who has at least
802 belonging to user with admin rights or user who has at least
807 read access to user group.
803 read access to user group.
808
804
809 :param usergroupid: id of user_group to edit
805 :param usergroupid: id of user_group to edit
810 :type usergroupid: str or int
806 :type usergroupid: str or int
811
807
812 OUTPUT::
808 OUTPUT::
813
809
814 id : <id_given_in_input>
810 id : <id_given_in_input>
815 result : None if group not exist
811 result : None if group not exist
816 {
812 {
817 "users_group_id" : "<id>",
813 "users_group_id" : "<id>",
818 "group_name" : "<groupname>",
814 "group_name" : "<groupname>",
819 "active": "<bool>",
815 "active": "<bool>",
820 "members" : [<user_obj>,...]
816 "members" : [<user_obj>,...]
821 }
817 }
822 error : null
818 error : null
823
819
824 """
820 """
825 user_group = get_user_group_or_error(usergroupid)
821 user_group = get_user_group_or_error(usergroupid)
826 if not HasPermissionAny('hg.admin')():
822 if not HasPermissionAny('hg.admin')():
827 # check if we have at least read permission for this user group !
823 # check if we have at least read permission for this user group !
828 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
824 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
829 if not HasUserGroupPermissionAny(*_perms)(
825 if not HasUserGroupPermissionAny(*_perms)(
830 user_group_name=user_group.users_group_name):
826 user_group_name=user_group.users_group_name):
831 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
827 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
832
828
833 data = user_group.get_api_data()
829 data = user_group.get_api_data()
834 return data
830 return data
835
831
836 # permission check inside
832 # permission check inside
837 def get_user_groups(self):
833 def get_user_groups(self):
838 """
834 """
839 Lists all existing user groups. This command can be executed only using
835 Lists all existing user groups. This command can be executed only using
840 api_key belonging to user with admin rights or user who has at least
836 api_key belonging to user with admin rights or user who has at least
841 read access to user group.
837 read access to user group.
842
838
843
839
844 OUTPUT::
840 OUTPUT::
845
841
846 id : <id_given_in_input>
842 id : <id_given_in_input>
847 result : [<user_group_obj>,...]
843 result : [<user_group_obj>,...]
848 error : null
844 error : null
849 """
845 """
850
846
851 result = []
847 result = []
852 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
848 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
853 for user_group in UserGroupList(UserGroup.query().all(),
849 for user_group in UserGroupList(UserGroup.query().all(),
854 perm_set=_perms):
850 perm_set=_perms):
855 result.append(user_group.get_api_data())
851 result.append(user_group.get_api_data())
856 return result
852 return result
857
853
858 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
854 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
859 def create_user_group(self, group_name, description=Optional(''),
855 def create_user_group(self, group_name, description=Optional(''),
860 owner=Optional(OAttr('apiuser')), active=Optional(True)):
856 owner=Optional(OAttr('apiuser')), active=Optional(True)):
861 """
857 """
862 Creates new user group. This command can be executed only using api_key
858 Creates new user group. This command can be executed only using api_key
863 belonging to user with admin rights or an user who has create user group
859 belonging to user with admin rights or an user who has create user group
864 permission
860 permission
865
861
866 :param group_name: name of new user group
862 :param group_name: name of new user group
867 :type group_name: str
863 :type group_name: str
868 :param description: group description
864 :param description: group description
869 :type description: str
865 :type description: str
870 :param owner: owner of group. If not passed apiuser is the owner
866 :param owner: owner of group. If not passed apiuser is the owner
871 :type owner: Optional(str or int)
867 :type owner: Optional(str or int)
872 :param active: group is active
868 :param active: group is active
873 :type active: Optional(bool)
869 :type active: Optional(bool)
874
870
875 OUTPUT::
871 OUTPUT::
876
872
877 id : <id_given_in_input>
873 id : <id_given_in_input>
878 result: {
874 result: {
879 "msg": "created new user group `<groupname>`",
875 "msg": "created new user group `<groupname>`",
880 "user_group": <user_group_object>
876 "user_group": <user_group_object>
881 }
877 }
882 error: null
878 error: null
883
879
884 ERROR OUTPUT::
880 ERROR OUTPUT::
885
881
886 id : <id_given_in_input>
882 id : <id_given_in_input>
887 result : null
883 result : null
888 error : {
884 error : {
889 "user group `<group name>` already exist"
885 "user group `<group name>` already exist"
890 or
886 or
891 "failed to create group `<group name>`"
887 "failed to create group `<group name>`"
892 }
888 }
893
889
894 """
890 """
895
891
896 if UserGroupModel().get_by_name(group_name):
892 if UserGroupModel().get_by_name(group_name):
897 raise JSONRPCError("user group `%s` already exist" % (group_name,))
893 raise JSONRPCError("user group `%s` already exist" % (group_name,))
898
894
899 try:
895 try:
900 if isinstance(owner, Optional):
896 if isinstance(owner, Optional):
901 owner = request.authuser.user_id
897 owner = request.authuser.user_id
902
898
903 owner = get_user_or_error(owner)
899 owner = get_user_or_error(owner)
904 active = Optional.extract(active)
900 active = Optional.extract(active)
905 description = Optional.extract(description)
901 description = Optional.extract(description)
906 ug = UserGroupModel().create(name=group_name, description=description,
902 ug = UserGroupModel().create(name=group_name, description=description,
907 owner=owner, active=active)
903 owner=owner, active=active)
908 Session().commit()
904 Session().commit()
909 return dict(
905 return dict(
910 msg='created new user group `%s`' % group_name,
906 msg='created new user group `%s`' % group_name,
911 user_group=ug.get_api_data()
907 user_group=ug.get_api_data()
912 )
908 )
913 except Exception:
909 except Exception:
914 log.error(traceback.format_exc())
910 log.error(traceback.format_exc())
915 raise JSONRPCError('failed to create group `%s`' % (group_name,))
911 raise JSONRPCError('failed to create group `%s`' % (group_name,))
916
912
917 # permission check inside
913 # permission check inside
918 def update_user_group(self, usergroupid, group_name=Optional(''),
914 def update_user_group(self, usergroupid, group_name=Optional(''),
919 description=Optional(''), owner=Optional(None),
915 description=Optional(''), owner=Optional(None),
920 active=Optional(True)):
916 active=Optional(True)):
921 """
917 """
922 Updates given usergroup. This command can be executed only using api_key
918 Updates given usergroup. This command can be executed only using api_key
923 belonging to user with admin rights or an admin of given user group
919 belonging to user with admin rights or an admin of given user group
924
920
925 :param usergroupid: id of user group to update
921 :param usergroupid: id of user group to update
926 :type usergroupid: str or int
922 :type usergroupid: str or int
927 :param group_name: name of new user group
923 :param group_name: name of new user group
928 :type group_name: str
924 :type group_name: str
929 :param description: group description
925 :param description: group description
930 :type description: str
926 :type description: str
931 :param owner: owner of group.
927 :param owner: owner of group.
932 :type owner: Optional(str or int)
928 :type owner: Optional(str or int)
933 :param active: group is active
929 :param active: group is active
934 :type active: Optional(bool)
930 :type active: Optional(bool)
935
931
936 OUTPUT::
932 OUTPUT::
937
933
938 id : <id_given_in_input>
934 id : <id_given_in_input>
939 result : {
935 result : {
940 "msg": 'updated user group ID:<user group id> <user group name>',
936 "msg": 'updated user group ID:<user group id> <user group name>',
941 "user_group": <user_group_object>
937 "user_group": <user_group_object>
942 }
938 }
943 error : null
939 error : null
944
940
945 ERROR OUTPUT::
941 ERROR OUTPUT::
946
942
947 id : <id_given_in_input>
943 id : <id_given_in_input>
948 result : null
944 result : null
949 error : {
945 error : {
950 "failed to update user group `<user group name>`"
946 "failed to update user group `<user group name>`"
951 }
947 }
952
948
953 """
949 """
954 user_group = get_user_group_or_error(usergroupid)
950 user_group = get_user_group_or_error(usergroupid)
955 if not HasPermissionAny('hg.admin')():
951 if not HasPermissionAny('hg.admin')():
956 # check if we have admin permission for this user group !
952 # check if we have admin permission for this user group !
957 _perms = ('usergroup.admin',)
953 _perms = ('usergroup.admin',)
958 if not HasUserGroupPermissionAny(*_perms)(
954 if not HasUserGroupPermissionAny(*_perms)(
959 user_group_name=user_group.users_group_name):
955 user_group_name=user_group.users_group_name):
960 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
956 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
961
957
962 if not isinstance(owner, Optional):
958 if not isinstance(owner, Optional):
963 owner = get_user_or_error(owner)
959 owner = get_user_or_error(owner)
964
960
965 updates = {}
961 updates = {}
966 store_update(updates, group_name, 'users_group_name')
962 store_update(updates, group_name, 'users_group_name')
967 store_update(updates, description, 'user_group_description')
963 store_update(updates, description, 'user_group_description')
968 store_update(updates, owner, 'owner')
964 store_update(updates, owner, 'owner')
969 store_update(updates, active, 'users_group_active')
965 store_update(updates, active, 'users_group_active')
970 try:
966 try:
971 UserGroupModel().update(user_group, updates)
967 UserGroupModel().update(user_group, updates)
972 Session().commit()
968 Session().commit()
973 return dict(
969 return dict(
974 msg='updated user group ID:%s %s' % (user_group.users_group_id,
970 msg='updated user group ID:%s %s' % (user_group.users_group_id,
975 user_group.users_group_name),
971 user_group.users_group_name),
976 user_group=user_group.get_api_data()
972 user_group=user_group.get_api_data()
977 )
973 )
978 except Exception:
974 except Exception:
979 log.error(traceback.format_exc())
975 log.error(traceback.format_exc())
980 raise JSONRPCError('failed to update user group `%s`' % (usergroupid,))
976 raise JSONRPCError('failed to update user group `%s`' % (usergroupid,))
981
977
982 # permission check inside
978 # permission check inside
983 def delete_user_group(self, usergroupid):
979 def delete_user_group(self, usergroupid):
984 """
980 """
985 Delete given user group by user group id or name.
981 Delete given user group by user group id or name.
986 This command can be executed only using api_key
982 This command can be executed only using api_key
987 belonging to user with admin rights or an admin of given user group
983 belonging to user with admin rights or an admin of given user group
988
984
989 :param usergroupid:
985 :param usergroupid:
990 :type usergroupid: int
986 :type usergroupid: int
991
987
992 OUTPUT::
988 OUTPUT::
993
989
994 id : <id_given_in_input>
990 id : <id_given_in_input>
995 result : {
991 result : {
996 "msg": "deleted user group ID:<user_group_id> <user_group_name>"
992 "msg": "deleted user group ID:<user_group_id> <user_group_name>"
997 }
993 }
998 error : null
994 error : null
999
995
1000 ERROR OUTPUT::
996 ERROR OUTPUT::
1001
997
1002 id : <id_given_in_input>
998 id : <id_given_in_input>
1003 result : null
999 result : null
1004 error : {
1000 error : {
1005 "failed to delete user group ID:<user_group_id> <user_group_name>"
1001 "failed to delete user group ID:<user_group_id> <user_group_name>"
1006 or
1002 or
1007 "RepoGroup assigned to <repo_groups_list>"
1003 "RepoGroup assigned to <repo_groups_list>"
1008 }
1004 }
1009
1005
1010 """
1006 """
1011 user_group = get_user_group_or_error(usergroupid)
1007 user_group = get_user_group_or_error(usergroupid)
1012 if not HasPermissionAny('hg.admin')():
1008 if not HasPermissionAny('hg.admin')():
1013 # check if we have admin permission for this user group !
1009 # check if we have admin permission for this user group !
1014 _perms = ('usergroup.admin',)
1010 _perms = ('usergroup.admin',)
1015 if not HasUserGroupPermissionAny(*_perms)(
1011 if not HasUserGroupPermissionAny(*_perms)(
1016 user_group_name=user_group.users_group_name):
1012 user_group_name=user_group.users_group_name):
1017 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1013 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1018
1014
1019 try:
1015 try:
1020 UserGroupModel().delete(user_group)
1016 UserGroupModel().delete(user_group)
1021 Session().commit()
1017 Session().commit()
1022 return dict(
1018 return dict(
1023 msg='deleted user group ID:%s %s' %
1019 msg='deleted user group ID:%s %s' %
1024 (user_group.users_group_id, user_group.users_group_name),
1020 (user_group.users_group_id, user_group.users_group_name),
1025 user_group=None
1021 user_group=None
1026 )
1022 )
1027 except UserGroupsAssignedException as e:
1023 except UserGroupsAssignedException as e:
1028 log.error(traceback.format_exc())
1024 log.error(traceback.format_exc())
1029 raise JSONRPCError(str(e))
1025 raise JSONRPCError(str(e))
1030 except Exception:
1026 except Exception:
1031 log.error(traceback.format_exc())
1027 log.error(traceback.format_exc())
1032 raise JSONRPCError('failed to delete user group ID:%s %s' %
1028 raise JSONRPCError('failed to delete user group ID:%s %s' %
1033 (user_group.users_group_id,
1029 (user_group.users_group_id,
1034 user_group.users_group_name)
1030 user_group.users_group_name)
1035 )
1031 )
1036
1032
1037 # permission check inside
1033 # permission check inside
1038 def add_user_to_user_group(self, usergroupid, userid):
1034 def add_user_to_user_group(self, usergroupid, userid):
1039 """
1035 """
1040 Adds a user to a user group. If user exists in that group success will be
1036 Adds a user to a user group. If user exists in that group success will be
1041 `false`. This command can be executed only using api_key
1037 `false`. This command can be executed only using api_key
1042 belonging to user with admin rights or an admin of given user group
1038 belonging to user with admin rights or an admin of given user group
1043
1039
1044 :param usergroupid:
1040 :param usergroupid:
1045 :type usergroupid: int
1041 :type usergroupid: int
1046 :param userid:
1042 :param userid:
1047 :type userid: int
1043 :type userid: int
1048
1044
1049 OUTPUT::
1045 OUTPUT::
1050
1046
1051 id : <id_given_in_input>
1047 id : <id_given_in_input>
1052 result : {
1048 result : {
1053 "success": True|False # depends on if member is in group
1049 "success": True|False # depends on if member is in group
1054 "msg": "added member `<username>` to user group `<groupname>` |
1050 "msg": "added member `<username>` to user group `<groupname>` |
1055 User is already in that group"
1051 User is already in that group"
1056
1052
1057 }
1053 }
1058 error : null
1054 error : null
1059
1055
1060 ERROR OUTPUT::
1056 ERROR OUTPUT::
1061
1057
1062 id : <id_given_in_input>
1058 id : <id_given_in_input>
1063 result : null
1059 result : null
1064 error : {
1060 error : {
1065 "failed to add member to user group `<user_group_name>`"
1061 "failed to add member to user group `<user_group_name>`"
1066 }
1062 }
1067
1063
1068 """
1064 """
1069 user = get_user_or_error(userid)
1065 user = get_user_or_error(userid)
1070 user_group = get_user_group_or_error(usergroupid)
1066 user_group = get_user_group_or_error(usergroupid)
1071 if not HasPermissionAny('hg.admin')():
1067 if not HasPermissionAny('hg.admin')():
1072 # check if we have admin permission for this user group !
1068 # check if we have admin permission for this user group !
1073 _perms = ('usergroup.admin',)
1069 _perms = ('usergroup.admin',)
1074 if not HasUserGroupPermissionAny(*_perms)(
1070 if not HasUserGroupPermissionAny(*_perms)(
1075 user_group_name=user_group.users_group_name):
1071 user_group_name=user_group.users_group_name):
1076 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1072 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1077
1073
1078 try:
1074 try:
1079 ugm = UserGroupModel().add_user_to_group(user_group, user)
1075 ugm = UserGroupModel().add_user_to_group(user_group, user)
1080 success = True if ugm != True else False
1076 success = True if ugm != True else False
1081 msg = 'added member `%s` to user group `%s`' % (
1077 msg = 'added member `%s` to user group `%s`' % (
1082 user.username, user_group.users_group_name
1078 user.username, user_group.users_group_name
1083 )
1079 )
1084 msg = msg if success else 'User is already in that group'
1080 msg = msg if success else 'User is already in that group'
1085 Session().commit()
1081 Session().commit()
1086
1082
1087 return dict(
1083 return dict(
1088 success=success,
1084 success=success,
1089 msg=msg
1085 msg=msg
1090 )
1086 )
1091 except Exception:
1087 except Exception:
1092 log.error(traceback.format_exc())
1088 log.error(traceback.format_exc())
1093 raise JSONRPCError(
1089 raise JSONRPCError(
1094 'failed to add member to user group `%s`' % (
1090 'failed to add member to user group `%s`' % (
1095 user_group.users_group_name,
1091 user_group.users_group_name,
1096 )
1092 )
1097 )
1093 )
1098
1094
1099 # permission check inside
1095 # permission check inside
1100 def remove_user_from_user_group(self, usergroupid, userid):
1096 def remove_user_from_user_group(self, usergroupid, userid):
1101 """
1097 """
1102 Removes a user from a user group. If user is not in given group success will
1098 Removes a user from a user group. If user is not in given group success will
1103 be `false`. This command can be executed only
1099 be `false`. This command can be executed only
1104 using api_key belonging to user with admin rights or an admin of given user group
1100 using api_key belonging to user with admin rights or an admin of given user group
1105
1101
1106 :param usergroupid:
1102 :param usergroupid:
1107 :param userid:
1103 :param userid:
1108
1104
1109
1105
1110 OUTPUT::
1106 OUTPUT::
1111
1107
1112 id : <id_given_in_input>
1108 id : <id_given_in_input>
1113 result: {
1109 result: {
1114 "success": True|False, # depends on if member is in group
1110 "success": True|False, # depends on if member is in group
1115 "msg": "removed member <username> from user group <groupname> |
1111 "msg": "removed member <username> from user group <groupname> |
1116 User wasn't in group"
1112 User wasn't in group"
1117 }
1113 }
1118 error: null
1114 error: null
1119
1115
1120 """
1116 """
1121 user = get_user_or_error(userid)
1117 user = get_user_or_error(userid)
1122 user_group = get_user_group_or_error(usergroupid)
1118 user_group = get_user_group_or_error(usergroupid)
1123 if not HasPermissionAny('hg.admin')():
1119 if not HasPermissionAny('hg.admin')():
1124 # check if we have admin permission for this user group !
1120 # check if we have admin permission for this user group !
1125 _perms = ('usergroup.admin',)
1121 _perms = ('usergroup.admin',)
1126 if not HasUserGroupPermissionAny(*_perms)(
1122 if not HasUserGroupPermissionAny(*_perms)(
1127 user_group_name=user_group.users_group_name):
1123 user_group_name=user_group.users_group_name):
1128 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1124 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1129
1125
1130 try:
1126 try:
1131 success = UserGroupModel().remove_user_from_group(user_group, user)
1127 success = UserGroupModel().remove_user_from_group(user_group, user)
1132 msg = 'removed member `%s` from user group `%s`' % (
1128 msg = 'removed member `%s` from user group `%s`' % (
1133 user.username, user_group.users_group_name
1129 user.username, user_group.users_group_name
1134 )
1130 )
1135 msg = msg if success else "User wasn't in group"
1131 msg = msg if success else "User wasn't in group"
1136 Session().commit()
1132 Session().commit()
1137 return dict(success=success, msg=msg)
1133 return dict(success=success, msg=msg)
1138 except Exception:
1134 except Exception:
1139 log.error(traceback.format_exc())
1135 log.error(traceback.format_exc())
1140 raise JSONRPCError(
1136 raise JSONRPCError(
1141 'failed to remove member from user group `%s`' % (
1137 'failed to remove member from user group `%s`' % (
1142 user_group.users_group_name,
1138 user_group.users_group_name,
1143 )
1139 )
1144 )
1140 )
1145
1141
1146 # permission check inside
1142 # permission check inside
1147 def get_repo(self, repoid):
1143 def get_repo(self, repoid):
1148 """
1144 """
1149 Gets an existing repository by it's name or repository_id. Members will return
1145 Gets an existing repository by it's name or repository_id. Members will return
1150 either users_group or user associated to that repository. This command can be
1146 either users_group or user associated to that repository. This command can be
1151 executed only using api_key belonging to user with admin
1147 executed only using api_key belonging to user with admin
1152 rights or regular user that have at least read access to repository.
1148 rights or regular user that have at least read access to repository.
1153
1149
1154 :param repoid: repository name or repository id
1150 :param repoid: repository name or repository id
1155 :type repoid: str or int
1151 :type repoid: str or int
1156
1152
1157 OUTPUT::
1153 OUTPUT::
1158
1154
1159 id : <id_given_in_input>
1155 id : <id_given_in_input>
1160 result : {
1156 result : {
1161 {
1157 {
1162 "repo_id" : "<repo_id>",
1158 "repo_id" : "<repo_id>",
1163 "repo_name" : "<reponame>"
1159 "repo_name" : "<reponame>"
1164 "repo_type" : "<repo_type>",
1160 "repo_type" : "<repo_type>",
1165 "clone_uri" : "<clone_uri>",
1161 "clone_uri" : "<clone_uri>",
1166 "enable_downloads": "<bool>",
1162 "enable_downloads": "<bool>",
1167 "enable_locking": "<bool>",
1163 "enable_locking": "<bool>",
1168 "enable_statistics": "<bool>",
1164 "enable_statistics": "<bool>",
1169 "private": "<bool>",
1165 "private": "<bool>",
1170 "created_on" : "<date_time_created>",
1166 "created_on" : "<date_time_created>",
1171 "description" : "<description>",
1167 "description" : "<description>",
1172 "landing_rev": "<landing_rev>",
1168 "landing_rev": "<landing_rev>",
1173 "last_changeset": {
1169 "last_changeset": {
1174 "author": "<full_author>",
1170 "author": "<full_author>",
1175 "date": "<date_time_of_commit>",
1171 "date": "<date_time_of_commit>",
1176 "message": "<commit_message>",
1172 "message": "<commit_message>",
1177 "raw_id": "<raw_id>",
1173 "raw_id": "<raw_id>",
1178 "revision": "<numeric_revision>",
1174 "revision": "<numeric_revision>",
1179 "short_id": "<short_id>"
1175 "short_id": "<short_id>"
1180 }
1176 }
1181 "owner": "<repo_owner>",
1177 "owner": "<repo_owner>",
1182 "fork_of": "<name_of_fork_parent>",
1178 "fork_of": "<name_of_fork_parent>",
1183 "members" : [
1179 "members" : [
1184 {
1180 {
1185 "name": "<username>",
1181 "name": "<username>",
1186 "type" : "user",
1182 "type" : "user",
1187 "permission" : "repository.(read|write|admin)"
1183 "permission" : "repository.(read|write|admin)"
1188 },
1184 },
1189
1185
1190 {
1186 {
1191 "name": "<usergroup name>",
1187 "name": "<usergroup name>",
1192 "type" : "user_group",
1188 "type" : "user_group",
1193 "permission" : "usergroup.(read|write|admin)"
1189 "permission" : "usergroup.(read|write|admin)"
1194 },
1190 },
1195
1191
1196 ]
1192 ]
1197 "followers": [<user_obj>, ...]
1193 "followers": [<user_obj>, ...]
1198 ]
1194 ]
1199 }
1195 }
1200 }
1196 }
1201 error : null
1197 error : null
1202
1198
1203 """
1199 """
1204 repo = get_repo_or_error(repoid)
1200 repo = get_repo_or_error(repoid)
1205
1201
1206 if not HasPermissionAny('hg.admin')():
1202 if not HasPermissionAny('hg.admin')():
1207 # check if we have admin permission for this repo !
1203 if not HasRepoPermissionLevel('read')(repo.repo_name):
1208 perms = ('repository.admin', 'repository.write', 'repository.read')
1209 if not HasRepoPermissionAny(*perms)(repo_name=repo.repo_name):
1210 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1204 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1211
1205
1212 members = []
1206 members = []
1213 followers = []
1207 followers = []
1214 for user in repo.repo_to_perm:
1208 for user in repo.repo_to_perm:
1215 perm = user.permission.permission_name
1209 perm = user.permission.permission_name
1216 user = user.user
1210 user = user.user
1217 user_data = {
1211 user_data = {
1218 'name': user.username,
1212 'name': user.username,
1219 'type': "user",
1213 'type': "user",
1220 'permission': perm
1214 'permission': perm
1221 }
1215 }
1222 members.append(user_data)
1216 members.append(user_data)
1223
1217
1224 for user_group in repo.users_group_to_perm:
1218 for user_group in repo.users_group_to_perm:
1225 perm = user_group.permission.permission_name
1219 perm = user_group.permission.permission_name
1226 user_group = user_group.users_group
1220 user_group = user_group.users_group
1227 user_group_data = {
1221 user_group_data = {
1228 'name': user_group.users_group_name,
1222 'name': user_group.users_group_name,
1229 'type': "user_group",
1223 'type': "user_group",
1230 'permission': perm
1224 'permission': perm
1231 }
1225 }
1232 members.append(user_group_data)
1226 members.append(user_group_data)
1233
1227
1234 for user in repo.followers:
1228 for user in repo.followers:
1235 followers.append(user.user.get_api_data())
1229 followers.append(user.user.get_api_data())
1236
1230
1237 data = repo.get_api_data()
1231 data = repo.get_api_data()
1238 data['members'] = members
1232 data['members'] = members
1239 data['followers'] = followers
1233 data['followers'] = followers
1240 return data
1234 return data
1241
1235
1242 # permission check inside
1236 # permission check inside
1243 def get_repos(self):
1237 def get_repos(self):
1244 """
1238 """
1245 Lists all existing repositories. This command can be executed only using
1239 Lists all existing repositories. This command can be executed only using
1246 api_key belonging to user with admin rights or regular user that have
1240 api_key belonging to user with admin rights or regular user that have
1247 admin, write or read access to repository.
1241 admin, write or read access to repository.
1248
1242
1249
1243
1250 OUTPUT::
1244 OUTPUT::
1251
1245
1252 id : <id_given_in_input>
1246 id : <id_given_in_input>
1253 result: [
1247 result: [
1254 {
1248 {
1255 "repo_id" : "<repo_id>",
1249 "repo_id" : "<repo_id>",
1256 "repo_name" : "<reponame>"
1250 "repo_name" : "<reponame>"
1257 "repo_type" : "<repo_type>",
1251 "repo_type" : "<repo_type>",
1258 "clone_uri" : "<clone_uri>",
1252 "clone_uri" : "<clone_uri>",
1259 "private": : "<bool>",
1253 "private": : "<bool>",
1260 "created_on" : "<datetimecreated>",
1254 "created_on" : "<datetimecreated>",
1261 "description" : "<description>",
1255 "description" : "<description>",
1262 "landing_rev": "<landing_rev>",
1256 "landing_rev": "<landing_rev>",
1263 "owner": "<repo_owner>",
1257 "owner": "<repo_owner>",
1264 "fork_of": "<name_of_fork_parent>",
1258 "fork_of": "<name_of_fork_parent>",
1265 "enable_downloads": "<bool>",
1259 "enable_downloads": "<bool>",
1266 "enable_locking": "<bool>",
1260 "enable_locking": "<bool>",
1267 "enable_statistics": "<bool>",
1261 "enable_statistics": "<bool>",
1268 },
1262 },
1269
1263
1270 ]
1264 ]
1271 error: null
1265 error: null
1272 """
1266 """
1273 result = []
1267 result = []
1274 if not HasPermissionAny('hg.admin')():
1268 if not HasPermissionAny('hg.admin')():
1275 repos = RepoModel().get_all_user_repos(user=request.authuser.user_id)
1269 repos = RepoModel().get_all_user_repos(user=request.authuser.user_id)
1276 else:
1270 else:
1277 repos = Repository.query()
1271 repos = Repository.query()
1278
1272
1279 for repo in repos:
1273 for repo in repos:
1280 result.append(repo.get_api_data())
1274 result.append(repo.get_api_data())
1281 return result
1275 return result
1282
1276
1283 # permission check inside
1277 # permission check inside
1284 def get_repo_nodes(self, repoid, revision, root_path,
1278 def get_repo_nodes(self, repoid, revision, root_path,
1285 ret_type=Optional('all')):
1279 ret_type=Optional('all')):
1286 """
1280 """
1287 returns a list of nodes and it's children in a flat list for a given path
1281 returns a list of nodes and it's children in a flat list for a given path
1288 at given revision. It's possible to specify ret_type to show only `files` or
1282 at given revision. It's possible to specify ret_type to show only `files` or
1289 `dirs`. This command can be executed only using api_key belonging to
1283 `dirs`. This command can be executed only using api_key belonging to
1290 user with admin rights or regular user that have at least read access to repository.
1284 user with admin rights or regular user that have at least read access to repository.
1291
1285
1292 :param repoid: repository name or repository id
1286 :param repoid: repository name or repository id
1293 :type repoid: str or int
1287 :type repoid: str or int
1294 :param revision: revision for which listing should be done
1288 :param revision: revision for which listing should be done
1295 :type revision: str
1289 :type revision: str
1296 :param root_path: path from which start displaying
1290 :param root_path: path from which start displaying
1297 :type root_path: str
1291 :type root_path: str
1298 :param ret_type: return type 'all|files|dirs' nodes
1292 :param ret_type: return type 'all|files|dirs' nodes
1299 :type ret_type: Optional(str)
1293 :type ret_type: Optional(str)
1300
1294
1301
1295
1302 OUTPUT::
1296 OUTPUT::
1303
1297
1304 id : <id_given_in_input>
1298 id : <id_given_in_input>
1305 result: [
1299 result: [
1306 {
1300 {
1307 "name" : "<name>"
1301 "name" : "<name>"
1308 "type" : "<type>",
1302 "type" : "<type>",
1309 },
1303 },
1310
1304
1311 ]
1305 ]
1312 error: null
1306 error: null
1313 """
1307 """
1314 repo = get_repo_or_error(repoid)
1308 repo = get_repo_or_error(repoid)
1315
1309
1316 if not HasPermissionAny('hg.admin')():
1310 if not HasPermissionAny('hg.admin')():
1317 # check if we have admin permission for this repo !
1311 if not HasRepoPermissionLevel('read')(repo.repo_name):
1318 perms = ('repository.admin', 'repository.write', 'repository.read')
1319 if not HasRepoPermissionAny(*perms)(repo_name=repo.repo_name):
1320 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1312 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1321
1313
1322 ret_type = Optional.extract(ret_type)
1314 ret_type = Optional.extract(ret_type)
1323 _map = {}
1315 _map = {}
1324 try:
1316 try:
1325 _d, _f = ScmModel().get_nodes(repo, revision, root_path,
1317 _d, _f = ScmModel().get_nodes(repo, revision, root_path,
1326 flat=False)
1318 flat=False)
1327 _map = {
1319 _map = {
1328 'all': _d + _f,
1320 'all': _d + _f,
1329 'files': _f,
1321 'files': _f,
1330 'dirs': _d,
1322 'dirs': _d,
1331 }
1323 }
1332 return _map[ret_type]
1324 return _map[ret_type]
1333 except KeyError:
1325 except KeyError:
1334 raise JSONRPCError('ret_type must be one of %s'
1326 raise JSONRPCError('ret_type must be one of %s'
1335 % (','.join(_map.keys())))
1327 % (','.join(_map.keys())))
1336 except Exception:
1328 except Exception:
1337 log.error(traceback.format_exc())
1329 log.error(traceback.format_exc())
1338 raise JSONRPCError(
1330 raise JSONRPCError(
1339 'failed to get repo: `%s` nodes' % repo.repo_name
1331 'failed to get repo: `%s` nodes' % repo.repo_name
1340 )
1332 )
1341
1333
1342 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
1334 @HasPermissionAnyDecorator('hg.admin', 'hg.create.repository')
1343 def create_repo(self, repo_name, owner=Optional(OAttr('apiuser')),
1335 def create_repo(self, repo_name, owner=Optional(OAttr('apiuser')),
1344 repo_type=Optional('hg'), description=Optional(''),
1336 repo_type=Optional('hg'), description=Optional(''),
1345 private=Optional(False), clone_uri=Optional(None),
1337 private=Optional(False), clone_uri=Optional(None),
1346 landing_rev=Optional('rev:tip'),
1338 landing_rev=Optional('rev:tip'),
1347 enable_statistics=Optional(False),
1339 enable_statistics=Optional(False),
1348 enable_locking=Optional(False),
1340 enable_locking=Optional(False),
1349 enable_downloads=Optional(False),
1341 enable_downloads=Optional(False),
1350 copy_permissions=Optional(False)):
1342 copy_permissions=Optional(False)):
1351 """
1343 """
1352 Creates a repository. If repository name contains "/", all needed repository
1344 Creates a repository. If repository name contains "/", all needed repository
1353 groups will be created. For example "foo/bar/baz" will create groups
1345 groups will be created. For example "foo/bar/baz" will create groups
1354 "foo", "bar" (with "foo" as parent), and create "baz" repository with
1346 "foo", "bar" (with "foo" as parent), and create "baz" repository with
1355 "bar" as group. This command can be executed only using api_key
1347 "bar" as group. This command can be executed only using api_key
1356 belonging to user with admin rights or regular user that have create
1348 belonging to user with admin rights or regular user that have create
1357 repository permission. Regular users cannot specify owner parameter
1349 repository permission. Regular users cannot specify owner parameter
1358
1350
1359 :param repo_name: repository name
1351 :param repo_name: repository name
1360 :type repo_name: str
1352 :type repo_name: str
1361 :param owner: user_id or username
1353 :param owner: user_id or username
1362 :type owner: Optional(str)
1354 :type owner: Optional(str)
1363 :param repo_type: 'hg' or 'git'
1355 :param repo_type: 'hg' or 'git'
1364 :type repo_type: Optional(str)
1356 :type repo_type: Optional(str)
1365 :param description: repository description
1357 :param description: repository description
1366 :type description: Optional(str)
1358 :type description: Optional(str)
1367 :param private:
1359 :param private:
1368 :type private: bool
1360 :type private: bool
1369 :param clone_uri:
1361 :param clone_uri:
1370 :type clone_uri: str
1362 :type clone_uri: str
1371 :param landing_rev: <rev_type>:<rev>
1363 :param landing_rev: <rev_type>:<rev>
1372 :type landing_rev: str
1364 :type landing_rev: str
1373 :param enable_locking:
1365 :param enable_locking:
1374 :type enable_locking: bool
1366 :type enable_locking: bool
1375 :param enable_downloads:
1367 :param enable_downloads:
1376 :type enable_downloads: bool
1368 :type enable_downloads: bool
1377 :param enable_statistics:
1369 :param enable_statistics:
1378 :type enable_statistics: bool
1370 :type enable_statistics: bool
1379 :param copy_permissions: Copy permission from group that repository is
1371 :param copy_permissions: Copy permission from group that repository is
1380 being created.
1372 being created.
1381 :type copy_permissions: bool
1373 :type copy_permissions: bool
1382
1374
1383 OUTPUT::
1375 OUTPUT::
1384
1376
1385 id : <id_given_in_input>
1377 id : <id_given_in_input>
1386 result: {
1378 result: {
1387 "msg": "Created new repository `<reponame>`",
1379 "msg": "Created new repository `<reponame>`",
1388 "success": true,
1380 "success": true,
1389 "task": "<celery task id or None if done sync>"
1381 "task": "<celery task id or None if done sync>"
1390 }
1382 }
1391 error: null
1383 error: null
1392
1384
1393 ERROR OUTPUT::
1385 ERROR OUTPUT::
1394
1386
1395 id : <id_given_in_input>
1387 id : <id_given_in_input>
1396 result : null
1388 result : null
1397 error : {
1389 error : {
1398 'failed to create repository `<repo_name>`
1390 'failed to create repository `<repo_name>`
1399 }
1391 }
1400
1392
1401 """
1393 """
1402 if not HasPermissionAny('hg.admin')():
1394 if not HasPermissionAny('hg.admin')():
1403 if not isinstance(owner, Optional):
1395 if not isinstance(owner, Optional):
1404 # forbid setting owner for non-admins
1396 # forbid setting owner for non-admins
1405 raise JSONRPCError(
1397 raise JSONRPCError(
1406 'Only Kallithea admin can specify `owner` param'
1398 'Only Kallithea admin can specify `owner` param'
1407 )
1399 )
1408 if isinstance(owner, Optional):
1400 if isinstance(owner, Optional):
1409 owner = request.authuser.user_id
1401 owner = request.authuser.user_id
1410
1402
1411 owner = get_user_or_error(owner)
1403 owner = get_user_or_error(owner)
1412
1404
1413 if RepoModel().get_by_repo_name(repo_name):
1405 if RepoModel().get_by_repo_name(repo_name):
1414 raise JSONRPCError("repo `%s` already exist" % repo_name)
1406 raise JSONRPCError("repo `%s` already exist" % repo_name)
1415
1407
1416 defs = Setting.get_default_repo_settings(strip_prefix=True)
1408 defs = Setting.get_default_repo_settings(strip_prefix=True)
1417 if isinstance(private, Optional):
1409 if isinstance(private, Optional):
1418 private = defs.get('repo_private') or Optional.extract(private)
1410 private = defs.get('repo_private') or Optional.extract(private)
1419 if isinstance(repo_type, Optional):
1411 if isinstance(repo_type, Optional):
1420 repo_type = defs.get('repo_type')
1412 repo_type = defs.get('repo_type')
1421 if isinstance(enable_statistics, Optional):
1413 if isinstance(enable_statistics, Optional):
1422 enable_statistics = defs.get('repo_enable_statistics')
1414 enable_statistics = defs.get('repo_enable_statistics')
1423 if isinstance(enable_locking, Optional):
1415 if isinstance(enable_locking, Optional):
1424 enable_locking = defs.get('repo_enable_locking')
1416 enable_locking = defs.get('repo_enable_locking')
1425 if isinstance(enable_downloads, Optional):
1417 if isinstance(enable_downloads, Optional):
1426 enable_downloads = defs.get('repo_enable_downloads')
1418 enable_downloads = defs.get('repo_enable_downloads')
1427
1419
1428 clone_uri = Optional.extract(clone_uri)
1420 clone_uri = Optional.extract(clone_uri)
1429 description = Optional.extract(description)
1421 description = Optional.extract(description)
1430 landing_rev = Optional.extract(landing_rev)
1422 landing_rev = Optional.extract(landing_rev)
1431 copy_permissions = Optional.extract(copy_permissions)
1423 copy_permissions = Optional.extract(copy_permissions)
1432
1424
1433 try:
1425 try:
1434 repo_name_cleaned = repo_name.split('/')[-1]
1426 repo_name_cleaned = repo_name.split('/')[-1]
1435 # create structure of groups and return the last group
1427 # create structure of groups and return the last group
1436 repo_group = map_groups(repo_name)
1428 repo_group = map_groups(repo_name)
1437 data = dict(
1429 data = dict(
1438 repo_name=repo_name_cleaned,
1430 repo_name=repo_name_cleaned,
1439 repo_name_full=repo_name,
1431 repo_name_full=repo_name,
1440 repo_type=repo_type,
1432 repo_type=repo_type,
1441 repo_description=description,
1433 repo_description=description,
1442 owner=owner,
1434 owner=owner,
1443 repo_private=private,
1435 repo_private=private,
1444 clone_uri=clone_uri,
1436 clone_uri=clone_uri,
1445 repo_group=repo_group,
1437 repo_group=repo_group,
1446 repo_landing_rev=landing_rev,
1438 repo_landing_rev=landing_rev,
1447 enable_statistics=enable_statistics,
1439 enable_statistics=enable_statistics,
1448 enable_locking=enable_locking,
1440 enable_locking=enable_locking,
1449 enable_downloads=enable_downloads,
1441 enable_downloads=enable_downloads,
1450 repo_copy_permissions=copy_permissions,
1442 repo_copy_permissions=copy_permissions,
1451 )
1443 )
1452
1444
1453 task = RepoModel().create(form_data=data, cur_user=owner)
1445 task = RepoModel().create(form_data=data, cur_user=owner)
1454 task_id = task.task_id
1446 task_id = task.task_id
1455 # no commit, it's done in RepoModel, or async via celery
1447 # no commit, it's done in RepoModel, or async via celery
1456 return dict(
1448 return dict(
1457 msg="Created new repository `%s`" % (repo_name,),
1449 msg="Created new repository `%s`" % (repo_name,),
1458 success=True, # cannot return the repo data here since fork
1450 success=True, # cannot return the repo data here since fork
1459 # can be done async
1451 # can be done async
1460 task=task_id
1452 task=task_id
1461 )
1453 )
1462 except Exception:
1454 except Exception:
1463 log.error(traceback.format_exc())
1455 log.error(traceback.format_exc())
1464 raise JSONRPCError(
1456 raise JSONRPCError(
1465 'failed to create repository `%s`' % (repo_name,))
1457 'failed to create repository `%s`' % (repo_name,))
1466
1458
1467 # permission check inside
1459 # permission check inside
1468 def update_repo(self, repoid, name=Optional(None),
1460 def update_repo(self, repoid, name=Optional(None),
1469 owner=Optional(OAttr('apiuser')),
1461 owner=Optional(OAttr('apiuser')),
1470 group=Optional(None),
1462 group=Optional(None),
1471 description=Optional(''), private=Optional(False),
1463 description=Optional(''), private=Optional(False),
1472 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
1464 clone_uri=Optional(None), landing_rev=Optional('rev:tip'),
1473 enable_statistics=Optional(False),
1465 enable_statistics=Optional(False),
1474 enable_locking=Optional(False),
1466 enable_locking=Optional(False),
1475 enable_downloads=Optional(False)):
1467 enable_downloads=Optional(False)):
1476
1468
1477 """
1469 """
1478 Updates repo
1470 Updates repo
1479
1471
1480 :param repoid: repository name or repository id
1472 :param repoid: repository name or repository id
1481 :type repoid: str or int
1473 :type repoid: str or int
1482 :param name:
1474 :param name:
1483 :param owner:
1475 :param owner:
1484 :param group:
1476 :param group:
1485 :param description:
1477 :param description:
1486 :param private:
1478 :param private:
1487 :param clone_uri:
1479 :param clone_uri:
1488 :param landing_rev:
1480 :param landing_rev:
1489 :param enable_statistics:
1481 :param enable_statistics:
1490 :param enable_locking:
1482 :param enable_locking:
1491 :param enable_downloads:
1483 :param enable_downloads:
1492 """
1484 """
1493 repo = get_repo_or_error(repoid)
1485 repo = get_repo_or_error(repoid)
1494 if not HasPermissionAny('hg.admin')():
1486 if not HasPermissionAny('hg.admin')():
1495 # check if we have admin permission for this repo !
1487 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1496 if not HasRepoPermissionAny('repository.admin')(repo_name=repo.repo_name):
1497 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1488 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1498
1489
1499 if (name != repo.repo_name and
1490 if (name != repo.repo_name and
1500 not HasPermissionAny('hg.create.repository')()
1491 not HasPermissionAny('hg.create.repository')()
1501 ):
1492 ):
1502 raise JSONRPCError('no permission to create (or move) repositories')
1493 raise JSONRPCError('no permission to create (or move) repositories')
1503
1494
1504 if not isinstance(owner, Optional):
1495 if not isinstance(owner, Optional):
1505 # forbid setting owner for non-admins
1496 # forbid setting owner for non-admins
1506 raise JSONRPCError(
1497 raise JSONRPCError(
1507 'Only Kallithea admin can specify `owner` param'
1498 'Only Kallithea admin can specify `owner` param'
1508 )
1499 )
1509
1500
1510 updates = {}
1501 updates = {}
1511 repo_group = group
1502 repo_group = group
1512 if not isinstance(repo_group, Optional):
1503 if not isinstance(repo_group, Optional):
1513 repo_group = get_repo_group_or_error(repo_group)
1504 repo_group = get_repo_group_or_error(repo_group)
1514 repo_group = repo_group.group_id
1505 repo_group = repo_group.group_id
1515 try:
1506 try:
1516 store_update(updates, name, 'repo_name')
1507 store_update(updates, name, 'repo_name')
1517 store_update(updates, repo_group, 'repo_group')
1508 store_update(updates, repo_group, 'repo_group')
1518 store_update(updates, owner, 'owner')
1509 store_update(updates, owner, 'owner')
1519 store_update(updates, description, 'repo_description')
1510 store_update(updates, description, 'repo_description')
1520 store_update(updates, private, 'repo_private')
1511 store_update(updates, private, 'repo_private')
1521 store_update(updates, clone_uri, 'clone_uri')
1512 store_update(updates, clone_uri, 'clone_uri')
1522 store_update(updates, landing_rev, 'repo_landing_rev')
1513 store_update(updates, landing_rev, 'repo_landing_rev')
1523 store_update(updates, enable_statistics, 'repo_enable_statistics')
1514 store_update(updates, enable_statistics, 'repo_enable_statistics')
1524 store_update(updates, enable_locking, 'repo_enable_locking')
1515 store_update(updates, enable_locking, 'repo_enable_locking')
1525 store_update(updates, enable_downloads, 'repo_enable_downloads')
1516 store_update(updates, enable_downloads, 'repo_enable_downloads')
1526
1517
1527 RepoModel().update(repo, **updates)
1518 RepoModel().update(repo, **updates)
1528 Session().commit()
1519 Session().commit()
1529 return dict(
1520 return dict(
1530 msg='updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1521 msg='updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
1531 repository=repo.get_api_data()
1522 repository=repo.get_api_data()
1532 )
1523 )
1533 except Exception:
1524 except Exception:
1534 log.error(traceback.format_exc())
1525 log.error(traceback.format_exc())
1535 raise JSONRPCError('failed to update repo `%s`' % repoid)
1526 raise JSONRPCError('failed to update repo `%s`' % repoid)
1536
1527
1537 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
1528 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
1538 def fork_repo(self, repoid, fork_name,
1529 def fork_repo(self, repoid, fork_name,
1539 owner=Optional(OAttr('apiuser')),
1530 owner=Optional(OAttr('apiuser')),
1540 description=Optional(''), copy_permissions=Optional(False),
1531 description=Optional(''), copy_permissions=Optional(False),
1541 private=Optional(False), landing_rev=Optional('rev:tip')):
1532 private=Optional(False), landing_rev=Optional('rev:tip')):
1542 """
1533 """
1543 Creates a fork of given repo. In case of using celery this will
1534 Creates a fork of given repo. In case of using celery this will
1544 immediately return success message, while fork is going to be created
1535 immediately return success message, while fork is going to be created
1545 asynchronous. This command can be executed only using api_key belonging to
1536 asynchronous. This command can be executed only using api_key belonging to
1546 user with admin rights or regular user that have fork permission, and at least
1537 user with admin rights or regular user that have fork permission, and at least
1547 read access to forking repository. Regular users cannot specify owner parameter.
1538 read access to forking repository. Regular users cannot specify owner parameter.
1548
1539
1549 :param repoid: repository name or repository id
1540 :param repoid: repository name or repository id
1550 :type repoid: str or int
1541 :type repoid: str or int
1551 :param fork_name:
1542 :param fork_name:
1552 :param owner:
1543 :param owner:
1553 :param description:
1544 :param description:
1554 :param copy_permissions:
1545 :param copy_permissions:
1555 :param private:
1546 :param private:
1556 :param landing_rev:
1547 :param landing_rev:
1557
1548
1558 INPUT::
1549 INPUT::
1559
1550
1560 id : <id_for_response>
1551 id : <id_for_response>
1561 api_key : "<api_key>"
1552 api_key : "<api_key>"
1562 args: {
1553 args: {
1563 "repoid" : "<reponame or repo_id>",
1554 "repoid" : "<reponame or repo_id>",
1564 "fork_name": "<forkname>",
1555 "fork_name": "<forkname>",
1565 "owner": "<username or user_id = Optional(=apiuser)>",
1556 "owner": "<username or user_id = Optional(=apiuser)>",
1566 "description": "<description>",
1557 "description": "<description>",
1567 "copy_permissions": "<bool>",
1558 "copy_permissions": "<bool>",
1568 "private": "<bool>",
1559 "private": "<bool>",
1569 "landing_rev": "<landing_rev>"
1560 "landing_rev": "<landing_rev>"
1570 }
1561 }
1571
1562
1572 OUTPUT::
1563 OUTPUT::
1573
1564
1574 id : <id_given_in_input>
1565 id : <id_given_in_input>
1575 result: {
1566 result: {
1576 "msg": "Created fork of `<reponame>` as `<forkname>`",
1567 "msg": "Created fork of `<reponame>` as `<forkname>`",
1577 "success": true,
1568 "success": true,
1578 "task": "<celery task id or None if done sync>"
1569 "task": "<celery task id or None if done sync>"
1579 }
1570 }
1580 error: null
1571 error: null
1581
1572
1582 """
1573 """
1583 repo = get_repo_or_error(repoid)
1574 repo = get_repo_or_error(repoid)
1584 repo_name = repo.repo_name
1575 repo_name = repo.repo_name
1585
1576
1586 _repo = RepoModel().get_by_repo_name(fork_name)
1577 _repo = RepoModel().get_by_repo_name(fork_name)
1587 if _repo:
1578 if _repo:
1588 type_ = 'fork' if _repo.fork else 'repo'
1579 type_ = 'fork' if _repo.fork else 'repo'
1589 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1580 raise JSONRPCError("%s `%s` already exist" % (type_, fork_name))
1590
1581
1591 if HasPermissionAny('hg.admin')():
1582 if HasPermissionAny('hg.admin')():
1592 pass
1583 pass
1593 elif HasRepoPermissionAny('repository.admin',
1584 elif HasRepoPermissionLevel('read')(repo.repo_name):
1594 'repository.write',
1595 'repository.read')(repo_name=repo.repo_name):
1596 if not isinstance(owner, Optional):
1585 if not isinstance(owner, Optional):
1597 # forbid setting owner for non-admins
1586 # forbid setting owner for non-admins
1598 raise JSONRPCError(
1587 raise JSONRPCError(
1599 'Only Kallithea admin can specify `owner` param'
1588 'Only Kallithea admin can specify `owner` param'
1600 )
1589 )
1601
1590
1602 if not HasPermissionAny('hg.create.repository')():
1591 if not HasPermissionAny('hg.create.repository')():
1603 raise JSONRPCError('no permission to create repositories')
1592 raise JSONRPCError('no permission to create repositories')
1604 else:
1593 else:
1605 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1594 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1606
1595
1607 if isinstance(owner, Optional):
1596 if isinstance(owner, Optional):
1608 owner = request.authuser.user_id
1597 owner = request.authuser.user_id
1609
1598
1610 owner = get_user_or_error(owner)
1599 owner = get_user_or_error(owner)
1611
1600
1612 try:
1601 try:
1613 # create structure of groups and return the last group
1602 # create structure of groups and return the last group
1614 group = map_groups(fork_name)
1603 group = map_groups(fork_name)
1615 fork_base_name = fork_name.rsplit('/', 1)[-1]
1604 fork_base_name = fork_name.rsplit('/', 1)[-1]
1616
1605
1617 form_data = dict(
1606 form_data = dict(
1618 repo_name=fork_base_name,
1607 repo_name=fork_base_name,
1619 repo_name_full=fork_name,
1608 repo_name_full=fork_name,
1620 repo_group=group,
1609 repo_group=group,
1621 repo_type=repo.repo_type,
1610 repo_type=repo.repo_type,
1622 description=Optional.extract(description),
1611 description=Optional.extract(description),
1623 private=Optional.extract(private),
1612 private=Optional.extract(private),
1624 copy_permissions=Optional.extract(copy_permissions),
1613 copy_permissions=Optional.extract(copy_permissions),
1625 landing_rev=Optional.extract(landing_rev),
1614 landing_rev=Optional.extract(landing_rev),
1626 update_after_clone=False,
1615 update_after_clone=False,
1627 fork_parent_id=repo.repo_id,
1616 fork_parent_id=repo.repo_id,
1628 )
1617 )
1629 task = RepoModel().create_fork(form_data, cur_user=owner)
1618 task = RepoModel().create_fork(form_data, cur_user=owner)
1630 # no commit, it's done in RepoModel, or async via celery
1619 # no commit, it's done in RepoModel, or async via celery
1631 task_id = task.task_id
1620 task_id = task.task_id
1632 return dict(
1621 return dict(
1633 msg='Created fork of `%s` as `%s`' % (repo.repo_name,
1622 msg='Created fork of `%s` as `%s`' % (repo.repo_name,
1634 fork_name),
1623 fork_name),
1635 success=True, # cannot return the repo data here since fork
1624 success=True, # cannot return the repo data here since fork
1636 # can be done async
1625 # can be done async
1637 task=task_id
1626 task=task_id
1638 )
1627 )
1639 except Exception:
1628 except Exception:
1640 log.error(traceback.format_exc())
1629 log.error(traceback.format_exc())
1641 raise JSONRPCError(
1630 raise JSONRPCError(
1642 'failed to fork repository `%s` as `%s`' % (repo_name,
1631 'failed to fork repository `%s` as `%s`' % (repo_name,
1643 fork_name)
1632 fork_name)
1644 )
1633 )
1645
1634
1646 # permission check inside
1635 # permission check inside
1647 def delete_repo(self, repoid, forks=Optional('')):
1636 def delete_repo(self, repoid, forks=Optional('')):
1648 """
1637 """
1649 Deletes a repository. This command can be executed only using api_key belonging
1638 Deletes a repository. This command can be executed only using api_key belonging
1650 to user with admin rights or regular user that have admin access to repository.
1639 to user with admin rights or regular user that have admin access to repository.
1651 When `forks` param is set it's possible to detach or delete forks of deleting
1640 When `forks` param is set it's possible to detach or delete forks of deleting
1652 repository
1641 repository
1653
1642
1654 :param repoid: repository name or repository id
1643 :param repoid: repository name or repository id
1655 :type repoid: str or int
1644 :type repoid: str or int
1656 :param forks: `detach` or `delete`, what do do with attached forks for repo
1645 :param forks: `detach` or `delete`, what do do with attached forks for repo
1657 :type forks: Optional(str)
1646 :type forks: Optional(str)
1658
1647
1659 OUTPUT::
1648 OUTPUT::
1660
1649
1661 id : <id_given_in_input>
1650 id : <id_given_in_input>
1662 result: {
1651 result: {
1663 "msg": "Deleted repository `<reponame>`",
1652 "msg": "Deleted repository `<reponame>`",
1664 "success": true
1653 "success": true
1665 }
1654 }
1666 error: null
1655 error: null
1667
1656
1668 """
1657 """
1669 repo = get_repo_or_error(repoid)
1658 repo = get_repo_or_error(repoid)
1670
1659
1671 if not HasPermissionAny('hg.admin')():
1660 if not HasPermissionAny('hg.admin')():
1672 # check if we have admin permission for this repo !
1661 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1673 if not HasRepoPermissionAny('repository.admin')(repo_name=repo.repo_name):
1674 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1662 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1675
1663
1676 try:
1664 try:
1677 handle_forks = Optional.extract(forks)
1665 handle_forks = Optional.extract(forks)
1678 _forks_msg = ''
1666 _forks_msg = ''
1679 _forks = [f for f in repo.forks]
1667 _forks = [f for f in repo.forks]
1680 if handle_forks == 'detach':
1668 if handle_forks == 'detach':
1681 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1669 _forks_msg = ' ' + 'Detached %s forks' % len(_forks)
1682 elif handle_forks == 'delete':
1670 elif handle_forks == 'delete':
1683 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1671 _forks_msg = ' ' + 'Deleted %s forks' % len(_forks)
1684 elif _forks:
1672 elif _forks:
1685 raise JSONRPCError(
1673 raise JSONRPCError(
1686 'Cannot delete `%s` it still contains attached forks' %
1674 'Cannot delete `%s` it still contains attached forks' %
1687 (repo.repo_name,)
1675 (repo.repo_name,)
1688 )
1676 )
1689
1677
1690 RepoModel().delete(repo, forks=forks)
1678 RepoModel().delete(repo, forks=forks)
1691 Session().commit()
1679 Session().commit()
1692 return dict(
1680 return dict(
1693 msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg),
1681 msg='Deleted repository `%s`%s' % (repo.repo_name, _forks_msg),
1694 success=True
1682 success=True
1695 )
1683 )
1696 except Exception:
1684 except Exception:
1697 log.error(traceback.format_exc())
1685 log.error(traceback.format_exc())
1698 raise JSONRPCError(
1686 raise JSONRPCError(
1699 'failed to delete repository `%s`' % (repo.repo_name,)
1687 'failed to delete repository `%s`' % (repo.repo_name,)
1700 )
1688 )
1701
1689
1702 @HasPermissionAnyDecorator('hg.admin')
1690 @HasPermissionAnyDecorator('hg.admin')
1703 def grant_user_permission(self, repoid, userid, perm):
1691 def grant_user_permission(self, repoid, userid, perm):
1704 """
1692 """
1705 Grant permission for user on given repository, or update existing one
1693 Grant permission for user on given repository, or update existing one
1706 if found. This command can be executed only using api_key belonging to user
1694 if found. This command can be executed only using api_key belonging to user
1707 with admin rights.
1695 with admin rights.
1708
1696
1709 :param repoid: repository name or repository id
1697 :param repoid: repository name or repository id
1710 :type repoid: str or int
1698 :type repoid: str or int
1711 :param userid:
1699 :param userid:
1712 :param perm: (repository.(none|read|write|admin))
1700 :param perm: (repository.(none|read|write|admin))
1713 :type perm: str
1701 :type perm: str
1714
1702
1715 OUTPUT::
1703 OUTPUT::
1716
1704
1717 id : <id_given_in_input>
1705 id : <id_given_in_input>
1718 result: {
1706 result: {
1719 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1707 "msg" : "Granted perm: `<perm>` for user: `<username>` in repo: `<reponame>`",
1720 "success": true
1708 "success": true
1721 }
1709 }
1722 error: null
1710 error: null
1723 """
1711 """
1724 repo = get_repo_or_error(repoid)
1712 repo = get_repo_or_error(repoid)
1725 user = get_user_or_error(userid)
1713 user = get_user_or_error(userid)
1726 perm = get_perm_or_error(perm)
1714 perm = get_perm_or_error(perm)
1727
1715
1728 try:
1716 try:
1729
1717
1730 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1718 RepoModel().grant_user_permission(repo=repo, user=user, perm=perm)
1731
1719
1732 Session().commit()
1720 Session().commit()
1733 return dict(
1721 return dict(
1734 msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1722 msg='Granted perm: `%s` for user: `%s` in repo: `%s`' % (
1735 perm.permission_name, user.username, repo.repo_name
1723 perm.permission_name, user.username, repo.repo_name
1736 ),
1724 ),
1737 success=True
1725 success=True
1738 )
1726 )
1739 except Exception:
1727 except Exception:
1740 log.error(traceback.format_exc())
1728 log.error(traceback.format_exc())
1741 raise JSONRPCError(
1729 raise JSONRPCError(
1742 'failed to edit permission for user: `%s` in repo: `%s`' % (
1730 'failed to edit permission for user: `%s` in repo: `%s`' % (
1743 userid, repoid
1731 userid, repoid
1744 )
1732 )
1745 )
1733 )
1746
1734
1747 @HasPermissionAnyDecorator('hg.admin')
1735 @HasPermissionAnyDecorator('hg.admin')
1748 def revoke_user_permission(self, repoid, userid):
1736 def revoke_user_permission(self, repoid, userid):
1749 """
1737 """
1750 Revoke permission for user on given repository. This command can be executed
1738 Revoke permission for user on given repository. This command can be executed
1751 only using api_key belonging to user with admin rights.
1739 only using api_key belonging to user with admin rights.
1752
1740
1753 :param repoid: repository name or repository id
1741 :param repoid: repository name or repository id
1754 :type repoid: str or int
1742 :type repoid: str or int
1755 :param userid:
1743 :param userid:
1756
1744
1757 OUTPUT::
1745 OUTPUT::
1758
1746
1759 id : <id_given_in_input>
1747 id : <id_given_in_input>
1760 result: {
1748 result: {
1761 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1749 "msg" : "Revoked perm for user: `<username>` in repo: `<reponame>`",
1762 "success": true
1750 "success": true
1763 }
1751 }
1764 error: null
1752 error: null
1765
1753
1766 """
1754 """
1767
1755
1768 repo = get_repo_or_error(repoid)
1756 repo = get_repo_or_error(repoid)
1769 user = get_user_or_error(userid)
1757 user = get_user_or_error(userid)
1770 try:
1758 try:
1771 RepoModel().revoke_user_permission(repo=repo, user=user)
1759 RepoModel().revoke_user_permission(repo=repo, user=user)
1772 Session().commit()
1760 Session().commit()
1773 return dict(
1761 return dict(
1774 msg='Revoked perm for user: `%s` in repo: `%s`' % (
1762 msg='Revoked perm for user: `%s` in repo: `%s`' % (
1775 user.username, repo.repo_name
1763 user.username, repo.repo_name
1776 ),
1764 ),
1777 success=True
1765 success=True
1778 )
1766 )
1779 except Exception:
1767 except Exception:
1780 log.error(traceback.format_exc())
1768 log.error(traceback.format_exc())
1781 raise JSONRPCError(
1769 raise JSONRPCError(
1782 'failed to edit permission for user: `%s` in repo: `%s`' % (
1770 'failed to edit permission for user: `%s` in repo: `%s`' % (
1783 userid, repoid
1771 userid, repoid
1784 )
1772 )
1785 )
1773 )
1786
1774
1787 # permission check inside
1775 # permission check inside
1788 def grant_user_group_permission(self, repoid, usergroupid, perm):
1776 def grant_user_group_permission(self, repoid, usergroupid, perm):
1789 """
1777 """
1790 Grant permission for user group on given repository, or update
1778 Grant permission for user group on given repository, or update
1791 existing one if found. This command can be executed only using
1779 existing one if found. This command can be executed only using
1792 api_key belonging to user with admin rights.
1780 api_key belonging to user with admin rights.
1793
1781
1794 :param repoid: repository name or repository id
1782 :param repoid: repository name or repository id
1795 :type repoid: str or int
1783 :type repoid: str or int
1796 :param usergroupid: id of usergroup
1784 :param usergroupid: id of usergroup
1797 :type usergroupid: str or int
1785 :type usergroupid: str or int
1798 :param perm: (repository.(none|read|write|admin))
1786 :param perm: (repository.(none|read|write|admin))
1799 :type perm: str
1787 :type perm: str
1800
1788
1801 OUTPUT::
1789 OUTPUT::
1802
1790
1803 id : <id_given_in_input>
1791 id : <id_given_in_input>
1804 result : {
1792 result : {
1805 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1793 "msg" : "Granted perm: `<perm>` for group: `<usersgroupname>` in repo: `<reponame>`",
1806 "success": true
1794 "success": true
1807
1795
1808 }
1796 }
1809 error : null
1797 error : null
1810
1798
1811 ERROR OUTPUT::
1799 ERROR OUTPUT::
1812
1800
1813 id : <id_given_in_input>
1801 id : <id_given_in_input>
1814 result : null
1802 result : null
1815 error : {
1803 error : {
1816 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1804 "failed to edit permission for user group: `<usergroup>` in repo `<repo>`'
1817 }
1805 }
1818
1806
1819 """
1807 """
1820 repo = get_repo_or_error(repoid)
1808 repo = get_repo_or_error(repoid)
1821 perm = get_perm_or_error(perm)
1809 perm = get_perm_or_error(perm)
1822 user_group = get_user_group_or_error(usergroupid)
1810 user_group = get_user_group_or_error(usergroupid)
1823 if not HasPermissionAny('hg.admin')():
1811 if not HasPermissionAny('hg.admin')():
1824 # check if we have admin permission for this repo !
1812 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1825 _perms = ('repository.admin',)
1826 if not HasRepoPermissionAny(*_perms)(
1827 repo_name=repo.repo_name):
1828 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1813 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1829
1814
1830 # check if we have at least read permission for this user group !
1815 # check if we have at least read permission for this user group !
1831 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1816 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1832 if not HasUserGroupPermissionAny(*_perms)(
1817 if not HasUserGroupPermissionAny(*_perms)(
1833 user_group_name=user_group.users_group_name):
1818 user_group_name=user_group.users_group_name):
1834 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1819 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1835
1820
1836 try:
1821 try:
1837 RepoModel().grant_user_group_permission(
1822 RepoModel().grant_user_group_permission(
1838 repo=repo, group_name=user_group, perm=perm)
1823 repo=repo, group_name=user_group, perm=perm)
1839
1824
1840 Session().commit()
1825 Session().commit()
1841 return dict(
1826 return dict(
1842 msg='Granted perm: `%s` for user group: `%s` in '
1827 msg='Granted perm: `%s` for user group: `%s` in '
1843 'repo: `%s`' % (
1828 'repo: `%s`' % (
1844 perm.permission_name, user_group.users_group_name,
1829 perm.permission_name, user_group.users_group_name,
1845 repo.repo_name
1830 repo.repo_name
1846 ),
1831 ),
1847 success=True
1832 success=True
1848 )
1833 )
1849 except Exception:
1834 except Exception:
1850 log.error(traceback.format_exc())
1835 log.error(traceback.format_exc())
1851 raise JSONRPCError(
1836 raise JSONRPCError(
1852 'failed to edit permission for user group: `%s` in '
1837 'failed to edit permission for user group: `%s` in '
1853 'repo: `%s`' % (
1838 'repo: `%s`' % (
1854 usergroupid, repo.repo_name
1839 usergroupid, repo.repo_name
1855 )
1840 )
1856 )
1841 )
1857
1842
1858 # permission check inside
1843 # permission check inside
1859 def revoke_user_group_permission(self, repoid, usergroupid):
1844 def revoke_user_group_permission(self, repoid, usergroupid):
1860 """
1845 """
1861 Revoke permission for user group on given repository. This command can be
1846 Revoke permission for user group on given repository. This command can be
1862 executed only using api_key belonging to user with admin rights.
1847 executed only using api_key belonging to user with admin rights.
1863
1848
1864 :param repoid: repository name or repository id
1849 :param repoid: repository name or repository id
1865 :type repoid: str or int
1850 :type repoid: str or int
1866 :param usergroupid:
1851 :param usergroupid:
1867
1852
1868 OUTPUT::
1853 OUTPUT::
1869
1854
1870 id : <id_given_in_input>
1855 id : <id_given_in_input>
1871 result: {
1856 result: {
1872 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1857 "msg" : "Revoked perm for group: `<usersgroupname>` in repo: `<reponame>`",
1873 "success": true
1858 "success": true
1874 }
1859 }
1875 error: null
1860 error: null
1876 """
1861 """
1877 repo = get_repo_or_error(repoid)
1862 repo = get_repo_or_error(repoid)
1878 user_group = get_user_group_or_error(usergroupid)
1863 user_group = get_user_group_or_error(usergroupid)
1879 if not HasPermissionAny('hg.admin')():
1864 if not HasPermissionAny('hg.admin')():
1880 # check if we have admin permission for this repo !
1865 if not HasRepoPermissionLevel('admin')(repo.repo_name):
1881 _perms = ('repository.admin',)
1882 if not HasRepoPermissionAny(*_perms)(
1883 repo_name=repo.repo_name):
1884 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1866 raise JSONRPCError('repository `%s` does not exist' % (repoid,))
1885
1867
1886 # check if we have at least read permission for this user group !
1868 # check if we have at least read permission for this user group !
1887 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1869 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
1888 if not HasUserGroupPermissionAny(*_perms)(
1870 if not HasUserGroupPermissionAny(*_perms)(
1889 user_group_name=user_group.users_group_name):
1871 user_group_name=user_group.users_group_name):
1890 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1872 raise JSONRPCError('user group `%s` does not exist' % (usergroupid,))
1891
1873
1892 try:
1874 try:
1893 RepoModel().revoke_user_group_permission(
1875 RepoModel().revoke_user_group_permission(
1894 repo=repo, group_name=user_group)
1876 repo=repo, group_name=user_group)
1895
1877
1896 Session().commit()
1878 Session().commit()
1897 return dict(
1879 return dict(
1898 msg='Revoked perm for user group: `%s` in repo: `%s`' % (
1880 msg='Revoked perm for user group: `%s` in repo: `%s`' % (
1899 user_group.users_group_name, repo.repo_name
1881 user_group.users_group_name, repo.repo_name
1900 ),
1882 ),
1901 success=True
1883 success=True
1902 )
1884 )
1903 except Exception:
1885 except Exception:
1904 log.error(traceback.format_exc())
1886 log.error(traceback.format_exc())
1905 raise JSONRPCError(
1887 raise JSONRPCError(
1906 'failed to edit permission for user group: `%s` in '
1888 'failed to edit permission for user group: `%s` in '
1907 'repo: `%s`' % (
1889 'repo: `%s`' % (
1908 user_group.users_group_name, repo.repo_name
1890 user_group.users_group_name, repo.repo_name
1909 )
1891 )
1910 )
1892 )
1911
1893
1912 @HasPermissionAnyDecorator('hg.admin')
1894 @HasPermissionAnyDecorator('hg.admin')
1913 def get_repo_group(self, repogroupid):
1895 def get_repo_group(self, repogroupid):
1914 """
1896 """
1915 Returns given repo group together with permissions, and repositories
1897 Returns given repo group together with permissions, and repositories
1916 inside the group
1898 inside the group
1917
1899
1918 :param repogroupid: id/name of repository group
1900 :param repogroupid: id/name of repository group
1919 :type repogroupid: str or int
1901 :type repogroupid: str or int
1920 """
1902 """
1921 repo_group = get_repo_group_or_error(repogroupid)
1903 repo_group = get_repo_group_or_error(repogroupid)
1922
1904
1923 members = []
1905 members = []
1924 for user in repo_group.repo_group_to_perm:
1906 for user in repo_group.repo_group_to_perm:
1925 perm = user.permission.permission_name
1907 perm = user.permission.permission_name
1926 user = user.user
1908 user = user.user
1927 user_data = {
1909 user_data = {
1928 'name': user.username,
1910 'name': user.username,
1929 'type': "user",
1911 'type': "user",
1930 'permission': perm
1912 'permission': perm
1931 }
1913 }
1932 members.append(user_data)
1914 members.append(user_data)
1933
1915
1934 for user_group in repo_group.users_group_to_perm:
1916 for user_group in repo_group.users_group_to_perm:
1935 perm = user_group.permission.permission_name
1917 perm = user_group.permission.permission_name
1936 user_group = user_group.users_group
1918 user_group = user_group.users_group
1937 user_group_data = {
1919 user_group_data = {
1938 'name': user_group.users_group_name,
1920 'name': user_group.users_group_name,
1939 'type': "user_group",
1921 'type': "user_group",
1940 'permission': perm
1922 'permission': perm
1941 }
1923 }
1942 members.append(user_group_data)
1924 members.append(user_group_data)
1943
1925
1944 data = repo_group.get_api_data()
1926 data = repo_group.get_api_data()
1945 data["members"] = members
1927 data["members"] = members
1946 return data
1928 return data
1947
1929
1948 @HasPermissionAnyDecorator('hg.admin')
1930 @HasPermissionAnyDecorator('hg.admin')
1949 def get_repo_groups(self):
1931 def get_repo_groups(self):
1950 """
1932 """
1951 Returns all repository groups
1933 Returns all repository groups
1952
1934
1953 """
1935 """
1954 result = []
1936 result = []
1955 for repo_group in RepoGroup.query():
1937 for repo_group in RepoGroup.query():
1956 result.append(repo_group.get_api_data())
1938 result.append(repo_group.get_api_data())
1957 return result
1939 return result
1958
1940
1959 @HasPermissionAnyDecorator('hg.admin')
1941 @HasPermissionAnyDecorator('hg.admin')
1960 def create_repo_group(self, group_name, description=Optional(''),
1942 def create_repo_group(self, group_name, description=Optional(''),
1961 owner=Optional(OAttr('apiuser')),
1943 owner=Optional(OAttr('apiuser')),
1962 parent=Optional(None),
1944 parent=Optional(None),
1963 copy_permissions=Optional(False)):
1945 copy_permissions=Optional(False)):
1964 """
1946 """
1965 Creates a repository group. This command can be executed only using
1947 Creates a repository group. This command can be executed only using
1966 api_key belonging to user with admin rights.
1948 api_key belonging to user with admin rights.
1967
1949
1968 :param group_name:
1950 :param group_name:
1969 :type group_name:
1951 :type group_name:
1970 :param description:
1952 :param description:
1971 :type description:
1953 :type description:
1972 :param owner:
1954 :param owner:
1973 :type owner:
1955 :type owner:
1974 :param parent:
1956 :param parent:
1975 :type parent:
1957 :type parent:
1976 :param copy_permissions:
1958 :param copy_permissions:
1977 :type copy_permissions:
1959 :type copy_permissions:
1978
1960
1979 OUTPUT::
1961 OUTPUT::
1980
1962
1981 id : <id_given_in_input>
1963 id : <id_given_in_input>
1982 result : {
1964 result : {
1983 "msg": "created new repo group `<repo_group_name>`"
1965 "msg": "created new repo group `<repo_group_name>`"
1984 "repo_group": <repogroup_object>
1966 "repo_group": <repogroup_object>
1985 }
1967 }
1986 error : null
1968 error : null
1987
1969
1988 ERROR OUTPUT::
1970 ERROR OUTPUT::
1989
1971
1990 id : <id_given_in_input>
1972 id : <id_given_in_input>
1991 result : null
1973 result : null
1992 error : {
1974 error : {
1993 failed to create repo group `<repogroupid>`
1975 failed to create repo group `<repogroupid>`
1994 }
1976 }
1995
1977
1996 """
1978 """
1997 if RepoGroup.get_by_group_name(group_name):
1979 if RepoGroup.get_by_group_name(group_name):
1998 raise JSONRPCError("repo group `%s` already exist" % (group_name,))
1980 raise JSONRPCError("repo group `%s` already exist" % (group_name,))
1999
1981
2000 if isinstance(owner, Optional):
1982 if isinstance(owner, Optional):
2001 owner = request.authuser.user_id
1983 owner = request.authuser.user_id
2002 group_description = Optional.extract(description)
1984 group_description = Optional.extract(description)
2003 parent_group = Optional.extract(parent)
1985 parent_group = Optional.extract(parent)
2004 if not isinstance(parent, Optional):
1986 if not isinstance(parent, Optional):
2005 parent_group = get_repo_group_or_error(parent_group)
1987 parent_group = get_repo_group_or_error(parent_group)
2006
1988
2007 copy_permissions = Optional.extract(copy_permissions)
1989 copy_permissions = Optional.extract(copy_permissions)
2008 try:
1990 try:
2009 repo_group = RepoGroupModel().create(
1991 repo_group = RepoGroupModel().create(
2010 group_name=group_name,
1992 group_name=group_name,
2011 group_description=group_description,
1993 group_description=group_description,
2012 owner=owner,
1994 owner=owner,
2013 parent=parent_group,
1995 parent=parent_group,
2014 copy_permissions=copy_permissions
1996 copy_permissions=copy_permissions
2015 )
1997 )
2016 Session().commit()
1998 Session().commit()
2017 return dict(
1999 return dict(
2018 msg='created new repo group `%s`' % group_name,
2000 msg='created new repo group `%s`' % group_name,
2019 repo_group=repo_group.get_api_data()
2001 repo_group=repo_group.get_api_data()
2020 )
2002 )
2021 except Exception:
2003 except Exception:
2022
2004
2023 log.error(traceback.format_exc())
2005 log.error(traceback.format_exc())
2024 raise JSONRPCError('failed to create repo group `%s`' % (group_name,))
2006 raise JSONRPCError('failed to create repo group `%s`' % (group_name,))
2025
2007
2026 @HasPermissionAnyDecorator('hg.admin')
2008 @HasPermissionAnyDecorator('hg.admin')
2027 def update_repo_group(self, repogroupid, group_name=Optional(''),
2009 def update_repo_group(self, repogroupid, group_name=Optional(''),
2028 description=Optional(''),
2010 description=Optional(''),
2029 owner=Optional(OAttr('apiuser')),
2011 owner=Optional(OAttr('apiuser')),
2030 parent=Optional(None), enable_locking=Optional(False)):
2012 parent=Optional(None), enable_locking=Optional(False)):
2031 repo_group = get_repo_group_or_error(repogroupid)
2013 repo_group = get_repo_group_or_error(repogroupid)
2032
2014
2033 updates = {}
2015 updates = {}
2034 try:
2016 try:
2035 store_update(updates, group_name, 'group_name')
2017 store_update(updates, group_name, 'group_name')
2036 store_update(updates, description, 'group_description')
2018 store_update(updates, description, 'group_description')
2037 store_update(updates, owner, 'owner')
2019 store_update(updates, owner, 'owner')
2038 store_update(updates, parent, 'parent_group')
2020 store_update(updates, parent, 'parent_group')
2039 store_update(updates, enable_locking, 'enable_locking')
2021 store_update(updates, enable_locking, 'enable_locking')
2040 repo_group = RepoGroupModel().update(repo_group, updates)
2022 repo_group = RepoGroupModel().update(repo_group, updates)
2041 Session().commit()
2023 Session().commit()
2042 return dict(
2024 return dict(
2043 msg='updated repository group ID:%s %s' % (repo_group.group_id,
2025 msg='updated repository group ID:%s %s' % (repo_group.group_id,
2044 repo_group.group_name),
2026 repo_group.group_name),
2045 repo_group=repo_group.get_api_data()
2027 repo_group=repo_group.get_api_data()
2046 )
2028 )
2047 except Exception:
2029 except Exception:
2048 log.error(traceback.format_exc())
2030 log.error(traceback.format_exc())
2049 raise JSONRPCError('failed to update repository group `%s`'
2031 raise JSONRPCError('failed to update repository group `%s`'
2050 % (repogroupid,))
2032 % (repogroupid,))
2051
2033
2052 @HasPermissionAnyDecorator('hg.admin')
2034 @HasPermissionAnyDecorator('hg.admin')
2053 def delete_repo_group(self, repogroupid):
2035 def delete_repo_group(self, repogroupid):
2054 """
2036 """
2055
2037
2056 :param repogroupid: name or id of repository group
2038 :param repogroupid: name or id of repository group
2057 :type repogroupid: str or int
2039 :type repogroupid: str or int
2058
2040
2059 OUTPUT::
2041 OUTPUT::
2060
2042
2061 id : <id_given_in_input>
2043 id : <id_given_in_input>
2062 result : {
2044 result : {
2063 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>
2045 'msg': 'deleted repo group ID:<repogroupid> <repogroupname>
2064 'repo_group': null
2046 'repo_group': null
2065 }
2047 }
2066 error : null
2048 error : null
2067
2049
2068 ERROR OUTPUT::
2050 ERROR OUTPUT::
2069
2051
2070 id : <id_given_in_input>
2052 id : <id_given_in_input>
2071 result : null
2053 result : null
2072 error : {
2054 error : {
2073 "failed to delete repo group ID:<repogroupid> <repogroupname>"
2055 "failed to delete repo group ID:<repogroupid> <repogroupname>"
2074 }
2056 }
2075
2057
2076 """
2058 """
2077 repo_group = get_repo_group_or_error(repogroupid)
2059 repo_group = get_repo_group_or_error(repogroupid)
2078
2060
2079 try:
2061 try:
2080 RepoGroupModel().delete(repo_group)
2062 RepoGroupModel().delete(repo_group)
2081 Session().commit()
2063 Session().commit()
2082 return dict(
2064 return dict(
2083 msg='deleted repo group ID:%s %s' %
2065 msg='deleted repo group ID:%s %s' %
2084 (repo_group.group_id, repo_group.group_name),
2066 (repo_group.group_id, repo_group.group_name),
2085 repo_group=None
2067 repo_group=None
2086 )
2068 )
2087 except Exception:
2069 except Exception:
2088 log.error(traceback.format_exc())
2070 log.error(traceback.format_exc())
2089 raise JSONRPCError('failed to delete repo group ID:%s %s' %
2071 raise JSONRPCError('failed to delete repo group ID:%s %s' %
2090 (repo_group.group_id, repo_group.group_name)
2072 (repo_group.group_id, repo_group.group_name)
2091 )
2073 )
2092
2074
2093 # permission check inside
2075 # permission check inside
2094 def grant_user_permission_to_repo_group(self, repogroupid, userid,
2076 def grant_user_permission_to_repo_group(self, repogroupid, userid,
2095 perm, apply_to_children=Optional('none')):
2077 perm, apply_to_children=Optional('none')):
2096 """
2078 """
2097 Grant permission for user on given repository group, or update existing
2079 Grant permission for user on given repository group, or update existing
2098 one if found. This command can be executed only using api_key belonging
2080 one if found. This command can be executed only using api_key belonging
2099 to user with admin rights, or user who has admin right to given repository
2081 to user with admin rights, or user who has admin right to given repository
2100 group.
2082 group.
2101
2083
2102 :param repogroupid: name or id of repository group
2084 :param repogroupid: name or id of repository group
2103 :type repogroupid: str or int
2085 :type repogroupid: str or int
2104 :param userid:
2086 :param userid:
2105 :param perm: (group.(none|read|write|admin))
2087 :param perm: (group.(none|read|write|admin))
2106 :type perm: str
2088 :type perm: str
2107 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2089 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2108 :type apply_to_children: str
2090 :type apply_to_children: str
2109
2091
2110 OUTPUT::
2092 OUTPUT::
2111
2093
2112 id : <id_given_in_input>
2094 id : <id_given_in_input>
2113 result: {
2095 result: {
2114 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
2096 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
2115 "success": true
2097 "success": true
2116 }
2098 }
2117 error: null
2099 error: null
2118
2100
2119 ERROR OUTPUT::
2101 ERROR OUTPUT::
2120
2102
2121 id : <id_given_in_input>
2103 id : <id_given_in_input>
2122 result : null
2104 result : null
2123 error : {
2105 error : {
2124 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
2106 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
2125 }
2107 }
2126
2108
2127 """
2109 """
2128
2110
2129 repo_group = get_repo_group_or_error(repogroupid)
2111 repo_group = get_repo_group_or_error(repogroupid)
2130
2112
2131 if not HasPermissionAny('hg.admin')():
2113 if not HasPermissionAny('hg.admin')():
2132 # check if we have admin permission for this repo group !
2114 # check if we have admin permission for this repo group !
2133 if not HasRepoGroupPermissionAny('group.admin')(group_name=repo_group.group_name):
2115 if not HasRepoGroupPermissionAny('group.admin')(group_name=repo_group.group_name):
2134 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
2116 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
2135
2117
2136 user = get_user_or_error(userid)
2118 user = get_user_or_error(userid)
2137 perm = get_perm_or_error(perm, prefix='group.')
2119 perm = get_perm_or_error(perm, prefix='group.')
2138 apply_to_children = Optional.extract(apply_to_children)
2120 apply_to_children = Optional.extract(apply_to_children)
2139
2121
2140 try:
2122 try:
2141 RepoGroupModel().add_permission(repo_group=repo_group,
2123 RepoGroupModel().add_permission(repo_group=repo_group,
2142 obj=user,
2124 obj=user,
2143 obj_type="user",
2125 obj_type="user",
2144 perm=perm,
2126 perm=perm,
2145 recursive=apply_to_children)
2127 recursive=apply_to_children)
2146 Session().commit()
2128 Session().commit()
2147 return dict(
2129 return dict(
2148 msg='Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
2130 msg='Granted perm: `%s` (recursive:%s) for user: `%s` in repo group: `%s`' % (
2149 perm.permission_name, apply_to_children, user.username, repo_group.name
2131 perm.permission_name, apply_to_children, user.username, repo_group.name
2150 ),
2132 ),
2151 success=True
2133 success=True
2152 )
2134 )
2153 except Exception:
2135 except Exception:
2154 log.error(traceback.format_exc())
2136 log.error(traceback.format_exc())
2155 raise JSONRPCError(
2137 raise JSONRPCError(
2156 'failed to edit permission for user: `%s` in repo group: `%s`' % (
2138 'failed to edit permission for user: `%s` in repo group: `%s`' % (
2157 userid, repo_group.name))
2139 userid, repo_group.name))
2158
2140
2159 # permission check inside
2141 # permission check inside
2160 def revoke_user_permission_from_repo_group(self, repogroupid, userid,
2142 def revoke_user_permission_from_repo_group(self, repogroupid, userid,
2161 apply_to_children=Optional('none')):
2143 apply_to_children=Optional('none')):
2162 """
2144 """
2163 Revoke permission for user on given repository group. This command can
2145 Revoke permission for user on given repository group. This command can
2164 be executed only using api_key belonging to user with admin rights, or
2146 be executed only using api_key belonging to user with admin rights, or
2165 user who has admin right to given repository group.
2147 user who has admin right to given repository group.
2166
2148
2167 :param repogroupid: name or id of repository group
2149 :param repogroupid: name or id of repository group
2168 :type repogroupid: str or int
2150 :type repogroupid: str or int
2169 :param userid:
2151 :param userid:
2170 :type userid:
2152 :type userid:
2171 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2153 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2172 :type apply_to_children: str
2154 :type apply_to_children: str
2173
2155
2174 OUTPUT::
2156 OUTPUT::
2175
2157
2176 id : <id_given_in_input>
2158 id : <id_given_in_input>
2177 result: {
2159 result: {
2178 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
2160 "msg" : "Revoked perm (recursive:<apply_to_children>) for user: `<username>` in repo group: `<repo_group_name>`",
2179 "success": true
2161 "success": true
2180 }
2162 }
2181 error: null
2163 error: null
2182
2164
2183 ERROR OUTPUT::
2165 ERROR OUTPUT::
2184
2166
2185 id : <id_given_in_input>
2167 id : <id_given_in_input>
2186 result : null
2168 result : null
2187 error : {
2169 error : {
2188 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
2170 "failed to edit permission for user: `<userid>` in repo group: `<repo_group_name>`"
2189 }
2171 }
2190
2172
2191 """
2173 """
2192
2174
2193 repo_group = get_repo_group_or_error(repogroupid)
2175 repo_group = get_repo_group_or_error(repogroupid)
2194
2176
2195 if not HasPermissionAny('hg.admin')():
2177 if not HasPermissionAny('hg.admin')():
2196 # check if we have admin permission for this repo group !
2178 # check if we have admin permission for this repo group !
2197 if not HasRepoGroupPermissionAny('group.admin')(group_name=repo_group.group_name):
2179 if not HasRepoGroupPermissionAny('group.admin')(group_name=repo_group.group_name):
2198 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
2180 raise JSONRPCError('repository group `%s` does not exist' % (repogroupid,))
2199
2181
2200 user = get_user_or_error(userid)
2182 user = get_user_or_error(userid)
2201 apply_to_children = Optional.extract(apply_to_children)
2183 apply_to_children = Optional.extract(apply_to_children)
2202
2184
2203 try:
2185 try:
2204 RepoGroupModel().delete_permission(repo_group=repo_group,
2186 RepoGroupModel().delete_permission(repo_group=repo_group,
2205 obj=user,
2187 obj=user,
2206 obj_type="user",
2188 obj_type="user",
2207 recursive=apply_to_children)
2189 recursive=apply_to_children)
2208
2190
2209 Session().commit()
2191 Session().commit()
2210 return dict(
2192 return dict(
2211 msg='Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
2193 msg='Revoked perm (recursive:%s) for user: `%s` in repo group: `%s`' % (
2212 apply_to_children, user.username, repo_group.name
2194 apply_to_children, user.username, repo_group.name
2213 ),
2195 ),
2214 success=True
2196 success=True
2215 )
2197 )
2216 except Exception:
2198 except Exception:
2217 log.error(traceback.format_exc())
2199 log.error(traceback.format_exc())
2218 raise JSONRPCError(
2200 raise JSONRPCError(
2219 'failed to edit permission for user: `%s` in repo group: `%s`' % (
2201 'failed to edit permission for user: `%s` in repo group: `%s`' % (
2220 userid, repo_group.name))
2202 userid, repo_group.name))
2221
2203
2222 # permission check inside
2204 # permission check inside
2223 def grant_user_group_permission_to_repo_group(
2205 def grant_user_group_permission_to_repo_group(
2224 self, repogroupid, usergroupid, perm,
2206 self, repogroupid, usergroupid, perm,
2225 apply_to_children=Optional('none')):
2207 apply_to_children=Optional('none')):
2226 """
2208 """
2227 Grant permission for user group on given repository group, or update
2209 Grant permission for user group on given repository group, or update
2228 existing one if found. This command can be executed only using
2210 existing one if found. This command can be executed only using
2229 api_key belonging to user with admin rights, or user who has admin
2211 api_key belonging to user with admin rights, or user who has admin
2230 right to given repository group.
2212 right to given repository group.
2231
2213
2232 :param repogroupid: name or id of repository group
2214 :param repogroupid: name or id of repository group
2233 :type repogroupid: str or int
2215 :type repogroupid: str or int
2234 :param usergroupid: id of usergroup
2216 :param usergroupid: id of usergroup
2235 :type usergroupid: str or int
2217 :type usergroupid: str or int
2236 :param perm: (group.(none|read|write|admin))
2218 :param perm: (group.(none|read|write|admin))
2237 :type perm: str
2219 :type perm: str
2238 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2220 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2239 :type apply_to_children: str
2221 :type apply_to_children: str
2240
2222
2241 OUTPUT::
2223 OUTPUT::
2242
2224
2243 id : <id_given_in_input>
2225 id : <id_given_in_input>
2244 result : {
2226 result : {
2245 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2227 "msg" : "Granted perm: `<perm>` (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2246 "success": true
2228 "success": true
2247
2229
2248 }
2230 }
2249 error : null
2231 error : null
2250
2232
2251 ERROR OUTPUT::
2233 ERROR OUTPUT::
2252
2234
2253 id : <id_given_in_input>
2235 id : <id_given_in_input>
2254 result : null
2236 result : null
2255 error : {
2237 error : {
2256 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2238 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2257 }
2239 }
2258
2240
2259 """
2241 """
2260 repo_group = get_repo_group_or_error(repogroupid)
2242 repo_group = get_repo_group_or_error(repogroupid)
2261 perm = get_perm_or_error(perm, prefix='group.')
2243 perm = get_perm_or_error(perm, prefix='group.')
2262 user_group = get_user_group_or_error(usergroupid)
2244 user_group = get_user_group_or_error(usergroupid)
2263 if not HasPermissionAny('hg.admin')():
2245 if not HasPermissionAny('hg.admin')():
2264 # check if we have admin permission for this repo group !
2246 # check if we have admin permission for this repo group !
2265 _perms = ('group.admin',)
2247 _perms = ('group.admin',)
2266 if not HasRepoGroupPermissionAny(*_perms)(
2248 if not HasRepoGroupPermissionAny(*_perms)(
2267 group_name=repo_group.group_name):
2249 group_name=repo_group.group_name):
2268 raise JSONRPCError(
2250 raise JSONRPCError(
2269 'repository group `%s` does not exist' % (repogroupid,))
2251 'repository group `%s` does not exist' % (repogroupid,))
2270
2252
2271 # check if we have at least read permission for this user group !
2253 # check if we have at least read permission for this user group !
2272 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2254 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2273 if not HasUserGroupPermissionAny(*_perms)(
2255 if not HasUserGroupPermissionAny(*_perms)(
2274 user_group_name=user_group.users_group_name):
2256 user_group_name=user_group.users_group_name):
2275 raise JSONRPCError(
2257 raise JSONRPCError(
2276 'user group `%s` does not exist' % (usergroupid,))
2258 'user group `%s` does not exist' % (usergroupid,))
2277
2259
2278 apply_to_children = Optional.extract(apply_to_children)
2260 apply_to_children = Optional.extract(apply_to_children)
2279
2261
2280 try:
2262 try:
2281 RepoGroupModel().add_permission(repo_group=repo_group,
2263 RepoGroupModel().add_permission(repo_group=repo_group,
2282 obj=user_group,
2264 obj=user_group,
2283 obj_type="user_group",
2265 obj_type="user_group",
2284 perm=perm,
2266 perm=perm,
2285 recursive=apply_to_children)
2267 recursive=apply_to_children)
2286 Session().commit()
2268 Session().commit()
2287 return dict(
2269 return dict(
2288 msg='Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2270 msg='Granted perm: `%s` (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2289 perm.permission_name, apply_to_children,
2271 perm.permission_name, apply_to_children,
2290 user_group.users_group_name, repo_group.name
2272 user_group.users_group_name, repo_group.name
2291 ),
2273 ),
2292 success=True
2274 success=True
2293 )
2275 )
2294 except Exception:
2276 except Exception:
2295 log.error(traceback.format_exc())
2277 log.error(traceback.format_exc())
2296 raise JSONRPCError(
2278 raise JSONRPCError(
2297 'failed to edit permission for user group: `%s` in '
2279 'failed to edit permission for user group: `%s` in '
2298 'repo group: `%s`' % (
2280 'repo group: `%s`' % (
2299 usergroupid, repo_group.name
2281 usergroupid, repo_group.name
2300 )
2282 )
2301 )
2283 )
2302
2284
2303 # permission check inside
2285 # permission check inside
2304 def revoke_user_group_permission_from_repo_group(
2286 def revoke_user_group_permission_from_repo_group(
2305 self, repogroupid, usergroupid,
2287 self, repogroupid, usergroupid,
2306 apply_to_children=Optional('none')):
2288 apply_to_children=Optional('none')):
2307 """
2289 """
2308 Revoke permission for user group on given repository. This command can be
2290 Revoke permission for user group on given repository. This command can be
2309 executed only using api_key belonging to user with admin rights, or
2291 executed only using api_key belonging to user with admin rights, or
2310 user who has admin right to given repository group.
2292 user who has admin right to given repository group.
2311
2293
2312 :param repogroupid: name or id of repository group
2294 :param repogroupid: name or id of repository group
2313 :type repogroupid: str or int
2295 :type repogroupid: str or int
2314 :param usergroupid:
2296 :param usergroupid:
2315 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2297 :param apply_to_children: 'none', 'repos', 'groups', 'all'
2316 :type apply_to_children: str
2298 :type apply_to_children: str
2317
2299
2318 OUTPUT::
2300 OUTPUT::
2319
2301
2320 id : <id_given_in_input>
2302 id : <id_given_in_input>
2321 result: {
2303 result: {
2322 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2304 "msg" : "Revoked perm (recursive:<apply_to_children>) for user group: `<usersgroupname>` in repo group: `<repo_group_name>`",
2323 "success": true
2305 "success": true
2324 }
2306 }
2325 error: null
2307 error: null
2326
2308
2327 ERROR OUTPUT::
2309 ERROR OUTPUT::
2328
2310
2329 id : <id_given_in_input>
2311 id : <id_given_in_input>
2330 result : null
2312 result : null
2331 error : {
2313 error : {
2332 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2314 "failed to edit permission for user group: `<usergroup>` in repo group: `<repo_group_name>`"
2333 }
2315 }
2334
2316
2335
2317
2336 """
2318 """
2337 repo_group = get_repo_group_or_error(repogroupid)
2319 repo_group = get_repo_group_or_error(repogroupid)
2338 user_group = get_user_group_or_error(usergroupid)
2320 user_group = get_user_group_or_error(usergroupid)
2339 if not HasPermissionAny('hg.admin')():
2321 if not HasPermissionAny('hg.admin')():
2340 # check if we have admin permission for this repo group !
2322 # check if we have admin permission for this repo group !
2341 _perms = ('group.admin',)
2323 _perms = ('group.admin',)
2342 if not HasRepoGroupPermissionAny(*_perms)(
2324 if not HasRepoGroupPermissionAny(*_perms)(
2343 group_name=repo_group.group_name):
2325 group_name=repo_group.group_name):
2344 raise JSONRPCError(
2326 raise JSONRPCError(
2345 'repository group `%s` does not exist' % (repogroupid,))
2327 'repository group `%s` does not exist' % (repogroupid,))
2346
2328
2347 # check if we have at least read permission for this user group !
2329 # check if we have at least read permission for this user group !
2348 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2330 _perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin',)
2349 if not HasUserGroupPermissionAny(*_perms)(
2331 if not HasUserGroupPermissionAny(*_perms)(
2350 user_group_name=user_group.users_group_name):
2332 user_group_name=user_group.users_group_name):
2351 raise JSONRPCError(
2333 raise JSONRPCError(
2352 'user group `%s` does not exist' % (usergroupid,))
2334 'user group `%s` does not exist' % (usergroupid,))
2353
2335
2354 apply_to_children = Optional.extract(apply_to_children)
2336 apply_to_children = Optional.extract(apply_to_children)
2355
2337
2356 try:
2338 try:
2357 RepoGroupModel().delete_permission(repo_group=repo_group,
2339 RepoGroupModel().delete_permission(repo_group=repo_group,
2358 obj=user_group,
2340 obj=user_group,
2359 obj_type="user_group",
2341 obj_type="user_group",
2360 recursive=apply_to_children)
2342 recursive=apply_to_children)
2361 Session().commit()
2343 Session().commit()
2362 return dict(
2344 return dict(
2363 msg='Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2345 msg='Revoked perm (recursive:%s) for user group: `%s` in repo group: `%s`' % (
2364 apply_to_children, user_group.users_group_name, repo_group.name
2346 apply_to_children, user_group.users_group_name, repo_group.name
2365 ),
2347 ),
2366 success=True
2348 success=True
2367 )
2349 )
2368 except Exception:
2350 except Exception:
2369 log.error(traceback.format_exc())
2351 log.error(traceback.format_exc())
2370 raise JSONRPCError(
2352 raise JSONRPCError(
2371 'failed to edit permission for user group: `%s` in repo group: `%s`' % (
2353 'failed to edit permission for user group: `%s` in repo group: `%s`' % (
2372 user_group.users_group_name, repo_group.name
2354 user_group.users_group_name, repo_group.name
2373 )
2355 )
2374 )
2356 )
2375
2357
2376 def get_gist(self, gistid):
2358 def get_gist(self, gistid):
2377 """
2359 """
2378 Get given gist by id
2360 Get given gist by id
2379
2361
2380 :param gistid: id of private or public gist
2362 :param gistid: id of private or public gist
2381 :type gistid: str
2363 :type gistid: str
2382 """
2364 """
2383 gist = get_gist_or_error(gistid)
2365 gist = get_gist_or_error(gistid)
2384 if not HasPermissionAny('hg.admin')():
2366 if not HasPermissionAny('hg.admin')():
2385 if gist.owner_id != request.authuser.user_id:
2367 if gist.owner_id != request.authuser.user_id:
2386 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2368 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2387 return gist.get_api_data()
2369 return gist.get_api_data()
2388
2370
2389 def get_gists(self, userid=Optional(OAttr('apiuser'))):
2371 def get_gists(self, userid=Optional(OAttr('apiuser'))):
2390 """
2372 """
2391 Get all gists for given user. If userid is empty returned gists
2373 Get all gists for given user. If userid is empty returned gists
2392 are for user who called the api
2374 are for user who called the api
2393
2375
2394 :param userid: user to get gists for
2376 :param userid: user to get gists for
2395 :type userid: Optional(str or int)
2377 :type userid: Optional(str or int)
2396 """
2378 """
2397 if not HasPermissionAny('hg.admin')():
2379 if not HasPermissionAny('hg.admin')():
2398 # make sure normal user does not pass someone else userid,
2380 # make sure normal user does not pass someone else userid,
2399 # he is not allowed to do that
2381 # he is not allowed to do that
2400 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
2382 if not isinstance(userid, Optional) and userid != request.authuser.user_id:
2401 raise JSONRPCError(
2383 raise JSONRPCError(
2402 'userid is not the same as your user'
2384 'userid is not the same as your user'
2403 )
2385 )
2404
2386
2405 if isinstance(userid, Optional):
2387 if isinstance(userid, Optional):
2406 user_id = request.authuser.user_id
2388 user_id = request.authuser.user_id
2407 else:
2389 else:
2408 user_id = get_user_or_error(userid).user_id
2390 user_id = get_user_or_error(userid).user_id
2409
2391
2410 gists = []
2392 gists = []
2411 _gists = Gist().query() \
2393 _gists = Gist().query() \
2412 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
2394 .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time())) \
2413 .filter(Gist.owner_id == user_id) \
2395 .filter(Gist.owner_id == user_id) \
2414 .order_by(Gist.created_on.desc())
2396 .order_by(Gist.created_on.desc())
2415 for gist in _gists:
2397 for gist in _gists:
2416 gists.append(gist.get_api_data())
2398 gists.append(gist.get_api_data())
2417 return gists
2399 return gists
2418
2400
2419 def create_gist(self, files, owner=Optional(OAttr('apiuser')),
2401 def create_gist(self, files, owner=Optional(OAttr('apiuser')),
2420 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
2402 gist_type=Optional(Gist.GIST_PUBLIC), lifetime=Optional(-1),
2421 description=Optional('')):
2403 description=Optional('')):
2422
2404
2423 """
2405 """
2424 Creates new Gist
2406 Creates new Gist
2425
2407
2426 :param files: files to be added to gist
2408 :param files: files to be added to gist
2427 {'filename': {'content':'...', 'lexer': null},
2409 {'filename': {'content':'...', 'lexer': null},
2428 'filename2': {'content':'...', 'lexer': null}}
2410 'filename2': {'content':'...', 'lexer': null}}
2429 :type files: dict
2411 :type files: dict
2430 :param owner: gist owner, defaults to api method caller
2412 :param owner: gist owner, defaults to api method caller
2431 :type owner: Optional(str or int)
2413 :type owner: Optional(str or int)
2432 :param gist_type: type of gist 'public' or 'private'
2414 :param gist_type: type of gist 'public' or 'private'
2433 :type gist_type: Optional(str)
2415 :type gist_type: Optional(str)
2434 :param lifetime: time in minutes of gist lifetime
2416 :param lifetime: time in minutes of gist lifetime
2435 :type lifetime: Optional(int)
2417 :type lifetime: Optional(int)
2436 :param description: gist description
2418 :param description: gist description
2437 :type description: Optional(str)
2419 :type description: Optional(str)
2438
2420
2439 OUTPUT::
2421 OUTPUT::
2440
2422
2441 id : <id_given_in_input>
2423 id : <id_given_in_input>
2442 result : {
2424 result : {
2443 "msg": "created new gist",
2425 "msg": "created new gist",
2444 "gist": {}
2426 "gist": {}
2445 }
2427 }
2446 error : null
2428 error : null
2447
2429
2448 ERROR OUTPUT::
2430 ERROR OUTPUT::
2449
2431
2450 id : <id_given_in_input>
2432 id : <id_given_in_input>
2451 result : null
2433 result : null
2452 error : {
2434 error : {
2453 "failed to create gist"
2435 "failed to create gist"
2454 }
2436 }
2455
2437
2456 """
2438 """
2457 try:
2439 try:
2458 if isinstance(owner, Optional):
2440 if isinstance(owner, Optional):
2459 owner = request.authuser.user_id
2441 owner = request.authuser.user_id
2460
2442
2461 owner = get_user_or_error(owner)
2443 owner = get_user_or_error(owner)
2462 description = Optional.extract(description)
2444 description = Optional.extract(description)
2463 gist_type = Optional.extract(gist_type)
2445 gist_type = Optional.extract(gist_type)
2464 lifetime = Optional.extract(lifetime)
2446 lifetime = Optional.extract(lifetime)
2465
2447
2466 gist = GistModel().create(description=description,
2448 gist = GistModel().create(description=description,
2467 owner=owner,
2449 owner=owner,
2468 gist_mapping=files,
2450 gist_mapping=files,
2469 gist_type=gist_type,
2451 gist_type=gist_type,
2470 lifetime=lifetime)
2452 lifetime=lifetime)
2471 Session().commit()
2453 Session().commit()
2472 return dict(
2454 return dict(
2473 msg='created new gist',
2455 msg='created new gist',
2474 gist=gist.get_api_data()
2456 gist=gist.get_api_data()
2475 )
2457 )
2476 except Exception:
2458 except Exception:
2477 log.error(traceback.format_exc())
2459 log.error(traceback.format_exc())
2478 raise JSONRPCError('failed to create gist')
2460 raise JSONRPCError('failed to create gist')
2479
2461
2480 # def update_gist(self, gistid, files, owner=Optional(OAttr('apiuser')),
2462 # def update_gist(self, gistid, files, owner=Optional(OAttr('apiuser')),
2481 # gist_type=Optional(Gist.GIST_PUBLIC),
2463 # gist_type=Optional(Gist.GIST_PUBLIC),
2482 # gist_lifetime=Optional(-1), gist_description=Optional('')):
2464 # gist_lifetime=Optional(-1), gist_description=Optional('')):
2483 # gist = get_gist_or_error(gistid)
2465 # gist = get_gist_or_error(gistid)
2484 # updates = {}
2466 # updates = {}
2485
2467
2486 # permission check inside
2468 # permission check inside
2487 def delete_gist(self, gistid):
2469 def delete_gist(self, gistid):
2488 """
2470 """
2489 Deletes existing gist
2471 Deletes existing gist
2490
2472
2491 :param gistid: id of gist to delete
2473 :param gistid: id of gist to delete
2492 :type gistid: str
2474 :type gistid: str
2493
2475
2494 OUTPUT::
2476 OUTPUT::
2495
2477
2496 id : <id_given_in_input>
2478 id : <id_given_in_input>
2497 result : {
2479 result : {
2498 "deleted gist ID: <gist_id>",
2480 "deleted gist ID: <gist_id>",
2499 "gist": null
2481 "gist": null
2500 }
2482 }
2501 error : null
2483 error : null
2502
2484
2503 ERROR OUTPUT::
2485 ERROR OUTPUT::
2504
2486
2505 id : <id_given_in_input>
2487 id : <id_given_in_input>
2506 result : null
2488 result : null
2507 error : {
2489 error : {
2508 "failed to delete gist ID:<gist_id>"
2490 "failed to delete gist ID:<gist_id>"
2509 }
2491 }
2510
2492
2511 """
2493 """
2512 gist = get_gist_or_error(gistid)
2494 gist = get_gist_or_error(gistid)
2513 if not HasPermissionAny('hg.admin')():
2495 if not HasPermissionAny('hg.admin')():
2514 if gist.owner_id != request.authuser.user_id:
2496 if gist.owner_id != request.authuser.user_id:
2515 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2497 raise JSONRPCError('gist `%s` does not exist' % (gistid,))
2516
2498
2517 try:
2499 try:
2518 GistModel().delete(gist)
2500 GistModel().delete(gist)
2519 Session().commit()
2501 Session().commit()
2520 return dict(
2502 return dict(
2521 msg='deleted gist ID:%s' % (gist.gist_access_id,),
2503 msg='deleted gist ID:%s' % (gist.gist_access_id,),
2522 gist=None
2504 gist=None
2523 )
2505 )
2524 except Exception:
2506 except Exception:
2525 log.error(traceback.format_exc())
2507 log.error(traceback.format_exc())
2526 raise JSONRPCError('failed to delete gist ID:%s'
2508 raise JSONRPCError('failed to delete gist ID:%s'
2527 % (gist.gist_access_id,))
2509 % (gist.gist_access_id,))
@@ -1,198 +1,195 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.changelog
15 kallithea.controllers.changelog
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 changelog controller for Kallithea
18 changelog 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 21, 2010
22 :created_on: Apr 21, 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 from pylons import request, session, tmpl_context as c
31 from pylons import request, session, tmpl_context as c
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from webob.exc import HTTPFound, HTTPNotFound, HTTPBadRequest
33 from webob.exc import HTTPFound, HTTPNotFound, HTTPBadRequest
34
34
35 import kallithea.lib.helpers as h
35 import kallithea.lib.helpers as h
36 from kallithea.config.routing import url
36 from kallithea.config.routing import url
37 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
38 from kallithea.lib.base import BaseRepoController, render
38 from kallithea.lib.base import BaseRepoController, render
39 from kallithea.lib.compat import json
39 from kallithea.lib.compat import json
40 from kallithea.lib.graphmod import graph_data
40 from kallithea.lib.graphmod import graph_data
41 from kallithea.lib.page import RepoPage
41 from kallithea.lib.page import RepoPage
42 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
42 from kallithea.lib.vcs.exceptions import RepositoryError, ChangesetDoesNotExistError, \
43 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
43 ChangesetError, NodeDoesNotExistError, EmptyRepositoryError
44 from kallithea.lib.utils2 import safe_int, safe_str
44 from kallithea.lib.utils2 import safe_int, safe_str
45
45
46
46
47 log = logging.getLogger(__name__)
47 log = logging.getLogger(__name__)
48
48
49
49
50 def _load_changelog_summary():
50 def _load_changelog_summary():
51 # also used from summary ...
51 # also used from summary ...
52 p = safe_int(request.GET.get('page'), 1)
52 p = safe_int(request.GET.get('page'), 1)
53 size = safe_int(request.GET.get('size'), 10)
53 size = safe_int(request.GET.get('size'), 10)
54
54
55 def url_generator(**kw):
55 def url_generator(**kw):
56 return url('changelog_summary_home',
56 return url('changelog_summary_home',
57 repo_name=c.db_repo.repo_name, size=size, **kw)
57 repo_name=c.db_repo.repo_name, size=size, **kw)
58
58
59 collection = c.db_repo_scm_instance
59 collection = c.db_repo_scm_instance
60
60
61 c.repo_changesets = RepoPage(collection, page=p,
61 c.repo_changesets = RepoPage(collection, page=p,
62 items_per_page=size,
62 items_per_page=size,
63 url=url_generator)
63 url=url_generator)
64 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
64 page_revisions = [x.raw_id for x in list(c.repo_changesets)]
65 c.comments = c.db_repo.get_comments(page_revisions)
65 c.comments = c.db_repo.get_comments(page_revisions)
66 c.statuses = c.db_repo.statuses(page_revisions)
66 c.statuses = c.db_repo.statuses(page_revisions)
67
67
68
68
69 class ChangelogController(BaseRepoController):
69 class ChangelogController(BaseRepoController):
70
70
71 def __before__(self):
71 def __before__(self):
72 super(ChangelogController, self).__before__()
72 super(ChangelogController, self).__before__()
73 c.affected_files_cut_off = 60
73 c.affected_files_cut_off = 60
74
74
75 @staticmethod
75 @staticmethod
76 def __get_cs(rev, repo):
76 def __get_cs(rev, repo):
77 """
77 """
78 Safe way to get changeset. If error occur fail with error message.
78 Safe way to get changeset. If error occur fail with error message.
79
79
80 :param rev: revision to fetch
80 :param rev: revision to fetch
81 :param repo: repo instance
81 :param repo: repo instance
82 """
82 """
83
83
84 try:
84 try:
85 return c.db_repo_scm_instance.get_changeset(rev)
85 return c.db_repo_scm_instance.get_changeset(rev)
86 except EmptyRepositoryError as e:
86 except EmptyRepositoryError as e:
87 h.flash(h.literal(_('There are no changesets yet')),
87 h.flash(h.literal(_('There are no changesets yet')),
88 category='error')
88 category='error')
89 except RepositoryError as e:
89 except RepositoryError as e:
90 log.error(traceback.format_exc())
90 log.error(traceback.format_exc())
91 h.flash(safe_str(e), category='error')
91 h.flash(safe_str(e), category='error')
92 raise HTTPBadRequest()
92 raise HTTPBadRequest()
93
93
94 @LoginRequired()
94 @LoginRequired()
95 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
95 @HasRepoPermissionLevelDecorator('read')
96 'repository.admin')
97 def index(self, repo_name, revision=None, f_path=None):
96 def index(self, repo_name, revision=None, f_path=None):
98 # Fix URL after page size form submission via GET
97 # Fix URL after page size form submission via GET
99 # TODO: Somehow just don't send this extra junk in the GET URL
98 # TODO: Somehow just don't send this extra junk in the GET URL
100 if request.GET.get('set'):
99 if request.GET.get('set'):
101 request.GET.pop('set', None)
100 request.GET.pop('set', None)
102 if revision is None:
101 if revision is None:
103 raise HTTPFound(location=url('changelog_home', repo_name=repo_name, **request.GET))
102 raise HTTPFound(location=url('changelog_home', repo_name=repo_name, **request.GET))
104 raise HTTPFound(location=url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
103 raise HTTPFound(location=url('changelog_file_home', repo_name=repo_name, revision=revision, f_path=f_path, **request.GET))
105
104
106 limit = 2000
105 limit = 2000
107 default = 100
106 default = 100
108 if request.GET.get('size'):
107 if request.GET.get('size'):
109 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
108 c.size = max(min(safe_int(request.GET.get('size')), limit), 1)
110 session['changelog_size'] = c.size
109 session['changelog_size'] = c.size
111 session.save()
110 session.save()
112 else:
111 else:
113 c.size = int(session.get('changelog_size', default))
112 c.size = int(session.get('changelog_size', default))
114 # min size must be 1
113 # min size must be 1
115 c.size = max(c.size, 1)
114 c.size = max(c.size, 1)
116 p = safe_int(request.GET.get('page'), 1)
115 p = safe_int(request.GET.get('page'), 1)
117 branch_name = request.GET.get('branch', None)
116 branch_name = request.GET.get('branch', None)
118 if (branch_name and
117 if (branch_name and
119 branch_name not in c.db_repo_scm_instance.branches and
118 branch_name not in c.db_repo_scm_instance.branches and
120 branch_name not in c.db_repo_scm_instance.closed_branches and
119 branch_name not in c.db_repo_scm_instance.closed_branches and
121 not revision):
120 not revision):
122 raise HTTPFound(location=url('changelog_file_home', repo_name=c.repo_name,
121 raise HTTPFound(location=url('changelog_file_home', repo_name=c.repo_name,
123 revision=branch_name, f_path=f_path or ''))
122 revision=branch_name, f_path=f_path or ''))
124
123
125 if revision == 'tip':
124 if revision == 'tip':
126 revision = None
125 revision = None
127
126
128 c.changelog_for_path = f_path
127 c.changelog_for_path = f_path
129 try:
128 try:
130
129
131 if f_path:
130 if f_path:
132 log.debug('generating changelog for path %s', f_path)
131 log.debug('generating changelog for path %s', f_path)
133 # get the history for the file !
132 # get the history for the file !
134 tip_cs = c.db_repo_scm_instance.get_changeset()
133 tip_cs = c.db_repo_scm_instance.get_changeset()
135 try:
134 try:
136 collection = tip_cs.get_file_history(f_path)
135 collection = tip_cs.get_file_history(f_path)
137 except (NodeDoesNotExistError, ChangesetError):
136 except (NodeDoesNotExistError, ChangesetError):
138 #this node is not present at tip !
137 #this node is not present at tip !
139 try:
138 try:
140 cs = self.__get_cs(revision, repo_name)
139 cs = self.__get_cs(revision, repo_name)
141 collection = cs.get_file_history(f_path)
140 collection = cs.get_file_history(f_path)
142 except RepositoryError as e:
141 except RepositoryError as e:
143 h.flash(safe_str(e), category='warning')
142 h.flash(safe_str(e), category='warning')
144 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
143 raise HTTPFound(location=h.url('changelog_home', repo_name=repo_name))
145 collection = list(reversed(collection))
144 collection = list(reversed(collection))
146 else:
145 else:
147 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
146 collection = c.db_repo_scm_instance.get_changesets(start=0, end=revision,
148 branch_name=branch_name)
147 branch_name=branch_name)
149 c.total_cs = len(collection)
148 c.total_cs = len(collection)
150
149
151 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
150 c.pagination = RepoPage(collection, page=p, item_count=c.total_cs,
152 items_per_page=c.size, branch=branch_name,)
151 items_per_page=c.size, branch=branch_name,)
153
152
154 page_revisions = [x.raw_id for x in c.pagination]
153 page_revisions = [x.raw_id for x in c.pagination]
155 c.comments = c.db_repo.get_comments(page_revisions)
154 c.comments = c.db_repo.get_comments(page_revisions)
156 c.statuses = c.db_repo.statuses(page_revisions)
155 c.statuses = c.db_repo.statuses(page_revisions)
157 except EmptyRepositoryError as e:
156 except EmptyRepositoryError as e:
158 h.flash(safe_str(e), category='warning')
157 h.flash(safe_str(e), category='warning')
159 raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
158 raise HTTPFound(location=url('summary_home', repo_name=c.repo_name))
160 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
159 except (RepositoryError, ChangesetDoesNotExistError, Exception) as e:
161 log.error(traceback.format_exc())
160 log.error(traceback.format_exc())
162 h.flash(safe_str(e), category='error')
161 h.flash(safe_str(e), category='error')
163 raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
162 raise HTTPFound(location=url('changelog_home', repo_name=c.repo_name))
164
163
165 c.branch_name = branch_name
164 c.branch_name = branch_name
166 c.branch_filters = [('', _('None'))] + \
165 c.branch_filters = [('', _('None'))] + \
167 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
166 [(k, k) for k in c.db_repo_scm_instance.branches.keys()]
168 if c.db_repo_scm_instance.closed_branches:
167 if c.db_repo_scm_instance.closed_branches:
169 prefix = _('(closed)') + ' '
168 prefix = _('(closed)') + ' '
170 c.branch_filters += [('-', '-')] + \
169 c.branch_filters += [('-', '-')] + \
171 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
170 [(k, prefix + k) for k in c.db_repo_scm_instance.closed_branches.keys()]
172 revs = []
171 revs = []
173 if not f_path:
172 if not f_path:
174 revs = [x.revision for x in c.pagination]
173 revs = [x.revision for x in c.pagination]
175 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
174 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
176
175
177 c.revision = revision # requested revision ref
176 c.revision = revision # requested revision ref
178 c.first_revision = c.pagination[0] # pagination is never empty here!
177 c.first_revision = c.pagination[0] # pagination is never empty here!
179 return render('changelog/changelog.html')
178 return render('changelog/changelog.html')
180
179
181 @LoginRequired()
180 @LoginRequired()
182 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
181 @HasRepoPermissionLevelDecorator('read')
183 'repository.admin')
184 def changelog_details(self, cs):
182 def changelog_details(self, cs):
185 if request.environ.get('HTTP_X_PARTIAL_XHR'):
183 if request.environ.get('HTTP_X_PARTIAL_XHR'):
186 c.cs = c.db_repo_scm_instance.get_changeset(cs)
184 c.cs = c.db_repo_scm_instance.get_changeset(cs)
187 return render('changelog/changelog_details.html')
185 return render('changelog/changelog_details.html')
188 raise HTTPNotFound()
186 raise HTTPNotFound()
189
187
190 @LoginRequired()
188 @LoginRequired()
191 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
189 @HasRepoPermissionLevelDecorator('read')
192 'repository.admin')
193 def changelog_summary(self, repo_name):
190 def changelog_summary(self, repo_name):
194 if request.environ.get('HTTP_X_PARTIAL_XHR'):
191 if request.environ.get('HTTP_X_PARTIAL_XHR'):
195 _load_changelog_summary()
192 _load_changelog_summary()
196
193
197 return render('changelog/changelog_summary_data.html')
194 return render('changelog/changelog_summary_data.html')
198 raise HTTPNotFound()
195 raise HTTPNotFound()
@@ -1,472 +1,463 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.changeset
15 kallithea.controllers.changeset
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 changeset controller showing changes between revisions
18 changeset controller showing changes between revisions
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 25, 2010
22 :created_on: Apr 25, 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 from collections import defaultdict
30 from collections import defaultdict
31
31
32 from pylons import tmpl_context as c, request, response
32 from pylons import tmpl_context as c, request, response
33 from pylons.i18n.translation import _
33 from pylons.i18n.translation import _
34 from webob.exc import HTTPFound, HTTPForbidden, HTTPBadRequest, HTTPNotFound
34 from webob.exc import HTTPFound, HTTPForbidden, HTTPBadRequest, HTTPNotFound
35
35
36 from kallithea.lib.vcs.exceptions import RepositoryError, \
36 from kallithea.lib.vcs.exceptions import RepositoryError, \
37 ChangesetDoesNotExistError, EmptyRepositoryError
37 ChangesetDoesNotExistError, EmptyRepositoryError
38
38
39 from kallithea.lib.compat import json
39 from kallithea.lib.compat import json
40 import kallithea.lib.helpers as h
40 import kallithea.lib.helpers as h
41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
41 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator, \
42 NotAnonymous
42 NotAnonymous
43 from kallithea.lib.base import BaseRepoController, render, jsonify
43 from kallithea.lib.base import BaseRepoController, render, jsonify
44 from kallithea.lib.utils import action_logger
44 from kallithea.lib.utils import action_logger
45 from kallithea.lib.compat import OrderedDict
45 from kallithea.lib.compat import OrderedDict
46 from kallithea.lib import diffs
46 from kallithea.lib import diffs
47 from kallithea.model.db import ChangesetComment, ChangesetStatus
47 from kallithea.model.db import ChangesetComment, ChangesetStatus
48 from kallithea.model.comment import ChangesetCommentsModel
48 from kallithea.model.comment import ChangesetCommentsModel
49 from kallithea.model.changeset_status import ChangesetStatusModel
49 from kallithea.model.changeset_status import ChangesetStatusModel
50 from kallithea.model.meta import Session
50 from kallithea.model.meta import Session
51 from kallithea.model.repo import RepoModel
51 from kallithea.model.repo import RepoModel
52 from kallithea.lib.diffs import LimitedDiffContainer
52 from kallithea.lib.diffs import LimitedDiffContainer
53 from kallithea.lib.exceptions import StatusChangeOnClosedPullRequestError
53 from kallithea.lib.exceptions import StatusChangeOnClosedPullRequestError
54 from kallithea.lib.vcs.backends.base import EmptyChangeset
54 from kallithea.lib.vcs.backends.base import EmptyChangeset
55 from kallithea.lib.utils2 import safe_unicode
55 from kallithea.lib.utils2 import safe_unicode
56 from kallithea.lib.graphmod import graph_data
56 from kallithea.lib.graphmod import graph_data
57
57
58 log = logging.getLogger(__name__)
58 log = logging.getLogger(__name__)
59
59
60
60
61 def _update_with_GET(params, GET):
61 def _update_with_GET(params, GET):
62 for k in ['diff1', 'diff2', 'diff']:
62 for k in ['diff1', 'diff2', 'diff']:
63 params[k] += GET.getall(k)
63 params[k] += GET.getall(k)
64
64
65
65
66 def anchor_url(revision, path, GET):
66 def anchor_url(revision, path, GET):
67 fid = h.FID(revision, path)
67 fid = h.FID(revision, path)
68 return h.url.current(anchor=fid, **dict(GET))
68 return h.url.current(anchor=fid, **dict(GET))
69
69
70
70
71 def get_ignore_ws(fid, GET):
71 def get_ignore_ws(fid, GET):
72 ig_ws_global = GET.get('ignorews')
72 ig_ws_global = GET.get('ignorews')
73 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
73 ig_ws = filter(lambda k: k.startswith('WS'), GET.getall(fid))
74 if ig_ws:
74 if ig_ws:
75 try:
75 try:
76 return int(ig_ws[0].split(':')[-1])
76 return int(ig_ws[0].split(':')[-1])
77 except ValueError:
77 except ValueError:
78 raise HTTPBadRequest()
78 raise HTTPBadRequest()
79 return ig_ws_global
79 return ig_ws_global
80
80
81
81
82 def _ignorews_url(GET, fileid=None):
82 def _ignorews_url(GET, fileid=None):
83 fileid = str(fileid) if fileid else None
83 fileid = str(fileid) if fileid else None
84 params = defaultdict(list)
84 params = defaultdict(list)
85 _update_with_GET(params, GET)
85 _update_with_GET(params, GET)
86 lbl = _('Show whitespace')
86 lbl = _('Show whitespace')
87 ig_ws = get_ignore_ws(fileid, GET)
87 ig_ws = get_ignore_ws(fileid, GET)
88 ln_ctx = get_line_ctx(fileid, GET)
88 ln_ctx = get_line_ctx(fileid, GET)
89 # global option
89 # global option
90 if fileid is None:
90 if fileid is None:
91 if ig_ws is None:
91 if ig_ws is None:
92 params['ignorews'] += [1]
92 params['ignorews'] += [1]
93 lbl = _('Ignore whitespace')
93 lbl = _('Ignore whitespace')
94 ctx_key = 'context'
94 ctx_key = 'context'
95 ctx_val = ln_ctx
95 ctx_val = ln_ctx
96 # per file options
96 # per file options
97 else:
97 else:
98 if ig_ws is None:
98 if ig_ws is None:
99 params[fileid] += ['WS:1']
99 params[fileid] += ['WS:1']
100 lbl = _('Ignore whitespace')
100 lbl = _('Ignore whitespace')
101
101
102 ctx_key = fileid
102 ctx_key = fileid
103 ctx_val = 'C:%s' % ln_ctx
103 ctx_val = 'C:%s' % ln_ctx
104 # if we have passed in ln_ctx pass it along to our params
104 # if we have passed in ln_ctx pass it along to our params
105 if ln_ctx:
105 if ln_ctx:
106 params[ctx_key] += [ctx_val]
106 params[ctx_key] += [ctx_val]
107
107
108 params['anchor'] = fileid
108 params['anchor'] = fileid
109 icon = h.literal('<i class="icon-strike"></i>')
109 icon = h.literal('<i class="icon-strike"></i>')
110 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
110 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
111
111
112
112
113 def get_line_ctx(fid, GET):
113 def get_line_ctx(fid, GET):
114 ln_ctx_global = GET.get('context')
114 ln_ctx_global = GET.get('context')
115 if fid:
115 if fid:
116 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
116 ln_ctx = filter(lambda k: k.startswith('C'), GET.getall(fid))
117 else:
117 else:
118 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
118 _ln_ctx = filter(lambda k: k.startswith('C'), GET)
119 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
119 ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
120 if ln_ctx:
120 if ln_ctx:
121 ln_ctx = [ln_ctx]
121 ln_ctx = [ln_ctx]
122
122
123 if ln_ctx:
123 if ln_ctx:
124 retval = ln_ctx[0].split(':')[-1]
124 retval = ln_ctx[0].split(':')[-1]
125 else:
125 else:
126 retval = ln_ctx_global
126 retval = ln_ctx_global
127
127
128 try:
128 try:
129 return int(retval)
129 return int(retval)
130 except Exception:
130 except Exception:
131 return 3
131 return 3
132
132
133
133
134 def _context_url(GET, fileid=None):
134 def _context_url(GET, fileid=None):
135 """
135 """
136 Generates url for context lines
136 Generates url for context lines
137
137
138 :param fileid:
138 :param fileid:
139 """
139 """
140
140
141 fileid = str(fileid) if fileid else None
141 fileid = str(fileid) if fileid else None
142 ig_ws = get_ignore_ws(fileid, GET)
142 ig_ws = get_ignore_ws(fileid, GET)
143 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
143 ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
144
144
145 params = defaultdict(list)
145 params = defaultdict(list)
146 _update_with_GET(params, GET)
146 _update_with_GET(params, GET)
147
147
148 # global option
148 # global option
149 if fileid is None:
149 if fileid is None:
150 if ln_ctx > 0:
150 if ln_ctx > 0:
151 params['context'] += [ln_ctx]
151 params['context'] += [ln_ctx]
152
152
153 if ig_ws:
153 if ig_ws:
154 ig_ws_key = 'ignorews'
154 ig_ws_key = 'ignorews'
155 ig_ws_val = 1
155 ig_ws_val = 1
156
156
157 # per file option
157 # per file option
158 else:
158 else:
159 params[fileid] += ['C:%s' % ln_ctx]
159 params[fileid] += ['C:%s' % ln_ctx]
160 ig_ws_key = fileid
160 ig_ws_key = fileid
161 ig_ws_val = 'WS:%s' % 1
161 ig_ws_val = 'WS:%s' % 1
162
162
163 if ig_ws:
163 if ig_ws:
164 params[ig_ws_key] += [ig_ws_val]
164 params[ig_ws_key] += [ig_ws_val]
165
165
166 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
166 lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
167
167
168 params['anchor'] = fileid
168 params['anchor'] = fileid
169 icon = h.literal('<i class="icon-sort"></i>')
169 icon = h.literal('<i class="icon-sort"></i>')
170 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
170 return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
171
171
172
172
173 # Could perhaps be nice to have in the model but is too high level ...
173 # Could perhaps be nice to have in the model but is too high level ...
174 def create_comment(text, status, f_path, line_no, revision=None, pull_request_id=None, closing_pr=None):
174 def create_comment(text, status, f_path, line_no, revision=None, pull_request_id=None, closing_pr=None):
175 """Comment functionality shared between changesets and pullrequests"""
175 """Comment functionality shared between changesets and pullrequests"""
176 f_path = f_path or None
176 f_path = f_path or None
177 line_no = line_no or None
177 line_no = line_no or None
178
178
179 comment = ChangesetCommentsModel().create(
179 comment = ChangesetCommentsModel().create(
180 text=text,
180 text=text,
181 repo=c.db_repo.repo_id,
181 repo=c.db_repo.repo_id,
182 author=request.authuser.user_id,
182 author=request.authuser.user_id,
183 revision=revision,
183 revision=revision,
184 pull_request=pull_request_id,
184 pull_request=pull_request_id,
185 f_path=f_path,
185 f_path=f_path,
186 line_no=line_no,
186 line_no=line_no,
187 status_change=ChangesetStatus.get_status_lbl(status) if status else None,
187 status_change=ChangesetStatus.get_status_lbl(status) if status else None,
188 closing_pr=closing_pr,
188 closing_pr=closing_pr,
189 )
189 )
190
190
191 return comment
191 return comment
192
192
193
193
194 class ChangesetController(BaseRepoController):
194 class ChangesetController(BaseRepoController):
195
195
196 def __before__(self):
196 def __before__(self):
197 super(ChangesetController, self).__before__()
197 super(ChangesetController, self).__before__()
198 c.affected_files_cut_off = 60
198 c.affected_files_cut_off = 60
199
199
200 def __load_data(self):
200 def __load_data(self):
201 repo_model = RepoModel()
201 repo_model = RepoModel()
202 c.users_array = repo_model.get_users_js()
202 c.users_array = repo_model.get_users_js()
203 c.user_groups_array = repo_model.get_user_groups_js()
203 c.user_groups_array = repo_model.get_user_groups_js()
204
204
205 def _index(self, revision, method):
205 def _index(self, revision, method):
206 c.pull_request = None
206 c.pull_request = None
207 c.anchor_url = anchor_url
207 c.anchor_url = anchor_url
208 c.ignorews_url = _ignorews_url
208 c.ignorews_url = _ignorews_url
209 c.context_url = _context_url
209 c.context_url = _context_url
210 c.fulldiff = fulldiff = request.GET.get('fulldiff')
210 c.fulldiff = fulldiff = request.GET.get('fulldiff')
211 #get ranges of revisions if preset
211 #get ranges of revisions if preset
212 rev_range = revision.split('...')[:2]
212 rev_range = revision.split('...')[:2]
213 enable_comments = True
213 enable_comments = True
214 c.cs_repo = c.db_repo
214 c.cs_repo = c.db_repo
215 try:
215 try:
216 if len(rev_range) == 2:
216 if len(rev_range) == 2:
217 enable_comments = False
217 enable_comments = False
218 rev_start = rev_range[0]
218 rev_start = rev_range[0]
219 rev_end = rev_range[1]
219 rev_end = rev_range[1]
220 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
220 rev_ranges = c.db_repo_scm_instance.get_changesets(start=rev_start,
221 end=rev_end)
221 end=rev_end)
222 else:
222 else:
223 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
223 rev_ranges = [c.db_repo_scm_instance.get_changeset(revision)]
224
224
225 c.cs_ranges = list(rev_ranges)
225 c.cs_ranges = list(rev_ranges)
226 if not c.cs_ranges:
226 if not c.cs_ranges:
227 raise RepositoryError('Changeset range returned empty result')
227 raise RepositoryError('Changeset range returned empty result')
228
228
229 except (ChangesetDoesNotExistError, EmptyRepositoryError):
229 except (ChangesetDoesNotExistError, EmptyRepositoryError):
230 log.debug(traceback.format_exc())
230 log.debug(traceback.format_exc())
231 msg = _('Such revision does not exist for this repository')
231 msg = _('Such revision does not exist for this repository')
232 h.flash(msg, category='error')
232 h.flash(msg, category='error')
233 raise HTTPNotFound()
233 raise HTTPNotFound()
234
234
235 c.changes = OrderedDict()
235 c.changes = OrderedDict()
236
236
237 c.lines_added = 0 # count of lines added
237 c.lines_added = 0 # count of lines added
238 c.lines_deleted = 0 # count of lines removes
238 c.lines_deleted = 0 # count of lines removes
239
239
240 c.changeset_statuses = ChangesetStatus.STATUSES
240 c.changeset_statuses = ChangesetStatus.STATUSES
241 comments = dict()
241 comments = dict()
242 c.statuses = []
242 c.statuses = []
243 c.inline_comments = []
243 c.inline_comments = []
244 c.inline_cnt = 0
244 c.inline_cnt = 0
245
245
246 # Iterate over ranges (default changeset view is always one changeset)
246 # Iterate over ranges (default changeset view is always one changeset)
247 for changeset in c.cs_ranges:
247 for changeset in c.cs_ranges:
248 if method == 'show':
248 if method == 'show':
249 c.statuses.extend([ChangesetStatusModel().get_status(
249 c.statuses.extend([ChangesetStatusModel().get_status(
250 c.db_repo.repo_id, changeset.raw_id)])
250 c.db_repo.repo_id, changeset.raw_id)])
251
251
252 # Changeset comments
252 # Changeset comments
253 comments.update((com.comment_id, com)
253 comments.update((com.comment_id, com)
254 for com in ChangesetCommentsModel()
254 for com in ChangesetCommentsModel()
255 .get_comments(c.db_repo.repo_id,
255 .get_comments(c.db_repo.repo_id,
256 revision=changeset.raw_id))
256 revision=changeset.raw_id))
257
257
258 # Status change comments - mostly from pull requests
258 # Status change comments - mostly from pull requests
259 comments.update((st.comment_id, st.comment)
259 comments.update((st.comment_id, st.comment)
260 for st in ChangesetStatusModel()
260 for st in ChangesetStatusModel()
261 .get_statuses(c.db_repo.repo_id,
261 .get_statuses(c.db_repo.repo_id,
262 changeset.raw_id, with_revisions=True)
262 changeset.raw_id, with_revisions=True)
263 if st.comment_id is not None)
263 if st.comment_id is not None)
264
264
265 inlines = ChangesetCommentsModel() \
265 inlines = ChangesetCommentsModel() \
266 .get_inline_comments(c.db_repo.repo_id,
266 .get_inline_comments(c.db_repo.repo_id,
267 revision=changeset.raw_id)
267 revision=changeset.raw_id)
268 c.inline_comments.extend(inlines)
268 c.inline_comments.extend(inlines)
269
269
270 cs2 = changeset.raw_id
270 cs2 = changeset.raw_id
271 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
271 cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
272 context_lcl = get_line_ctx('', request.GET)
272 context_lcl = get_line_ctx('', request.GET)
273 ign_whitespace_lcl = get_ignore_ws('', request.GET)
273 ign_whitespace_lcl = get_ignore_ws('', request.GET)
274
274
275 _diff = c.db_repo_scm_instance.get_diff(cs1, cs2,
275 _diff = c.db_repo_scm_instance.get_diff(cs1, cs2,
276 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
276 ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
277 diff_limit = self.cut_off_limit if not fulldiff else None
277 diff_limit = self.cut_off_limit if not fulldiff else None
278 diff_processor = diffs.DiffProcessor(_diff,
278 diff_processor = diffs.DiffProcessor(_diff,
279 vcs=c.db_repo_scm_instance.alias,
279 vcs=c.db_repo_scm_instance.alias,
280 format='gitdiff',
280 format='gitdiff',
281 diff_limit=diff_limit)
281 diff_limit=diff_limit)
282 file_diff_data = []
282 file_diff_data = []
283 if method == 'show':
283 if method == 'show':
284 _parsed = diff_processor.prepare()
284 _parsed = diff_processor.prepare()
285 c.limited_diff = False
285 c.limited_diff = False
286 if isinstance(_parsed, LimitedDiffContainer):
286 if isinstance(_parsed, LimitedDiffContainer):
287 c.limited_diff = True
287 c.limited_diff = True
288 for f in _parsed:
288 for f in _parsed:
289 st = f['stats']
289 st = f['stats']
290 c.lines_added += st['added']
290 c.lines_added += st['added']
291 c.lines_deleted += st['deleted']
291 c.lines_deleted += st['deleted']
292 filename = f['filename']
292 filename = f['filename']
293 fid = h.FID(changeset.raw_id, filename)
293 fid = h.FID(changeset.raw_id, filename)
294 url_fid = h.FID('', filename)
294 url_fid = h.FID('', filename)
295 diff = diff_processor.as_html(enable_comments=enable_comments,
295 diff = diff_processor.as_html(enable_comments=enable_comments,
296 parsed_lines=[f])
296 parsed_lines=[f])
297 file_diff_data.append((fid, url_fid, f['operation'], f['old_filename'], filename, diff, st))
297 file_diff_data.append((fid, url_fid, f['operation'], f['old_filename'], filename, diff, st))
298 else:
298 else:
299 # downloads/raw we only need RAW diff nothing else
299 # downloads/raw we only need RAW diff nothing else
300 diff = diff_processor.as_raw()
300 diff = diff_processor.as_raw()
301 file_diff_data.append(('', None, None, None, diff, None))
301 file_diff_data.append(('', None, None, None, diff, None))
302 c.changes[changeset.raw_id] = (cs1, cs2, file_diff_data)
302 c.changes[changeset.raw_id] = (cs1, cs2, file_diff_data)
303
303
304 #sort comments in creation order
304 #sort comments in creation order
305 c.comments = [com for com_id, com in sorted(comments.items())]
305 c.comments = [com for com_id, com in sorted(comments.items())]
306
306
307 # count inline comments
307 # count inline comments
308 for __, lines in c.inline_comments:
308 for __, lines in c.inline_comments:
309 for comments in lines.values():
309 for comments in lines.values():
310 c.inline_cnt += len(comments)
310 c.inline_cnt += len(comments)
311
311
312 if len(c.cs_ranges) == 1:
312 if len(c.cs_ranges) == 1:
313 c.changeset = c.cs_ranges[0]
313 c.changeset = c.cs_ranges[0]
314 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
314 c.parent_tmpl = ''.join(['# Parent %s\n' % x.raw_id
315 for x in c.changeset.parents])
315 for x in c.changeset.parents])
316 if method == 'download':
316 if method == 'download':
317 response.content_type = 'text/plain'
317 response.content_type = 'text/plain'
318 response.content_disposition = 'attachment; filename=%s.diff' \
318 response.content_disposition = 'attachment; filename=%s.diff' \
319 % revision[:12]
319 % revision[:12]
320 return diff
320 return diff
321 elif method == 'patch':
321 elif method == 'patch':
322 response.content_type = 'text/plain'
322 response.content_type = 'text/plain'
323 c.diff = safe_unicode(diff)
323 c.diff = safe_unicode(diff)
324 return render('changeset/patch_changeset.html')
324 return render('changeset/patch_changeset.html')
325 elif method == 'raw':
325 elif method == 'raw':
326 response.content_type = 'text/plain'
326 response.content_type = 'text/plain'
327 return diff
327 return diff
328 elif method == 'show':
328 elif method == 'show':
329 self.__load_data()
329 self.__load_data()
330 if len(c.cs_ranges) == 1:
330 if len(c.cs_ranges) == 1:
331 return render('changeset/changeset.html')
331 return render('changeset/changeset.html')
332 else:
332 else:
333 c.cs_ranges_org = None
333 c.cs_ranges_org = None
334 c.cs_comments = {}
334 c.cs_comments = {}
335 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
335 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
336 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
336 c.jsdata = json.dumps(graph_data(c.db_repo_scm_instance, revs))
337 return render('changeset/changeset_range.html')
337 return render('changeset/changeset_range.html')
338
338
339 @LoginRequired()
339 @LoginRequired()
340 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
340 @HasRepoPermissionLevelDecorator('read')
341 'repository.admin')
342 def index(self, revision, method='show'):
341 def index(self, revision, method='show'):
343 return self._index(revision, method=method)
342 return self._index(revision, method=method)
344
343
345 @LoginRequired()
344 @LoginRequired()
346 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
345 @HasRepoPermissionLevelDecorator('read')
347 'repository.admin')
348 def changeset_raw(self, revision):
346 def changeset_raw(self, revision):
349 return self._index(revision, method='raw')
347 return self._index(revision, method='raw')
350
348
351 @LoginRequired()
349 @LoginRequired()
352 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
350 @HasRepoPermissionLevelDecorator('read')
353 'repository.admin')
354 def changeset_patch(self, revision):
351 def changeset_patch(self, revision):
355 return self._index(revision, method='patch')
352 return self._index(revision, method='patch')
356
353
357 @LoginRequired()
354 @LoginRequired()
358 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
355 @HasRepoPermissionLevelDecorator('read')
359 'repository.admin')
360 def changeset_download(self, revision):
356 def changeset_download(self, revision):
361 return self._index(revision, method='download')
357 return self._index(revision, method='download')
362
358
363 @LoginRequired()
359 @LoginRequired()
364 @NotAnonymous()
360 @NotAnonymous()
365 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
361 @HasRepoPermissionLevelDecorator('read')
366 'repository.admin')
367 @jsonify
362 @jsonify
368 def comment(self, repo_name, revision):
363 def comment(self, repo_name, revision):
369 assert request.environ.get('HTTP_X_PARTIAL_XHR')
364 assert request.environ.get('HTTP_X_PARTIAL_XHR')
370
365
371 status = request.POST.get('changeset_status')
366 status = request.POST.get('changeset_status')
372 text = request.POST.get('text', '').strip()
367 text = request.POST.get('text', '').strip()
373
368
374 c.comment = create_comment(
369 c.comment = create_comment(
375 text,
370 text,
376 status,
371 status,
377 revision=revision,
372 revision=revision,
378 f_path=request.POST.get('f_path'),
373 f_path=request.POST.get('f_path'),
379 line_no=request.POST.get('line'),
374 line_no=request.POST.get('line'),
380 )
375 )
381
376
382 # get status if set !
377 # get status if set !
383 if status:
378 if status:
384 # if latest status was from pull request and it's closed
379 # if latest status was from pull request and it's closed
385 # disallow changing status ! RLY?
380 # disallow changing status ! RLY?
386 try:
381 try:
387 ChangesetStatusModel().set_status(
382 ChangesetStatusModel().set_status(
388 c.db_repo.repo_id,
383 c.db_repo.repo_id,
389 status,
384 status,
390 request.authuser.user_id,
385 request.authuser.user_id,
391 c.comment,
386 c.comment,
392 revision=revision,
387 revision=revision,
393 dont_allow_on_closed_pull_request=True,
388 dont_allow_on_closed_pull_request=True,
394 )
389 )
395 except StatusChangeOnClosedPullRequestError:
390 except StatusChangeOnClosedPullRequestError:
396 log.debug('cannot change status on %s with closed pull request', revision)
391 log.debug('cannot change status on %s with closed pull request', revision)
397 raise HTTPBadRequest()
392 raise HTTPBadRequest()
398
393
399 action_logger(request.authuser,
394 action_logger(request.authuser,
400 'user_commented_revision:%s' % revision,
395 'user_commented_revision:%s' % revision,
401 c.db_repo, request.ip_addr, self.sa)
396 c.db_repo, request.ip_addr, self.sa)
402
397
403 Session().commit()
398 Session().commit()
404
399
405 data = {
400 data = {
406 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
401 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
407 }
402 }
408 if c.comment is not None:
403 if c.comment is not None:
409 data.update(c.comment.get_dict())
404 data.update(c.comment.get_dict())
410 data.update({'rendered_text':
405 data.update({'rendered_text':
411 render('changeset/changeset_comment_block.html')})
406 render('changeset/changeset_comment_block.html')})
412
407
413 return data
408 return data
414
409
415 @LoginRequired()
410 @LoginRequired()
416 @NotAnonymous()
411 @NotAnonymous()
417 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
412 @HasRepoPermissionLevelDecorator('read')
418 'repository.admin')
419 @jsonify
413 @jsonify
420 def delete_comment(self, repo_name, comment_id):
414 def delete_comment(self, repo_name, comment_id):
421 co = ChangesetComment.get_or_404(comment_id)
415 co = ChangesetComment.get_or_404(comment_id)
422 if co.repo.repo_name != repo_name:
416 if co.repo.repo_name != repo_name:
423 raise HTTPNotFound()
417 raise HTTPNotFound()
424 owner = co.author_id == request.authuser.user_id
418 owner = co.author_id == request.authuser.user_id
425 repo_admin = h.HasRepoPermissionAny('repository.admin')(repo_name)
419 repo_admin = h.HasRepoPermissionLevel('admin')(repo_name)
426 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
420 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
427 ChangesetCommentsModel().delete(comment=co)
421 ChangesetCommentsModel().delete(comment=co)
428 Session().commit()
422 Session().commit()
429 return True
423 return True
430 else:
424 else:
431 raise HTTPForbidden()
425 raise HTTPForbidden()
432
426
433 @LoginRequired()
427 @LoginRequired()
434 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
428 @HasRepoPermissionLevelDecorator('read')
435 'repository.admin')
436 @jsonify
429 @jsonify
437 def changeset_info(self, repo_name, revision):
430 def changeset_info(self, repo_name, revision):
438 if request.is_xhr:
431 if request.is_xhr:
439 try:
432 try:
440 return c.db_repo_scm_instance.get_changeset(revision)
433 return c.db_repo_scm_instance.get_changeset(revision)
441 except ChangesetDoesNotExistError as e:
434 except ChangesetDoesNotExistError as e:
442 return EmptyChangeset(message=str(e))
435 return EmptyChangeset(message=str(e))
443 else:
436 else:
444 raise HTTPBadRequest()
437 raise HTTPBadRequest()
445
438
446 @LoginRequired()
439 @LoginRequired()
447 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
440 @HasRepoPermissionLevelDecorator('read')
448 'repository.admin')
449 @jsonify
441 @jsonify
450 def changeset_children(self, repo_name, revision):
442 def changeset_children(self, repo_name, revision):
451 if request.is_xhr:
443 if request.is_xhr:
452 changeset = c.db_repo_scm_instance.get_changeset(revision)
444 changeset = c.db_repo_scm_instance.get_changeset(revision)
453 result = {"results": []}
445 result = {"results": []}
454 if changeset.children:
446 if changeset.children:
455 result = {"results": changeset.children}
447 result = {"results": changeset.children}
456 return result
448 return result
457 else:
449 else:
458 raise HTTPBadRequest()
450 raise HTTPBadRequest()
459
451
460 @LoginRequired()
452 @LoginRequired()
461 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
453 @HasRepoPermissionLevelDecorator('read')
462 'repository.admin')
463 @jsonify
454 @jsonify
464 def changeset_parents(self, repo_name, revision):
455 def changeset_parents(self, repo_name, revision):
465 if request.is_xhr:
456 if request.is_xhr:
466 changeset = c.db_repo_scm_instance.get_changeset(revision)
457 changeset = c.db_repo_scm_instance.get_changeset(revision)
467 result = {"results": []}
458 result = {"results": []}
468 if changeset.parents:
459 if changeset.parents:
469 result = {"results": changeset.parents}
460 result = {"results": changeset.parents}
470 return result
461 return result
471 else:
462 else:
472 raise HTTPBadRequest()
463 raise HTTPBadRequest()
@@ -1,298 +1,296 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.compare
15 kallithea.controllers.compare
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 compare controller showing differences between two
18 compare controller showing differences between two
19 repos, branches, bookmarks or tips
19 repos, branches, bookmarks or tips
20
20
21 This file was forked by the Kallithea project in July 2014.
21 This file was forked by the Kallithea project in July 2014.
22 Original author and date, and relevant copyright and licensing information is below:
22 Original author and date, and relevant copyright and licensing information is below:
23 :created_on: May 6, 2012
23 :created_on: May 6, 2012
24 :author: marcink
24 :author: marcink
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
25 :copyright: (c) 2013 RhodeCode GmbH, and others.
26 :license: GPLv3, see LICENSE.md for more details.
26 :license: GPLv3, see LICENSE.md for more details.
27 """
27 """
28
28
29
29
30 import logging
30 import logging
31 import re
31 import re
32
32
33 from pylons import request, tmpl_context as c
33 from pylons import request, tmpl_context as c
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPBadRequest, HTTPNotFound
35 from webob.exc import HTTPFound, HTTPBadRequest, HTTPNotFound
36
36
37 from kallithea.config.routing import url
37 from kallithea.config.routing import url
38 from kallithea.lib.utils2 import safe_str, safe_int
38 from kallithea.lib.utils2 import safe_str, safe_int
39 from kallithea.lib.vcs.utils.hgcompat import unionrepo
39 from kallithea.lib.vcs.utils.hgcompat import unionrepo
40 from kallithea.lib import helpers as h
40 from kallithea.lib import helpers as h
41 from kallithea.lib.base import BaseRepoController, render
41 from kallithea.lib.base import BaseRepoController, render
42 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
42 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
43 from kallithea.lib import diffs
43 from kallithea.lib import diffs
44 from kallithea.model.db import Repository
44 from kallithea.model.db import Repository
45 from kallithea.lib.diffs import LimitedDiffContainer
45 from kallithea.lib.diffs import LimitedDiffContainer
46 from kallithea.controllers.changeset import _ignorews_url, _context_url
46 from kallithea.controllers.changeset import _ignorews_url, _context_url
47 from kallithea.lib.graphmod import graph_data
47 from kallithea.lib.graphmod import graph_data
48 from kallithea.lib.compat import json, OrderedDict
48 from kallithea.lib.compat import json, OrderedDict
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class CompareController(BaseRepoController):
53 class CompareController(BaseRepoController):
54
54
55 def __before__(self):
55 def __before__(self):
56 super(CompareController, self).__before__()
56 super(CompareController, self).__before__()
57
57
58 # The base repository has already been retrieved.
58 # The base repository has already been retrieved.
59 c.a_repo = c.db_repo
59 c.a_repo = c.db_repo
60
60
61 # Retrieve the "changeset" repository (default: same as base).
61 # Retrieve the "changeset" repository (default: same as base).
62 other_repo = request.GET.get('other_repo', None)
62 other_repo = request.GET.get('other_repo', None)
63 if other_repo is None:
63 if other_repo is None:
64 c.cs_repo = c.a_repo
64 c.cs_repo = c.a_repo
65 else:
65 else:
66 c.cs_repo = Repository.get_by_repo_name(other_repo)
66 c.cs_repo = Repository.get_by_repo_name(other_repo)
67 if c.cs_repo is None:
67 if c.cs_repo is None:
68 msg = _('Could not find other repository %s') % other_repo
68 msg = _('Could not find other repository %s') % other_repo
69 h.flash(msg, category='error')
69 h.flash(msg, category='error')
70 raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name))
70 raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name))
71
71
72 # Verify that it's even possible to compare these two repositories.
72 # Verify that it's even possible to compare these two repositories.
73 if c.a_repo.scm_instance.alias != c.cs_repo.scm_instance.alias:
73 if c.a_repo.scm_instance.alias != c.cs_repo.scm_instance.alias:
74 msg = _('Cannot compare repositories of different types')
74 msg = _('Cannot compare repositories of different types')
75 h.flash(msg, category='error')
75 h.flash(msg, category='error')
76 raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name))
76 raise HTTPFound(location=url('compare_home', repo_name=c.a_repo.repo_name))
77
77
78 @staticmethod
78 @staticmethod
79 def _get_changesets(alias, org_repo, org_rev, other_repo, other_rev):
79 def _get_changesets(alias, org_repo, org_rev, other_repo, other_rev):
80 """
80 """
81 Returns lists of changesets that can be merged from org_repo@org_rev
81 Returns lists of changesets that can be merged from org_repo@org_rev
82 to other_repo@other_rev
82 to other_repo@other_rev
83 ... and the other way
83 ... and the other way
84 ... and the ancestors that would be used for merge
84 ... and the ancestors that would be used for merge
85
85
86 :param org_repo: repo object, that is most likely the original repo we forked from
86 :param org_repo: repo object, that is most likely the original repo we forked from
87 :param org_rev: the revision we want our compare to be made
87 :param org_rev: the revision we want our compare to be made
88 :param other_repo: repo object, most likely the fork of org_repo. It has
88 :param other_repo: repo object, most likely the fork of org_repo. It has
89 all changesets that we need to obtain
89 all changesets that we need to obtain
90 :param other_rev: revision we want out compare to be made on other_repo
90 :param other_rev: revision we want out compare to be made on other_repo
91 """
91 """
92 ancestors = None
92 ancestors = None
93 if org_rev == other_rev:
93 if org_rev == other_rev:
94 org_changesets = []
94 org_changesets = []
95 other_changesets = []
95 other_changesets = []
96
96
97 elif alias == 'hg':
97 elif alias == 'hg':
98 #case two independent repos
98 #case two independent repos
99 if org_repo != other_repo:
99 if org_repo != other_repo:
100 hgrepo = unionrepo.unionrepository(other_repo.baseui,
100 hgrepo = unionrepo.unionrepository(other_repo.baseui,
101 other_repo.path,
101 other_repo.path,
102 org_repo.path)
102 org_repo.path)
103 # all ancestors of other_rev will be in other_repo and
103 # all ancestors of other_rev will be in other_repo and
104 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
104 # rev numbers from hgrepo can be used in other_repo - org_rev ancestors cannot
105
105
106 #no remote compare do it on the same repository
106 #no remote compare do it on the same repository
107 else:
107 else:
108 hgrepo = other_repo._repo
108 hgrepo = other_repo._repo
109
109
110 ancestors = [hgrepo[ancestor].hex() for ancestor in
110 ancestors = [hgrepo[ancestor].hex() for ancestor in
111 hgrepo.revs("id(%s) & ::id(%s)", other_rev, org_rev)]
111 hgrepo.revs("id(%s) & ::id(%s)", other_rev, org_rev)]
112 if ancestors:
112 if ancestors:
113 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
113 log.debug("shortcut found: %s is already an ancestor of %s", other_rev, org_rev)
114 else:
114 else:
115 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
115 log.debug("no shortcut found: %s is not an ancestor of %s", other_rev, org_rev)
116 ancestors = [hgrepo[ancestor].hex() for ancestor in
116 ancestors = [hgrepo[ancestor].hex() for ancestor in
117 hgrepo.revs("heads(::id(%s) & ::id(%s))", org_rev, other_rev)] # FIXME: expensive!
117 hgrepo.revs("heads(::id(%s) & ::id(%s))", org_rev, other_rev)] # FIXME: expensive!
118
118
119 other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
119 other_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
120 other_rev, org_rev, org_rev)
120 other_rev, org_rev, org_rev)
121 other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
121 other_changesets = [other_repo.get_changeset(rev) for rev in other_revs]
122 org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
122 org_revs = hgrepo.revs("ancestors(id(%s)) and not ancestors(id(%s)) and not id(%s)",
123 org_rev, other_rev, other_rev)
123 org_rev, other_rev, other_rev)
124 org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
124 org_changesets = [org_repo.get_changeset(hgrepo[rev].hex()) for rev in org_revs]
125
125
126 elif alias == 'git':
126 elif alias == 'git':
127 if org_repo != other_repo:
127 if org_repo != other_repo:
128 from dulwich.repo import Repo
128 from dulwich.repo import Repo
129 from dulwich.client import SubprocessGitClient
129 from dulwich.client import SubprocessGitClient
130
130
131 gitrepo = Repo(org_repo.path)
131 gitrepo = Repo(org_repo.path)
132 SubprocessGitClient(thin_packs=False).fetch(safe_str(other_repo.path), gitrepo)
132 SubprocessGitClient(thin_packs=False).fetch(safe_str(other_repo.path), gitrepo)
133
133
134 gitrepo_remote = Repo(other_repo.path)
134 gitrepo_remote = Repo(other_repo.path)
135 SubprocessGitClient(thin_packs=False).fetch(safe_str(org_repo.path), gitrepo_remote)
135 SubprocessGitClient(thin_packs=False).fetch(safe_str(org_repo.path), gitrepo_remote)
136
136
137 revs = []
137 revs = []
138 for x in gitrepo_remote.get_walker(include=[other_rev],
138 for x in gitrepo_remote.get_walker(include=[other_rev],
139 exclude=[org_rev]):
139 exclude=[org_rev]):
140 revs.append(x.commit.id)
140 revs.append(x.commit.id)
141
141
142 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
142 other_changesets = [other_repo.get_changeset(rev) for rev in reversed(revs)]
143 if other_changesets:
143 if other_changesets:
144 ancestors = [other_changesets[0].parents[0].raw_id]
144 ancestors = [other_changesets[0].parents[0].raw_id]
145 else:
145 else:
146 # no changesets from other repo, ancestor is the other_rev
146 # no changesets from other repo, ancestor is the other_rev
147 ancestors = [other_rev]
147 ancestors = [other_rev]
148
148
149 gitrepo.close()
149 gitrepo.close()
150 gitrepo_remote.close()
150 gitrepo_remote.close()
151
151
152 else:
152 else:
153 so, se = org_repo.run_git_command(
153 so, se = org_repo.run_git_command(
154 ['log', '--reverse', '--pretty=format:%H',
154 ['log', '--reverse', '--pretty=format:%H',
155 '-s', '%s..%s' % (org_rev, other_rev)]
155 '-s', '%s..%s' % (org_rev, other_rev)]
156 )
156 )
157 other_changesets = [org_repo.get_changeset(cs)
157 other_changesets = [org_repo.get_changeset(cs)
158 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
158 for cs in re.findall(r'[0-9a-fA-F]{40}', so)]
159 so, se = org_repo.run_git_command(
159 so, se = org_repo.run_git_command(
160 ['merge-base', org_rev, other_rev]
160 ['merge-base', org_rev, other_rev]
161 )
161 )
162 ancestors = [re.findall(r'[0-9a-fA-F]{40}', so)[0]]
162 ancestors = [re.findall(r'[0-9a-fA-F]{40}', so)[0]]
163 org_changesets = []
163 org_changesets = []
164
164
165 else:
165 else:
166 raise Exception('Bad alias only git and hg is allowed')
166 raise Exception('Bad alias only git and hg is allowed')
167
167
168 return other_changesets, org_changesets, ancestors
168 return other_changesets, org_changesets, ancestors
169
169
170 @LoginRequired()
170 @LoginRequired()
171 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
171 @HasRepoPermissionLevelDecorator('read')
172 'repository.admin')
173 def index(self, repo_name):
172 def index(self, repo_name):
174 c.compare_home = True
173 c.compare_home = True
175 c.a_ref_name = c.cs_ref_name = _('Select changeset')
174 c.a_ref_name = c.cs_ref_name = _('Select changeset')
176 return render('compare/compare_diff.html')
175 return render('compare/compare_diff.html')
177
176
178 @LoginRequired()
177 @LoginRequired()
179 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
178 @HasRepoPermissionLevelDecorator('read')
180 'repository.admin')
181 def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
179 def compare(self, repo_name, org_ref_type, org_ref_name, other_ref_type, other_ref_name):
182 org_ref_name = org_ref_name.strip()
180 org_ref_name = org_ref_name.strip()
183 other_ref_name = other_ref_name.strip()
181 other_ref_name = other_ref_name.strip()
184
182
185 # If merge is True:
183 # If merge is True:
186 # Show what org would get if merged with other:
184 # Show what org would get if merged with other:
187 # List changesets that are ancestors of other but not of org.
185 # List changesets that are ancestors of other but not of org.
188 # New changesets in org is thus ignored.
186 # New changesets in org is thus ignored.
189 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
187 # Diff will be from common ancestor, and merges of org to other will thus be ignored.
190 # If merge is False:
188 # If merge is False:
191 # Make a raw diff from org to other, no matter if related or not.
189 # Make a raw diff from org to other, no matter if related or not.
192 # Changesets in one and not in the other will be ignored
190 # Changesets in one and not in the other will be ignored
193 merge = bool(request.GET.get('merge'))
191 merge = bool(request.GET.get('merge'))
194 # fulldiff disables cut_off_limit
192 # fulldiff disables cut_off_limit
195 c.fulldiff = request.GET.get('fulldiff')
193 c.fulldiff = request.GET.get('fulldiff')
196 # partial uses compare_cs.html template directly
194 # partial uses compare_cs.html template directly
197 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
195 partial = request.environ.get('HTTP_X_PARTIAL_XHR')
198 # as_form puts hidden input field with changeset revisions
196 # as_form puts hidden input field with changeset revisions
199 c.as_form = partial and request.GET.get('as_form')
197 c.as_form = partial and request.GET.get('as_form')
200 # swap url for compare_diff page - never partial and never as_form
198 # swap url for compare_diff page - never partial and never as_form
201 c.swap_url = h.url('compare_url',
199 c.swap_url = h.url('compare_url',
202 repo_name=c.cs_repo.repo_name,
200 repo_name=c.cs_repo.repo_name,
203 org_ref_type=other_ref_type, org_ref_name=other_ref_name,
201 org_ref_type=other_ref_type, org_ref_name=other_ref_name,
204 other_repo=c.a_repo.repo_name,
202 other_repo=c.a_repo.repo_name,
205 other_ref_type=org_ref_type, other_ref_name=org_ref_name,
203 other_ref_type=org_ref_type, other_ref_name=org_ref_name,
206 merge=merge or '')
204 merge=merge or '')
207
205
208 # set callbacks for generating markup for icons
206 # set callbacks for generating markup for icons
209 c.ignorews_url = _ignorews_url
207 c.ignorews_url = _ignorews_url
210 c.context_url = _context_url
208 c.context_url = _context_url
211 ignore_whitespace = request.GET.get('ignorews') == '1'
209 ignore_whitespace = request.GET.get('ignorews') == '1'
212 line_context = safe_int(request.GET.get('context'), 3)
210 line_context = safe_int(request.GET.get('context'), 3)
213
211
214 c.a_rev = self._get_ref_rev(c.a_repo, org_ref_type, org_ref_name,
212 c.a_rev = self._get_ref_rev(c.a_repo, org_ref_type, org_ref_name,
215 returnempty=True)
213 returnempty=True)
216 c.cs_rev = self._get_ref_rev(c.cs_repo, other_ref_type, other_ref_name)
214 c.cs_rev = self._get_ref_rev(c.cs_repo, other_ref_type, other_ref_name)
217
215
218 c.compare_home = False
216 c.compare_home = False
219 c.a_ref_name = org_ref_name
217 c.a_ref_name = org_ref_name
220 c.a_ref_type = org_ref_type
218 c.a_ref_type = org_ref_type
221 c.cs_ref_name = other_ref_name
219 c.cs_ref_name = other_ref_name
222 c.cs_ref_type = other_ref_type
220 c.cs_ref_type = other_ref_type
223
221
224 c.cs_ranges, c.cs_ranges_org, c.ancestors = self._get_changesets(
222 c.cs_ranges, c.cs_ranges_org, c.ancestors = self._get_changesets(
225 c.a_repo.scm_instance.alias, c.a_repo.scm_instance, c.a_rev,
223 c.a_repo.scm_instance.alias, c.a_repo.scm_instance, c.a_rev,
226 c.cs_repo.scm_instance, c.cs_rev)
224 c.cs_repo.scm_instance, c.cs_rev)
227 raw_ids = [x.raw_id for x in c.cs_ranges]
225 raw_ids = [x.raw_id for x in c.cs_ranges]
228 c.cs_comments = c.cs_repo.get_comments(raw_ids)
226 c.cs_comments = c.cs_repo.get_comments(raw_ids)
229 c.statuses = c.cs_repo.statuses(raw_ids)
227 c.statuses = c.cs_repo.statuses(raw_ids)
230
228
231 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
229 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
232 c.jsdata = json.dumps(graph_data(c.cs_repo.scm_instance, revs))
230 c.jsdata = json.dumps(graph_data(c.cs_repo.scm_instance, revs))
233
231
234 if partial:
232 if partial:
235 return render('compare/compare_cs.html')
233 return render('compare/compare_cs.html')
236
234
237 org_repo = c.a_repo
235 org_repo = c.a_repo
238 other_repo = c.cs_repo
236 other_repo = c.cs_repo
239
237
240 if merge:
238 if merge:
241 rev1 = msg = None
239 rev1 = msg = None
242 if not c.cs_ranges:
240 if not c.cs_ranges:
243 msg = _('Cannot show empty diff')
241 msg = _('Cannot show empty diff')
244 elif not c.ancestors:
242 elif not c.ancestors:
245 msg = _('No ancestor found for merge diff')
243 msg = _('No ancestor found for merge diff')
246 elif len(c.ancestors) == 1:
244 elif len(c.ancestors) == 1:
247 rev1 = c.ancestors[0]
245 rev1 = c.ancestors[0]
248 else:
246 else:
249 msg = _('Multiple merge ancestors found for merge compare')
247 msg = _('Multiple merge ancestors found for merge compare')
250 if rev1 is None:
248 if rev1 is None:
251 h.flash(msg, category='error')
249 h.flash(msg, category='error')
252 log.error(msg)
250 log.error(msg)
253 raise HTTPNotFound
251 raise HTTPNotFound
254
252
255 # case we want a simple diff without incoming changesets,
253 # case we want a simple diff without incoming changesets,
256 # previewing what will be merged.
254 # previewing what will be merged.
257 # Make the diff on the other repo (which is known to have other_rev)
255 # Make the diff on the other repo (which is known to have other_rev)
258 log.debug('Using ancestor %s as rev1 instead of %s',
256 log.debug('Using ancestor %s as rev1 instead of %s',
259 rev1, c.a_rev)
257 rev1, c.a_rev)
260 org_repo = other_repo
258 org_repo = other_repo
261 else: # comparing tips, not necessarily linearly related
259 else: # comparing tips, not necessarily linearly related
262 if org_repo != other_repo:
260 if org_repo != other_repo:
263 # TODO: we could do this by using hg unionrepo
261 # TODO: we could do this by using hg unionrepo
264 log.error('cannot compare across repos %s and %s', org_repo, other_repo)
262 log.error('cannot compare across repos %s and %s', org_repo, other_repo)
265 h.flash(_('Cannot compare repositories without using common ancestor'), category='error')
263 h.flash(_('Cannot compare repositories without using common ancestor'), category='error')
266 raise HTTPBadRequest
264 raise HTTPBadRequest
267 rev1 = c.a_rev
265 rev1 = c.a_rev
268
266
269 diff_limit = self.cut_off_limit if not c.fulldiff else None
267 diff_limit = self.cut_off_limit if not c.fulldiff else None
270
268
271 log.debug('running diff between %s and %s in %s',
269 log.debug('running diff between %s and %s in %s',
272 rev1, c.cs_rev, org_repo.scm_instance.path)
270 rev1, c.cs_rev, org_repo.scm_instance.path)
273 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.cs_rev,
271 txtdiff = org_repo.scm_instance.get_diff(rev1=rev1, rev2=c.cs_rev,
274 ignore_whitespace=ignore_whitespace,
272 ignore_whitespace=ignore_whitespace,
275 context=line_context)
273 context=line_context)
276
274
277 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
275 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
278 diff_limit=diff_limit)
276 diff_limit=diff_limit)
279 _parsed = diff_processor.prepare()
277 _parsed = diff_processor.prepare()
280
278
281 c.limited_diff = False
279 c.limited_diff = False
282 if isinstance(_parsed, LimitedDiffContainer):
280 if isinstance(_parsed, LimitedDiffContainer):
283 c.limited_diff = True
281 c.limited_diff = True
284
282
285 c.file_diff_data = []
283 c.file_diff_data = []
286 c.lines_added = 0
284 c.lines_added = 0
287 c.lines_deleted = 0
285 c.lines_deleted = 0
288 for f in _parsed:
286 for f in _parsed:
289 st = f['stats']
287 st = f['stats']
290 c.lines_added += st['added']
288 c.lines_added += st['added']
291 c.lines_deleted += st['deleted']
289 c.lines_deleted += st['deleted']
292 filename = f['filename']
290 filename = f['filename']
293 fid = h.FID('', filename)
291 fid = h.FID('', filename)
294 diff = diff_processor.as_html(enable_comments=False,
292 diff = diff_processor.as_html(enable_comments=False,
295 parsed_lines=[f])
293 parsed_lines=[f])
296 c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, diff, st))
294 c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, diff, st))
297
295
298 return render('compare/compare_diff.html')
296 return render('compare/compare_diff.html')
@@ -1,173 +1,172 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.feed
15 kallithea.controllers.feed
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Feed controller for Kallithea
18 Feed 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 23, 2010
22 :created_on: Apr 23, 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
30
31 from pylons import response, tmpl_context as c
31 from pylons import response, tmpl_context as c
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33
33
34 from beaker.cache import cache_region, region_invalidate
34 from beaker.cache import cache_region, region_invalidate
35 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
35 from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
36
36
37 from kallithea import CONFIG
37 from kallithea import CONFIG
38 from kallithea.lib import helpers as h
38 from kallithea.lib import helpers as h
39 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
39 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
40 from kallithea.lib.base import BaseRepoController
40 from kallithea.lib.base import BaseRepoController
41 from kallithea.lib.diffs import DiffProcessor, LimitedDiffContainer
41 from kallithea.lib.diffs import DiffProcessor, LimitedDiffContainer
42 from kallithea.model.db import CacheInvalidation
42 from kallithea.model.db import CacheInvalidation
43 from kallithea.lib.utils2 import safe_int, str2bool, safe_unicode
43 from kallithea.lib.utils2 import safe_int, str2bool, safe_unicode
44
44
45 log = logging.getLogger(__name__)
45 log = logging.getLogger(__name__)
46
46
47
47
48 language = 'en-us'
48 language = 'en-us'
49 ttl = "5"
49 ttl = "5"
50
50
51
51
52 class FeedController(BaseRepoController):
52 class FeedController(BaseRepoController):
53
53
54 @LoginRequired(api_access=True)
54 @LoginRequired(api_access=True)
55 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
55 @HasRepoPermissionLevelDecorator('read')
56 'repository.admin')
57 def __before__(self):
56 def __before__(self):
58 super(FeedController, self).__before__()
57 super(FeedController, self).__before__()
59
58
60 def _get_title(self, cs):
59 def _get_title(self, cs):
61 return h.shorter(cs.message, 160)
60 return h.shorter(cs.message, 160)
62
61
63 def __changes(self, cs):
62 def __changes(self, cs):
64 changes = []
63 changes = []
65 rss_cut_off_limit = safe_int(CONFIG.get('rss_cut_off_limit', 32 * 1024))
64 rss_cut_off_limit = safe_int(CONFIG.get('rss_cut_off_limit', 32 * 1024))
66 diff_processor = DiffProcessor(cs.diff(),
65 diff_processor = DiffProcessor(cs.diff(),
67 diff_limit=rss_cut_off_limit)
66 diff_limit=rss_cut_off_limit)
68 _parsed = diff_processor.prepare(inline_diff=False)
67 _parsed = diff_processor.prepare(inline_diff=False)
69 limited_diff = False
68 limited_diff = False
70 if isinstance(_parsed, LimitedDiffContainer):
69 if isinstance(_parsed, LimitedDiffContainer):
71 limited_diff = True
70 limited_diff = True
72
71
73 for st in _parsed:
72 for st in _parsed:
74 st.update({'added': st['stats']['added'],
73 st.update({'added': st['stats']['added'],
75 'removed': st['stats']['deleted']})
74 'removed': st['stats']['deleted']})
76 changes.append('\n %(operation)s %(filename)s '
75 changes.append('\n %(operation)s %(filename)s '
77 '(%(added)s lines added, %(removed)s lines removed)'
76 '(%(added)s lines added, %(removed)s lines removed)'
78 % st)
77 % st)
79 if limited_diff:
78 if limited_diff:
80 changes = changes + ['\n ' +
79 changes = changes + ['\n ' +
81 _('Changeset was too big and was cut off...')]
80 _('Changeset was too big and was cut off...')]
82 return diff_processor, changes
81 return diff_processor, changes
83
82
84 def __get_desc(self, cs):
83 def __get_desc(self, cs):
85 desc_msg = [(_('%s committed on %s')
84 desc_msg = [(_('%s committed on %s')
86 % (h.person(cs.author), h.fmt_date(cs.date))) + '<br/>']
85 % (h.person(cs.author), h.fmt_date(cs.date))) + '<br/>']
87 #branches, tags, bookmarks
86 #branches, tags, bookmarks
88 if cs.branch:
87 if cs.branch:
89 desc_msg.append('branch: %s<br/>' % cs.branch)
88 desc_msg.append('branch: %s<br/>' % cs.branch)
90 if h.is_hg(c.db_repo_scm_instance):
89 if h.is_hg(c.db_repo_scm_instance):
91 for book in cs.bookmarks:
90 for book in cs.bookmarks:
92 desc_msg.append('bookmark: %s<br/>' % book)
91 desc_msg.append('bookmark: %s<br/>' % book)
93 for tag in cs.tags:
92 for tag in cs.tags:
94 desc_msg.append('tag: %s<br/>' % tag)
93 desc_msg.append('tag: %s<br/>' % tag)
95 diff_processor, changes = self.__changes(cs)
94 diff_processor, changes = self.__changes(cs)
96 # rev link
95 # rev link
97 _url = h.canonical_url('changeset_home', repo_name=c.db_repo.repo_name,
96 _url = h.canonical_url('changeset_home', repo_name=c.db_repo.repo_name,
98 revision=cs.raw_id)
97 revision=cs.raw_id)
99 desc_msg.append('changeset: <a href="%s">%s</a>' % (_url, cs.raw_id[:8]))
98 desc_msg.append('changeset: <a href="%s">%s</a>' % (_url, cs.raw_id[:8]))
100
99
101 desc_msg.append('<pre>')
100 desc_msg.append('<pre>')
102 desc_msg.append(h.urlify_text(cs.message))
101 desc_msg.append(h.urlify_text(cs.message))
103 desc_msg.append('\n')
102 desc_msg.append('\n')
104 desc_msg.extend(changes)
103 desc_msg.extend(changes)
105 if str2bool(CONFIG.get('rss_include_diff', False)):
104 if str2bool(CONFIG.get('rss_include_diff', False)):
106 desc_msg.append('\n\n')
105 desc_msg.append('\n\n')
107 desc_msg.append(diff_processor.as_raw())
106 desc_msg.append(diff_processor.as_raw())
108 desc_msg.append('</pre>')
107 desc_msg.append('</pre>')
109 return map(safe_unicode, desc_msg)
108 return map(safe_unicode, desc_msg)
110
109
111 def atom(self, repo_name):
110 def atom(self, repo_name):
112 """Produce an atom-1.0 feed via feedgenerator module"""
111 """Produce an atom-1.0 feed via feedgenerator module"""
113
112
114 @cache_region('long_term', '_get_feed_from_cache')
113 @cache_region('long_term', '_get_feed_from_cache')
115 def _get_feed_from_cache(key, kind):
114 def _get_feed_from_cache(key, kind):
116 feed = Atom1Feed(
115 feed = Atom1Feed(
117 title=_('%s %s feed') % (c.site_name, repo_name),
116 title=_('%s %s feed') % (c.site_name, repo_name),
118 link=h.canonical_url('summary_home', repo_name=repo_name),
117 link=h.canonical_url('summary_home', repo_name=repo_name),
119 description=_('Changes on %s repository') % repo_name,
118 description=_('Changes on %s repository') % repo_name,
120 language=language,
119 language=language,
121 ttl=ttl
120 ttl=ttl
122 )
121 )
123
122
124 rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
123 rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
125 for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])):
124 for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])):
126 feed.add_item(title=self._get_title(cs),
125 feed.add_item(title=self._get_title(cs),
127 link=h.canonical_url('changeset_home', repo_name=repo_name,
126 link=h.canonical_url('changeset_home', repo_name=repo_name,
128 revision=cs.raw_id),
127 revision=cs.raw_id),
129 author_name=cs.author,
128 author_name=cs.author,
130 description=''.join(self.__get_desc(cs)),
129 description=''.join(self.__get_desc(cs)),
131 pubdate=cs.date,
130 pubdate=cs.date,
132 )
131 )
133
132
134 response.content_type = feed.mime_type
133 response.content_type = feed.mime_type
135 return feed.writeString('utf-8')
134 return feed.writeString('utf-8')
136
135
137 kind = 'ATOM'
136 kind = 'ATOM'
138 valid = CacheInvalidation.test_and_set_valid(repo_name, kind)
137 valid = CacheInvalidation.test_and_set_valid(repo_name, kind)
139 if not valid:
138 if not valid:
140 region_invalidate(_get_feed_from_cache, None, '_get_feed_from_cache', repo_name, kind)
139 region_invalidate(_get_feed_from_cache, None, '_get_feed_from_cache', repo_name, kind)
141 return _get_feed_from_cache(repo_name, kind)
140 return _get_feed_from_cache(repo_name, kind)
142
141
143 def rss(self, repo_name):
142 def rss(self, repo_name):
144 """Produce an rss2 feed via feedgenerator module"""
143 """Produce an rss2 feed via feedgenerator module"""
145
144
146 @cache_region('long_term', '_get_feed_from_cache')
145 @cache_region('long_term', '_get_feed_from_cache')
147 def _get_feed_from_cache(key, kind):
146 def _get_feed_from_cache(key, kind):
148 feed = Rss201rev2Feed(
147 feed = Rss201rev2Feed(
149 title=_('%s %s feed') % (c.site_name, repo_name),
148 title=_('%s %s feed') % (c.site_name, repo_name),
150 link=h.canonical_url('summary_home', repo_name=repo_name),
149 link=h.canonical_url('summary_home', repo_name=repo_name),
151 description=_('Changes on %s repository') % repo_name,
150 description=_('Changes on %s repository') % repo_name,
152 language=language,
151 language=language,
153 ttl=ttl
152 ttl=ttl
154 )
153 )
155
154
156 rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
155 rss_items_per_page = safe_int(CONFIG.get('rss_items_per_page', 20))
157 for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])):
156 for cs in reversed(list(c.db_repo_scm_instance[-rss_items_per_page:])):
158 feed.add_item(title=self._get_title(cs),
157 feed.add_item(title=self._get_title(cs),
159 link=h.canonical_url('changeset_home', repo_name=repo_name,
158 link=h.canonical_url('changeset_home', repo_name=repo_name,
160 revision=cs.raw_id),
159 revision=cs.raw_id),
161 author_name=cs.author,
160 author_name=cs.author,
162 description=''.join(self.__get_desc(cs)),
161 description=''.join(self.__get_desc(cs)),
163 pubdate=cs.date,
162 pubdate=cs.date,
164 )
163 )
165
164
166 response.content_type = feed.mime_type
165 response.content_type = feed.mime_type
167 return feed.writeString('utf-8')
166 return feed.writeString('utf-8')
168
167
169 kind = 'RSS'
168 kind = 'RSS'
170 valid = CacheInvalidation.test_and_set_valid(repo_name, kind)
169 valid = CacheInvalidation.test_and_set_valid(repo_name, kind)
171 if not valid:
170 if not valid:
172 region_invalidate(_get_feed_from_cache, None, '_get_feed_from_cache', repo_name, kind)
171 region_invalidate(_get_feed_from_cache, None, '_get_feed_from_cache', repo_name, kind)
173 return _get_feed_from_cache(repo_name, kind)
172 return _get_feed_from_cache(repo_name, kind)
@@ -1,792 +1,783 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.files
15 kallithea.controllers.files
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Files controller for Kallithea
18 Files 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 21, 2010
22 :created_on: Apr 21, 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 posixpath
29 import posixpath
30 import logging
30 import logging
31 import traceback
31 import traceback
32 import tempfile
32 import tempfile
33 import shutil
33 import shutil
34
34
35 from pylons import request, response, tmpl_context as c
35 from pylons import request, response, tmpl_context as c
36 from pylons.i18n.translation import _
36 from pylons.i18n.translation import _
37 from webob.exc import HTTPFound
37 from webob.exc import HTTPFound
38
38
39 from kallithea.config.routing import url
39 from kallithea.config.routing import url
40 from kallithea.lib.utils import action_logger
40 from kallithea.lib.utils import action_logger
41 from kallithea.lib import diffs
41 from kallithea.lib import diffs
42 from kallithea.lib import helpers as h
42 from kallithea.lib import helpers as h
43
43
44 from kallithea.lib.compat import OrderedDict
44 from kallithea.lib.compat import OrderedDict
45 from kallithea.lib.utils2 import convert_line_endings, detect_mode, safe_str, \
45 from kallithea.lib.utils2 import convert_line_endings, detect_mode, safe_str, \
46 str2bool, safe_int
46 str2bool, safe_int
47 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
47 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
48 from kallithea.lib.base import BaseRepoController, render, jsonify
48 from kallithea.lib.base import BaseRepoController, render, jsonify
49 from kallithea.lib.vcs.backends.base import EmptyChangeset
49 from kallithea.lib.vcs.backends.base import EmptyChangeset
50 from kallithea.lib.vcs.conf import settings
50 from kallithea.lib.vcs.conf import settings
51 from kallithea.lib.vcs.exceptions import RepositoryError, \
51 from kallithea.lib.vcs.exceptions import RepositoryError, \
52 ChangesetDoesNotExistError, EmptyRepositoryError, \
52 ChangesetDoesNotExistError, EmptyRepositoryError, \
53 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError, \
53 ImproperArchiveTypeError, VCSError, NodeAlreadyExistsError, \
54 NodeDoesNotExistError, ChangesetError, NodeError
54 NodeDoesNotExistError, ChangesetError, NodeError
55 from kallithea.lib.vcs.nodes import FileNode
55 from kallithea.lib.vcs.nodes import FileNode
56
56
57 from kallithea.model.repo import RepoModel
57 from kallithea.model.repo import RepoModel
58 from kallithea.model.scm import ScmModel
58 from kallithea.model.scm import ScmModel
59 from kallithea.model.db import Repository
59 from kallithea.model.db import Repository
60
60
61 from kallithea.controllers.changeset import anchor_url, _ignorews_url, \
61 from kallithea.controllers.changeset import anchor_url, _ignorews_url, \
62 _context_url, get_line_ctx, get_ignore_ws
62 _context_url, get_line_ctx, get_ignore_ws
63 from webob.exc import HTTPNotFound
63 from webob.exc import HTTPNotFound
64 from kallithea.lib.exceptions import NonRelativePathError
64 from kallithea.lib.exceptions import NonRelativePathError
65
65
66
66
67 log = logging.getLogger(__name__)
67 log = logging.getLogger(__name__)
68
68
69
69
70 class FilesController(BaseRepoController):
70 class FilesController(BaseRepoController):
71
71
72 def __before__(self):
72 def __before__(self):
73 super(FilesController, self).__before__()
73 super(FilesController, self).__before__()
74 c.cut_off_limit = self.cut_off_limit
74 c.cut_off_limit = self.cut_off_limit
75
75
76 def __get_cs(self, rev, silent_empty=False):
76 def __get_cs(self, rev, silent_empty=False):
77 """
77 """
78 Safe way to get changeset if error occur it redirects to tip with
78 Safe way to get changeset if error occur it redirects to tip with
79 proper message
79 proper message
80
80
81 :param rev: revision to fetch
81 :param rev: revision to fetch
82 :silent_empty: return None if repository is empty
82 :silent_empty: return None if repository is empty
83 """
83 """
84
84
85 try:
85 try:
86 return c.db_repo_scm_instance.get_changeset(rev)
86 return c.db_repo_scm_instance.get_changeset(rev)
87 except EmptyRepositoryError as e:
87 except EmptyRepositoryError as e:
88 if silent_empty:
88 if silent_empty:
89 return None
89 return None
90 url_ = url('files_add_home',
90 url_ = url('files_add_home',
91 repo_name=c.repo_name,
91 repo_name=c.repo_name,
92 revision=0, f_path='', anchor='edit')
92 revision=0, f_path='', anchor='edit')
93 add_new = h.link_to(_('Click here to add new file'), url_, class_="alert-link")
93 add_new = h.link_to(_('Click here to add new file'), url_, class_="alert-link")
94 h.flash(h.literal(_('There are no files yet. %s') % add_new),
94 h.flash(h.literal(_('There are no files yet. %s') % add_new),
95 category='warning')
95 category='warning')
96 raise HTTPNotFound()
96 raise HTTPNotFound()
97 except (ChangesetDoesNotExistError, LookupError):
97 except (ChangesetDoesNotExistError, LookupError):
98 msg = _('Such revision does not exist for this repository')
98 msg = _('Such revision does not exist for this repository')
99 h.flash(msg, category='error')
99 h.flash(msg, category='error')
100 raise HTTPNotFound()
100 raise HTTPNotFound()
101 except RepositoryError as e:
101 except RepositoryError as e:
102 h.flash(safe_str(e), category='error')
102 h.flash(safe_str(e), category='error')
103 raise HTTPNotFound()
103 raise HTTPNotFound()
104
104
105 def __get_filenode(self, cs, path):
105 def __get_filenode(self, cs, path):
106 """
106 """
107 Returns file_node or raise HTTP error.
107 Returns file_node or raise HTTP error.
108
108
109 :param cs: given changeset
109 :param cs: given changeset
110 :param path: path to lookup
110 :param path: path to lookup
111 """
111 """
112
112
113 try:
113 try:
114 file_node = cs.get_node(path)
114 file_node = cs.get_node(path)
115 if file_node.is_dir():
115 if file_node.is_dir():
116 raise RepositoryError('given path is a directory')
116 raise RepositoryError('given path is a directory')
117 except ChangesetDoesNotExistError:
117 except ChangesetDoesNotExistError:
118 msg = _('Such revision does not exist for this repository')
118 msg = _('Such revision does not exist for this repository')
119 h.flash(msg, category='error')
119 h.flash(msg, category='error')
120 raise HTTPNotFound()
120 raise HTTPNotFound()
121 except RepositoryError as e:
121 except RepositoryError as e:
122 h.flash(safe_str(e), category='error')
122 h.flash(safe_str(e), category='error')
123 raise HTTPNotFound()
123 raise HTTPNotFound()
124
124
125 return file_node
125 return file_node
126
126
127 @LoginRequired()
127 @LoginRequired()
128 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
128 @HasRepoPermissionLevelDecorator('read')
129 'repository.admin')
130 def index(self, repo_name, revision, f_path, annotate=False):
129 def index(self, repo_name, revision, f_path, annotate=False):
131 # redirect to given revision from form if given
130 # redirect to given revision from form if given
132 post_revision = request.POST.get('at_rev', None)
131 post_revision = request.POST.get('at_rev', None)
133 if post_revision:
132 if post_revision:
134 cs = self.__get_cs(post_revision) # FIXME - unused!
133 cs = self.__get_cs(post_revision) # FIXME - unused!
135
134
136 c.revision = revision
135 c.revision = revision
137 c.changeset = self.__get_cs(revision)
136 c.changeset = self.__get_cs(revision)
138 c.branch = request.GET.get('branch', None)
137 c.branch = request.GET.get('branch', None)
139 c.f_path = f_path
138 c.f_path = f_path
140 c.annotate = annotate
139 c.annotate = annotate
141 cur_rev = c.changeset.revision
140 cur_rev = c.changeset.revision
142 c.fulldiff = request.GET.get('fulldiff')
141 c.fulldiff = request.GET.get('fulldiff')
143
142
144 # prev link
143 # prev link
145 try:
144 try:
146 prev_rev = c.db_repo_scm_instance.get_changeset(cur_rev).prev(c.branch)
145 prev_rev = c.db_repo_scm_instance.get_changeset(cur_rev).prev(c.branch)
147 c.url_prev = url('files_home', repo_name=c.repo_name,
146 c.url_prev = url('files_home', repo_name=c.repo_name,
148 revision=prev_rev.raw_id, f_path=f_path)
147 revision=prev_rev.raw_id, f_path=f_path)
149 if c.branch:
148 if c.branch:
150 c.url_prev += '?branch=%s' % c.branch
149 c.url_prev += '?branch=%s' % c.branch
151 except (ChangesetDoesNotExistError, VCSError):
150 except (ChangesetDoesNotExistError, VCSError):
152 c.url_prev = '#'
151 c.url_prev = '#'
153
152
154 # next link
153 # next link
155 try:
154 try:
156 next_rev = c.db_repo_scm_instance.get_changeset(cur_rev).next(c.branch)
155 next_rev = c.db_repo_scm_instance.get_changeset(cur_rev).next(c.branch)
157 c.url_next = url('files_home', repo_name=c.repo_name,
156 c.url_next = url('files_home', repo_name=c.repo_name,
158 revision=next_rev.raw_id, f_path=f_path)
157 revision=next_rev.raw_id, f_path=f_path)
159 if c.branch:
158 if c.branch:
160 c.url_next += '?branch=%s' % c.branch
159 c.url_next += '?branch=%s' % c.branch
161 except (ChangesetDoesNotExistError, VCSError):
160 except (ChangesetDoesNotExistError, VCSError):
162 c.url_next = '#'
161 c.url_next = '#'
163
162
164 # files or dirs
163 # files or dirs
165 try:
164 try:
166 c.file = c.changeset.get_node(f_path)
165 c.file = c.changeset.get_node(f_path)
167
166
168 if c.file.is_file():
167 if c.file.is_file():
169 c.load_full_history = False
168 c.load_full_history = False
170 #determine if we're on branch head
169 #determine if we're on branch head
171 _branches = c.db_repo_scm_instance.branches
170 _branches = c.db_repo_scm_instance.branches
172 c.on_branch_head = revision in _branches.keys() + _branches.values()
171 c.on_branch_head = revision in _branches.keys() + _branches.values()
173 _hist = []
172 _hist = []
174 c.file_history = []
173 c.file_history = []
175 if c.load_full_history:
174 if c.load_full_history:
176 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
175 c.file_history, _hist = self._get_node_history(c.changeset, f_path)
177
176
178 c.authors = []
177 c.authors = []
179 for a in set([x.author for x in _hist]):
178 for a in set([x.author for x in _hist]):
180 c.authors.append((h.email(a), h.person(a)))
179 c.authors.append((h.email(a), h.person(a)))
181 else:
180 else:
182 c.authors = c.file_history = []
181 c.authors = c.file_history = []
183 except RepositoryError as e:
182 except RepositoryError as e:
184 h.flash(safe_str(e), category='error')
183 h.flash(safe_str(e), category='error')
185 raise HTTPNotFound()
184 raise HTTPNotFound()
186
185
187 if request.environ.get('HTTP_X_PARTIAL_XHR'):
186 if request.environ.get('HTTP_X_PARTIAL_XHR'):
188 return render('files/files_ypjax.html')
187 return render('files/files_ypjax.html')
189
188
190 # TODO: tags and bookmarks?
189 # TODO: tags and bookmarks?
191 c.revision_options = [(c.changeset.raw_id,
190 c.revision_options = [(c.changeset.raw_id,
192 _('%s at %s') % (c.changeset.branch, h.short_id(c.changeset.raw_id)))] + \
191 _('%s at %s') % (c.changeset.branch, h.short_id(c.changeset.raw_id)))] + \
193 [(n, b) for b, n in c.db_repo_scm_instance.branches.items()]
192 [(n, b) for b, n in c.db_repo_scm_instance.branches.items()]
194 if c.db_repo_scm_instance.closed_branches:
193 if c.db_repo_scm_instance.closed_branches:
195 prefix = _('(closed)') + ' '
194 prefix = _('(closed)') + ' '
196 c.revision_options += [('-', '-')] + \
195 c.revision_options += [('-', '-')] + \
197 [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()]
196 [(n, prefix + b) for b, n in c.db_repo_scm_instance.closed_branches.items()]
198
197
199 return render('files/files.html')
198 return render('files/files.html')
200
199
201 @LoginRequired()
200 @LoginRequired()
202 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
201 @HasRepoPermissionLevelDecorator('read')
203 'repository.admin')
204 @jsonify
202 @jsonify
205 def history(self, repo_name, revision, f_path):
203 def history(self, repo_name, revision, f_path):
206 changeset = self.__get_cs(revision)
204 changeset = self.__get_cs(revision)
207 _file = changeset.get_node(f_path)
205 _file = changeset.get_node(f_path)
208 if _file.is_file():
206 if _file.is_file():
209 file_history, _hist = self._get_node_history(changeset, f_path)
207 file_history, _hist = self._get_node_history(changeset, f_path)
210
208
211 res = []
209 res = []
212 for obj in file_history:
210 for obj in file_history:
213 res.append({
211 res.append({
214 'text': obj[1],
212 'text': obj[1],
215 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
213 'children': [{'id': o[0], 'text': o[1]} for o in obj[0]]
216 })
214 })
217
215
218 data = {
216 data = {
219 'more': False,
217 'more': False,
220 'results': res
218 'results': res
221 }
219 }
222 return data
220 return data
223
221
224 @LoginRequired()
222 @LoginRequired()
225 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
223 @HasRepoPermissionLevelDecorator('read')
226 'repository.admin')
227 def authors(self, repo_name, revision, f_path):
224 def authors(self, repo_name, revision, f_path):
228 changeset = self.__get_cs(revision)
225 changeset = self.__get_cs(revision)
229 _file = changeset.get_node(f_path)
226 _file = changeset.get_node(f_path)
230 if _file.is_file():
227 if _file.is_file():
231 file_history, _hist = self._get_node_history(changeset, f_path)
228 file_history, _hist = self._get_node_history(changeset, f_path)
232 c.authors = []
229 c.authors = []
233 for a in set([x.author for x in _hist]):
230 for a in set([x.author for x in _hist]):
234 c.authors.append((h.email(a), h.person(a)))
231 c.authors.append((h.email(a), h.person(a)))
235 return render('files/files_history_box.html')
232 return render('files/files_history_box.html')
236
233
237 @LoginRequired()
234 @LoginRequired()
238 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
235 @HasRepoPermissionLevelDecorator('read')
239 'repository.admin')
240 def rawfile(self, repo_name, revision, f_path):
236 def rawfile(self, repo_name, revision, f_path):
241 cs = self.__get_cs(revision)
237 cs = self.__get_cs(revision)
242 file_node = self.__get_filenode(cs, f_path)
238 file_node = self.__get_filenode(cs, f_path)
243
239
244 response.content_disposition = 'attachment; filename=%s' % \
240 response.content_disposition = 'attachment; filename=%s' % \
245 safe_str(f_path.split(Repository.url_sep())[-1])
241 safe_str(f_path.split(Repository.url_sep())[-1])
246
242
247 response.content_type = file_node.mimetype
243 response.content_type = file_node.mimetype
248 return file_node.content
244 return file_node.content
249
245
250 @LoginRequired()
246 @LoginRequired()
251 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
247 @HasRepoPermissionLevelDecorator('read')
252 'repository.admin')
253 def raw(self, repo_name, revision, f_path):
248 def raw(self, repo_name, revision, f_path):
254 cs = self.__get_cs(revision)
249 cs = self.__get_cs(revision)
255 file_node = self.__get_filenode(cs, f_path)
250 file_node = self.__get_filenode(cs, f_path)
256
251
257 raw_mimetype_mapping = {
252 raw_mimetype_mapping = {
258 # map original mimetype to a mimetype used for "show as raw"
253 # map original mimetype to a mimetype used for "show as raw"
259 # you can also provide a content-disposition to override the
254 # you can also provide a content-disposition to override the
260 # default "attachment" disposition.
255 # default "attachment" disposition.
261 # orig_type: (new_type, new_dispo)
256 # orig_type: (new_type, new_dispo)
262
257
263 # show images inline:
258 # show images inline:
264 'image/x-icon': ('image/x-icon', 'inline'),
259 'image/x-icon': ('image/x-icon', 'inline'),
265 'image/png': ('image/png', 'inline'),
260 'image/png': ('image/png', 'inline'),
266 'image/gif': ('image/gif', 'inline'),
261 'image/gif': ('image/gif', 'inline'),
267 'image/jpeg': ('image/jpeg', 'inline'),
262 'image/jpeg': ('image/jpeg', 'inline'),
268 'image/svg+xml': ('image/svg+xml', 'inline'),
263 'image/svg+xml': ('image/svg+xml', 'inline'),
269 }
264 }
270
265
271 mimetype = file_node.mimetype
266 mimetype = file_node.mimetype
272 try:
267 try:
273 mimetype, dispo = raw_mimetype_mapping[mimetype]
268 mimetype, dispo = raw_mimetype_mapping[mimetype]
274 except KeyError:
269 except KeyError:
275 # we don't know anything special about this, handle it safely
270 # we don't know anything special about this, handle it safely
276 if file_node.is_binary:
271 if file_node.is_binary:
277 # do same as download raw for binary files
272 # do same as download raw for binary files
278 mimetype, dispo = 'application/octet-stream', 'attachment'
273 mimetype, dispo = 'application/octet-stream', 'attachment'
279 else:
274 else:
280 # do not just use the original mimetype, but force text/plain,
275 # do not just use the original mimetype, but force text/plain,
281 # otherwise it would serve text/html and that might be unsafe.
276 # otherwise it would serve text/html and that might be unsafe.
282 # Note: underlying vcs library fakes text/plain mimetype if the
277 # Note: underlying vcs library fakes text/plain mimetype if the
283 # mimetype can not be determined and it thinks it is not
278 # mimetype can not be determined and it thinks it is not
284 # binary.This might lead to erroneous text display in some
279 # binary.This might lead to erroneous text display in some
285 # cases, but helps in other cases, like with text files
280 # cases, but helps in other cases, like with text files
286 # without extension.
281 # without extension.
287 mimetype, dispo = 'text/plain', 'inline'
282 mimetype, dispo = 'text/plain', 'inline'
288
283
289 if dispo == 'attachment':
284 if dispo == 'attachment':
290 dispo = 'attachment; filename=%s' % \
285 dispo = 'attachment; filename=%s' % \
291 safe_str(f_path.split(os.sep)[-1])
286 safe_str(f_path.split(os.sep)[-1])
292
287
293 response.content_disposition = dispo
288 response.content_disposition = dispo
294 response.content_type = mimetype
289 response.content_type = mimetype
295 return file_node.content
290 return file_node.content
296
291
297 @LoginRequired()
292 @LoginRequired()
298 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
293 @HasRepoPermissionLevelDecorator('write')
299 def delete(self, repo_name, revision, f_path):
294 def delete(self, repo_name, revision, f_path):
300 repo = c.db_repo
295 repo = c.db_repo
301 if repo.enable_locking and repo.locked[0]:
296 if repo.enable_locking and repo.locked[0]:
302 h.flash(_('This repository has been locked by %s on %s')
297 h.flash(_('This repository has been locked by %s on %s')
303 % (h.person_by_id(repo.locked[0]),
298 % (h.person_by_id(repo.locked[0]),
304 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
299 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
305 'warning')
300 'warning')
306 raise HTTPFound(location=h.url('files_home',
301 raise HTTPFound(location=h.url('files_home',
307 repo_name=repo_name, revision='tip'))
302 repo_name=repo_name, revision='tip'))
308
303
309 # check if revision is a branch identifier- basically we cannot
304 # check if revision is a branch identifier- basically we cannot
310 # create multiple heads via file editing
305 # create multiple heads via file editing
311 _branches = repo.scm_instance.branches
306 _branches = repo.scm_instance.branches
312 # check if revision is a branch name or branch hash
307 # check if revision is a branch name or branch hash
313 if revision not in _branches.keys() + _branches.values():
308 if revision not in _branches.keys() + _branches.values():
314 h.flash(_('You can only delete files with revision '
309 h.flash(_('You can only delete files with revision '
315 'being a valid branch'), category='warning')
310 'being a valid branch'), category='warning')
316 raise HTTPFound(location=h.url('files_home',
311 raise HTTPFound(location=h.url('files_home',
317 repo_name=repo_name, revision='tip',
312 repo_name=repo_name, revision='tip',
318 f_path=f_path))
313 f_path=f_path))
319
314
320 r_post = request.POST
315 r_post = request.POST
321
316
322 c.cs = self.__get_cs(revision)
317 c.cs = self.__get_cs(revision)
323 c.file = self.__get_filenode(c.cs, f_path)
318 c.file = self.__get_filenode(c.cs, f_path)
324
319
325 c.default_message = _('Deleted file %s via Kallithea') % (f_path)
320 c.default_message = _('Deleted file %s via Kallithea') % (f_path)
326 c.f_path = f_path
321 c.f_path = f_path
327 node_path = f_path
322 node_path = f_path
328 author = request.authuser.full_contact
323 author = request.authuser.full_contact
329
324
330 if r_post:
325 if r_post:
331 message = r_post.get('message') or c.default_message
326 message = r_post.get('message') or c.default_message
332
327
333 try:
328 try:
334 nodes = {
329 nodes = {
335 node_path: {
330 node_path: {
336 'content': ''
331 'content': ''
337 }
332 }
338 }
333 }
339 self.scm_model.delete_nodes(
334 self.scm_model.delete_nodes(
340 user=request.authuser.user_id, repo=c.db_repo,
335 user=request.authuser.user_id, repo=c.db_repo,
341 message=message,
336 message=message,
342 nodes=nodes,
337 nodes=nodes,
343 parent_cs=c.cs,
338 parent_cs=c.cs,
344 author=author,
339 author=author,
345 )
340 )
346
341
347 h.flash(_('Successfully deleted file %s') % f_path,
342 h.flash(_('Successfully deleted file %s') % f_path,
348 category='success')
343 category='success')
349 except Exception:
344 except Exception:
350 log.error(traceback.format_exc())
345 log.error(traceback.format_exc())
351 h.flash(_('Error occurred during commit'), category='error')
346 h.flash(_('Error occurred during commit'), category='error')
352 raise HTTPFound(location=url('changeset_home',
347 raise HTTPFound(location=url('changeset_home',
353 repo_name=c.repo_name, revision='tip'))
348 repo_name=c.repo_name, revision='tip'))
354
349
355 return render('files/files_delete.html')
350 return render('files/files_delete.html')
356
351
357 @LoginRequired()
352 @LoginRequired()
358 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
353 @HasRepoPermissionLevelDecorator('write')
359 def edit(self, repo_name, revision, f_path):
354 def edit(self, repo_name, revision, f_path):
360 repo = c.db_repo
355 repo = c.db_repo
361 if repo.enable_locking and repo.locked[0]:
356 if repo.enable_locking and repo.locked[0]:
362 h.flash(_('This repository has been locked by %s on %s')
357 h.flash(_('This repository has been locked by %s on %s')
363 % (h.person_by_id(repo.locked[0]),
358 % (h.person_by_id(repo.locked[0]),
364 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
359 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
365 'warning')
360 'warning')
366 raise HTTPFound(location=h.url('files_home',
361 raise HTTPFound(location=h.url('files_home',
367 repo_name=repo_name, revision='tip'))
362 repo_name=repo_name, revision='tip'))
368
363
369 # check if revision is a branch identifier- basically we cannot
364 # check if revision is a branch identifier- basically we cannot
370 # create multiple heads via file editing
365 # create multiple heads via file editing
371 _branches = repo.scm_instance.branches
366 _branches = repo.scm_instance.branches
372 # check if revision is a branch name or branch hash
367 # check if revision is a branch name or branch hash
373 if revision not in _branches.keys() + _branches.values():
368 if revision not in _branches.keys() + _branches.values():
374 h.flash(_('You can only edit files with revision '
369 h.flash(_('You can only edit files with revision '
375 'being a valid branch'), category='warning')
370 'being a valid branch'), category='warning')
376 raise HTTPFound(location=h.url('files_home',
371 raise HTTPFound(location=h.url('files_home',
377 repo_name=repo_name, revision='tip',
372 repo_name=repo_name, revision='tip',
378 f_path=f_path))
373 f_path=f_path))
379
374
380 r_post = request.POST
375 r_post = request.POST
381
376
382 c.cs = self.__get_cs(revision)
377 c.cs = self.__get_cs(revision)
383 c.file = self.__get_filenode(c.cs, f_path)
378 c.file = self.__get_filenode(c.cs, f_path)
384
379
385 if c.file.is_binary:
380 if c.file.is_binary:
386 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
381 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
387 revision=c.cs.raw_id, f_path=f_path))
382 revision=c.cs.raw_id, f_path=f_path))
388 c.default_message = _('Edited file %s via Kallithea') % (f_path)
383 c.default_message = _('Edited file %s via Kallithea') % (f_path)
389 c.f_path = f_path
384 c.f_path = f_path
390
385
391 if r_post:
386 if r_post:
392
387
393 old_content = c.file.content
388 old_content = c.file.content
394 sl = old_content.splitlines(1)
389 sl = old_content.splitlines(1)
395 first_line = sl[0] if sl else ''
390 first_line = sl[0] if sl else ''
396 # modes: 0 - Unix, 1 - Mac, 2 - DOS
391 # modes: 0 - Unix, 1 - Mac, 2 - DOS
397 mode = detect_mode(first_line, 0)
392 mode = detect_mode(first_line, 0)
398 content = convert_line_endings(r_post.get('content', ''), mode)
393 content = convert_line_endings(r_post.get('content', ''), mode)
399
394
400 message = r_post.get('message') or c.default_message
395 message = r_post.get('message') or c.default_message
401 author = request.authuser.full_contact
396 author = request.authuser.full_contact
402
397
403 if content == old_content:
398 if content == old_content:
404 h.flash(_('No changes'), category='warning')
399 h.flash(_('No changes'), category='warning')
405 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
400 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
406 revision='tip'))
401 revision='tip'))
407 try:
402 try:
408 self.scm_model.commit_change(repo=c.db_repo_scm_instance,
403 self.scm_model.commit_change(repo=c.db_repo_scm_instance,
409 repo_name=repo_name, cs=c.cs,
404 repo_name=repo_name, cs=c.cs,
410 user=request.authuser.user_id,
405 user=request.authuser.user_id,
411 author=author, message=message,
406 author=author, message=message,
412 content=content, f_path=f_path)
407 content=content, f_path=f_path)
413 h.flash(_('Successfully committed to %s') % f_path,
408 h.flash(_('Successfully committed to %s') % f_path,
414 category='success')
409 category='success')
415 except Exception:
410 except Exception:
416 log.error(traceback.format_exc())
411 log.error(traceback.format_exc())
417 h.flash(_('Error occurred during commit'), category='error')
412 h.flash(_('Error occurred during commit'), category='error')
418 raise HTTPFound(location=url('changeset_home',
413 raise HTTPFound(location=url('changeset_home',
419 repo_name=c.repo_name, revision='tip'))
414 repo_name=c.repo_name, revision='tip'))
420
415
421 return render('files/files_edit.html')
416 return render('files/files_edit.html')
422
417
423 @LoginRequired()
418 @LoginRequired()
424 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
419 @HasRepoPermissionLevelDecorator('write')
425 def add(self, repo_name, revision, f_path):
420 def add(self, repo_name, revision, f_path):
426
421
427 repo = c.db_repo
422 repo = c.db_repo
428 if repo.enable_locking and repo.locked[0]:
423 if repo.enable_locking and repo.locked[0]:
429 h.flash(_('This repository has been locked by %s on %s')
424 h.flash(_('This repository has been locked by %s on %s')
430 % (h.person_by_id(repo.locked[0]),
425 % (h.person_by_id(repo.locked[0]),
431 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
426 h.fmt_date(h.time_to_datetime(repo.locked[1]))),
432 'warning')
427 'warning')
433 raise HTTPFound(location=h.url('files_home',
428 raise HTTPFound(location=h.url('files_home',
434 repo_name=repo_name, revision='tip'))
429 repo_name=repo_name, revision='tip'))
435
430
436 r_post = request.POST
431 r_post = request.POST
437 c.cs = self.__get_cs(revision, silent_empty=True)
432 c.cs = self.__get_cs(revision, silent_empty=True)
438 if c.cs is None:
433 if c.cs is None:
439 c.cs = EmptyChangeset(alias=c.db_repo_scm_instance.alias)
434 c.cs = EmptyChangeset(alias=c.db_repo_scm_instance.alias)
440 c.default_message = (_('Added file via Kallithea'))
435 c.default_message = (_('Added file via Kallithea'))
441 c.f_path = f_path
436 c.f_path = f_path
442
437
443 if r_post:
438 if r_post:
444 unix_mode = 0
439 unix_mode = 0
445 content = convert_line_endings(r_post.get('content', ''), unix_mode)
440 content = convert_line_endings(r_post.get('content', ''), unix_mode)
446
441
447 message = r_post.get('message') or c.default_message
442 message = r_post.get('message') or c.default_message
448 filename = r_post.get('filename')
443 filename = r_post.get('filename')
449 location = r_post.get('location', '')
444 location = r_post.get('location', '')
450 file_obj = r_post.get('upload_file', None)
445 file_obj = r_post.get('upload_file', None)
451
446
452 if file_obj is not None and hasattr(file_obj, 'filename'):
447 if file_obj is not None and hasattr(file_obj, 'filename'):
453 filename = file_obj.filename
448 filename = file_obj.filename
454 content = file_obj.file
449 content = file_obj.file
455
450
456 if hasattr(content, 'file'):
451 if hasattr(content, 'file'):
457 # non posix systems store real file under file attr
452 # non posix systems store real file under file attr
458 content = content.file
453 content = content.file
459
454
460 if not content:
455 if not content:
461 h.flash(_('No content'), category='warning')
456 h.flash(_('No content'), category='warning')
462 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
457 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
463 revision='tip'))
458 revision='tip'))
464 if not filename:
459 if not filename:
465 h.flash(_('No filename'), category='warning')
460 h.flash(_('No filename'), category='warning')
466 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
461 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
467 revision='tip'))
462 revision='tip'))
468 #strip all crap out of file, just leave the basename
463 #strip all crap out of file, just leave the basename
469 filename = os.path.basename(filename)
464 filename = os.path.basename(filename)
470 node_path = posixpath.join(location, filename)
465 node_path = posixpath.join(location, filename)
471 author = request.authuser.full_contact
466 author = request.authuser.full_contact
472
467
473 try:
468 try:
474 nodes = {
469 nodes = {
475 node_path: {
470 node_path: {
476 'content': content
471 'content': content
477 }
472 }
478 }
473 }
479 self.scm_model.create_nodes(
474 self.scm_model.create_nodes(
480 user=request.authuser.user_id, repo=c.db_repo,
475 user=request.authuser.user_id, repo=c.db_repo,
481 message=message,
476 message=message,
482 nodes=nodes,
477 nodes=nodes,
483 parent_cs=c.cs,
478 parent_cs=c.cs,
484 author=author,
479 author=author,
485 )
480 )
486
481
487 h.flash(_('Successfully committed to %s') % node_path,
482 h.flash(_('Successfully committed to %s') % node_path,
488 category='success')
483 category='success')
489 except NonRelativePathError as e:
484 except NonRelativePathError as e:
490 h.flash(_('Location must be relative path and must not '
485 h.flash(_('Location must be relative path and must not '
491 'contain .. in path'), category='warning')
486 'contain .. in path'), category='warning')
492 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
487 raise HTTPFound(location=url('changeset_home', repo_name=c.repo_name,
493 revision='tip'))
488 revision='tip'))
494 except (NodeError, NodeAlreadyExistsError) as e:
489 except (NodeError, NodeAlreadyExistsError) as e:
495 h.flash(_(e), category='error')
490 h.flash(_(e), category='error')
496 except Exception:
491 except Exception:
497 log.error(traceback.format_exc())
492 log.error(traceback.format_exc())
498 h.flash(_('Error occurred during commit'), category='error')
493 h.flash(_('Error occurred during commit'), category='error')
499 raise HTTPFound(location=url('changeset_home',
494 raise HTTPFound(location=url('changeset_home',
500 repo_name=c.repo_name, revision='tip'))
495 repo_name=c.repo_name, revision='tip'))
501
496
502 return render('files/files_add.html')
497 return render('files/files_add.html')
503
498
504 @LoginRequired()
499 @LoginRequired()
505 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
500 @HasRepoPermissionLevelDecorator('read')
506 'repository.admin')
507 def archivefile(self, repo_name, fname):
501 def archivefile(self, repo_name, fname):
508 fileformat = None
502 fileformat = None
509 revision = None
503 revision = None
510 ext = None
504 ext = None
511 subrepos = request.GET.get('subrepos') == 'true'
505 subrepos = request.GET.get('subrepos') == 'true'
512
506
513 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
507 for a_type, ext_data in settings.ARCHIVE_SPECS.items():
514 archive_spec = fname.split(ext_data[1])
508 archive_spec = fname.split(ext_data[1])
515 if len(archive_spec) == 2 and archive_spec[1] == '':
509 if len(archive_spec) == 2 and archive_spec[1] == '':
516 fileformat = a_type or ext_data[1]
510 fileformat = a_type or ext_data[1]
517 revision = archive_spec[0]
511 revision = archive_spec[0]
518 ext = ext_data[1]
512 ext = ext_data[1]
519
513
520 try:
514 try:
521 dbrepo = RepoModel().get_by_repo_name(repo_name)
515 dbrepo = RepoModel().get_by_repo_name(repo_name)
522 if not dbrepo.enable_downloads:
516 if not dbrepo.enable_downloads:
523 return _('Downloads disabled') # TODO: do something else?
517 return _('Downloads disabled') # TODO: do something else?
524
518
525 if c.db_repo_scm_instance.alias == 'hg':
519 if c.db_repo_scm_instance.alias == 'hg':
526 # patch and reset hooks section of UI config to not run any
520 # patch and reset hooks section of UI config to not run any
527 # hooks on fetching archives with subrepos
521 # hooks on fetching archives with subrepos
528 for k, v in c.db_repo_scm_instance._repo.ui.configitems('hooks'):
522 for k, v in c.db_repo_scm_instance._repo.ui.configitems('hooks'):
529 c.db_repo_scm_instance._repo.ui.setconfig('hooks', k, None)
523 c.db_repo_scm_instance._repo.ui.setconfig('hooks', k, None)
530
524
531 cs = c.db_repo_scm_instance.get_changeset(revision)
525 cs = c.db_repo_scm_instance.get_changeset(revision)
532 content_type = settings.ARCHIVE_SPECS[fileformat][0]
526 content_type = settings.ARCHIVE_SPECS[fileformat][0]
533 except ChangesetDoesNotExistError:
527 except ChangesetDoesNotExistError:
534 return _('Unknown revision %s') % revision
528 return _('Unknown revision %s') % revision
535 except EmptyRepositoryError:
529 except EmptyRepositoryError:
536 return _('Empty repository')
530 return _('Empty repository')
537 except (ImproperArchiveTypeError, KeyError):
531 except (ImproperArchiveTypeError, KeyError):
538 return _('Unknown archive type')
532 return _('Unknown archive type')
539
533
540 from kallithea import CONFIG
534 from kallithea import CONFIG
541 rev_name = cs.raw_id[:12]
535 rev_name = cs.raw_id[:12]
542 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
536 archive_name = '%s-%s%s' % (safe_str(repo_name.replace('/', '_')),
543 safe_str(rev_name), ext)
537 safe_str(rev_name), ext)
544
538
545 archive_path = None
539 archive_path = None
546 cached_archive_path = None
540 cached_archive_path = None
547 archive_cache_dir = CONFIG.get('archive_cache_dir')
541 archive_cache_dir = CONFIG.get('archive_cache_dir')
548 if archive_cache_dir and not subrepos: # TODO: subrepo caching?
542 if archive_cache_dir and not subrepos: # TODO: subrepo caching?
549 if not os.path.isdir(archive_cache_dir):
543 if not os.path.isdir(archive_cache_dir):
550 os.makedirs(archive_cache_dir)
544 os.makedirs(archive_cache_dir)
551 cached_archive_path = os.path.join(archive_cache_dir, archive_name)
545 cached_archive_path = os.path.join(archive_cache_dir, archive_name)
552 if os.path.isfile(cached_archive_path):
546 if os.path.isfile(cached_archive_path):
553 log.debug('Found cached archive in %s', cached_archive_path)
547 log.debug('Found cached archive in %s', cached_archive_path)
554 archive_path = cached_archive_path
548 archive_path = cached_archive_path
555 else:
549 else:
556 log.debug('Archive %s is not yet cached', archive_name)
550 log.debug('Archive %s is not yet cached', archive_name)
557
551
558 if archive_path is None:
552 if archive_path is None:
559 # generate new archive
553 # generate new archive
560 fd, archive_path = tempfile.mkstemp()
554 fd, archive_path = tempfile.mkstemp()
561 log.debug('Creating new temp archive in %s', archive_path)
555 log.debug('Creating new temp archive in %s', archive_path)
562 with os.fdopen(fd, 'wb') as stream:
556 with os.fdopen(fd, 'wb') as stream:
563 cs.fill_archive(stream=stream, kind=fileformat, subrepos=subrepos)
557 cs.fill_archive(stream=stream, kind=fileformat, subrepos=subrepos)
564 # stream (and thus fd) has been closed by cs.fill_archive
558 # stream (and thus fd) has been closed by cs.fill_archive
565 if cached_archive_path is not None:
559 if cached_archive_path is not None:
566 # we generated the archive - move it to cache
560 # we generated the archive - move it to cache
567 log.debug('Storing new archive in %s', cached_archive_path)
561 log.debug('Storing new archive in %s', cached_archive_path)
568 shutil.move(archive_path, cached_archive_path)
562 shutil.move(archive_path, cached_archive_path)
569 archive_path = cached_archive_path
563 archive_path = cached_archive_path
570
564
571 def get_chunked_archive(archive_path):
565 def get_chunked_archive(archive_path):
572 stream = open(archive_path, 'rb')
566 stream = open(archive_path, 'rb')
573 while True:
567 while True:
574 data = stream.read(16 * 1024)
568 data = stream.read(16 * 1024)
575 if not data:
569 if not data:
576 break
570 break
577 yield data
571 yield data
578 stream.close()
572 stream.close()
579 if archive_path != cached_archive_path:
573 if archive_path != cached_archive_path:
580 log.debug('Destroying temp archive %s', archive_path)
574 log.debug('Destroying temp archive %s', archive_path)
581 os.remove(archive_path)
575 os.remove(archive_path)
582
576
583 action_logger(user=request.authuser,
577 action_logger(user=request.authuser,
584 action='user_downloaded_archive:%s' % (archive_name),
578 action='user_downloaded_archive:%s' % (archive_name),
585 repo=repo_name, ipaddr=request.ip_addr, commit=True)
579 repo=repo_name, ipaddr=request.ip_addr, commit=True)
586
580
587 response.content_disposition = str('attachment; filename=%s' % (archive_name))
581 response.content_disposition = str('attachment; filename=%s' % (archive_name))
588 response.content_type = str(content_type)
582 response.content_type = str(content_type)
589 return get_chunked_archive(archive_path)
583 return get_chunked_archive(archive_path)
590
584
591 @LoginRequired()
585 @LoginRequired()
592 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
586 @HasRepoPermissionLevelDecorator('read')
593 'repository.admin')
594 def diff(self, repo_name, f_path):
587 def diff(self, repo_name, f_path):
595 ignore_whitespace = request.GET.get('ignorews') == '1'
588 ignore_whitespace = request.GET.get('ignorews') == '1'
596 line_context = safe_int(request.GET.get('context'), 3)
589 line_context = safe_int(request.GET.get('context'), 3)
597 diff2 = request.GET.get('diff2', '')
590 diff2 = request.GET.get('diff2', '')
598 diff1 = request.GET.get('diff1', '') or diff2
591 diff1 = request.GET.get('diff1', '') or diff2
599 c.action = request.GET.get('diff')
592 c.action = request.GET.get('diff')
600 c.no_changes = diff1 == diff2
593 c.no_changes = diff1 == diff2
601 c.f_path = f_path
594 c.f_path = f_path
602 c.big_diff = False
595 c.big_diff = False
603 c.anchor_url = anchor_url
596 c.anchor_url = anchor_url
604 c.ignorews_url = _ignorews_url
597 c.ignorews_url = _ignorews_url
605 c.context_url = _context_url
598 c.context_url = _context_url
606 c.changes = OrderedDict()
599 c.changes = OrderedDict()
607 c.changes[diff2] = []
600 c.changes[diff2] = []
608
601
609 #special case if we want a show rev only, it's impl here
602 #special case if we want a show rev only, it's impl here
610 #to reduce JS and callbacks
603 #to reduce JS and callbacks
611
604
612 if request.GET.get('show_rev'):
605 if request.GET.get('show_rev'):
613 if str2bool(request.GET.get('annotate', 'False')):
606 if str2bool(request.GET.get('annotate', 'False')):
614 _url = url('files_annotate_home', repo_name=c.repo_name,
607 _url = url('files_annotate_home', repo_name=c.repo_name,
615 revision=diff1, f_path=c.f_path)
608 revision=diff1, f_path=c.f_path)
616 else:
609 else:
617 _url = url('files_home', repo_name=c.repo_name,
610 _url = url('files_home', repo_name=c.repo_name,
618 revision=diff1, f_path=c.f_path)
611 revision=diff1, f_path=c.f_path)
619
612
620 raise HTTPFound(location=_url)
613 raise HTTPFound(location=_url)
621 try:
614 try:
622 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
615 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
623 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
616 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
624 try:
617 try:
625 node1 = c.changeset_1.get_node(f_path)
618 node1 = c.changeset_1.get_node(f_path)
626 if node1.is_dir():
619 if node1.is_dir():
627 raise NodeError('%s path is a %s not a file'
620 raise NodeError('%s path is a %s not a file'
628 % (node1, type(node1)))
621 % (node1, type(node1)))
629 except NodeDoesNotExistError:
622 except NodeDoesNotExistError:
630 c.changeset_1 = EmptyChangeset(cs=diff1,
623 c.changeset_1 = EmptyChangeset(cs=diff1,
631 revision=c.changeset_1.revision,
624 revision=c.changeset_1.revision,
632 repo=c.db_repo_scm_instance)
625 repo=c.db_repo_scm_instance)
633 node1 = FileNode(f_path, '', changeset=c.changeset_1)
626 node1 = FileNode(f_path, '', changeset=c.changeset_1)
634 else:
627 else:
635 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
628 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
636 node1 = FileNode(f_path, '', changeset=c.changeset_1)
629 node1 = FileNode(f_path, '', changeset=c.changeset_1)
637
630
638 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
631 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
639 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
632 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
640 try:
633 try:
641 node2 = c.changeset_2.get_node(f_path)
634 node2 = c.changeset_2.get_node(f_path)
642 if node2.is_dir():
635 if node2.is_dir():
643 raise NodeError('%s path is a %s not a file'
636 raise NodeError('%s path is a %s not a file'
644 % (node2, type(node2)))
637 % (node2, type(node2)))
645 except NodeDoesNotExistError:
638 except NodeDoesNotExistError:
646 c.changeset_2 = EmptyChangeset(cs=diff2,
639 c.changeset_2 = EmptyChangeset(cs=diff2,
647 revision=c.changeset_2.revision,
640 revision=c.changeset_2.revision,
648 repo=c.db_repo_scm_instance)
641 repo=c.db_repo_scm_instance)
649 node2 = FileNode(f_path, '', changeset=c.changeset_2)
642 node2 = FileNode(f_path, '', changeset=c.changeset_2)
650 else:
643 else:
651 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
644 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
652 node2 = FileNode(f_path, '', changeset=c.changeset_2)
645 node2 = FileNode(f_path, '', changeset=c.changeset_2)
653 except (RepositoryError, NodeError):
646 except (RepositoryError, NodeError):
654 log.error(traceback.format_exc())
647 log.error(traceback.format_exc())
655 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
648 raise HTTPFound(location=url('files_home', repo_name=c.repo_name,
656 f_path=f_path))
649 f_path=f_path))
657
650
658 if c.action == 'download':
651 if c.action == 'download':
659 _diff = diffs.get_gitdiff(node1, node2,
652 _diff = diffs.get_gitdiff(node1, node2,
660 ignore_whitespace=ignore_whitespace,
653 ignore_whitespace=ignore_whitespace,
661 context=line_context)
654 context=line_context)
662 diff = diffs.DiffProcessor(_diff, format='gitdiff')
655 diff = diffs.DiffProcessor(_diff, format='gitdiff')
663
656
664 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
657 diff_name = '%s_vs_%s.diff' % (diff1, diff2)
665 response.content_type = 'text/plain'
658 response.content_type = 'text/plain'
666 response.content_disposition = (
659 response.content_disposition = (
667 'attachment; filename=%s' % diff_name
660 'attachment; filename=%s' % diff_name
668 )
661 )
669 return diff.as_raw()
662 return diff.as_raw()
670
663
671 elif c.action == 'raw':
664 elif c.action == 'raw':
672 _diff = diffs.get_gitdiff(node1, node2,
665 _diff = diffs.get_gitdiff(node1, node2,
673 ignore_whitespace=ignore_whitespace,
666 ignore_whitespace=ignore_whitespace,
674 context=line_context)
667 context=line_context)
675 diff = diffs.DiffProcessor(_diff, format='gitdiff')
668 diff = diffs.DiffProcessor(_diff, format='gitdiff')
676 response.content_type = 'text/plain'
669 response.content_type = 'text/plain'
677 return diff.as_raw()
670 return diff.as_raw()
678
671
679 else:
672 else:
680 fid = h.FID(diff2, node2.path)
673 fid = h.FID(diff2, node2.path)
681 line_context_lcl = get_line_ctx(fid, request.GET)
674 line_context_lcl = get_line_ctx(fid, request.GET)
682 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
675 ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
683
676
684 lim = request.GET.get('fulldiff') or self.cut_off_limit
677 lim = request.GET.get('fulldiff') or self.cut_off_limit
685 c.a_rev, c.cs_rev, a_path, diff, st, op = diffs.wrapped_diff(filenode_old=node1,
678 c.a_rev, c.cs_rev, a_path, diff, st, op = diffs.wrapped_diff(filenode_old=node1,
686 filenode_new=node2,
679 filenode_new=node2,
687 cut_off_limit=lim,
680 cut_off_limit=lim,
688 ignore_whitespace=ign_whitespace_lcl,
681 ignore_whitespace=ign_whitespace_lcl,
689 line_context=line_context_lcl,
682 line_context=line_context_lcl,
690 enable_comments=False)
683 enable_comments=False)
691 c.file_diff_data = [(fid, fid, op, a_path, node2.path, diff, st)]
684 c.file_diff_data = [(fid, fid, op, a_path, node2.path, diff, st)]
692
685
693 return render('files/file_diff.html')
686 return render('files/file_diff.html')
694
687
695 @LoginRequired()
688 @LoginRequired()
696 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
689 @HasRepoPermissionLevelDecorator('read')
697 'repository.admin')
698 def diff_2way(self, repo_name, f_path):
690 def diff_2way(self, repo_name, f_path):
699 diff1 = request.GET.get('diff1', '')
691 diff1 = request.GET.get('diff1', '')
700 diff2 = request.GET.get('diff2', '')
692 diff2 = request.GET.get('diff2', '')
701 try:
693 try:
702 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
694 if diff1 not in ['', None, 'None', '0' * 12, '0' * 40]:
703 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
695 c.changeset_1 = c.db_repo_scm_instance.get_changeset(diff1)
704 try:
696 try:
705 node1 = c.changeset_1.get_node(f_path)
697 node1 = c.changeset_1.get_node(f_path)
706 if node1.is_dir():
698 if node1.is_dir():
707 raise NodeError('%s path is a %s not a file'
699 raise NodeError('%s path is a %s not a file'
708 % (node1, type(node1)))
700 % (node1, type(node1)))
709 except NodeDoesNotExistError:
701 except NodeDoesNotExistError:
710 c.changeset_1 = EmptyChangeset(cs=diff1,
702 c.changeset_1 = EmptyChangeset(cs=diff1,
711 revision=c.changeset_1.revision,
703 revision=c.changeset_1.revision,
712 repo=c.db_repo_scm_instance)
704 repo=c.db_repo_scm_instance)
713 node1 = FileNode(f_path, '', changeset=c.changeset_1)
705 node1 = FileNode(f_path, '', changeset=c.changeset_1)
714 else:
706 else:
715 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
707 c.changeset_1 = EmptyChangeset(repo=c.db_repo_scm_instance)
716 node1 = FileNode(f_path, '', changeset=c.changeset_1)
708 node1 = FileNode(f_path, '', changeset=c.changeset_1)
717
709
718 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
710 if diff2 not in ['', None, 'None', '0' * 12, '0' * 40]:
719 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
711 c.changeset_2 = c.db_repo_scm_instance.get_changeset(diff2)
720 try:
712 try:
721 node2 = c.changeset_2.get_node(f_path)
713 node2 = c.changeset_2.get_node(f_path)
722 if node2.is_dir():
714 if node2.is_dir():
723 raise NodeError('%s path is a %s not a file'
715 raise NodeError('%s path is a %s not a file'
724 % (node2, type(node2)))
716 % (node2, type(node2)))
725 except NodeDoesNotExistError:
717 except NodeDoesNotExistError:
726 c.changeset_2 = EmptyChangeset(cs=diff2,
718 c.changeset_2 = EmptyChangeset(cs=diff2,
727 revision=c.changeset_2.revision,
719 revision=c.changeset_2.revision,
728 repo=c.db_repo_scm_instance)
720 repo=c.db_repo_scm_instance)
729 node2 = FileNode(f_path, '', changeset=c.changeset_2)
721 node2 = FileNode(f_path, '', changeset=c.changeset_2)
730 else:
722 else:
731 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
723 c.changeset_2 = EmptyChangeset(repo=c.db_repo_scm_instance)
732 node2 = FileNode(f_path, '', changeset=c.changeset_2)
724 node2 = FileNode(f_path, '', changeset=c.changeset_2)
733 except ChangesetDoesNotExistError as e:
725 except ChangesetDoesNotExistError as e:
734 msg = _('Such revision does not exist for this repository')
726 msg = _('Such revision does not exist for this repository')
735 h.flash(msg, category='error')
727 h.flash(msg, category='error')
736 raise HTTPNotFound()
728 raise HTTPNotFound()
737 c.node1 = node1
729 c.node1 = node1
738 c.node2 = node2
730 c.node2 = node2
739 c.cs1 = c.changeset_1
731 c.cs1 = c.changeset_1
740 c.cs2 = c.changeset_2
732 c.cs2 = c.changeset_2
741
733
742 return render('files/diff_2way.html')
734 return render('files/diff_2way.html')
743
735
744 def _get_node_history(self, cs, f_path, changesets=None):
736 def _get_node_history(self, cs, f_path, changesets=None):
745 """
737 """
746 get changesets history for given node
738 get changesets history for given node
747
739
748 :param cs: changeset to calculate history
740 :param cs: changeset to calculate history
749 :param f_path: path for node to calculate history for
741 :param f_path: path for node to calculate history for
750 :param changesets: if passed don't calculate history and take
742 :param changesets: if passed don't calculate history and take
751 changesets defined in this list
743 changesets defined in this list
752 """
744 """
753 # calculate history based on tip
745 # calculate history based on tip
754 tip_cs = c.db_repo_scm_instance.get_changeset()
746 tip_cs = c.db_repo_scm_instance.get_changeset()
755 if changesets is None:
747 if changesets is None:
756 try:
748 try:
757 changesets = tip_cs.get_file_history(f_path)
749 changesets = tip_cs.get_file_history(f_path)
758 except (NodeDoesNotExistError, ChangesetError):
750 except (NodeDoesNotExistError, ChangesetError):
759 #this node is not present at tip !
751 #this node is not present at tip !
760 changesets = cs.get_file_history(f_path)
752 changesets = cs.get_file_history(f_path)
761 hist_l = []
753 hist_l = []
762
754
763 changesets_group = ([], _("Changesets"))
755 changesets_group = ([], _("Changesets"))
764 branches_group = ([], _("Branches"))
756 branches_group = ([], _("Branches"))
765 tags_group = ([], _("Tags"))
757 tags_group = ([], _("Tags"))
766 for chs in changesets:
758 for chs in changesets:
767 #_branch = '(%s)' % chs.branch if (cs.repository.alias == 'hg') else ''
759 #_branch = '(%s)' % chs.branch if (cs.repository.alias == 'hg') else ''
768 _branch = chs.branch
760 _branch = chs.branch
769 n_desc = '%s (%s)' % (h.show_id(chs), _branch)
761 n_desc = '%s (%s)' % (h.show_id(chs), _branch)
770 changesets_group[0].append((chs.raw_id, n_desc,))
762 changesets_group[0].append((chs.raw_id, n_desc,))
771 hist_l.append(changesets_group)
763 hist_l.append(changesets_group)
772
764
773 for name, chs in c.db_repo_scm_instance.branches.items():
765 for name, chs in c.db_repo_scm_instance.branches.items():
774 branches_group[0].append((chs, name),)
766 branches_group[0].append((chs, name),)
775 hist_l.append(branches_group)
767 hist_l.append(branches_group)
776
768
777 for name, chs in c.db_repo_scm_instance.tags.items():
769 for name, chs in c.db_repo_scm_instance.tags.items():
778 tags_group[0].append((chs, name),)
770 tags_group[0].append((chs, name),)
779 hist_l.append(tags_group)
771 hist_l.append(tags_group)
780
772
781 return hist_l, changesets
773 return hist_l, changesets
782
774
783 @LoginRequired()
775 @LoginRequired()
784 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
776 @HasRepoPermissionLevelDecorator('read')
785 'repository.admin')
786 @jsonify
777 @jsonify
787 def nodelist(self, repo_name, revision, f_path):
778 def nodelist(self, repo_name, revision, f_path):
788 if request.environ.get('HTTP_X_PARTIAL_XHR'):
779 if request.environ.get('HTTP_X_PARTIAL_XHR'):
789 cs = self.__get_cs(revision)
780 cs = self.__get_cs(revision)
790 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
781 _d, _f = ScmModel().get_nodes(repo_name, cs.raw_id, f_path,
791 flat=False)
782 flat=False)
792 return {'nodes': _d + _f}
783 return {'nodes': _d + _f}
@@ -1,59 +1,58 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.followers
15 kallithea.controllers.followers
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Followers controller for Kallithea
18 Followers 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 23, 2011
22 :created_on: Apr 23, 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 logging
28 import logging
29
29
30 from pylons import tmpl_context as c, request
30 from pylons import tmpl_context as c, request
31
31
32 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
32 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
33 from kallithea.lib.base import BaseRepoController, render
33 from kallithea.lib.base import BaseRepoController, render
34 from kallithea.lib.page import Page
34 from kallithea.lib.page import Page
35 from kallithea.lib.utils2 import safe_int
35 from kallithea.lib.utils2 import safe_int
36 from kallithea.model.db import UserFollowing
36 from kallithea.model.db import UserFollowing
37
37
38 log = logging.getLogger(__name__)
38 log = logging.getLogger(__name__)
39
39
40
40
41 class FollowersController(BaseRepoController):
41 class FollowersController(BaseRepoController):
42
42
43 def __before__(self):
43 def __before__(self):
44 super(FollowersController, self).__before__()
44 super(FollowersController, self).__before__()
45
45
46 @LoginRequired()
46 @LoginRequired()
47 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
47 @HasRepoPermissionLevelDecorator('read')
48 'repository.admin')
49 def followers(self, repo_name):
48 def followers(self, repo_name):
50 p = safe_int(request.GET.get('page'), 1)
49 p = safe_int(request.GET.get('page'), 1)
51 repo_id = c.db_repo.repo_id
50 repo_id = c.db_repo.repo_id
52 d = UserFollowing.get_repo_followers(repo_id) \
51 d = UserFollowing.get_repo_followers(repo_id) \
53 .order_by(UserFollowing.follows_from)
52 .order_by(UserFollowing.follows_from)
54 c.followers_pager = Page(d, page=p, items_per_page=20)
53 c.followers_pager = Page(d, page=p, items_per_page=20)
55
54
56 if request.environ.get('HTTP_X_PARTIAL_XHR'):
55 if request.environ.get('HTTP_X_PARTIAL_XHR'):
57 return render('/followers/followers_data.html')
56 return render('/followers/followers_data.html')
58
57
59 return render('/followers/followers.html')
58 return render('/followers/followers.html')
@@ -1,188 +1,183 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.forks
15 kallithea.controllers.forks
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 forks controller for Kallithea
18 forks 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 23, 2011
22 :created_on: Apr 23, 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 logging
28 import logging
29 import formencode
29 import formencode
30 import traceback
30 import traceback
31 from formencode import htmlfill
31 from formencode import htmlfill
32
32
33 from pylons import tmpl_context as c, request
33 from pylons import tmpl_context as c, request
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound
35 from webob.exc import HTTPFound
36
36
37 import kallithea.lib.helpers as h
37 import kallithea.lib.helpers as h
38
38
39 from kallithea.config.routing import url
39 from kallithea.config.routing import url
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator, \
41 NotAnonymous, HasRepoPermissionAny, HasPermissionAnyDecorator, HasPermissionAny
41 NotAnonymous, HasRepoPermissionLevel, HasPermissionAnyDecorator, HasPermissionAny
42 from kallithea.lib.base import BaseRepoController, render
42 from kallithea.lib.base import BaseRepoController, render
43 from kallithea.lib.page import Page
43 from kallithea.lib.page import Page
44 from kallithea.lib.utils2 import safe_int
44 from kallithea.lib.utils2 import safe_int
45 from kallithea.model.db import Repository, UserFollowing, User, Ui
45 from kallithea.model.db import Repository, UserFollowing, User, Ui
46 from kallithea.model.repo import RepoModel
46 from kallithea.model.repo import RepoModel
47 from kallithea.model.forms import RepoForkForm
47 from kallithea.model.forms import RepoForkForm
48 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices
48 from kallithea.model.scm import ScmModel, AvailableRepoGroupChoices
49
49
50 log = logging.getLogger(__name__)
50 log = logging.getLogger(__name__)
51
51
52
52
53 class ForksController(BaseRepoController):
53 class ForksController(BaseRepoController):
54
54
55 def __before__(self):
55 def __before__(self):
56 super(ForksController, self).__before__()
56 super(ForksController, self).__before__()
57
57
58 def __load_defaults(self):
58 def __load_defaults(self):
59 repo_group_perms = ['group.admin']
59 repo_group_perms = ['group.admin']
60 if HasPermissionAny('hg.create.write_on_repogroup.true')():
60 if HasPermissionAny('hg.create.write_on_repogroup.true')():
61 repo_group_perms.append('group.write')
61 repo_group_perms.append('group.write')
62 c.repo_groups = AvailableRepoGroupChoices(['hg.create.repository'], repo_group_perms)
62 c.repo_groups = AvailableRepoGroupChoices(['hg.create.repository'], repo_group_perms)
63
63
64 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs()
64 c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs()
65
65
66 c.can_update = Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active
66 c.can_update = Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active
67
67
68 def __load_data(self):
68 def __load_data(self):
69 """
69 """
70 Load defaults settings for edit, and update
70 Load defaults settings for edit, and update
71 """
71 """
72 self.__load_defaults()
72 self.__load_defaults()
73
73
74 c.repo_info = c.db_repo
74 c.repo_info = c.db_repo
75 repo = c.db_repo.scm_instance
75 repo = c.db_repo.scm_instance
76
76
77 if c.repo_info is None:
77 if c.repo_info is None:
78 h.not_mapped_error(c.repo_name)
78 h.not_mapped_error(c.repo_name)
79 raise HTTPFound(location=url('repos'))
79 raise HTTPFound(location=url('repos'))
80
80
81 c.default_user_id = User.get_default_user().user_id
81 c.default_user_id = User.get_default_user().user_id
82 c.in_public_journal = UserFollowing.query() \
82 c.in_public_journal = UserFollowing.query() \
83 .filter(UserFollowing.user_id == c.default_user_id) \
83 .filter(UserFollowing.user_id == c.default_user_id) \
84 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
84 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
85
85
86 if c.repo_info.stats:
86 if c.repo_info.stats:
87 last_rev = c.repo_info.stats.stat_on_revision+1
87 last_rev = c.repo_info.stats.stat_on_revision+1
88 else:
88 else:
89 last_rev = 0
89 last_rev = 0
90 c.stats_revision = last_rev
90 c.stats_revision = last_rev
91
91
92 c.repo_last_rev = repo.count() if repo.revisions else 0
92 c.repo_last_rev = repo.count() if repo.revisions else 0
93
93
94 if last_rev == 0 or c.repo_last_rev == 0:
94 if last_rev == 0 or c.repo_last_rev == 0:
95 c.stats_percentage = 0
95 c.stats_percentage = 0
96 else:
96 else:
97 c.stats_percentage = '%.2f' % ((float((last_rev)) /
97 c.stats_percentage = '%.2f' % ((float((last_rev)) /
98 c.repo_last_rev) * 100)
98 c.repo_last_rev) * 100)
99
99
100 defaults = RepoModel()._get_defaults(c.repo_name)
100 defaults = RepoModel()._get_defaults(c.repo_name)
101 # alter the description to indicate a fork
101 # alter the description to indicate a fork
102 defaults['description'] = ('fork of repository: %s \n%s'
102 defaults['description'] = ('fork of repository: %s \n%s'
103 % (defaults['repo_name'],
103 % (defaults['repo_name'],
104 defaults['description']))
104 defaults['description']))
105 # add suffix to fork
105 # add suffix to fork
106 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
106 defaults['repo_name'] = '%s-fork' % defaults['repo_name']
107
107
108 return defaults
108 return defaults
109
109
110 @LoginRequired()
110 @LoginRequired()
111 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
111 @HasRepoPermissionLevelDecorator('read')
112 'repository.admin')
113 def forks(self, repo_name):
112 def forks(self, repo_name):
114 p = safe_int(request.GET.get('page'), 1)
113 p = safe_int(request.GET.get('page'), 1)
115 repo_id = c.db_repo.repo_id
114 repo_id = c.db_repo.repo_id
116 d = []
115 d = []
117 for r in Repository.get_repo_forks(repo_id):
116 for r in Repository.get_repo_forks(repo_id):
118 if not HasRepoPermissionAny(
117 if not HasRepoPermissionLevel('read')(r.repo_name, 'get forks check'):
119 'repository.read', 'repository.write', 'repository.admin'
120 )(r.repo_name, 'get forks check'):
121 continue
118 continue
122 d.append(r)
119 d.append(r)
123 c.forks_pager = Page(d, page=p, items_per_page=20)
120 c.forks_pager = Page(d, page=p, items_per_page=20)
124
121
125 if request.environ.get('HTTP_X_PARTIAL_XHR'):
122 if request.environ.get('HTTP_X_PARTIAL_XHR'):
126 return render('/forks/forks_data.html')
123 return render('/forks/forks_data.html')
127
124
128 return render('/forks/forks.html')
125 return render('/forks/forks.html')
129
126
130 @LoginRequired()
127 @LoginRequired()
131 @NotAnonymous()
128 @NotAnonymous()
132 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
129 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
133 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
130 @HasRepoPermissionLevelDecorator('read')
134 'repository.admin')
135 def fork(self, repo_name):
131 def fork(self, repo_name):
136 c.repo_info = Repository.get_by_repo_name(repo_name)
132 c.repo_info = Repository.get_by_repo_name(repo_name)
137 if not c.repo_info:
133 if not c.repo_info:
138 h.not_mapped_error(repo_name)
134 h.not_mapped_error(repo_name)
139 raise HTTPFound(location=url('home'))
135 raise HTTPFound(location=url('home'))
140
136
141 defaults = self.__load_data()
137 defaults = self.__load_data()
142
138
143 return htmlfill.render(
139 return htmlfill.render(
144 render('forks/fork.html'),
140 render('forks/fork.html'),
145 defaults=defaults,
141 defaults=defaults,
146 encoding="UTF-8",
142 encoding="UTF-8",
147 force_defaults=False)
143 force_defaults=False)
148
144
149 @LoginRequired()
145 @LoginRequired()
150 @NotAnonymous()
146 @NotAnonymous()
151 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
147 @HasPermissionAnyDecorator('hg.admin', 'hg.fork.repository')
152 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
148 @HasRepoPermissionLevelDecorator('read')
153 'repository.admin')
154 def fork_create(self, repo_name):
149 def fork_create(self, repo_name):
155 self.__load_defaults()
150 self.__load_defaults()
156 c.repo_info = Repository.get_by_repo_name(repo_name)
151 c.repo_info = Repository.get_by_repo_name(repo_name)
157 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
152 _form = RepoForkForm(old_data={'repo_type': c.repo_info.repo_type},
158 repo_groups=c.repo_groups,
153 repo_groups=c.repo_groups,
159 landing_revs=c.landing_revs_choices)()
154 landing_revs=c.landing_revs_choices)()
160 form_result = {}
155 form_result = {}
161 task_id = None
156 task_id = None
162 try:
157 try:
163 form_result = _form.to_python(dict(request.POST))
158 form_result = _form.to_python(dict(request.POST))
164
159
165 # an approximation that is better than nothing
160 # an approximation that is better than nothing
166 if not Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active:
161 if not Ui.get_by_key('hooks', Ui.HOOK_UPDATE).ui_active:
167 form_result['update_after_clone'] = False
162 form_result['update_after_clone'] = False
168
163
169 # create fork is done sometimes async on celery, db transaction
164 # create fork is done sometimes async on celery, db transaction
170 # management is handled there.
165 # management is handled there.
171 task = RepoModel().create_fork(form_result, request.authuser.user_id)
166 task = RepoModel().create_fork(form_result, request.authuser.user_id)
172 task_id = task.task_id
167 task_id = task.task_id
173 except formencode.Invalid as errors:
168 except formencode.Invalid as errors:
174 return htmlfill.render(
169 return htmlfill.render(
175 render('forks/fork.html'),
170 render('forks/fork.html'),
176 defaults=errors.value,
171 defaults=errors.value,
177 errors=errors.error_dict or {},
172 errors=errors.error_dict or {},
178 prefix_error=False,
173 prefix_error=False,
179 encoding="UTF-8",
174 encoding="UTF-8",
180 force_defaults=False)
175 force_defaults=False)
181 except Exception:
176 except Exception:
182 log.error(traceback.format_exc())
177 log.error(traceback.format_exc())
183 h.flash(_('An error occurred during repository forking %s') %
178 h.flash(_('An error occurred during repository forking %s') %
184 repo_name, category='error')
179 repo_name, category='error')
185
180
186 raise HTTPFound(location=h.url('repo_creating_home',
181 raise HTTPFound(location=h.url('repo_creating_home',
187 repo_name=form_result['repo_name_full'],
182 repo_name=form_result['repo_name_full'],
188 task_id=task_id))
183 task_id=task_id))
@@ -1,150 +1,149 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.home
15 kallithea.controllers.home
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Home controller for Kallithea
18 Home 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: Feb 18, 2010
22 :created_on: Feb 18, 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
30
31 from pylons import tmpl_context as c, request
31 from pylons import tmpl_context as c, request
32 from pylons.i18n.translation import _
32 from pylons.i18n.translation import _
33 from webob.exc import HTTPBadRequest
33 from webob.exc import HTTPBadRequest
34 from sqlalchemy.sql.expression import func
34 from sqlalchemy.sql.expression import func
35
35
36 from kallithea.lib.utils import conditional_cache
36 from kallithea.lib.utils import conditional_cache
37 from kallithea.lib.compat import json
37 from kallithea.lib.compat import json
38 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
38 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator
39 from kallithea.lib.base import BaseController, render, jsonify
39 from kallithea.lib.base import BaseController, render, jsonify
40 from kallithea.model.db import Repository, RepoGroup
40 from kallithea.model.db import Repository, RepoGroup
41 from kallithea.model.repo import RepoModel
41 from kallithea.model.repo import RepoModel
42
42
43
43
44 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
45
45
46
46
47 class HomeController(BaseController):
47 class HomeController(BaseController):
48
48
49 def __before__(self):
49 def __before__(self):
50 super(HomeController, self).__before__()
50 super(HomeController, self).__before__()
51
51
52 def about(self):
52 def about(self):
53 return render('/about.html')
53 return render('/about.html')
54
54
55 @LoginRequired()
55 @LoginRequired()
56 def index(self):
56 def index(self):
57 c.groups = self.scm_model.get_repo_groups()
57 c.groups = self.scm_model.get_repo_groups()
58 c.group = None
58 c.group = None
59
59
60 repos_list = Repository.query(sorted=True).filter_by(group=None).all()
60 repos_list = Repository.query(sorted=True).filter_by(group=None).all()
61
61
62 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
62 repos_data = RepoModel().get_repos_as_dict(repos_list=repos_list,
63 admin=False, short_name=True)
63 admin=False, short_name=True)
64 #json used to render the grid
64 #json used to render the grid
65 c.data = json.dumps(repos_data)
65 c.data = json.dumps(repos_data)
66
66
67 return render('/index.html')
67 return render('/index.html')
68
68
69 @LoginRequired()
69 @LoginRequired()
70 @jsonify
70 @jsonify
71 def repo_switcher_data(self):
71 def repo_switcher_data(self):
72 #wrapper for conditional cache
72 #wrapper for conditional cache
73 def _c():
73 def _c():
74 log.debug('generating switcher repo/groups list')
74 log.debug('generating switcher repo/groups list')
75 all_repos = Repository.query(sorted=True).all()
75 all_repos = Repository.query(sorted=True).all()
76 repo_iter = self.scm_model.get_repos(all_repos)
76 repo_iter = self.scm_model.get_repos(all_repos)
77 all_groups = RepoGroup.query(sorted=True).all()
77 all_groups = RepoGroup.query(sorted=True).all()
78 repo_groups_iter = self.scm_model.get_repo_groups(all_groups)
78 repo_groups_iter = self.scm_model.get_repo_groups(all_groups)
79
79
80 res = [{
80 res = [{
81 'text': _('Groups'),
81 'text': _('Groups'),
82 'children': [
82 'children': [
83 {'id': obj.group_name,
83 {'id': obj.group_name,
84 'text': obj.group_name,
84 'text': obj.group_name,
85 'type': 'group',
85 'type': 'group',
86 'obj': {}}
86 'obj': {}}
87 for obj in repo_groups_iter
87 for obj in repo_groups_iter
88 ],
88 ],
89 },
89 },
90 {
90 {
91 'text': _('Repositories'),
91 'text': _('Repositories'),
92 'children': [
92 'children': [
93 {'id': obj.repo_name,
93 {'id': obj.repo_name,
94 'text': obj.repo_name,
94 'text': obj.repo_name,
95 'type': 'repo',
95 'type': 'repo',
96 'obj': obj.get_dict()}
96 'obj': obj.get_dict()}
97 for obj in repo_iter
97 for obj in repo_iter
98 ],
98 ],
99 }]
99 }]
100
100
101 data = {
101 data = {
102 'more': False,
102 'more': False,
103 'results': res,
103 'results': res,
104 }
104 }
105 return data
105 return data
106
106
107 if request.is_xhr:
107 if request.is_xhr:
108 condition = False
108 condition = False
109 compute = conditional_cache('short_term', 'cache_desc',
109 compute = conditional_cache('short_term', 'cache_desc',
110 condition=condition, func=_c)
110 condition=condition, func=_c)
111 return compute()
111 return compute()
112 else:
112 else:
113 raise HTTPBadRequest()
113 raise HTTPBadRequest()
114
114
115 @LoginRequired()
115 @LoginRequired()
116 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
116 @HasRepoPermissionLevelDecorator('read')
117 'repository.admin')
118 @jsonify
117 @jsonify
119 def repo_refs_data(self, repo_name):
118 def repo_refs_data(self, repo_name):
120 repo = Repository.get_by_repo_name(repo_name).scm_instance
119 repo = Repository.get_by_repo_name(repo_name).scm_instance
121 res = []
120 res = []
122 _branches = repo.branches.items()
121 _branches = repo.branches.items()
123 if _branches:
122 if _branches:
124 res.append({
123 res.append({
125 'text': _('Branch'),
124 'text': _('Branch'),
126 'children': [{'id': rev, 'text': name, 'type': 'branch'} for name, rev in _branches]
125 'children': [{'id': rev, 'text': name, 'type': 'branch'} for name, rev in _branches]
127 })
126 })
128 _closed_branches = repo.closed_branches.items()
127 _closed_branches = repo.closed_branches.items()
129 if _closed_branches:
128 if _closed_branches:
130 res.append({
129 res.append({
131 'text': _('Closed Branches'),
130 'text': _('Closed Branches'),
132 'children': [{'id': rev, 'text': name, 'type': 'closed-branch'} for name, rev in _closed_branches]
131 'children': [{'id': rev, 'text': name, 'type': 'closed-branch'} for name, rev in _closed_branches]
133 })
132 })
134 _tags = repo.tags.items()
133 _tags = repo.tags.items()
135 if _tags:
134 if _tags:
136 res.append({
135 res.append({
137 'text': _('Tag'),
136 'text': _('Tag'),
138 'children': [{'id': rev, 'text': name, 'type': 'tag'} for name, rev in _tags]
137 'children': [{'id': rev, 'text': name, 'type': 'tag'} for name, rev in _tags]
139 })
138 })
140 _bookmarks = repo.bookmarks.items()
139 _bookmarks = repo.bookmarks.items()
141 if _bookmarks:
140 if _bookmarks:
142 res.append({
141 res.append({
143 'text': _('Bookmark'),
142 'text': _('Bookmark'),
144 'children': [{'id': rev, 'text': name, 'type': 'book'} for name, rev in _bookmarks]
143 'children': [{'id': rev, 'text': name, 'type': 'book'} for name, rev in _bookmarks]
145 })
144 })
146 data = {
145 data = {
147 'more': False,
146 'more': False,
148 'results': res
147 'results': res
149 }
148 }
150 return data
149 return data
@@ -1,880 +1,871 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.pullrequests
15 kallithea.controllers.pullrequests
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 pull requests controller for Kallithea for initializing pull requests
18 pull requests controller for Kallithea for initializing pull requests
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: May 7, 2012
22 :created_on: May 7, 2012
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 import formencode
30 import formencode
31 import re
31 import re
32
32
33 from pylons import request, tmpl_context as c
33 from pylons import request, tmpl_context as c
34 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
35 from webob.exc import HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest
36
36
37 from kallithea.config.routing import url
37 from kallithea.config.routing import url
38 from kallithea.lib import helpers as h
38 from kallithea.lib import helpers as h
39 from kallithea.lib import diffs
39 from kallithea.lib import diffs
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
40 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator, \
41 NotAnonymous
41 NotAnonymous
42 from kallithea.lib.base import BaseRepoController, render, jsonify
42 from kallithea.lib.base import BaseRepoController, render, jsonify
43 from kallithea.lib.compat import json, OrderedDict
43 from kallithea.lib.compat import json, OrderedDict
44 from kallithea.lib.diffs import LimitedDiffContainer
44 from kallithea.lib.diffs import LimitedDiffContainer
45 from kallithea.lib.exceptions import UserInvalidException
45 from kallithea.lib.exceptions import UserInvalidException
46 from kallithea.lib.page import Page
46 from kallithea.lib.page import Page
47 from kallithea.lib.utils import action_logger
47 from kallithea.lib.utils import action_logger
48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
48 from kallithea.lib.vcs.exceptions import EmptyRepositoryError, ChangesetDoesNotExistError
49 from kallithea.lib.vcs.utils import safe_str
49 from kallithea.lib.vcs.utils import safe_str
50 from kallithea.lib.vcs.utils.hgcompat import unionrepo
50 from kallithea.lib.vcs.utils.hgcompat import unionrepo
51 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
51 from kallithea.model.db import PullRequest, ChangesetStatus, ChangesetComment, \
52 PullRequestReviewer, Repository, User
52 PullRequestReviewer, Repository, User
53 from kallithea.model.pull_request import PullRequestModel
53 from kallithea.model.pull_request import PullRequestModel
54 from kallithea.model.meta import Session
54 from kallithea.model.meta import Session
55 from kallithea.model.repo import RepoModel
55 from kallithea.model.repo import RepoModel
56 from kallithea.model.comment import ChangesetCommentsModel
56 from kallithea.model.comment import ChangesetCommentsModel
57 from kallithea.model.changeset_status import ChangesetStatusModel
57 from kallithea.model.changeset_status import ChangesetStatusModel
58 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
58 from kallithea.model.forms import PullRequestForm, PullRequestPostForm
59 from kallithea.lib.utils2 import safe_int
59 from kallithea.lib.utils2 import safe_int
60 from kallithea.controllers.changeset import _ignorews_url, _context_url, \
60 from kallithea.controllers.changeset import _ignorews_url, _context_url, \
61 create_comment
61 create_comment
62 from kallithea.controllers.compare import CompareController
62 from kallithea.controllers.compare import CompareController
63 from kallithea.lib.graphmod import graph_data
63 from kallithea.lib.graphmod import graph_data
64
64
65 log = logging.getLogger(__name__)
65 log = logging.getLogger(__name__)
66
66
67
67
68 class PullrequestsController(BaseRepoController):
68 class PullrequestsController(BaseRepoController):
69
69
70 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
70 def _get_repo_refs(self, repo, rev=None, branch=None, branch_rev=None):
71 """return a structure with repo's interesting changesets, suitable for
71 """return a structure with repo's interesting changesets, suitable for
72 the selectors in pullrequest.html
72 the selectors in pullrequest.html
73
73
74 rev: a revision that must be in the list somehow and selected by default
74 rev: a revision that must be in the list somehow and selected by default
75 branch: a branch that must be in the list and selected by default - even if closed
75 branch: a branch that must be in the list and selected by default - even if closed
76 branch_rev: a revision of which peers should be preferred and available."""
76 branch_rev: a revision of which peers should be preferred and available."""
77 # list named branches that has been merged to this named branch - it should probably merge back
77 # list named branches that has been merged to this named branch - it should probably merge back
78 peers = []
78 peers = []
79
79
80 if rev:
80 if rev:
81 rev = safe_str(rev)
81 rev = safe_str(rev)
82
82
83 if branch:
83 if branch:
84 branch = safe_str(branch)
84 branch = safe_str(branch)
85
85
86 if branch_rev:
86 if branch_rev:
87 branch_rev = safe_str(branch_rev)
87 branch_rev = safe_str(branch_rev)
88 # a revset not restricting to merge() would be better
88 # a revset not restricting to merge() would be better
89 # (especially because it would get the branch point)
89 # (especially because it would get the branch point)
90 # ... but is currently too expensive
90 # ... but is currently too expensive
91 # including branches of children could be nice too
91 # including branches of children could be nice too
92 peerbranches = set()
92 peerbranches = set()
93 for i in repo._repo.revs(
93 for i in repo._repo.revs(
94 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
94 "sort(parents(branch(id(%s)) and merge()) - branch(id(%s)), -rev)",
95 branch_rev, branch_rev):
95 branch_rev, branch_rev):
96 abranch = repo.get_changeset(i).branch
96 abranch = repo.get_changeset(i).branch
97 if abranch not in peerbranches:
97 if abranch not in peerbranches:
98 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
98 n = 'branch:%s:%s' % (abranch, repo.get_changeset(abranch).raw_id)
99 peers.append((n, abranch))
99 peers.append((n, abranch))
100 peerbranches.add(abranch)
100 peerbranches.add(abranch)
101
101
102 selected = None
102 selected = None
103 tiprev = repo.tags.get('tip')
103 tiprev = repo.tags.get('tip')
104 tipbranch = None
104 tipbranch = None
105
105
106 branches = []
106 branches = []
107 for abranch, branchrev in repo.branches.iteritems():
107 for abranch, branchrev in repo.branches.iteritems():
108 n = 'branch:%s:%s' % (abranch, branchrev)
108 n = 'branch:%s:%s' % (abranch, branchrev)
109 desc = abranch
109 desc = abranch
110 if branchrev == tiprev:
110 if branchrev == tiprev:
111 tipbranch = abranch
111 tipbranch = abranch
112 desc = '%s (current tip)' % desc
112 desc = '%s (current tip)' % desc
113 branches.append((n, desc))
113 branches.append((n, desc))
114 if rev == branchrev:
114 if rev == branchrev:
115 selected = n
115 selected = n
116 if branch == abranch:
116 if branch == abranch:
117 if not rev:
117 if not rev:
118 selected = n
118 selected = n
119 branch = None
119 branch = None
120 if branch: # branch not in list - it is probably closed
120 if branch: # branch not in list - it is probably closed
121 branchrev = repo.closed_branches.get(branch)
121 branchrev = repo.closed_branches.get(branch)
122 if branchrev:
122 if branchrev:
123 n = 'branch:%s:%s' % (branch, branchrev)
123 n = 'branch:%s:%s' % (branch, branchrev)
124 branches.append((n, _('%s (closed)') % branch))
124 branches.append((n, _('%s (closed)') % branch))
125 selected = n
125 selected = n
126 branch = None
126 branch = None
127 if branch:
127 if branch:
128 log.debug('branch %r not found in %s', branch, repo)
128 log.debug('branch %r not found in %s', branch, repo)
129
129
130 bookmarks = []
130 bookmarks = []
131 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
131 for bookmark, bookmarkrev in repo.bookmarks.iteritems():
132 n = 'book:%s:%s' % (bookmark, bookmarkrev)
132 n = 'book:%s:%s' % (bookmark, bookmarkrev)
133 bookmarks.append((n, bookmark))
133 bookmarks.append((n, bookmark))
134 if rev == bookmarkrev:
134 if rev == bookmarkrev:
135 selected = n
135 selected = n
136
136
137 tags = []
137 tags = []
138 for tag, tagrev in repo.tags.iteritems():
138 for tag, tagrev in repo.tags.iteritems():
139 if tag == 'tip':
139 if tag == 'tip':
140 continue
140 continue
141 n = 'tag:%s:%s' % (tag, tagrev)
141 n = 'tag:%s:%s' % (tag, tagrev)
142 tags.append((n, tag))
142 tags.append((n, tag))
143 # note: even if rev == tagrev, don't select the static tag - it must be chosen explicitly
143 # note: even if rev == tagrev, don't select the static tag - it must be chosen explicitly
144
144
145 # prio 1: rev was selected as existing entry above
145 # prio 1: rev was selected as existing entry above
146
146
147 # prio 2: create special entry for rev; rev _must_ be used
147 # prio 2: create special entry for rev; rev _must_ be used
148 specials = []
148 specials = []
149 if rev and selected is None:
149 if rev and selected is None:
150 selected = 'rev:%s:%s' % (rev, rev)
150 selected = 'rev:%s:%s' % (rev, rev)
151 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
151 specials = [(selected, '%s: %s' % (_("Changeset"), rev[:12]))]
152
152
153 # prio 3: most recent peer branch
153 # prio 3: most recent peer branch
154 if peers and not selected:
154 if peers and not selected:
155 selected = peers[0][0]
155 selected = peers[0][0]
156
156
157 # prio 4: tip revision
157 # prio 4: tip revision
158 if not selected:
158 if not selected:
159 if h.is_hg(repo):
159 if h.is_hg(repo):
160 if tipbranch:
160 if tipbranch:
161 selected = 'branch:%s:%s' % (tipbranch, tiprev)
161 selected = 'branch:%s:%s' % (tipbranch, tiprev)
162 else:
162 else:
163 selected = 'tag:null:' + repo.EMPTY_CHANGESET
163 selected = 'tag:null:' + repo.EMPTY_CHANGESET
164 tags.append((selected, 'null'))
164 tags.append((selected, 'null'))
165 else:
165 else:
166 if 'master' in repo.branches:
166 if 'master' in repo.branches:
167 selected = 'branch:master:%s' % repo.branches['master']
167 selected = 'branch:master:%s' % repo.branches['master']
168 else:
168 else:
169 k, v = repo.branches.items()[0]
169 k, v = repo.branches.items()[0]
170 selected = 'branch:%s:%s' % (k, v)
170 selected = 'branch:%s:%s' % (k, v)
171
171
172 groups = [(specials, _("Special")),
172 groups = [(specials, _("Special")),
173 (peers, _("Peer branches")),
173 (peers, _("Peer branches")),
174 (bookmarks, _("Bookmarks")),
174 (bookmarks, _("Bookmarks")),
175 (branches, _("Branches")),
175 (branches, _("Branches")),
176 (tags, _("Tags")),
176 (tags, _("Tags")),
177 ]
177 ]
178 return [g for g in groups if g[0]], selected
178 return [g for g in groups if g[0]], selected
179
179
180 def _get_is_allowed_change_status(self, pull_request):
180 def _get_is_allowed_change_status(self, pull_request):
181 if pull_request.is_closed():
181 if pull_request.is_closed():
182 return False
182 return False
183
183
184 owner = request.authuser.user_id == pull_request.owner_id
184 owner = request.authuser.user_id == pull_request.owner_id
185 reviewer = PullRequestReviewer.query() \
185 reviewer = PullRequestReviewer.query() \
186 .filter(PullRequestReviewer.pull_request == pull_request) \
186 .filter(PullRequestReviewer.pull_request == pull_request) \
187 .filter(PullRequestReviewer.user_id == request.authuser.user_id) \
187 .filter(PullRequestReviewer.user_id == request.authuser.user_id) \
188 .count() != 0
188 .count() != 0
189
189
190 return request.authuser.admin or owner or reviewer
190 return request.authuser.admin or owner or reviewer
191
191
192 @LoginRequired()
192 @LoginRequired()
193 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
193 @HasRepoPermissionLevelDecorator('read')
194 'repository.admin')
195 def show_all(self, repo_name):
194 def show_all(self, repo_name):
196 c.from_ = request.GET.get('from_') or ''
195 c.from_ = request.GET.get('from_') or ''
197 c.closed = request.GET.get('closed') or ''
196 c.closed = request.GET.get('closed') or ''
198 p = safe_int(request.GET.get('page'), 1)
197 p = safe_int(request.GET.get('page'), 1)
199
198
200 q = PullRequest.query(include_closed=c.closed, sorted=True)
199 q = PullRequest.query(include_closed=c.closed, sorted=True)
201 if c.from_:
200 if c.from_:
202 q = q.filter_by(org_repo=c.db_repo)
201 q = q.filter_by(org_repo=c.db_repo)
203 else:
202 else:
204 q = q.filter_by(other_repo=c.db_repo)
203 q = q.filter_by(other_repo=c.db_repo)
205 c.pull_requests = q.all()
204 c.pull_requests = q.all()
206
205
207 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
206 c.pullrequests_pager = Page(c.pull_requests, page=p, items_per_page=100)
208
207
209 return render('/pullrequests/pullrequest_show_all.html')
208 return render('/pullrequests/pullrequest_show_all.html')
210
209
211 @LoginRequired()
210 @LoginRequired()
212 @NotAnonymous()
211 @NotAnonymous()
213 def show_my(self):
212 def show_my(self):
214 c.closed = request.GET.get('closed') or ''
213 c.closed = request.GET.get('closed') or ''
215
214
216 c.my_pull_requests = PullRequest.query(
215 c.my_pull_requests = PullRequest.query(
217 include_closed=c.closed,
216 include_closed=c.closed,
218 sorted=True,
217 sorted=True,
219 ).filter_by(owner_id=request.authuser.user_id).all()
218 ).filter_by(owner_id=request.authuser.user_id).all()
220
219
221 c.participate_in_pull_requests = []
220 c.participate_in_pull_requests = []
222 c.participate_in_pull_requests_todo = []
221 c.participate_in_pull_requests_todo = []
223 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
222 done_status = set([ChangesetStatus.STATUS_APPROVED, ChangesetStatus.STATUS_REJECTED])
224 for pr in PullRequest.query(
223 for pr in PullRequest.query(
225 include_closed=c.closed,
224 include_closed=c.closed,
226 reviewer_id=request.authuser.user_id,
225 reviewer_id=request.authuser.user_id,
227 sorted=True,
226 sorted=True,
228 ):
227 ):
229 status = pr.user_review_status(request.authuser.user_id) # very inefficient!!!
228 status = pr.user_review_status(request.authuser.user_id) # very inefficient!!!
230 if status in done_status:
229 if status in done_status:
231 c.participate_in_pull_requests.append(pr)
230 c.participate_in_pull_requests.append(pr)
232 else:
231 else:
233 c.participate_in_pull_requests_todo.append(pr)
232 c.participate_in_pull_requests_todo.append(pr)
234
233
235 return render('/pullrequests/pullrequest_show_my.html')
234 return render('/pullrequests/pullrequest_show_my.html')
236
235
237 @LoginRequired()
236 @LoginRequired()
238 @NotAnonymous()
237 @NotAnonymous()
239 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
238 @HasRepoPermissionLevelDecorator('read')
240 'repository.admin')
241 def index(self):
239 def index(self):
242 org_repo = c.db_repo
240 org_repo = c.db_repo
243 org_scm_instance = org_repo.scm_instance
241 org_scm_instance = org_repo.scm_instance
244 try:
242 try:
245 org_scm_instance.get_changeset()
243 org_scm_instance.get_changeset()
246 except EmptyRepositoryError as e:
244 except EmptyRepositoryError as e:
247 h.flash(h.literal(_('There are no changesets yet')),
245 h.flash(h.literal(_('There are no changesets yet')),
248 category='warning')
246 category='warning')
249 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
247 raise HTTPFound(location=url('summary_home', repo_name=org_repo.repo_name))
250
248
251 org_rev = request.GET.get('rev_end')
249 org_rev = request.GET.get('rev_end')
252 # rev_start is not directly useful - its parent could however be used
250 # rev_start is not directly useful - its parent could however be used
253 # as default for other and thus give a simple compare view
251 # as default for other and thus give a simple compare view
254 rev_start = request.GET.get('rev_start')
252 rev_start = request.GET.get('rev_start')
255 other_rev = None
253 other_rev = None
256 if rev_start:
254 if rev_start:
257 starters = org_repo.get_changeset(rev_start).parents
255 starters = org_repo.get_changeset(rev_start).parents
258 if starters:
256 if starters:
259 other_rev = starters[0].raw_id
257 other_rev = starters[0].raw_id
260 else:
258 else:
261 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
259 other_rev = org_repo.scm_instance.EMPTY_CHANGESET
262 branch = request.GET.get('branch')
260 branch = request.GET.get('branch')
263
261
264 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
262 c.cs_repos = [(org_repo.repo_name, org_repo.repo_name)]
265 c.default_cs_repo = org_repo.repo_name
263 c.default_cs_repo = org_repo.repo_name
266 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
264 c.cs_refs, c.default_cs_ref = self._get_repo_refs(org_scm_instance, rev=org_rev, branch=branch)
267
265
268 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
266 default_cs_ref_type, default_cs_branch, default_cs_rev = c.default_cs_ref.split(':')
269 if default_cs_ref_type != 'branch':
267 if default_cs_ref_type != 'branch':
270 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
268 default_cs_branch = org_repo.get_changeset(default_cs_rev).branch
271
269
272 # add org repo to other so we can open pull request against peer branches on itself
270 # add org repo to other so we can open pull request against peer branches on itself
273 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
271 c.a_repos = [(org_repo.repo_name, '%s (self)' % org_repo.repo_name)]
274
272
275 if org_repo.parent:
273 if org_repo.parent:
276 # add parent of this fork also and select it.
274 # add parent of this fork also and select it.
277 # use the same branch on destination as on source, if available.
275 # use the same branch on destination as on source, if available.
278 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
276 c.a_repos.append((org_repo.parent.repo_name, '%s (parent)' % org_repo.parent.repo_name))
279 c.a_repo = org_repo.parent
277 c.a_repo = org_repo.parent
280 c.a_refs, c.default_a_ref = self._get_repo_refs(
278 c.a_refs, c.default_a_ref = self._get_repo_refs(
281 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
279 org_repo.parent.scm_instance, branch=default_cs_branch, rev=other_rev)
282
280
283 else:
281 else:
284 c.a_repo = org_repo
282 c.a_repo = org_repo
285 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
283 c.a_refs, c.default_a_ref = self._get_repo_refs(org_scm_instance, rev=other_rev)
286
284
287 # gather forks and add to this list ... even though it is rare to
285 # gather forks and add to this list ... even though it is rare to
288 # request forks to pull from their parent
286 # request forks to pull from their parent
289 for fork in org_repo.forks:
287 for fork in org_repo.forks:
290 c.a_repos.append((fork.repo_name, fork.repo_name))
288 c.a_repos.append((fork.repo_name, fork.repo_name))
291
289
292 return render('/pullrequests/pullrequest.html')
290 return render('/pullrequests/pullrequest.html')
293
291
294 @LoginRequired()
292 @LoginRequired()
295 @NotAnonymous()
293 @NotAnonymous()
296 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
294 @HasRepoPermissionLevelDecorator('read')
297 'repository.admin')
298 @jsonify
295 @jsonify
299 def repo_info(self, repo_name):
296 def repo_info(self, repo_name):
300 repo = c.db_repo
297 repo = c.db_repo
301 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
298 refs, selected_ref = self._get_repo_refs(repo.scm_instance)
302 return {
299 return {
303 'description': repo.description.split('\n', 1)[0],
300 'description': repo.description.split('\n', 1)[0],
304 'selected_ref': selected_ref,
301 'selected_ref': selected_ref,
305 'refs': refs,
302 'refs': refs,
306 }
303 }
307
304
308 @LoginRequired()
305 @LoginRequired()
309 @NotAnonymous()
306 @NotAnonymous()
310 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
307 @HasRepoPermissionLevelDecorator('read')
311 'repository.admin')
312 def create(self, repo_name):
308 def create(self, repo_name):
313 repo = c.db_repo
309 repo = c.db_repo
314 try:
310 try:
315 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
311 _form = PullRequestForm(repo.repo_id)().to_python(request.POST)
316 except formencode.Invalid as errors:
312 except formencode.Invalid as errors:
317 log.error(traceback.format_exc())
313 log.error(traceback.format_exc())
318 log.error(str(errors))
314 log.error(str(errors))
319 msg = _('Error creating pull request: %s') % errors.msg
315 msg = _('Error creating pull request: %s') % errors.msg
320 h.flash(msg, 'error')
316 h.flash(msg, 'error')
321 raise HTTPBadRequest
317 raise HTTPBadRequest
322
318
323 # heads up: org and other might seem backward here ...
319 # heads up: org and other might seem backward here ...
324 org_repo_name = _form['org_repo']
320 org_repo_name = _form['org_repo']
325 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
321 org_ref = _form['org_ref'] # will have merge_rev as rev but symbolic name
326 org_repo = Repository.guess_instance(org_repo_name)
322 org_repo = Repository.guess_instance(org_repo_name)
327 (org_ref_type,
323 (org_ref_type,
328 org_ref_name,
324 org_ref_name,
329 org_rev) = org_ref.split(':')
325 org_rev) = org_ref.split(':')
330 org_display = h.short_ref(org_ref_type, org_ref_name)
326 org_display = h.short_ref(org_ref_type, org_ref_name)
331 if org_ref_type == 'rev':
327 if org_ref_type == 'rev':
332 cs = org_repo.scm_instance.get_changeset(org_rev)
328 cs = org_repo.scm_instance.get_changeset(org_rev)
333 org_ref = 'branch:%s:%s' % (cs.branch, cs.raw_id)
329 org_ref = 'branch:%s:%s' % (cs.branch, cs.raw_id)
334
330
335 other_repo_name = _form['other_repo']
331 other_repo_name = _form['other_repo']
336 other_ref = _form['other_ref'] # will have symbolic name and head revision
332 other_ref = _form['other_ref'] # will have symbolic name and head revision
337 other_repo = Repository.guess_instance(other_repo_name)
333 other_repo = Repository.guess_instance(other_repo_name)
338 (other_ref_type,
334 (other_ref_type,
339 other_ref_name,
335 other_ref_name,
340 other_rev) = other_ref.split(':')
336 other_rev) = other_ref.split(':')
341 if other_ref_type == 'rev':
337 if other_ref_type == 'rev':
342 cs = other_repo.scm_instance.get_changeset(other_rev)
338 cs = other_repo.scm_instance.get_changeset(other_rev)
343 other_ref_name = cs.raw_id[:12]
339 other_ref_name = cs.raw_id[:12]
344 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, cs.raw_id)
340 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, cs.raw_id)
345 other_display = h.short_ref(other_ref_type, other_ref_name)
341 other_display = h.short_ref(other_ref_type, other_ref_name)
346
342
347 cs_ranges, _cs_ranges_not, ancestor_revs = \
343 cs_ranges, _cs_ranges_not, ancestor_revs = \
348 CompareController._get_changesets(org_repo.scm_instance.alias,
344 CompareController._get_changesets(org_repo.scm_instance.alias,
349 other_repo.scm_instance, other_rev, # org and other "swapped"
345 other_repo.scm_instance, other_rev, # org and other "swapped"
350 org_repo.scm_instance, org_rev,
346 org_repo.scm_instance, org_rev,
351 )
347 )
352 ancestor_rev = msg = None
348 ancestor_rev = msg = None
353 if not cs_ranges:
349 if not cs_ranges:
354 msg = _('Cannot create empty pull request')
350 msg = _('Cannot create empty pull request')
355 elif not ancestor_revs:
351 elif not ancestor_revs:
356 ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
352 ancestor_rev = org_repo.scm_instance.EMPTY_CHANGESET
357 elif len(ancestor_revs) == 1:
353 elif len(ancestor_revs) == 1:
358 ancestor_rev = ancestor_revs[0]
354 ancestor_rev = ancestor_revs[0]
359 else:
355 else:
360 msg = _('Cannot create pull request - criss cross merge detected, please merge a later %s revision to %s'
356 msg = _('Cannot create pull request - criss cross merge detected, please merge a later %s revision to %s'
361 ) % (other_ref_name, org_ref_name)
357 ) % (other_ref_name, org_ref_name)
362 if ancestor_rev is None:
358 if ancestor_rev is None:
363 h.flash(msg, category='error')
359 h.flash(msg, category='error')
364 log.error(msg)
360 log.error(msg)
365 raise HTTPNotFound
361 raise HTTPNotFound
366
362
367 revisions = [cs_.raw_id for cs_ in cs_ranges]
363 revisions = [cs_.raw_id for cs_ in cs_ranges]
368
364
369 # hack: ancestor_rev is not an other_rev but we want to show the
365 # hack: ancestor_rev is not an other_rev but we want to show the
370 # requested destination and have the exact ancestor
366 # requested destination and have the exact ancestor
371 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
367 other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
372
368
373 reviewer_ids = []
369 reviewer_ids = []
374
370
375 title = _form['pullrequest_title']
371 title = _form['pullrequest_title']
376 if not title:
372 if not title:
377 if org_repo_name == other_repo_name:
373 if org_repo_name == other_repo_name:
378 title = '%s to %s' % (org_display, other_display)
374 title = '%s to %s' % (org_display, other_display)
379 else:
375 else:
380 title = '%s#%s to %s#%s' % (org_repo_name, org_display,
376 title = '%s#%s to %s#%s' % (org_repo_name, org_display,
381 other_repo_name, other_display)
377 other_repo_name, other_display)
382 description = _form['pullrequest_desc'].strip() or _('No description')
378 description = _form['pullrequest_desc'].strip() or _('No description')
383 try:
379 try:
384 created_by = User.get(request.authuser.user_id)
380 created_by = User.get(request.authuser.user_id)
385 pull_request = PullRequestModel().create(
381 pull_request = PullRequestModel().create(
386 created_by, org_repo, org_ref, other_repo, other_ref, revisions,
382 created_by, org_repo, org_ref, other_repo, other_ref, revisions,
387 title, description, reviewer_ids)
383 title, description, reviewer_ids)
388 Session().commit()
384 Session().commit()
389 h.flash(_('Successfully opened new pull request'),
385 h.flash(_('Successfully opened new pull request'),
390 category='success')
386 category='success')
391 except UserInvalidException as u:
387 except UserInvalidException as u:
392 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
388 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
393 raise HTTPBadRequest()
389 raise HTTPBadRequest()
394 except Exception:
390 except Exception:
395 h.flash(_('Error occurred while creating pull request'),
391 h.flash(_('Error occurred while creating pull request'),
396 category='error')
392 category='error')
397 log.error(traceback.format_exc())
393 log.error(traceback.format_exc())
398 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
394 raise HTTPFound(location=url('pullrequest_home', repo_name=repo_name))
399
395
400 raise HTTPFound(location=pull_request.url())
396 raise HTTPFound(location=pull_request.url())
401
397
402 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewer_ids):
398 def create_new_iteration(self, old_pull_request, new_rev, title, description, reviewer_ids):
403 org_repo = old_pull_request.org_repo
399 org_repo = old_pull_request.org_repo
404 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
400 org_ref_type, org_ref_name, org_rev = old_pull_request.org_ref.split(':')
405 new_org_rev = self._get_ref_rev(org_repo, 'rev', new_rev)
401 new_org_rev = self._get_ref_rev(org_repo, 'rev', new_rev)
406
402
407 other_repo = old_pull_request.other_repo
403 other_repo = old_pull_request.other_repo
408 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
404 other_ref_type, other_ref_name, other_rev = old_pull_request.other_ref.split(':') # other_rev is ancestor
409 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
405 #assert other_ref_type == 'branch', other_ref_type # TODO: what if not?
410 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
406 new_other_rev = self._get_ref_rev(other_repo, other_ref_type, other_ref_name)
411
407
412 cs_ranges, _cs_ranges_not, ancestor_revs = CompareController._get_changesets(org_repo.scm_instance.alias,
408 cs_ranges, _cs_ranges_not, ancestor_revs = CompareController._get_changesets(org_repo.scm_instance.alias,
413 other_repo.scm_instance, new_other_rev, # org and other "swapped"
409 other_repo.scm_instance, new_other_rev, # org and other "swapped"
414 org_repo.scm_instance, new_org_rev)
410 org_repo.scm_instance, new_org_rev)
415 ancestor_rev = msg = None
411 ancestor_rev = msg = None
416 if not cs_ranges:
412 if not cs_ranges:
417 msg = _('Cannot create empty pull request update') # cannot happen!
413 msg = _('Cannot create empty pull request update') # cannot happen!
418 elif not ancestor_revs:
414 elif not ancestor_revs:
419 msg = _('Cannot create pull request update - no common ancestor found') # cannot happen
415 msg = _('Cannot create pull request update - no common ancestor found') # cannot happen
420 elif len(ancestor_revs) == 1:
416 elif len(ancestor_revs) == 1:
421 ancestor_rev = ancestor_revs[0]
417 ancestor_rev = ancestor_revs[0]
422 else:
418 else:
423 msg = _('Cannot create pull request update - criss cross merge detected, please merge a later %s revision to %s'
419 msg = _('Cannot create pull request update - criss cross merge detected, please merge a later %s revision to %s'
424 ) % (other_ref_name, org_ref_name)
420 ) % (other_ref_name, org_ref_name)
425 if ancestor_rev is None:
421 if ancestor_rev is None:
426 h.flash(msg, category='error')
422 h.flash(msg, category='error')
427 log.error(msg)
423 log.error(msg)
428 raise HTTPNotFound
424 raise HTTPNotFound
429
425
430 old_revisions = set(old_pull_request.revisions)
426 old_revisions = set(old_pull_request.revisions)
431 revisions = [cs.raw_id for cs in cs_ranges]
427 revisions = [cs.raw_id for cs in cs_ranges]
432 new_revisions = [r for r in revisions if r not in old_revisions]
428 new_revisions = [r for r in revisions if r not in old_revisions]
433 lost = old_revisions.difference(revisions)
429 lost = old_revisions.difference(revisions)
434
430
435 infos = ['This is a new iteration of %s "%s".' %
431 infos = ['This is a new iteration of %s "%s".' %
436 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
432 (h.canonical_url('pullrequest_show', repo_name=old_pull_request.other_repo.repo_name,
437 pull_request_id=old_pull_request.pull_request_id),
433 pull_request_id=old_pull_request.pull_request_id),
438 old_pull_request.title)]
434 old_pull_request.title)]
439
435
440 if lost:
436 if lost:
441 infos.append(_('Missing changesets since the previous iteration:'))
437 infos.append(_('Missing changesets since the previous iteration:'))
442 for r in old_pull_request.revisions:
438 for r in old_pull_request.revisions:
443 if r in lost:
439 if r in lost:
444 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
440 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
445 infos.append(' %s %s' % (h.short_id(r), rev_desc))
441 infos.append(' %s %s' % (h.short_id(r), rev_desc))
446
442
447 if new_revisions:
443 if new_revisions:
448 infos.append(_('New changesets on %s %s since the previous iteration:') % (org_ref_type, org_ref_name))
444 infos.append(_('New changesets on %s %s since the previous iteration:') % (org_ref_type, org_ref_name))
449 for r in reversed(revisions):
445 for r in reversed(revisions):
450 if r in new_revisions:
446 if r in new_revisions:
451 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
447 rev_desc = org_repo.get_changeset(r).message.split('\n')[0]
452 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
448 infos.append(' %s %s' % (h.short_id(r), h.shorter(rev_desc, 80)))
453
449
454 if ancestor_rev == other_rev:
450 if ancestor_rev == other_rev:
455 infos.append(_("Ancestor didn't change - diff since previous iteration:"))
451 infos.append(_("Ancestor didn't change - diff since previous iteration:"))
456 infos.append(h.canonical_url('compare_url',
452 infos.append(h.canonical_url('compare_url',
457 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
453 repo_name=org_repo.repo_name, # other_repo is always same as repo_name
458 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
454 org_ref_type='rev', org_ref_name=h.short_id(org_rev), # use old org_rev as base
459 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
455 other_ref_type='rev', other_ref_name=h.short_id(new_org_rev),
460 )) # note: linear diff, merge or not doesn't matter
456 )) # note: linear diff, merge or not doesn't matter
461 else:
457 else:
462 infos.append(_('This iteration is based on another %s revision and there is no simple diff.') % other_ref_name)
458 infos.append(_('This iteration is based on another %s revision and there is no simple diff.') % other_ref_name)
463 else:
459 else:
464 infos.append(_('No changes found on %s %s since previous iteration.') % (org_ref_type, org_ref_name))
460 infos.append(_('No changes found on %s %s since previous iteration.') % (org_ref_type, org_ref_name))
465 # TODO: fail?
461 # TODO: fail?
466
462
467 # hack: ancestor_rev is not an other_ref but we want to show the
463 # hack: ancestor_rev is not an other_ref but we want to show the
468 # requested destination and have the exact ancestor
464 # requested destination and have the exact ancestor
469 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
465 new_other_ref = '%s:%s:%s' % (other_ref_type, other_ref_name, ancestor_rev)
470 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
466 new_org_ref = '%s:%s:%s' % (org_ref_type, org_ref_name, new_org_rev)
471
467
472 try:
468 try:
473 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
469 title, old_v = re.match(r'(.*)\(v(\d+)\)\s*$', title).groups()
474 v = int(old_v) + 1
470 v = int(old_v) + 1
475 except (AttributeError, ValueError):
471 except (AttributeError, ValueError):
476 v = 2
472 v = 2
477 title = '%s (v%s)' % (title.strip(), v)
473 title = '%s (v%s)' % (title.strip(), v)
478
474
479 # using a mail-like separator, insert new iteration info in description with latest first
475 # using a mail-like separator, insert new iteration info in description with latest first
480 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
476 descriptions = description.replace('\r\n', '\n').split('\n-- \n', 1)
481 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
477 description = descriptions[0].strip() + '\n\n-- \n' + '\n'.join(infos)
482 if len(descriptions) > 1:
478 if len(descriptions) > 1:
483 description += '\n\n' + descriptions[1].strip()
479 description += '\n\n' + descriptions[1].strip()
484
480
485 try:
481 try:
486 created_by = User.get(request.authuser.user_id)
482 created_by = User.get(request.authuser.user_id)
487 pull_request = PullRequestModel().create(
483 pull_request = PullRequestModel().create(
488 created_by, org_repo, new_org_ref, other_repo, new_other_ref, revisions,
484 created_by, org_repo, new_org_ref, other_repo, new_other_ref, revisions,
489 title, description, reviewer_ids)
485 title, description, reviewer_ids)
490 except UserInvalidException as u:
486 except UserInvalidException as u:
491 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
487 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
492 raise HTTPBadRequest()
488 raise HTTPBadRequest()
493 except Exception:
489 except Exception:
494 h.flash(_('Error occurred while creating pull request'),
490 h.flash(_('Error occurred while creating pull request'),
495 category='error')
491 category='error')
496 log.error(traceback.format_exc())
492 log.error(traceback.format_exc())
497 raise HTTPFound(location=old_pull_request.url())
493 raise HTTPFound(location=old_pull_request.url())
498
494
499 ChangesetCommentsModel().create(
495 ChangesetCommentsModel().create(
500 text=_('Closed, next iteration: %s .') % pull_request.url(canonical=True),
496 text=_('Closed, next iteration: %s .') % pull_request.url(canonical=True),
501 repo=old_pull_request.other_repo_id,
497 repo=old_pull_request.other_repo_id,
502 author=request.authuser.user_id,
498 author=request.authuser.user_id,
503 pull_request=old_pull_request.pull_request_id,
499 pull_request=old_pull_request.pull_request_id,
504 closing_pr=True)
500 closing_pr=True)
505 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
501 PullRequestModel().close_pull_request(old_pull_request.pull_request_id)
506
502
507 Session().commit()
503 Session().commit()
508 h.flash(_('New pull request iteration created'),
504 h.flash(_('New pull request iteration created'),
509 category='success')
505 category='success')
510
506
511 raise HTTPFound(location=pull_request.url())
507 raise HTTPFound(location=pull_request.url())
512
508
513 # pullrequest_post for PR editing
509 # pullrequest_post for PR editing
514 @LoginRequired()
510 @LoginRequired()
515 @NotAnonymous()
511 @NotAnonymous()
516 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
512 @HasRepoPermissionLevelDecorator('read')
517 'repository.admin')
518 def post(self, repo_name, pull_request_id):
513 def post(self, repo_name, pull_request_id):
519 pull_request = PullRequest.get_or_404(pull_request_id)
514 pull_request = PullRequest.get_or_404(pull_request_id)
520 if pull_request.is_closed():
515 if pull_request.is_closed():
521 raise HTTPForbidden()
516 raise HTTPForbidden()
522 assert pull_request.other_repo.repo_name == repo_name
517 assert pull_request.other_repo.repo_name == repo_name
523 #only owner or admin can update it
518 #only owner or admin can update it
524 owner = pull_request.owner_id == request.authuser.user_id
519 owner = pull_request.owner_id == request.authuser.user_id
525 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
520 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
526 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
521 if not (h.HasPermissionAny('hg.admin')() or repo_admin or owner):
527 raise HTTPForbidden()
522 raise HTTPForbidden()
528
523
529 _form = PullRequestPostForm()().to_python(request.POST)
524 _form = PullRequestPostForm()().to_python(request.POST)
530 reviewer_ids = set(int(s) for s in _form['review_members'])
525 reviewer_ids = set(int(s) for s in _form['review_members'])
531
526
532 org_reviewer_ids = set(int(s) for s in _form['org_review_members'])
527 org_reviewer_ids = set(int(s) for s in _form['org_review_members'])
533 current_reviewer_ids = set(prr.user_id for prr in pull_request.reviewers)
528 current_reviewer_ids = set(prr.user_id for prr in pull_request.reviewers)
534 other_added = [User.get(u) for u in current_reviewer_ids - org_reviewer_ids]
529 other_added = [User.get(u) for u in current_reviewer_ids - org_reviewer_ids]
535 other_removed = [User.get(u) for u in org_reviewer_ids - current_reviewer_ids]
530 other_removed = [User.get(u) for u in org_reviewer_ids - current_reviewer_ids]
536 if other_added:
531 if other_added:
537 h.flash(_('Meanwhile, the following reviewers have been added: %s') %
532 h.flash(_('Meanwhile, the following reviewers have been added: %s') %
538 (', '.join(u.username for u in other_added)),
533 (', '.join(u.username for u in other_added)),
539 category='warning')
534 category='warning')
540 if other_removed:
535 if other_removed:
541 h.flash(_('Meanwhile, the following reviewers have been removed: %s') %
536 h.flash(_('Meanwhile, the following reviewers have been removed: %s') %
542 (', '.join(u.username for u in other_removed)),
537 (', '.join(u.username for u in other_removed)),
543 category='warning')
538 category='warning')
544
539
545 if _form['updaterev']:
540 if _form['updaterev']:
546 return self.create_new_iteration(pull_request,
541 return self.create_new_iteration(pull_request,
547 _form['updaterev'],
542 _form['updaterev'],
548 _form['pullrequest_title'],
543 _form['pullrequest_title'],
549 _form['pullrequest_desc'],
544 _form['pullrequest_desc'],
550 reviewer_ids)
545 reviewer_ids)
551
546
552 old_description = pull_request.description
547 old_description = pull_request.description
553 pull_request.title = _form['pullrequest_title']
548 pull_request.title = _form['pullrequest_title']
554 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
549 pull_request.description = _form['pullrequest_desc'].strip() or _('No description')
555 pull_request.owner = User.get_by_username(_form['owner'])
550 pull_request.owner = User.get_by_username(_form['owner'])
556 user = User.get(request.authuser.user_id)
551 user = User.get(request.authuser.user_id)
557 add_reviewer_ids = reviewer_ids - org_reviewer_ids - current_reviewer_ids
552 add_reviewer_ids = reviewer_ids - org_reviewer_ids - current_reviewer_ids
558 remove_reviewer_ids = (org_reviewer_ids - reviewer_ids) & current_reviewer_ids
553 remove_reviewer_ids = (org_reviewer_ids - reviewer_ids) & current_reviewer_ids
559 try:
554 try:
560 PullRequestModel().mention_from_description(user, pull_request, old_description)
555 PullRequestModel().mention_from_description(user, pull_request, old_description)
561 PullRequestModel().add_reviewers(user, pull_request, add_reviewer_ids)
556 PullRequestModel().add_reviewers(user, pull_request, add_reviewer_ids)
562 PullRequestModel().remove_reviewers(user, pull_request, remove_reviewer_ids)
557 PullRequestModel().remove_reviewers(user, pull_request, remove_reviewer_ids)
563 except UserInvalidException as u:
558 except UserInvalidException as u:
564 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
559 h.flash(_('Invalid reviewer "%s" specified') % u, category='error')
565 raise HTTPBadRequest()
560 raise HTTPBadRequest()
566
561
567 Session().commit()
562 Session().commit()
568 h.flash(_('Pull request updated'), category='success')
563 h.flash(_('Pull request updated'), category='success')
569
564
570 raise HTTPFound(location=pull_request.url())
565 raise HTTPFound(location=pull_request.url())
571
566
572 @LoginRequired()
567 @LoginRequired()
573 @NotAnonymous()
568 @NotAnonymous()
574 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
569 @HasRepoPermissionLevelDecorator('read')
575 'repository.admin')
576 @jsonify
570 @jsonify
577 def delete(self, repo_name, pull_request_id):
571 def delete(self, repo_name, pull_request_id):
578 pull_request = PullRequest.get_or_404(pull_request_id)
572 pull_request = PullRequest.get_or_404(pull_request_id)
579 #only owner can delete it !
573 #only owner can delete it !
580 if pull_request.owner_id == request.authuser.user_id:
574 if pull_request.owner_id == request.authuser.user_id:
581 PullRequestModel().delete(pull_request)
575 PullRequestModel().delete(pull_request)
582 Session().commit()
576 Session().commit()
583 h.flash(_('Successfully deleted pull request'),
577 h.flash(_('Successfully deleted pull request'),
584 category='success')
578 category='success')
585 raise HTTPFound(location=url('my_pullrequests'))
579 raise HTTPFound(location=url('my_pullrequests'))
586 raise HTTPForbidden()
580 raise HTTPForbidden()
587
581
588 @LoginRequired()
582 @LoginRequired()
589 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
583 @HasRepoPermissionLevelDecorator('read')
590 'repository.admin')
591 def show(self, repo_name, pull_request_id, extra=None):
584 def show(self, repo_name, pull_request_id, extra=None):
592 repo_model = RepoModel()
585 repo_model = RepoModel()
593 c.users_array = repo_model.get_users_js()
586 c.users_array = repo_model.get_users_js()
594 c.user_groups_array = repo_model.get_user_groups_js()
587 c.user_groups_array = repo_model.get_user_groups_js()
595 c.pull_request = PullRequest.get_or_404(pull_request_id)
588 c.pull_request = PullRequest.get_or_404(pull_request_id)
596 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
589 c.allowed_to_change_status = self._get_is_allowed_change_status(c.pull_request)
597 cc_model = ChangesetCommentsModel()
590 cc_model = ChangesetCommentsModel()
598 cs_model = ChangesetStatusModel()
591 cs_model = ChangesetStatusModel()
599
592
600 # pull_requests repo_name we opened it against
593 # pull_requests repo_name we opened it against
601 # ie. other_repo must match
594 # ie. other_repo must match
602 if repo_name != c.pull_request.other_repo.repo_name:
595 if repo_name != c.pull_request.other_repo.repo_name:
603 raise HTTPNotFound
596 raise HTTPNotFound
604
597
605 # load compare data into template context
598 # load compare data into template context
606 c.cs_repo = c.pull_request.org_repo
599 c.cs_repo = c.pull_request.org_repo
607 (c.cs_ref_type,
600 (c.cs_ref_type,
608 c.cs_ref_name,
601 c.cs_ref_name,
609 c.cs_rev) = c.pull_request.org_ref.split(':')
602 c.cs_rev) = c.pull_request.org_ref.split(':')
610
603
611 c.a_repo = c.pull_request.other_repo
604 c.a_repo = c.pull_request.other_repo
612 (c.a_ref_type,
605 (c.a_ref_type,
613 c.a_ref_name,
606 c.a_ref_name,
614 c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor
607 c.a_rev) = c.pull_request.other_ref.split(':') # a_rev is ancestor
615
608
616 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
609 org_scm_instance = c.cs_repo.scm_instance # property with expensive cache invalidation check!!!
617 try:
610 try:
618 c.cs_ranges = [org_scm_instance.get_changeset(x)
611 c.cs_ranges = [org_scm_instance.get_changeset(x)
619 for x in c.pull_request.revisions]
612 for x in c.pull_request.revisions]
620 except ChangesetDoesNotExistError:
613 except ChangesetDoesNotExistError:
621 c.cs_ranges = []
614 c.cs_ranges = []
622 h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
615 h.flash(_('Revision %s not found in %s') % (x, c.cs_repo.repo_name),
623 'error')
616 'error')
624 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
617 c.cs_ranges_org = None # not stored and not important and moving target - could be calculated ...
625 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
618 revs = [ctx.revision for ctx in reversed(c.cs_ranges)]
626 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
619 c.jsdata = json.dumps(graph_data(org_scm_instance, revs))
627
620
628 c.is_range = False
621 c.is_range = False
629 try:
622 try:
630 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
623 if c.a_ref_type == 'rev': # this looks like a free range where target is ancestor
631 cs_a = org_scm_instance.get_changeset(c.a_rev)
624 cs_a = org_scm_instance.get_changeset(c.a_rev)
632 root_parents = c.cs_ranges[0].parents
625 root_parents = c.cs_ranges[0].parents
633 c.is_range = cs_a in root_parents
626 c.is_range = cs_a in root_parents
634 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
627 #c.merge_root = len(root_parents) > 1 # a range starting with a merge might deserve a warning
635 except ChangesetDoesNotExistError: # probably because c.a_rev not found
628 except ChangesetDoesNotExistError: # probably because c.a_rev not found
636 pass
629 pass
637 except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing
630 except IndexError: # probably because c.cs_ranges is empty, probably because revisions are missing
638 pass
631 pass
639
632
640 avail_revs = set()
633 avail_revs = set()
641 avail_show = []
634 avail_show = []
642 c.cs_branch_name = c.cs_ref_name
635 c.cs_branch_name = c.cs_ref_name
643 c.a_branch_name = None
636 c.a_branch_name = None
644 other_scm_instance = c.a_repo.scm_instance
637 other_scm_instance = c.a_repo.scm_instance
645 c.update_msg = ""
638 c.update_msg = ""
646 c.update_msg_other = ""
639 c.update_msg_other = ""
647 try:
640 try:
648 if not c.cs_ranges:
641 if not c.cs_ranges:
649 c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev
642 c.update_msg = _('Error: changesets not found when displaying pull request from %s.') % c.cs_rev
650 elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
643 elif org_scm_instance.alias == 'hg' and c.a_ref_name != 'ancestor':
651 if c.cs_ref_type != 'branch':
644 if c.cs_ref_type != 'branch':
652 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
645 c.cs_branch_name = org_scm_instance.get_changeset(c.cs_ref_name).branch # use ref_type ?
653 c.a_branch_name = c.a_ref_name
646 c.a_branch_name = c.a_ref_name
654 if c.a_ref_type != 'branch':
647 if c.a_ref_type != 'branch':
655 try:
648 try:
656 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
649 c.a_branch_name = other_scm_instance.get_changeset(c.a_ref_name).branch # use ref_type ?
657 except EmptyRepositoryError:
650 except EmptyRepositoryError:
658 c.a_branch_name = 'null' # not a branch name ... but close enough
651 c.a_branch_name = 'null' # not a branch name ... but close enough
659 # candidates: descendants of old head that are on the right branch
652 # candidates: descendants of old head that are on the right branch
660 # and not are the old head itself ...
653 # and not are the old head itself ...
661 # and nothing at all if old head is a descendant of target ref name
654 # and nothing at all if old head is a descendant of target ref name
662 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
655 if not c.is_range and other_scm_instance._repo.revs('present(%s)::&%s', c.cs_ranges[-1].raw_id, c.a_branch_name):
663 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
656 c.update_msg = _('This pull request has already been merged to %s.') % c.a_branch_name
664 elif c.pull_request.is_closed():
657 elif c.pull_request.is_closed():
665 c.update_msg = _('This pull request has been closed and can not be updated.')
658 c.update_msg = _('This pull request has been closed and can not be updated.')
666 else: # look for descendants of PR head on source branch in org repo
659 else: # look for descendants of PR head on source branch in org repo
667 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
660 avail_revs = org_scm_instance._repo.revs('%s:: & branch(%s)',
668 revs[0], c.cs_branch_name)
661 revs[0], c.cs_branch_name)
669 if len(avail_revs) > 1: # more than just revs[0]
662 if len(avail_revs) > 1: # more than just revs[0]
670 # also show changesets that not are descendants but would be merged in
663 # also show changesets that not are descendants but would be merged in
671 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
664 targethead = other_scm_instance.get_changeset(c.a_branch_name).raw_id
672 if org_scm_instance.path != other_scm_instance.path:
665 if org_scm_instance.path != other_scm_instance.path:
673 # Note: org_scm_instance.path must come first so all
666 # Note: org_scm_instance.path must come first so all
674 # valid revision numbers are 100% org_scm compatible
667 # valid revision numbers are 100% org_scm compatible
675 # - both for avail_revs and for revset results
668 # - both for avail_revs and for revset results
676 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
669 hgrepo = unionrepo.unionrepository(org_scm_instance.baseui,
677 org_scm_instance.path,
670 org_scm_instance.path,
678 other_scm_instance.path)
671 other_scm_instance.path)
679 else:
672 else:
680 hgrepo = org_scm_instance._repo
673 hgrepo = org_scm_instance._repo
681 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
674 show = set(hgrepo.revs('::%ld & !::parents(%s) & !::%s',
682 avail_revs, revs[0], targethead))
675 avail_revs, revs[0], targethead))
683 c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name
676 c.update_msg = _('The following additional changes are available on %s:') % c.cs_branch_name
684 else:
677 else:
685 show = set()
678 show = set()
686 avail_revs = set() # drop revs[0]
679 avail_revs = set() # drop revs[0]
687 c.update_msg = _('No additional changesets found for iterating on this pull request.')
680 c.update_msg = _('No additional changesets found for iterating on this pull request.')
688
681
689 # TODO: handle branch heads that not are tip-most
682 # TODO: handle branch heads that not are tip-most
690 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
683 brevs = org_scm_instance._repo.revs('%s - %ld - %s', c.cs_branch_name, avail_revs, revs[0])
691 if brevs:
684 if brevs:
692 # also show changesets that are on branch but neither ancestors nor descendants
685 # also show changesets that are on branch but neither ancestors nor descendants
693 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
686 show.update(org_scm_instance._repo.revs('::%ld - ::%ld - ::%s', brevs, avail_revs, c.a_branch_name))
694 show.add(revs[0]) # make sure graph shows this so we can see how they relate
687 show.add(revs[0]) # make sure graph shows this so we can see how they relate
695 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
688 c.update_msg_other = _('Note: Branch %s has another head: %s.') % (c.cs_branch_name,
696 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
689 h.short_id(org_scm_instance.get_changeset((max(brevs))).raw_id))
697
690
698 avail_show = sorted(show, reverse=True)
691 avail_show = sorted(show, reverse=True)
699
692
700 elif org_scm_instance.alias == 'git':
693 elif org_scm_instance.alias == 'git':
701 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
694 c.cs_repo.scm_instance.get_changeset(c.cs_rev) # check it exists - raise ChangesetDoesNotExistError if not
702 c.update_msg = _("Git pull requests don't support iterating yet.")
695 c.update_msg = _("Git pull requests don't support iterating yet.")
703 except ChangesetDoesNotExistError:
696 except ChangesetDoesNotExistError:
704 c.update_msg = _('Error: some changesets not found when displaying pull request from %s.') % c.cs_rev
697 c.update_msg = _('Error: some changesets not found when displaying pull request from %s.') % c.cs_rev
705
698
706 c.avail_revs = avail_revs
699 c.avail_revs = avail_revs
707 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
700 c.avail_cs = [org_scm_instance.get_changeset(r) for r in avail_show]
708 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
701 c.avail_jsdata = json.dumps(graph_data(org_scm_instance, avail_show))
709
702
710 raw_ids = [x.raw_id for x in c.cs_ranges]
703 raw_ids = [x.raw_id for x in c.cs_ranges]
711 c.cs_comments = c.cs_repo.get_comments(raw_ids)
704 c.cs_comments = c.cs_repo.get_comments(raw_ids)
712 c.statuses = c.cs_repo.statuses(raw_ids)
705 c.statuses = c.cs_repo.statuses(raw_ids)
713
706
714 ignore_whitespace = request.GET.get('ignorews') == '1'
707 ignore_whitespace = request.GET.get('ignorews') == '1'
715 line_context = safe_int(request.GET.get('context'), 3)
708 line_context = safe_int(request.GET.get('context'), 3)
716 c.ignorews_url = _ignorews_url
709 c.ignorews_url = _ignorews_url
717 c.context_url = _context_url
710 c.context_url = _context_url
718 c.fulldiff = request.GET.get('fulldiff')
711 c.fulldiff = request.GET.get('fulldiff')
719 diff_limit = self.cut_off_limit if not c.fulldiff else None
712 diff_limit = self.cut_off_limit if not c.fulldiff else None
720
713
721 # we swap org/other ref since we run a simple diff on one repo
714 # we swap org/other ref since we run a simple diff on one repo
722 log.debug('running diff between %s and %s in %s',
715 log.debug('running diff between %s and %s in %s',
723 c.a_rev, c.cs_rev, org_scm_instance.path)
716 c.a_rev, c.cs_rev, org_scm_instance.path)
724 try:
717 try:
725 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
718 txtdiff = org_scm_instance.get_diff(rev1=safe_str(c.a_rev), rev2=safe_str(c.cs_rev),
726 ignore_whitespace=ignore_whitespace,
719 ignore_whitespace=ignore_whitespace,
727 context=line_context)
720 context=line_context)
728 except ChangesetDoesNotExistError:
721 except ChangesetDoesNotExistError:
729 txtdiff = _("The diff can't be shown - the PR revisions could not be found.")
722 txtdiff = _("The diff can't be shown - the PR revisions could not be found.")
730 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
723 diff_processor = diffs.DiffProcessor(txtdiff or '', format='gitdiff',
731 diff_limit=diff_limit)
724 diff_limit=diff_limit)
732 _parsed = diff_processor.prepare()
725 _parsed = diff_processor.prepare()
733
726
734 c.limited_diff = False
727 c.limited_diff = False
735 if isinstance(_parsed, LimitedDiffContainer):
728 if isinstance(_parsed, LimitedDiffContainer):
736 c.limited_diff = True
729 c.limited_diff = True
737
730
738 c.file_diff_data = []
731 c.file_diff_data = []
739 c.lines_added = 0
732 c.lines_added = 0
740 c.lines_deleted = 0
733 c.lines_deleted = 0
741
734
742 for f in _parsed:
735 for f in _parsed:
743 st = f['stats']
736 st = f['stats']
744 c.lines_added += st['added']
737 c.lines_added += st['added']
745 c.lines_deleted += st['deleted']
738 c.lines_deleted += st['deleted']
746 filename = f['filename']
739 filename = f['filename']
747 fid = h.FID('', filename)
740 fid = h.FID('', filename)
748 diff = diff_processor.as_html(enable_comments=True,
741 diff = diff_processor.as_html(enable_comments=True,
749 parsed_lines=[f])
742 parsed_lines=[f])
750 c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, diff, st))
743 c.file_diff_data.append((fid, None, f['operation'], f['old_filename'], filename, diff, st))
751
744
752 # inline comments
745 # inline comments
753 c.inline_cnt = 0
746 c.inline_cnt = 0
754 c.inline_comments = cc_model.get_inline_comments(
747 c.inline_comments = cc_model.get_inline_comments(
755 c.db_repo.repo_id,
748 c.db_repo.repo_id,
756 pull_request=pull_request_id)
749 pull_request=pull_request_id)
757 # count inline comments
750 # count inline comments
758 for __, lines in c.inline_comments:
751 for __, lines in c.inline_comments:
759 for comments in lines.values():
752 for comments in lines.values():
760 c.inline_cnt += len(comments)
753 c.inline_cnt += len(comments)
761 # comments
754 # comments
762 c.comments = cc_model.get_comments(c.db_repo.repo_id,
755 c.comments = cc_model.get_comments(c.db_repo.repo_id,
763 pull_request=pull_request_id)
756 pull_request=pull_request_id)
764
757
765 # (badly named) pull-request status calculation based on reviewer votes
758 # (badly named) pull-request status calculation based on reviewer votes
766 (c.pull_request_reviewers,
759 (c.pull_request_reviewers,
767 c.pull_request_pending_reviewers,
760 c.pull_request_pending_reviewers,
768 c.current_voting_result,
761 c.current_voting_result,
769 ) = cs_model.calculate_pull_request_result(c.pull_request)
762 ) = cs_model.calculate_pull_request_result(c.pull_request)
770 c.changeset_statuses = ChangesetStatus.STATUSES
763 c.changeset_statuses = ChangesetStatus.STATUSES
771
764
772 c.as_form = False
765 c.as_form = False
773 c.ancestors = None # [c.a_rev] ... but that is shown in an other way
766 c.ancestors = None # [c.a_rev] ... but that is shown in an other way
774 return render('/pullrequests/pullrequest_show.html')
767 return render('/pullrequests/pullrequest_show.html')
775
768
776 @LoginRequired()
769 @LoginRequired()
777 @NotAnonymous()
770 @NotAnonymous()
778 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
771 @HasRepoPermissionLevelDecorator('read')
779 'repository.admin')
780 @jsonify
772 @jsonify
781 def comment(self, repo_name, pull_request_id):
773 def comment(self, repo_name, pull_request_id):
782 pull_request = PullRequest.get_or_404(pull_request_id)
774 pull_request = PullRequest.get_or_404(pull_request_id)
783
775
784 status = request.POST.get('changeset_status')
776 status = request.POST.get('changeset_status')
785 close_pr = request.POST.get('save_close')
777 close_pr = request.POST.get('save_close')
786 delete = request.POST.get('save_delete')
778 delete = request.POST.get('save_delete')
787 f_path = request.POST.get('f_path')
779 f_path = request.POST.get('f_path')
788 line_no = request.POST.get('line')
780 line_no = request.POST.get('line')
789
781
790 if (status or close_pr or delete) and (f_path or line_no):
782 if (status or close_pr or delete) and (f_path or line_no):
791 # status votes and closing is only possible in general comments
783 # status votes and closing is only possible in general comments
792 raise HTTPBadRequest()
784 raise HTTPBadRequest()
793
785
794 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
786 allowed_to_change_status = self._get_is_allowed_change_status(pull_request)
795 if not allowed_to_change_status:
787 if not allowed_to_change_status:
796 if status or close_pr:
788 if status or close_pr:
797 h.flash(_('No permission to change pull request status'), 'error')
789 h.flash(_('No permission to change pull request status'), 'error')
798 raise HTTPForbidden()
790 raise HTTPForbidden()
799
791
800 if delete == "delete":
792 if delete == "delete":
801 if (pull_request.owner_id == request.authuser.user_id or
793 if (pull_request.owner_id == request.authuser.user_id or
802 h.HasPermissionAny('hg.admin')() or
794 h.HasPermissionAny('hg.admin')() or
803 h.HasRepoPermissionAny('repository.admin')(pull_request.org_repo.repo_name) or
795 h.HasRepoPermissionLevel('admin')(pull_request.org_repo.repo_name) or
804 h.HasRepoPermissionAny('repository.admin')(pull_request.other_repo.repo_name)
796 h.HasRepoPermissionLevel('admin')(pull_request.other_repo.repo_name)
805 ) and not pull_request.is_closed():
797 ) and not pull_request.is_closed():
806 PullRequestModel().delete(pull_request)
798 PullRequestModel().delete(pull_request)
807 Session().commit()
799 Session().commit()
808 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
800 h.flash(_('Successfully deleted pull request %s') % pull_request_id,
809 category='success')
801 category='success')
810 return {
802 return {
811 'location': url('my_pullrequests'), # or repo pr list?
803 'location': url('my_pullrequests'), # or repo pr list?
812 }
804 }
813 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
805 raise HTTPFound(location=url('my_pullrequests')) # or repo pr list?
814 raise HTTPForbidden()
806 raise HTTPForbidden()
815
807
816 text = request.POST.get('text', '').strip()
808 text = request.POST.get('text', '').strip()
817
809
818 comment = create_comment(
810 comment = create_comment(
819 text,
811 text,
820 status,
812 status,
821 pull_request_id=pull_request_id,
813 pull_request_id=pull_request_id,
822 f_path=f_path,
814 f_path=f_path,
823 line_no=line_no,
815 line_no=line_no,
824 closing_pr=close_pr,
816 closing_pr=close_pr,
825 )
817 )
826
818
827 action_logger(request.authuser,
819 action_logger(request.authuser,
828 'user_commented_pull_request:%s' % pull_request_id,
820 'user_commented_pull_request:%s' % pull_request_id,
829 c.db_repo, request.ip_addr, self.sa)
821 c.db_repo, request.ip_addr, self.sa)
830
822
831 if status:
823 if status:
832 ChangesetStatusModel().set_status(
824 ChangesetStatusModel().set_status(
833 c.db_repo.repo_id,
825 c.db_repo.repo_id,
834 status,
826 status,
835 request.authuser.user_id,
827 request.authuser.user_id,
836 comment,
828 comment,
837 pull_request=pull_request_id
829 pull_request=pull_request_id
838 )
830 )
839
831
840 if close_pr:
832 if close_pr:
841 PullRequestModel().close_pull_request(pull_request_id)
833 PullRequestModel().close_pull_request(pull_request_id)
842 action_logger(request.authuser,
834 action_logger(request.authuser,
843 'user_closed_pull_request:%s' % pull_request_id,
835 'user_closed_pull_request:%s' % pull_request_id,
844 c.db_repo, request.ip_addr, self.sa)
836 c.db_repo, request.ip_addr, self.sa)
845
837
846 Session().commit()
838 Session().commit()
847
839
848 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
840 if not request.environ.get('HTTP_X_PARTIAL_XHR'):
849 raise HTTPFound(location=pull_request.url())
841 raise HTTPFound(location=pull_request.url())
850
842
851 data = {
843 data = {
852 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
844 'target_id': h.safeid(h.safe_unicode(request.POST.get('f_path'))),
853 }
845 }
854 if comment is not None:
846 if comment is not None:
855 c.comment = comment
847 c.comment = comment
856 data.update(comment.get_dict())
848 data.update(comment.get_dict())
857 data.update({'rendered_text':
849 data.update({'rendered_text':
858 render('changeset/changeset_comment_block.html')})
850 render('changeset/changeset_comment_block.html')})
859
851
860 return data
852 return data
861
853
862 @LoginRequired()
854 @LoginRequired()
863 @NotAnonymous()
855 @NotAnonymous()
864 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
856 @HasRepoPermissionLevelDecorator('read')
865 'repository.admin')
866 @jsonify
857 @jsonify
867 def delete_comment(self, repo_name, comment_id):
858 def delete_comment(self, repo_name, comment_id):
868 co = ChangesetComment.get(comment_id)
859 co = ChangesetComment.get(comment_id)
869 if co.pull_request.is_closed():
860 if co.pull_request.is_closed():
870 #don't allow deleting comments on closed pull request
861 #don't allow deleting comments on closed pull request
871 raise HTTPForbidden()
862 raise HTTPForbidden()
872
863
873 owner = co.author_id == request.authuser.user_id
864 owner = co.author_id == request.authuser.user_id
874 repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
865 repo_admin = h.HasRepoPermissionLevel('admin')(c.repo_name)
875 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
866 if h.HasPermissionAny('hg.admin')() or repo_admin or owner:
876 ChangesetCommentsModel().delete(comment=co)
867 ChangesetCommentsModel().delete(comment=co)
877 Session().commit()
868 Session().commit()
878 return True
869 return True
879 else:
870 else:
880 raise HTTPForbidden()
871 raise HTTPForbidden()
@@ -1,227 +1,224 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.summary
15 kallithea.controllers.summary
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
17
17
18 Summary controller for Kallithea
18 Summary 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 18, 2010
22 :created_on: Apr 18, 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 traceback
28 import traceback
29 import calendar
29 import calendar
30 import logging
30 import logging
31 import itertools
31 import itertools
32 from time import mktime
32 from time import mktime
33 from datetime import timedelta, date
33 from datetime import timedelta, date
34
34
35 from pylons import tmpl_context as c, request
35 from pylons import tmpl_context as c, request
36 from pylons.i18n.translation import _
36 from pylons.i18n.translation import _
37 from webob.exc import HTTPBadRequest
37 from webob.exc import HTTPBadRequest
38
38
39 from beaker.cache import cache_region, region_invalidate
39 from beaker.cache import cache_region, region_invalidate
40
40
41 from kallithea.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
41 from kallithea.lib.vcs.exceptions import ChangesetError, EmptyRepositoryError, \
42 NodeDoesNotExistError
42 NodeDoesNotExistError
43 from kallithea.config.conf import ALL_READMES, ALL_EXTS, LANGUAGES_EXTENSIONS_MAP
43 from kallithea.config.conf import ALL_READMES, ALL_EXTS, LANGUAGES_EXTENSIONS_MAP
44 from kallithea.model.db import Statistics, CacheInvalidation, User
44 from kallithea.model.db import Statistics, CacheInvalidation, User
45 from kallithea.lib.utils2 import safe_str
45 from kallithea.lib.utils2 import safe_str
46 from kallithea.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
46 from kallithea.lib.auth import LoginRequired, HasRepoPermissionLevelDecorator, \
47 NotAnonymous
47 NotAnonymous
48 from kallithea.lib.base import BaseRepoController, render, jsonify
48 from kallithea.lib.base import BaseRepoController, render, jsonify
49 from kallithea.lib.vcs.backends.base import EmptyChangeset
49 from kallithea.lib.vcs.backends.base import EmptyChangeset
50 from kallithea.lib.markup_renderer import MarkupRenderer
50 from kallithea.lib.markup_renderer import MarkupRenderer
51 from kallithea.lib.celerylib.tasks import get_commits_stats
51 from kallithea.lib.celerylib.tasks import get_commits_stats
52 from kallithea.lib.compat import json
52 from kallithea.lib.compat import json
53 from kallithea.lib.vcs.nodes import FileNode
53 from kallithea.lib.vcs.nodes import FileNode
54 from kallithea.controllers.changelog import _load_changelog_summary
54 from kallithea.controllers.changelog import _load_changelog_summary
55
55
56 log = logging.getLogger(__name__)
56 log = logging.getLogger(__name__)
57
57
58 README_FILES = [''.join([x[0][0], x[1][0]]) for x in
58 README_FILES = [''.join([x[0][0], x[1][0]]) for x in
59 sorted(list(itertools.product(ALL_READMES, ALL_EXTS)),
59 sorted(list(itertools.product(ALL_READMES, ALL_EXTS)),
60 key=lambda y:y[0][1] + y[1][1])]
60 key=lambda y:y[0][1] + y[1][1])]
61
61
62
62
63 class SummaryController(BaseRepoController):
63 class SummaryController(BaseRepoController):
64
64
65 def __before__(self):
65 def __before__(self):
66 super(SummaryController, self).__before__()
66 super(SummaryController, self).__before__()
67
67
68 def __get_readme_data(self, db_repo):
68 def __get_readme_data(self, db_repo):
69 repo_name = db_repo.repo_name
69 repo_name = db_repo.repo_name
70 log.debug('Looking for README file')
70 log.debug('Looking for README file')
71
71
72 @cache_region('long_term', '_get_readme_from_cache')
72 @cache_region('long_term', '_get_readme_from_cache')
73 def _get_readme_from_cache(key, kind):
73 def _get_readme_from_cache(key, kind):
74 readme_data = None
74 readme_data = None
75 readme_file = None
75 readme_file = None
76 try:
76 try:
77 # gets the landing revision! or tip if fails
77 # gets the landing revision! or tip if fails
78 cs = db_repo.get_landing_changeset()
78 cs = db_repo.get_landing_changeset()
79 if isinstance(cs, EmptyChangeset):
79 if isinstance(cs, EmptyChangeset):
80 raise EmptyRepositoryError()
80 raise EmptyRepositoryError()
81 renderer = MarkupRenderer()
81 renderer = MarkupRenderer()
82 for f in README_FILES:
82 for f in README_FILES:
83 try:
83 try:
84 readme = cs.get_node(f)
84 readme = cs.get_node(f)
85 if not isinstance(readme, FileNode):
85 if not isinstance(readme, FileNode):
86 continue
86 continue
87 readme_file = f
87 readme_file = f
88 log.debug('Found README file `%s` rendering...',
88 log.debug('Found README file `%s` rendering...',
89 readme_file)
89 readme_file)
90 readme_data = renderer.render(readme.content,
90 readme_data = renderer.render(readme.content,
91 filename=f)
91 filename=f)
92 break
92 break
93 except NodeDoesNotExistError:
93 except NodeDoesNotExistError:
94 continue
94 continue
95 except ChangesetError:
95 except ChangesetError:
96 log.error(traceback.format_exc())
96 log.error(traceback.format_exc())
97 pass
97 pass
98 except EmptyRepositoryError:
98 except EmptyRepositoryError:
99 pass
99 pass
100
100
101 return readme_data, readme_file
101 return readme_data, readme_file
102
102
103 kind = 'README'
103 kind = 'README'
104 valid = CacheInvalidation.test_and_set_valid(repo_name, kind)
104 valid = CacheInvalidation.test_and_set_valid(repo_name, kind)
105 if not valid:
105 if not valid:
106 region_invalidate(_get_readme_from_cache, None, '_get_readme_from_cache', repo_name, kind)
106 region_invalidate(_get_readme_from_cache, None, '_get_readme_from_cache', repo_name, kind)
107 return _get_readme_from_cache(repo_name, kind)
107 return _get_readme_from_cache(repo_name, kind)
108
108
109 @LoginRequired()
109 @LoginRequired()
110 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
110 @HasRepoPermissionLevelDecorator('read')
111 'repository.admin')
112 def index(self, repo_name):
111 def index(self, repo_name):
113 _load_changelog_summary()
112 _load_changelog_summary()
114
113
115 if request.authuser.is_default_user:
114 if request.authuser.is_default_user:
116 username = ''
115 username = ''
117 else:
116 else:
118 username = safe_str(request.authuser.username)
117 username = safe_str(request.authuser.username)
119
118
120 _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
119 _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
121 if '{repo}' in _def_clone_uri:
120 if '{repo}' in _def_clone_uri:
122 _def_clone_uri_by_id = _def_clone_uri.replace('{repo}', '_{repoid}')
121 _def_clone_uri_by_id = _def_clone_uri.replace('{repo}', '_{repoid}')
123 elif '{repoid}' in _def_clone_uri:
122 elif '{repoid}' in _def_clone_uri:
124 _def_clone_uri_by_id = _def_clone_uri.replace('_{repoid}', '{repo}')
123 _def_clone_uri_by_id = _def_clone_uri.replace('_{repoid}', '{repo}')
125
124
126 c.clone_repo_url = c.db_repo.clone_url(user=username,
125 c.clone_repo_url = c.db_repo.clone_url(user=username,
127 uri_tmpl=_def_clone_uri)
126 uri_tmpl=_def_clone_uri)
128 c.clone_repo_url_id = c.db_repo.clone_url(user=username,
127 c.clone_repo_url_id = c.db_repo.clone_url(user=username,
129 uri_tmpl=_def_clone_uri_by_id)
128 uri_tmpl=_def_clone_uri_by_id)
130
129
131 if c.db_repo.enable_statistics:
130 if c.db_repo.enable_statistics:
132 c.show_stats = True
131 c.show_stats = True
133 else:
132 else:
134 c.show_stats = False
133 c.show_stats = False
135
134
136 stats = self.sa.query(Statistics) \
135 stats = self.sa.query(Statistics) \
137 .filter(Statistics.repository == c.db_repo) \
136 .filter(Statistics.repository == c.db_repo) \
138 .scalar()
137 .scalar()
139
138
140 c.stats_percentage = 0
139 c.stats_percentage = 0
141
140
142 if stats and stats.languages:
141 if stats and stats.languages:
143 c.no_data = False is c.db_repo.enable_statistics
142 c.no_data = False is c.db_repo.enable_statistics
144 lang_stats_d = json.loads(stats.languages)
143 lang_stats_d = json.loads(stats.languages)
145
144
146 lang_stats = ((x, {"count": y,
145 lang_stats = ((x, {"count": y,
147 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
146 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
148 for x, y in lang_stats_d.items())
147 for x, y in lang_stats_d.items())
149
148
150 c.trending_languages = json.dumps(
149 c.trending_languages = json.dumps(
151 sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
150 sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
152 )
151 )
153 else:
152 else:
154 c.no_data = True
153 c.no_data = True
155 c.trending_languages = json.dumps([])
154 c.trending_languages = json.dumps([])
156
155
157 c.enable_downloads = c.db_repo.enable_downloads
156 c.enable_downloads = c.db_repo.enable_downloads
158 c.readme_data, c.readme_file = \
157 c.readme_data, c.readme_file = \
159 self.__get_readme_data(c.db_repo)
158 self.__get_readme_data(c.db_repo)
160 return render('summary/summary.html')
159 return render('summary/summary.html')
161
160
162 @LoginRequired()
161 @LoginRequired()
163 @NotAnonymous()
162 @NotAnonymous()
164 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
163 @HasRepoPermissionLevelDecorator('read')
165 'repository.admin')
166 @jsonify
164 @jsonify
167 def repo_size(self, repo_name):
165 def repo_size(self, repo_name):
168 if request.is_xhr:
166 if request.is_xhr:
169 return c.db_repo._repo_size()
167 return c.db_repo._repo_size()
170 else:
168 else:
171 raise HTTPBadRequest()
169 raise HTTPBadRequest()
172
170
173 @LoginRequired()
171 @LoginRequired()
174 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
172 @HasRepoPermissionLevelDecorator('read')
175 'repository.admin')
176 def statistics(self, repo_name):
173 def statistics(self, repo_name):
177 if c.db_repo.enable_statistics:
174 if c.db_repo.enable_statistics:
178 c.show_stats = True
175 c.show_stats = True
179 c.no_data_msg = _('No data ready yet')
176 c.no_data_msg = _('No data ready yet')
180 else:
177 else:
181 c.show_stats = False
178 c.show_stats = False
182 c.no_data_msg = _('Statistics are disabled for this repository')
179 c.no_data_msg = _('Statistics are disabled for this repository')
183
180
184 td = date.today() + timedelta(days=1)
181 td = date.today() + timedelta(days=1)
185 td_1m = td - timedelta(days=calendar.mdays[td.month])
182 td_1m = td - timedelta(days=calendar.mdays[td.month])
186 td_1y = td - timedelta(days=365)
183 td_1y = td - timedelta(days=365)
187
184
188 ts_min_m = mktime(td_1m.timetuple())
185 ts_min_m = mktime(td_1m.timetuple())
189 ts_min_y = mktime(td_1y.timetuple())
186 ts_min_y = mktime(td_1y.timetuple())
190 ts_max_y = mktime(td.timetuple())
187 ts_max_y = mktime(td.timetuple())
191 c.ts_min = ts_min_m
188 c.ts_min = ts_min_m
192 c.ts_max = ts_max_y
189 c.ts_max = ts_max_y
193
190
194 stats = self.sa.query(Statistics) \
191 stats = self.sa.query(Statistics) \
195 .filter(Statistics.repository == c.db_repo) \
192 .filter(Statistics.repository == c.db_repo) \
196 .scalar()
193 .scalar()
197 c.stats_percentage = 0
194 c.stats_percentage = 0
198 if stats and stats.languages:
195 if stats and stats.languages:
199 c.no_data = False is c.db_repo.enable_statistics
196 c.no_data = False is c.db_repo.enable_statistics
200 lang_stats_d = json.loads(stats.languages)
197 lang_stats_d = json.loads(stats.languages)
201 c.commit_data = stats.commit_activity
198 c.commit_data = stats.commit_activity
202 c.overview_data = stats.commit_activity_combined
199 c.overview_data = stats.commit_activity_combined
203
200
204 lang_stats = ((x, {"count": y,
201 lang_stats = ((x, {"count": y,
205 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
202 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
206 for x, y in lang_stats_d.items())
203 for x, y in lang_stats_d.items())
207
204
208 c.trending_languages = json.dumps(
205 c.trending_languages = json.dumps(
209 sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
206 sorted(lang_stats, reverse=True, key=lambda k: k[1])[:10]
210 )
207 )
211 last_rev = stats.stat_on_revision + 1
208 last_rev = stats.stat_on_revision + 1
212 c.repo_last_rev = c.db_repo_scm_instance.count() \
209 c.repo_last_rev = c.db_repo_scm_instance.count() \
213 if c.db_repo_scm_instance.revisions else 0
210 if c.db_repo_scm_instance.revisions else 0
214 if last_rev == 0 or c.repo_last_rev == 0:
211 if last_rev == 0 or c.repo_last_rev == 0:
215 pass
212 pass
216 else:
213 else:
217 c.stats_percentage = '%.2f' % ((float((last_rev)) /
214 c.stats_percentage = '%.2f' % ((float((last_rev)) /
218 c.repo_last_rev) * 100)
215 c.repo_last_rev) * 100)
219 else:
216 else:
220 c.commit_data = json.dumps({})
217 c.commit_data = json.dumps({})
221 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]])
218 c.overview_data = json.dumps([[ts_min_y, 0], [ts_max_y, 10]])
222 c.trending_languages = json.dumps({})
219 c.trending_languages = json.dumps({})
223 c.no_data = True
220 c.no_data = True
224
221
225 recurse_limit = 500 # don't recurse more than 500 times when parsing
222 recurse_limit = 500 # don't recurse more than 500 times when parsing
226 get_commits_stats(c.db_repo.repo_name, ts_min_y, ts_max_y, recurse_limit)
223 get_commits_stats(c.db_repo.repo_name, ts_min_y, ts_max_y, recurse_limit)
227 return render('summary/statistics.html')
224 return render('summary/statistics.html')
@@ -1,988 +1,992 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.auth
15 kallithea.lib.auth
16 ~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~
17
17
18 authentication and permission libraries
18 authentication and permission libraries
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 4, 2010
22 :created_on: Apr 4, 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 import time
27 import time
28 import os
28 import os
29 import logging
29 import logging
30 import traceback
30 import traceback
31 import hashlib
31 import hashlib
32 import itertools
32 import itertools
33 import collections
33 import collections
34
34
35 from decorator import decorator
35 from decorator import decorator
36
36
37 from pylons import request, session
37 from pylons import request, session
38 from pylons.i18n.translation import _
38 from pylons.i18n.translation import _
39 from webhelpers.pylonslib import secure_form
39 from webhelpers.pylonslib import secure_form
40 from sqlalchemy import or_
40 from sqlalchemy import or_
41 from sqlalchemy.orm.exc import ObjectDeletedError
41 from sqlalchemy.orm.exc import ObjectDeletedError
42 from sqlalchemy.orm import joinedload
42 from sqlalchemy.orm import joinedload
43 from webob.exc import HTTPFound, HTTPBadRequest, HTTPForbidden, HTTPMethodNotAllowed
43 from webob.exc import HTTPFound, HTTPBadRequest, HTTPForbidden, HTTPMethodNotAllowed
44
44
45 from kallithea import __platform__, is_windows, is_unix
45 from kallithea import __platform__, is_windows, is_unix
46 from kallithea.config.routing import url
46 from kallithea.config.routing import url
47 from kallithea.lib.vcs.utils.lazy import LazyProperty
47 from kallithea.lib.vcs.utils.lazy import LazyProperty
48 from kallithea.model import meta
48 from kallithea.model import meta
49 from kallithea.model.meta import Session
49 from kallithea.model.meta import Session
50 from kallithea.model.user import UserModel
50 from kallithea.model.user import UserModel
51 from kallithea.model.db import User, Repository, Permission, \
51 from kallithea.model.db import User, Repository, Permission, \
52 UserToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupMember, \
52 UserToPerm, UserGroupRepoToPerm, UserGroupToPerm, UserGroupMember, \
53 RepoGroup, UserGroupRepoGroupToPerm, UserIpMap, UserGroupUserGroupToPerm, \
53 RepoGroup, UserGroupRepoGroupToPerm, UserIpMap, UserGroupUserGroupToPerm, \
54 UserGroup, UserApiKeys
54 UserGroup, UserApiKeys
55
55
56 from kallithea.lib.utils2 import safe_str, safe_unicode, aslist
56 from kallithea.lib.utils2 import safe_str, safe_unicode, aslist
57 from kallithea.lib.utils import get_repo_slug, get_repo_group_slug, \
57 from kallithea.lib.utils import get_repo_slug, get_repo_group_slug, \
58 get_user_group_slug, conditional_cache
58 get_user_group_slug, conditional_cache
59 from kallithea.lib.caching_query import FromCache
59 from kallithea.lib.caching_query import FromCache
60
60
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 class PasswordGenerator(object):
65 class PasswordGenerator(object):
66 """
66 """
67 This is a simple class for generating password from different sets of
67 This is a simple class for generating password from different sets of
68 characters
68 characters
69 usage::
69 usage::
70
70
71 passwd_gen = PasswordGenerator()
71 passwd_gen = PasswordGenerator()
72 #print 8-letter password containing only big and small letters
72 #print 8-letter password containing only big and small letters
73 of alphabet
73 of alphabet
74 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
74 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
75 """
75 """
76 ALPHABETS_NUM = r'''1234567890'''
76 ALPHABETS_NUM = r'''1234567890'''
77 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
77 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
78 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
78 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
79 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
79 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
80 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
80 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
81 + ALPHABETS_NUM + ALPHABETS_SPECIAL
81 + ALPHABETS_NUM + ALPHABETS_SPECIAL
82 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
82 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
83 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
83 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
84 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
84 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
85 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
85 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
86
86
87 def gen_password(self, length, alphabet=ALPHABETS_FULL):
87 def gen_password(self, length, alphabet=ALPHABETS_FULL):
88 assert len(alphabet) <= 256, alphabet
88 assert len(alphabet) <= 256, alphabet
89 l = []
89 l = []
90 while len(l) < length:
90 while len(l) < length:
91 i = ord(os.urandom(1))
91 i = ord(os.urandom(1))
92 if i < len(alphabet):
92 if i < len(alphabet):
93 l.append(alphabet[i])
93 l.append(alphabet[i])
94 return ''.join(l)
94 return ''.join(l)
95
95
96
96
97 def get_crypt_password(password):
97 def get_crypt_password(password):
98 """
98 """
99 Cryptographic function used for password hashing based on pybcrypt
99 Cryptographic function used for password hashing based on pybcrypt
100 or Python's own OpenSSL wrapper on windows
100 or Python's own OpenSSL wrapper on windows
101
101
102 :param password: password to hash
102 :param password: password to hash
103 """
103 """
104 if is_windows:
104 if is_windows:
105 return hashlib.sha256(password).hexdigest()
105 return hashlib.sha256(password).hexdigest()
106 elif is_unix:
106 elif is_unix:
107 import bcrypt
107 import bcrypt
108 return bcrypt.hashpw(safe_str(password), bcrypt.gensalt(10))
108 return bcrypt.hashpw(safe_str(password), bcrypt.gensalt(10))
109 else:
109 else:
110 raise Exception('Unknown or unsupported platform %s' \
110 raise Exception('Unknown or unsupported platform %s' \
111 % __platform__)
111 % __platform__)
112
112
113
113
114 def check_password(password, hashed):
114 def check_password(password, hashed):
115 """
115 """
116 Checks matching password with it's hashed value, runs different
116 Checks matching password with it's hashed value, runs different
117 implementation based on platform it runs on
117 implementation based on platform it runs on
118
118
119 :param password: password
119 :param password: password
120 :param hashed: password in hashed form
120 :param hashed: password in hashed form
121 """
121 """
122
122
123 if is_windows:
123 if is_windows:
124 return hashlib.sha256(password).hexdigest() == hashed
124 return hashlib.sha256(password).hexdigest() == hashed
125 elif is_unix:
125 elif is_unix:
126 import bcrypt
126 import bcrypt
127 return bcrypt.checkpw(safe_str(password), safe_str(hashed))
127 return bcrypt.checkpw(safe_str(password), safe_str(hashed))
128 else:
128 else:
129 raise Exception('Unknown or unsupported platform %s' \
129 raise Exception('Unknown or unsupported platform %s' \
130 % __platform__)
130 % __platform__)
131
131
132
132
133 def _cached_perms_data(user_id, user_is_admin, user_inherit_default_permissions,
133 def _cached_perms_data(user_id, user_is_admin, user_inherit_default_permissions,
134 explicit, algo):
134 explicit, algo):
135 RK = 'repositories'
135 RK = 'repositories'
136 GK = 'repositories_groups'
136 GK = 'repositories_groups'
137 UK = 'user_groups'
137 UK = 'user_groups'
138 GLOBAL = 'global'
138 GLOBAL = 'global'
139 PERM_WEIGHTS = Permission.PERM_WEIGHTS
139 PERM_WEIGHTS = Permission.PERM_WEIGHTS
140 permissions = {RK: {}, GK: {}, UK: {}, GLOBAL: set()}
140 permissions = {RK: {}, GK: {}, UK: {}, GLOBAL: set()}
141
141
142 def _choose_perm(new_perm, cur_perm):
142 def _choose_perm(new_perm, cur_perm):
143 new_perm_val = PERM_WEIGHTS[new_perm]
143 new_perm_val = PERM_WEIGHTS[new_perm]
144 cur_perm_val = PERM_WEIGHTS[cur_perm]
144 cur_perm_val = PERM_WEIGHTS[cur_perm]
145 if algo == 'higherwin':
145 if algo == 'higherwin':
146 if new_perm_val > cur_perm_val:
146 if new_perm_val > cur_perm_val:
147 return new_perm
147 return new_perm
148 return cur_perm
148 return cur_perm
149 elif algo == 'lowerwin':
149 elif algo == 'lowerwin':
150 if new_perm_val < cur_perm_val:
150 if new_perm_val < cur_perm_val:
151 return new_perm
151 return new_perm
152 return cur_perm
152 return cur_perm
153
153
154 #======================================================================
154 #======================================================================
155 # fetch default permissions
155 # fetch default permissions
156 #======================================================================
156 #======================================================================
157 default_user = User.get_by_username('default', cache=True)
157 default_user = User.get_by_username('default', cache=True)
158 default_user_id = default_user.user_id
158 default_user_id = default_user.user_id
159
159
160 default_repo_perms = Permission.get_default_perms(default_user_id)
160 default_repo_perms = Permission.get_default_perms(default_user_id)
161 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
161 default_repo_groups_perms = Permission.get_default_group_perms(default_user_id)
162 default_user_group_perms = Permission.get_default_user_group_perms(default_user_id)
162 default_user_group_perms = Permission.get_default_user_group_perms(default_user_id)
163
163
164 if user_is_admin:
164 if user_is_admin:
165 #==================================================================
165 #==================================================================
166 # admin users have all rights;
166 # admin users have all rights;
167 # based on default permissions, just set everything to admin
167 # based on default permissions, just set everything to admin
168 #==================================================================
168 #==================================================================
169 permissions[GLOBAL].add('hg.admin')
169 permissions[GLOBAL].add('hg.admin')
170 permissions[GLOBAL].add('hg.create.write_on_repogroup.true')
170 permissions[GLOBAL].add('hg.create.write_on_repogroup.true')
171
171
172 # repositories
172 # repositories
173 for perm in default_repo_perms:
173 for perm in default_repo_perms:
174 r_k = perm.UserRepoToPerm.repository.repo_name
174 r_k = perm.UserRepoToPerm.repository.repo_name
175 p = 'repository.admin'
175 p = 'repository.admin'
176 permissions[RK][r_k] = p
176 permissions[RK][r_k] = p
177
177
178 # repository groups
178 # repository groups
179 for perm in default_repo_groups_perms:
179 for perm in default_repo_groups_perms:
180 rg_k = perm.UserRepoGroupToPerm.group.group_name
180 rg_k = perm.UserRepoGroupToPerm.group.group_name
181 p = 'group.admin'
181 p = 'group.admin'
182 permissions[GK][rg_k] = p
182 permissions[GK][rg_k] = p
183
183
184 # user groups
184 # user groups
185 for perm in default_user_group_perms:
185 for perm in default_user_group_perms:
186 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
186 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
187 p = 'usergroup.admin'
187 p = 'usergroup.admin'
188 permissions[UK][u_k] = p
188 permissions[UK][u_k] = p
189 return permissions
189 return permissions
190
190
191 #==================================================================
191 #==================================================================
192 # SET DEFAULTS GLOBAL, REPOS, REPOSITORY GROUPS
192 # SET DEFAULTS GLOBAL, REPOS, REPOSITORY GROUPS
193 #==================================================================
193 #==================================================================
194
194
195 # default global permissions taken from the default user
195 # default global permissions taken from the default user
196 default_global_perms = UserToPerm.query() \
196 default_global_perms = UserToPerm.query() \
197 .filter(UserToPerm.user_id == default_user_id) \
197 .filter(UserToPerm.user_id == default_user_id) \
198 .options(joinedload(UserToPerm.permission))
198 .options(joinedload(UserToPerm.permission))
199
199
200 for perm in default_global_perms:
200 for perm in default_global_perms:
201 permissions[GLOBAL].add(perm.permission.permission_name)
201 permissions[GLOBAL].add(perm.permission.permission_name)
202
202
203 # defaults for repositories, taken from default user
203 # defaults for repositories, taken from default user
204 for perm in default_repo_perms:
204 for perm in default_repo_perms:
205 r_k = perm.UserRepoToPerm.repository.repo_name
205 r_k = perm.UserRepoToPerm.repository.repo_name
206 if perm.Repository.private and not (perm.Repository.owner_id == user_id):
206 if perm.Repository.private and not (perm.Repository.owner_id == user_id):
207 # disable defaults for private repos,
207 # disable defaults for private repos,
208 p = 'repository.none'
208 p = 'repository.none'
209 elif perm.Repository.owner_id == user_id:
209 elif perm.Repository.owner_id == user_id:
210 # set admin if owner
210 # set admin if owner
211 p = 'repository.admin'
211 p = 'repository.admin'
212 else:
212 else:
213 p = perm.Permission.permission_name
213 p = perm.Permission.permission_name
214
214
215 permissions[RK][r_k] = p
215 permissions[RK][r_k] = p
216
216
217 # defaults for repository groups taken from default user permission
217 # defaults for repository groups taken from default user permission
218 # on given group
218 # on given group
219 for perm in default_repo_groups_perms:
219 for perm in default_repo_groups_perms:
220 rg_k = perm.UserRepoGroupToPerm.group.group_name
220 rg_k = perm.UserRepoGroupToPerm.group.group_name
221 p = perm.Permission.permission_name
221 p = perm.Permission.permission_name
222 permissions[GK][rg_k] = p
222 permissions[GK][rg_k] = p
223
223
224 # defaults for user groups taken from default user permission
224 # defaults for user groups taken from default user permission
225 # on given user group
225 # on given user group
226 for perm in default_user_group_perms:
226 for perm in default_user_group_perms:
227 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
227 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
228 p = perm.Permission.permission_name
228 p = perm.Permission.permission_name
229 permissions[UK][u_k] = p
229 permissions[UK][u_k] = p
230
230
231 #======================================================================
231 #======================================================================
232 # !! OVERRIDE GLOBALS !! with user permissions if any found
232 # !! OVERRIDE GLOBALS !! with user permissions if any found
233 #======================================================================
233 #======================================================================
234 # those can be configured from groups or users explicitly
234 # those can be configured from groups or users explicitly
235 _configurable = set([
235 _configurable = set([
236 'hg.fork.none', 'hg.fork.repository',
236 'hg.fork.none', 'hg.fork.repository',
237 'hg.create.none', 'hg.create.repository',
237 'hg.create.none', 'hg.create.repository',
238 'hg.usergroup.create.false', 'hg.usergroup.create.true'
238 'hg.usergroup.create.false', 'hg.usergroup.create.true'
239 ])
239 ])
240
240
241 # USER GROUPS comes first
241 # USER GROUPS comes first
242 # user group global permissions
242 # user group global permissions
243 user_perms_from_users_groups = Session().query(UserGroupToPerm) \
243 user_perms_from_users_groups = Session().query(UserGroupToPerm) \
244 .options(joinedload(UserGroupToPerm.permission)) \
244 .options(joinedload(UserGroupToPerm.permission)) \
245 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
245 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
246 UserGroupMember.users_group_id)) \
246 UserGroupMember.users_group_id)) \
247 .filter(UserGroupMember.user_id == user_id) \
247 .filter(UserGroupMember.user_id == user_id) \
248 .join((UserGroup, UserGroupMember.users_group_id ==
248 .join((UserGroup, UserGroupMember.users_group_id ==
249 UserGroup.users_group_id)) \
249 UserGroup.users_group_id)) \
250 .filter(UserGroup.users_group_active == True) \
250 .filter(UserGroup.users_group_active == True) \
251 .order_by(UserGroupToPerm.users_group_id) \
251 .order_by(UserGroupToPerm.users_group_id) \
252 .all()
252 .all()
253 # need to group here by groups since user can be in more than
253 # need to group here by groups since user can be in more than
254 # one group
254 # one group
255 _grouped = [[x, list(y)] for x, y in
255 _grouped = [[x, list(y)] for x, y in
256 itertools.groupby(user_perms_from_users_groups,
256 itertools.groupby(user_perms_from_users_groups,
257 lambda x:x.users_group)]
257 lambda x:x.users_group)]
258 for gr, perms in _grouped:
258 for gr, perms in _grouped:
259 # since user can be in multiple groups iterate over them and
259 # since user can be in multiple groups iterate over them and
260 # select the lowest permissions first (more explicit)
260 # select the lowest permissions first (more explicit)
261 ##TODO: do this^^
261 ##TODO: do this^^
262 if not gr.inherit_default_permissions:
262 if not gr.inherit_default_permissions:
263 # NEED TO IGNORE all configurable permissions and
263 # NEED TO IGNORE all configurable permissions and
264 # replace them with explicitly set
264 # replace them with explicitly set
265 permissions[GLOBAL] = permissions[GLOBAL] \
265 permissions[GLOBAL] = permissions[GLOBAL] \
266 .difference(_configurable)
266 .difference(_configurable)
267 for perm in perms:
267 for perm in perms:
268 permissions[GLOBAL].add(perm.permission.permission_name)
268 permissions[GLOBAL].add(perm.permission.permission_name)
269
269
270 # user specific global permissions
270 # user specific global permissions
271 user_perms = Session().query(UserToPerm) \
271 user_perms = Session().query(UserToPerm) \
272 .options(joinedload(UserToPerm.permission)) \
272 .options(joinedload(UserToPerm.permission)) \
273 .filter(UserToPerm.user_id == user_id).all()
273 .filter(UserToPerm.user_id == user_id).all()
274
274
275 if not user_inherit_default_permissions:
275 if not user_inherit_default_permissions:
276 # NEED TO IGNORE all configurable permissions and
276 # NEED TO IGNORE all configurable permissions and
277 # replace them with explicitly set
277 # replace them with explicitly set
278 permissions[GLOBAL] = permissions[GLOBAL] \
278 permissions[GLOBAL] = permissions[GLOBAL] \
279 .difference(_configurable)
279 .difference(_configurable)
280
280
281 for perm in user_perms:
281 for perm in user_perms:
282 permissions[GLOBAL].add(perm.permission.permission_name)
282 permissions[GLOBAL].add(perm.permission.permission_name)
283 ## END GLOBAL PERMISSIONS
283 ## END GLOBAL PERMISSIONS
284
284
285 #======================================================================
285 #======================================================================
286 # !! PERMISSIONS FOR REPOSITORIES !!
286 # !! PERMISSIONS FOR REPOSITORIES !!
287 #======================================================================
287 #======================================================================
288 #======================================================================
288 #======================================================================
289 # check if user is part of user groups for this repository and
289 # check if user is part of user groups for this repository and
290 # fill in his permission from it. _choose_perm decides of which
290 # fill in his permission from it. _choose_perm decides of which
291 # permission should be selected based on selected method
291 # permission should be selected based on selected method
292 #======================================================================
292 #======================================================================
293
293
294 # user group for repositories permissions
294 # user group for repositories permissions
295 user_repo_perms_from_users_groups = \
295 user_repo_perms_from_users_groups = \
296 Session().query(UserGroupRepoToPerm, Permission, Repository,) \
296 Session().query(UserGroupRepoToPerm, Permission, Repository,) \
297 .join((Repository, UserGroupRepoToPerm.repository_id ==
297 .join((Repository, UserGroupRepoToPerm.repository_id ==
298 Repository.repo_id)) \
298 Repository.repo_id)) \
299 .join((Permission, UserGroupRepoToPerm.permission_id ==
299 .join((Permission, UserGroupRepoToPerm.permission_id ==
300 Permission.permission_id)) \
300 Permission.permission_id)) \
301 .join((UserGroup, UserGroupRepoToPerm.users_group_id ==
301 .join((UserGroup, UserGroupRepoToPerm.users_group_id ==
302 UserGroup.users_group_id)) \
302 UserGroup.users_group_id)) \
303 .filter(UserGroup.users_group_active == True) \
303 .filter(UserGroup.users_group_active == True) \
304 .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
304 .join((UserGroupMember, UserGroupRepoToPerm.users_group_id ==
305 UserGroupMember.users_group_id)) \
305 UserGroupMember.users_group_id)) \
306 .filter(UserGroupMember.user_id == user_id) \
306 .filter(UserGroupMember.user_id == user_id) \
307 .all()
307 .all()
308
308
309 multiple_counter = collections.defaultdict(int)
309 multiple_counter = collections.defaultdict(int)
310 for perm in user_repo_perms_from_users_groups:
310 for perm in user_repo_perms_from_users_groups:
311 r_k = perm.UserGroupRepoToPerm.repository.repo_name
311 r_k = perm.UserGroupRepoToPerm.repository.repo_name
312 multiple_counter[r_k] += 1
312 multiple_counter[r_k] += 1
313 p = perm.Permission.permission_name
313 p = perm.Permission.permission_name
314 cur_perm = permissions[RK][r_k]
314 cur_perm = permissions[RK][r_k]
315
315
316 if perm.Repository.owner_id == user_id:
316 if perm.Repository.owner_id == user_id:
317 # set admin if owner
317 # set admin if owner
318 p = 'repository.admin'
318 p = 'repository.admin'
319 else:
319 else:
320 if multiple_counter[r_k] > 1:
320 if multiple_counter[r_k] > 1:
321 p = _choose_perm(p, cur_perm)
321 p = _choose_perm(p, cur_perm)
322 permissions[RK][r_k] = p
322 permissions[RK][r_k] = p
323
323
324 # user explicit permissions for repositories, overrides any specified
324 # user explicit permissions for repositories, overrides any specified
325 # by the group permission
325 # by the group permission
326 user_repo_perms = Permission.get_default_perms(user_id)
326 user_repo_perms = Permission.get_default_perms(user_id)
327 for perm in user_repo_perms:
327 for perm in user_repo_perms:
328 r_k = perm.UserRepoToPerm.repository.repo_name
328 r_k = perm.UserRepoToPerm.repository.repo_name
329 cur_perm = permissions[RK][r_k]
329 cur_perm = permissions[RK][r_k]
330 # set admin if owner
330 # set admin if owner
331 if perm.Repository.owner_id == user_id:
331 if perm.Repository.owner_id == user_id:
332 p = 'repository.admin'
332 p = 'repository.admin'
333 else:
333 else:
334 p = perm.Permission.permission_name
334 p = perm.Permission.permission_name
335 if not explicit:
335 if not explicit:
336 p = _choose_perm(p, cur_perm)
336 p = _choose_perm(p, cur_perm)
337 permissions[RK][r_k] = p
337 permissions[RK][r_k] = p
338
338
339 #======================================================================
339 #======================================================================
340 # !! PERMISSIONS FOR REPOSITORY GROUPS !!
340 # !! PERMISSIONS FOR REPOSITORY GROUPS !!
341 #======================================================================
341 #======================================================================
342 #======================================================================
342 #======================================================================
343 # check if user is part of user groups for this repository groups and
343 # check if user is part of user groups for this repository groups and
344 # fill in his permission from it. _choose_perm decides of which
344 # fill in his permission from it. _choose_perm decides of which
345 # permission should be selected based on selected method
345 # permission should be selected based on selected method
346 #======================================================================
346 #======================================================================
347 # user group for repo groups permissions
347 # user group for repo groups permissions
348 user_repo_group_perms_from_users_groups = \
348 user_repo_group_perms_from_users_groups = \
349 Session().query(UserGroupRepoGroupToPerm, Permission, RepoGroup) \
349 Session().query(UserGroupRepoGroupToPerm, Permission, RepoGroup) \
350 .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)) \
350 .join((RepoGroup, UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)) \
351 .join((Permission, UserGroupRepoGroupToPerm.permission_id
351 .join((Permission, UserGroupRepoGroupToPerm.permission_id
352 == Permission.permission_id)) \
352 == Permission.permission_id)) \
353 .join((UserGroup, UserGroupRepoGroupToPerm.users_group_id ==
353 .join((UserGroup, UserGroupRepoGroupToPerm.users_group_id ==
354 UserGroup.users_group_id)) \
354 UserGroup.users_group_id)) \
355 .filter(UserGroup.users_group_active == True) \
355 .filter(UserGroup.users_group_active == True) \
356 .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
356 .join((UserGroupMember, UserGroupRepoGroupToPerm.users_group_id
357 == UserGroupMember.users_group_id)) \
357 == UserGroupMember.users_group_id)) \
358 .filter(UserGroupMember.user_id == user_id) \
358 .filter(UserGroupMember.user_id == user_id) \
359 .all()
359 .all()
360
360
361 multiple_counter = collections.defaultdict(int)
361 multiple_counter = collections.defaultdict(int)
362 for perm in user_repo_group_perms_from_users_groups:
362 for perm in user_repo_group_perms_from_users_groups:
363 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
363 g_k = perm.UserGroupRepoGroupToPerm.group.group_name
364 multiple_counter[g_k] += 1
364 multiple_counter[g_k] += 1
365 p = perm.Permission.permission_name
365 p = perm.Permission.permission_name
366 cur_perm = permissions[GK][g_k]
366 cur_perm = permissions[GK][g_k]
367 if multiple_counter[g_k] > 1:
367 if multiple_counter[g_k] > 1:
368 p = _choose_perm(p, cur_perm)
368 p = _choose_perm(p, cur_perm)
369 permissions[GK][g_k] = p
369 permissions[GK][g_k] = p
370
370
371 # user explicit permissions for repository groups
371 # user explicit permissions for repository groups
372 user_repo_groups_perms = Permission.get_default_group_perms(user_id)
372 user_repo_groups_perms = Permission.get_default_group_perms(user_id)
373 for perm in user_repo_groups_perms:
373 for perm in user_repo_groups_perms:
374 rg_k = perm.UserRepoGroupToPerm.group.group_name
374 rg_k = perm.UserRepoGroupToPerm.group.group_name
375 p = perm.Permission.permission_name
375 p = perm.Permission.permission_name
376 cur_perm = permissions[GK][rg_k]
376 cur_perm = permissions[GK][rg_k]
377 if not explicit:
377 if not explicit:
378 p = _choose_perm(p, cur_perm)
378 p = _choose_perm(p, cur_perm)
379 permissions[GK][rg_k] = p
379 permissions[GK][rg_k] = p
380
380
381 #======================================================================
381 #======================================================================
382 # !! PERMISSIONS FOR USER GROUPS !!
382 # !! PERMISSIONS FOR USER GROUPS !!
383 #======================================================================
383 #======================================================================
384 # user group for user group permissions
384 # user group for user group permissions
385 user_group_user_groups_perms = \
385 user_group_user_groups_perms = \
386 Session().query(UserGroupUserGroupToPerm, Permission, UserGroup) \
386 Session().query(UserGroupUserGroupToPerm, Permission, UserGroup) \
387 .join((UserGroup, UserGroupUserGroupToPerm.target_user_group_id
387 .join((UserGroup, UserGroupUserGroupToPerm.target_user_group_id
388 == UserGroup.users_group_id)) \
388 == UserGroup.users_group_id)) \
389 .join((Permission, UserGroupUserGroupToPerm.permission_id
389 .join((Permission, UserGroupUserGroupToPerm.permission_id
390 == Permission.permission_id)) \
390 == Permission.permission_id)) \
391 .join((UserGroupMember, UserGroupUserGroupToPerm.user_group_id
391 .join((UserGroupMember, UserGroupUserGroupToPerm.user_group_id
392 == UserGroupMember.users_group_id)) \
392 == UserGroupMember.users_group_id)) \
393 .filter(UserGroupMember.user_id == user_id) \
393 .filter(UserGroupMember.user_id == user_id) \
394 .join((UserGroup, UserGroupMember.users_group_id ==
394 .join((UserGroup, UserGroupMember.users_group_id ==
395 UserGroup.users_group_id), aliased=True, from_joinpoint=True) \
395 UserGroup.users_group_id), aliased=True, from_joinpoint=True) \
396 .filter(UserGroup.users_group_active == True) \
396 .filter(UserGroup.users_group_active == True) \
397 .all()
397 .all()
398
398
399 multiple_counter = collections.defaultdict(int)
399 multiple_counter = collections.defaultdict(int)
400 for perm in user_group_user_groups_perms:
400 for perm in user_group_user_groups_perms:
401 g_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
401 g_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
402 multiple_counter[g_k] += 1
402 multiple_counter[g_k] += 1
403 p = perm.Permission.permission_name
403 p = perm.Permission.permission_name
404 cur_perm = permissions[UK][g_k]
404 cur_perm = permissions[UK][g_k]
405 if multiple_counter[g_k] > 1:
405 if multiple_counter[g_k] > 1:
406 p = _choose_perm(p, cur_perm)
406 p = _choose_perm(p, cur_perm)
407 permissions[UK][g_k] = p
407 permissions[UK][g_k] = p
408
408
409 #user explicit permission for user groups
409 #user explicit permission for user groups
410 user_user_groups_perms = Permission.get_default_user_group_perms(user_id)
410 user_user_groups_perms = Permission.get_default_user_group_perms(user_id)
411 for perm in user_user_groups_perms:
411 for perm in user_user_groups_perms:
412 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
412 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
413 p = perm.Permission.permission_name
413 p = perm.Permission.permission_name
414 cur_perm = permissions[UK][u_k]
414 cur_perm = permissions[UK][u_k]
415 if not explicit:
415 if not explicit:
416 p = _choose_perm(p, cur_perm)
416 p = _choose_perm(p, cur_perm)
417 permissions[UK][u_k] = p
417 permissions[UK][u_k] = p
418
418
419 return permissions
419 return permissions
420
420
421
421
422 def allowed_api_access(controller_name, whitelist=None, api_key=None):
422 def allowed_api_access(controller_name, whitelist=None, api_key=None):
423 """
423 """
424 Check if given controller_name is in whitelist API access
424 Check if given controller_name is in whitelist API access
425 """
425 """
426 if not whitelist:
426 if not whitelist:
427 from kallithea import CONFIG
427 from kallithea import CONFIG
428 whitelist = aslist(CONFIG.get('api_access_controllers_whitelist'),
428 whitelist = aslist(CONFIG.get('api_access_controllers_whitelist'),
429 sep=',')
429 sep=',')
430 log.debug('whitelist of API access is: %s', whitelist)
430 log.debug('whitelist of API access is: %s', whitelist)
431 api_access_valid = controller_name in whitelist
431 api_access_valid = controller_name in whitelist
432 if api_access_valid:
432 if api_access_valid:
433 log.debug('controller:%s is in API whitelist', controller_name)
433 log.debug('controller:%s is in API whitelist', controller_name)
434 else:
434 else:
435 msg = 'controller: %s is *NOT* in API whitelist' % (controller_name)
435 msg = 'controller: %s is *NOT* in API whitelist' % (controller_name)
436 if api_key:
436 if api_key:
437 #if we use API key and don't have access it's a warning
437 #if we use API key and don't have access it's a warning
438 log.warning(msg)
438 log.warning(msg)
439 else:
439 else:
440 log.debug(msg)
440 log.debug(msg)
441 return api_access_valid
441 return api_access_valid
442
442
443
443
444 class AuthUser(object):
444 class AuthUser(object):
445 """
445 """
446 Represents a Kallithea user, including various authentication and
446 Represents a Kallithea user, including various authentication and
447 authorization information. Typically used to store the current user,
447 authorization information. Typically used to store the current user,
448 but is also used as a generic user information data structure in
448 but is also used as a generic user information data structure in
449 parts of the code, e.g. user management.
449 parts of the code, e.g. user management.
450
450
451 Constructed from a database `User` object, a user ID or cookie dict,
451 Constructed from a database `User` object, a user ID or cookie dict,
452 it looks up the user (if needed) and copies all attributes to itself,
452 it looks up the user (if needed) and copies all attributes to itself,
453 adding various non-persistent data. If lookup fails but anonymous
453 adding various non-persistent data. If lookup fails but anonymous
454 access to Kallithea is enabled, the default user is loaded instead.
454 access to Kallithea is enabled, the default user is loaded instead.
455
455
456 `AuthUser` does not by itself authenticate users and the constructor
456 `AuthUser` does not by itself authenticate users and the constructor
457 sets the `is_authenticated` field to False. It's up to other parts
457 sets the `is_authenticated` field to False. It's up to other parts
458 of the code to check e.g. if a supplied password is correct, and if
458 of the code to check e.g. if a supplied password is correct, and if
459 so, set `is_authenticated` to True.
459 so, set `is_authenticated` to True.
460
460
461 However, `AuthUser` does refuse to load a user that is not `active`.
461 However, `AuthUser` does refuse to load a user that is not `active`.
462
462
463 Note that Kallithea distinguishes between the default user (an actual
463 Note that Kallithea distinguishes between the default user (an actual
464 user in the database with username "default") and "no user" (no actual
464 user in the database with username "default") and "no user" (no actual
465 User object, AuthUser filled with blank values and username "None").
465 User object, AuthUser filled with blank values and username "None").
466
466
467 If the default user is active, that will always be used instead of
467 If the default user is active, that will always be used instead of
468 "no user". On the other hand, if the default user is disabled (and
468 "no user". On the other hand, if the default user is disabled (and
469 there is no login information), we instead get "no user"; this should
469 there is no login information), we instead get "no user"; this should
470 only happen on the login page (as all other requests are redirected).
470 only happen on the login page (as all other requests are redirected).
471
471
472 `is_default_user` specifically checks if the AuthUser is the user named
472 `is_default_user` specifically checks if the AuthUser is the user named
473 "default". Use `is_anonymous` to check for both "default" and "no user".
473 "default". Use `is_anonymous` to check for both "default" and "no user".
474 """
474 """
475
475
476 def __init__(self, user_id=None, dbuser=None, authenticating_api_key=None,
476 def __init__(self, user_id=None, dbuser=None, authenticating_api_key=None,
477 is_external_auth=False):
477 is_external_auth=False):
478
478
479 self.is_authenticated = False
479 self.is_authenticated = False
480 self.is_external_auth = is_external_auth
480 self.is_external_auth = is_external_auth
481 self.authenticating_api_key = authenticating_api_key
481 self.authenticating_api_key = authenticating_api_key
482
482
483 user_model = UserModel()
483 user_model = UserModel()
484 self._default_user = User.get_default_user(cache=True)
484 self._default_user = User.get_default_user(cache=True)
485
485
486 # These attributes will be overridden by fill_data, below, unless the
486 # These attributes will be overridden by fill_data, below, unless the
487 # requested user cannot be found and the default anonymous user is
487 # requested user cannot be found and the default anonymous user is
488 # not enabled.
488 # not enabled.
489 self.user_id = None
489 self.user_id = None
490 self.username = None
490 self.username = None
491 self.api_key = None
491 self.api_key = None
492 self.name = ''
492 self.name = ''
493 self.lastname = ''
493 self.lastname = ''
494 self.email = ''
494 self.email = ''
495 self.admin = False
495 self.admin = False
496 self.inherit_default_permissions = False
496 self.inherit_default_permissions = False
497
497
498 # Look up database user, if necessary.
498 # Look up database user, if necessary.
499 if user_id is not None:
499 if user_id is not None:
500 log.debug('Auth User lookup by USER ID %s', user_id)
500 log.debug('Auth User lookup by USER ID %s', user_id)
501 dbuser = user_model.get(user_id)
501 dbuser = user_model.get(user_id)
502 else:
502 else:
503 # Note: dbuser is allowed to be None.
503 # Note: dbuser is allowed to be None.
504 log.debug('Auth User lookup by database user %s', dbuser)
504 log.debug('Auth User lookup by database user %s', dbuser)
505
505
506 is_user_loaded = self._fill_data(dbuser)
506 is_user_loaded = self._fill_data(dbuser)
507
507
508 # If user cannot be found, try falling back to anonymous.
508 # If user cannot be found, try falling back to anonymous.
509 if not is_user_loaded:
509 if not is_user_loaded:
510 is_user_loaded = self._fill_data(self._default_user)
510 is_user_loaded = self._fill_data(self._default_user)
511
511
512 self.is_default_user = (self.user_id == self._default_user.user_id)
512 self.is_default_user = (self.user_id == self._default_user.user_id)
513 self.is_anonymous = not is_user_loaded or self.is_default_user
513 self.is_anonymous = not is_user_loaded or self.is_default_user
514
514
515 if not self.username:
515 if not self.username:
516 self.username = 'None'
516 self.username = 'None'
517
517
518 log.debug('Auth User is now %s', self)
518 log.debug('Auth User is now %s', self)
519
519
520 def _fill_data(self, dbuser):
520 def _fill_data(self, dbuser):
521 """
521 """
522 Copies database fields from a `db.User` to this `AuthUser`. Does
522 Copies database fields from a `db.User` to this `AuthUser`. Does
523 not copy `api_keys` and `permissions` attributes.
523 not copy `api_keys` and `permissions` attributes.
524
524
525 Checks that `dbuser` is `active` (and not None) before copying;
525 Checks that `dbuser` is `active` (and not None) before copying;
526 returns True on success.
526 returns True on success.
527 """
527 """
528 if dbuser is not None and dbuser.active:
528 if dbuser is not None and dbuser.active:
529 log.debug('filling %s data', dbuser)
529 log.debug('filling %s data', dbuser)
530 for k, v in dbuser.get_dict().iteritems():
530 for k, v in dbuser.get_dict().iteritems():
531 assert k not in ['api_keys', 'permissions']
531 assert k not in ['api_keys', 'permissions']
532 setattr(self, k, v)
532 setattr(self, k, v)
533 return True
533 return True
534 return False
534 return False
535
535
536 @LazyProperty
536 @LazyProperty
537 def permissions(self):
537 def permissions(self):
538 return self.__get_perms(user=self, cache=False)
538 return self.__get_perms(user=self, cache=False)
539
539
540 def has_repository_permission_level(self, repo_name, level, purpose=None):
541 required_perms = {
542 'read': ['repository.read', 'repository.write', 'repository.admin'],
543 'write': ['repository.write', 'repository.admin'],
544 'admin': ['repository.admin'],
545 }[level]
546 actual_perm = self.permissions['repositories'].get(repo_name)
547 ok = actual_perm in required_perms
548 log.debug('Checking if user %r can %r repo %r (%s): %s (has %r)',
549 self.username, level, repo_name, purpose, ok, actual_perm)
550 return ok
551
540 @property
552 @property
541 def api_keys(self):
553 def api_keys(self):
542 return self._get_api_keys()
554 return self._get_api_keys()
543
555
544 def __get_perms(self, user, explicit=True, algo='higherwin', cache=False):
556 def __get_perms(self, user, explicit=True, algo='higherwin', cache=False):
545 """
557 """
546 Fills user permission attribute with permissions taken from database
558 Fills user permission attribute with permissions taken from database
547 works for permissions given for repositories, and for permissions that
559 works for permissions given for repositories, and for permissions that
548 are granted to groups
560 are granted to groups
549
561
550 :param user: `AuthUser` instance
562 :param user: `AuthUser` instance
551 :param explicit: In case there are permissions both for user and a group
563 :param explicit: In case there are permissions both for user and a group
552 that user is part of, explicit flag will define if user will
564 that user is part of, explicit flag will define if user will
553 explicitly override permissions from group, if it's False it will
565 explicitly override permissions from group, if it's False it will
554 make decision based on the algo
566 make decision based on the algo
555 :param algo: algorithm to decide what permission should be choose if
567 :param algo: algorithm to decide what permission should be choose if
556 it's multiple defined, eg user in two different groups. It also
568 it's multiple defined, eg user in two different groups. It also
557 decides if explicit flag is turned off how to specify the permission
569 decides if explicit flag is turned off how to specify the permission
558 for case when user is in a group + have defined separate permission
570 for case when user is in a group + have defined separate permission
559 """
571 """
560 user_id = user.user_id
572 user_id = user.user_id
561 user_is_admin = user.is_admin
573 user_is_admin = user.is_admin
562 user_inherit_default_permissions = user.inherit_default_permissions
574 user_inherit_default_permissions = user.inherit_default_permissions
563
575
564 log.debug('Getting PERMISSION tree')
576 log.debug('Getting PERMISSION tree')
565 compute = conditional_cache('short_term', 'cache_desc',
577 compute = conditional_cache('short_term', 'cache_desc',
566 condition=cache, func=_cached_perms_data)
578 condition=cache, func=_cached_perms_data)
567 return compute(user_id, user_is_admin,
579 return compute(user_id, user_is_admin,
568 user_inherit_default_permissions, explicit, algo)
580 user_inherit_default_permissions, explicit, algo)
569
581
570 def _get_api_keys(self):
582 def _get_api_keys(self):
571 api_keys = [self.api_key]
583 api_keys = [self.api_key]
572 for api_key in UserApiKeys.query() \
584 for api_key in UserApiKeys.query() \
573 .filter(UserApiKeys.user_id == self.user_id) \
585 .filter(UserApiKeys.user_id == self.user_id) \
574 .filter(or_(UserApiKeys.expires == -1,
586 .filter(or_(UserApiKeys.expires == -1,
575 UserApiKeys.expires >= time.time())).all():
587 UserApiKeys.expires >= time.time())).all():
576 api_keys.append(api_key.api_key)
588 api_keys.append(api_key.api_key)
577
589
578 return api_keys
590 return api_keys
579
591
580 @property
592 @property
581 def is_admin(self):
593 def is_admin(self):
582 return self.admin
594 return self.admin
583
595
584 @property
596 @property
585 def repositories_admin(self):
597 def repositories_admin(self):
586 """
598 """
587 Returns list of repositories you're an admin of
599 Returns list of repositories you're an admin of
588 """
600 """
589 return [x[0] for x in self.permissions['repositories'].iteritems()
601 return [x[0] for x in self.permissions['repositories'].iteritems()
590 if x[1] == 'repository.admin']
602 if x[1] == 'repository.admin']
591
603
592 @property
604 @property
593 def repository_groups_admin(self):
605 def repository_groups_admin(self):
594 """
606 """
595 Returns list of repository groups you're an admin of
607 Returns list of repository groups you're an admin of
596 """
608 """
597 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
609 return [x[0] for x in self.permissions['repositories_groups'].iteritems()
598 if x[1] == 'group.admin']
610 if x[1] == 'group.admin']
599
611
600 @property
612 @property
601 def user_groups_admin(self):
613 def user_groups_admin(self):
602 """
614 """
603 Returns list of user groups you're an admin of
615 Returns list of user groups you're an admin of
604 """
616 """
605 return [x[0] for x in self.permissions['user_groups'].iteritems()
617 return [x[0] for x in self.permissions['user_groups'].iteritems()
606 if x[1] == 'usergroup.admin']
618 if x[1] == 'usergroup.admin']
607
619
608 @staticmethod
620 @staticmethod
609 def check_ip_allowed(user, ip_addr):
621 def check_ip_allowed(user, ip_addr):
610 """
622 """
611 Check if the given IP address (a `str`) is allowed for the given
623 Check if the given IP address (a `str`) is allowed for the given
612 user (an `AuthUser` or `db.User`).
624 user (an `AuthUser` or `db.User`).
613 """
625 """
614 allowed_ips = AuthUser.get_allowed_ips(user.user_id, cache=True,
626 allowed_ips = AuthUser.get_allowed_ips(user.user_id, cache=True,
615 inherit_from_default=user.inherit_default_permissions)
627 inherit_from_default=user.inherit_default_permissions)
616 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
628 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
617 log.debug('IP:%s is in range of %s', ip_addr, allowed_ips)
629 log.debug('IP:%s is in range of %s', ip_addr, allowed_ips)
618 return True
630 return True
619 else:
631 else:
620 log.info('Access for IP:%s forbidden, '
632 log.info('Access for IP:%s forbidden, '
621 'not in %s' % (ip_addr, allowed_ips))
633 'not in %s' % (ip_addr, allowed_ips))
622 return False
634 return False
623
635
624 def __repr__(self):
636 def __repr__(self):
625 return "<AuthUser('id:%s[%s] auth:%s')>" \
637 return "<AuthUser('id:%s[%s] auth:%s')>" \
626 % (self.user_id, self.username, (self.is_authenticated or self.is_default_user))
638 % (self.user_id, self.username, (self.is_authenticated or self.is_default_user))
627
639
628 def to_cookie(self):
640 def to_cookie(self):
629 """ Serializes this login session to a cookie `dict`. """
641 """ Serializes this login session to a cookie `dict`. """
630 return {
642 return {
631 'user_id': self.user_id,
643 'user_id': self.user_id,
632 'is_external_auth': self.is_external_auth,
644 'is_external_auth': self.is_external_auth,
633 }
645 }
634
646
635 @staticmethod
647 @staticmethod
636 def from_cookie(cookie):
648 def from_cookie(cookie):
637 """
649 """
638 Deserializes an `AuthUser` from a cookie `dict`.
650 Deserializes an `AuthUser` from a cookie `dict`.
639 """
651 """
640
652
641 au = AuthUser(
653 au = AuthUser(
642 user_id=cookie.get('user_id'),
654 user_id=cookie.get('user_id'),
643 is_external_auth=cookie.get('is_external_auth', False),
655 is_external_auth=cookie.get('is_external_auth', False),
644 )
656 )
645 au.is_authenticated = True
657 au.is_authenticated = True
646 return au
658 return au
647
659
648 @classmethod
660 @classmethod
649 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
661 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
650 _set = set()
662 _set = set()
651
663
652 if inherit_from_default:
664 if inherit_from_default:
653 default_ips = UserIpMap.query().filter(UserIpMap.user ==
665 default_ips = UserIpMap.query().filter(UserIpMap.user ==
654 User.get_default_user(cache=True))
666 User.get_default_user(cache=True))
655 if cache:
667 if cache:
656 default_ips = default_ips.options(FromCache("sql_cache_short",
668 default_ips = default_ips.options(FromCache("sql_cache_short",
657 "get_user_ips_default"))
669 "get_user_ips_default"))
658
670
659 # populate from default user
671 # populate from default user
660 for ip in default_ips:
672 for ip in default_ips:
661 try:
673 try:
662 _set.add(ip.ip_addr)
674 _set.add(ip.ip_addr)
663 except ObjectDeletedError:
675 except ObjectDeletedError:
664 # since we use heavy caching sometimes it happens that we get
676 # since we use heavy caching sometimes it happens that we get
665 # deleted objects here, we just skip them
677 # deleted objects here, we just skip them
666 pass
678 pass
667
679
668 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
680 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
669 if cache:
681 if cache:
670 user_ips = user_ips.options(FromCache("sql_cache_short",
682 user_ips = user_ips.options(FromCache("sql_cache_short",
671 "get_user_ips_%s" % user_id))
683 "get_user_ips_%s" % user_id))
672
684
673 for ip in user_ips:
685 for ip in user_ips:
674 try:
686 try:
675 _set.add(ip.ip_addr)
687 _set.add(ip.ip_addr)
676 except ObjectDeletedError:
688 except ObjectDeletedError:
677 # since we use heavy caching sometimes it happens that we get
689 # since we use heavy caching sometimes it happens that we get
678 # deleted objects here, we just skip them
690 # deleted objects here, we just skip them
679 pass
691 pass
680 return _set or set(['0.0.0.0/0', '::/0'])
692 return _set or set(['0.0.0.0/0', '::/0'])
681
693
682
694
683 def set_available_permissions(config):
695 def set_available_permissions(config):
684 """
696 """
685 This function will propagate globals with all available defined
697 This function will propagate globals with all available defined
686 permission given in db. We don't want to check each time from db for new
698 permission given in db. We don't want to check each time from db for new
687 permissions since adding a new permission also requires application restart
699 permissions since adding a new permission also requires application restart
688 ie. to decorate new views with the newly created permission
700 ie. to decorate new views with the newly created permission
689
701
690 :param config: current config instance
702 :param config: current config instance
691
703
692 """
704 """
693 log.info('getting information about all available permissions')
705 log.info('getting information about all available permissions')
694 try:
706 try:
695 sa = meta.Session
707 sa = meta.Session
696 all_perms = sa.query(Permission).all()
708 all_perms = sa.query(Permission).all()
697 config['available_permissions'] = [x.permission_name for x in all_perms]
709 config['available_permissions'] = [x.permission_name for x in all_perms]
698 finally:
710 finally:
699 meta.Session.remove()
711 meta.Session.remove()
700
712
701
713
702 #==============================================================================
714 #==============================================================================
703 # CHECK DECORATORS
715 # CHECK DECORATORS
704 #==============================================================================
716 #==============================================================================
705
717
706 def _redirect_to_login(message=None):
718 def _redirect_to_login(message=None):
707 """Return an exception that must be raised. It will redirect to the login
719 """Return an exception that must be raised. It will redirect to the login
708 page which will redirect back to the current URL after authentication.
720 page which will redirect back to the current URL after authentication.
709 The optional message will be shown in a flash message."""
721 The optional message will be shown in a flash message."""
710 from kallithea.lib import helpers as h
722 from kallithea.lib import helpers as h
711 if message:
723 if message:
712 h.flash(h.literal(message), category='warning')
724 h.flash(h.literal(message), category='warning')
713 p = request.path_qs
725 p = request.path_qs
714 log.debug('Redirecting to login page, origin: %s', p)
726 log.debug('Redirecting to login page, origin: %s', p)
715 return HTTPFound(location=url('login_home', came_from=p))
727 return HTTPFound(location=url('login_home', came_from=p))
716
728
717
729
718 # Use as decorator
730 # Use as decorator
719 class LoginRequired(object):
731 class LoginRequired(object):
720 """Client must be logged in as a valid User (but the "default" user,
732 """Client must be logged in as a valid User (but the "default" user,
721 if enabled, is considered valid), or we'll redirect to the login page.
733 if enabled, is considered valid), or we'll redirect to the login page.
722
734
723 Also checks that IP address is allowed, and if using API key instead
735 Also checks that IP address is allowed, and if using API key instead
724 of regular cookie authentication, checks that API key access is allowed
736 of regular cookie authentication, checks that API key access is allowed
725 (based on `api_access` parameter and the API view whitelist).
737 (based on `api_access` parameter and the API view whitelist).
726 """
738 """
727
739
728 def __init__(self, api_access=False):
740 def __init__(self, api_access=False):
729 self.api_access = api_access
741 self.api_access = api_access
730
742
731 def __call__(self, func):
743 def __call__(self, func):
732 return decorator(self.__wrapper, func)
744 return decorator(self.__wrapper, func)
733
745
734 def __wrapper(self, func, *fargs, **fkwargs):
746 def __wrapper(self, func, *fargs, **fkwargs):
735 controller = fargs[0]
747 controller = fargs[0]
736 user = request.authuser
748 user = request.authuser
737 loc = "%s:%s" % (controller.__class__.__name__, func.__name__)
749 loc = "%s:%s" % (controller.__class__.__name__, func.__name__)
738 log.debug('Checking access for user %s @ %s', user, loc)
750 log.debug('Checking access for user %s @ %s', user, loc)
739
751
740 if not AuthUser.check_ip_allowed(user, request.ip_addr):
752 if not AuthUser.check_ip_allowed(user, request.ip_addr):
741 raise _redirect_to_login(_('IP %s not allowed') % request.ip_addr)
753 raise _redirect_to_login(_('IP %s not allowed') % request.ip_addr)
742
754
743 # Check if we used an API key to authenticate.
755 # Check if we used an API key to authenticate.
744 api_key = user.authenticating_api_key
756 api_key = user.authenticating_api_key
745 if api_key is not None:
757 if api_key is not None:
746 # Check that controller is enabled for API key usage.
758 # Check that controller is enabled for API key usage.
747 if not self.api_access and not allowed_api_access(loc, api_key=api_key):
759 if not self.api_access and not allowed_api_access(loc, api_key=api_key):
748 # controller does not allow API access
760 # controller does not allow API access
749 log.warning('API access to %s is not allowed', loc)
761 log.warning('API access to %s is not allowed', loc)
750 raise HTTPForbidden()
762 raise HTTPForbidden()
751
763
752 log.info('user %s authenticated with API key ****%s @ %s',
764 log.info('user %s authenticated with API key ****%s @ %s',
753 user, api_key[-4:], loc)
765 user, api_key[-4:], loc)
754 return func(*fargs, **fkwargs)
766 return func(*fargs, **fkwargs)
755
767
756 # CSRF protection: Whenever a request has ambient authority (whether
768 # CSRF protection: Whenever a request has ambient authority (whether
757 # through a session cookie or its origin IP address), it must include
769 # through a session cookie or its origin IP address), it must include
758 # the correct token, unless the HTTP method is GET or HEAD (and thus
770 # the correct token, unless the HTTP method is GET or HEAD (and thus
759 # guaranteed to be side effect free. In practice, the only situation
771 # guaranteed to be side effect free. In practice, the only situation
760 # where we allow side effects without ambient authority is when the
772 # where we allow side effects without ambient authority is when the
761 # authority comes from an API key; and that is handled above.
773 # authority comes from an API key; and that is handled above.
762 if request.method not in ['GET', 'HEAD']:
774 if request.method not in ['GET', 'HEAD']:
763 token = request.POST.get(secure_form.token_key)
775 token = request.POST.get(secure_form.token_key)
764 if not token or token != secure_form.authentication_token():
776 if not token or token != secure_form.authentication_token():
765 log.error('CSRF check failed')
777 log.error('CSRF check failed')
766 raise HTTPForbidden()
778 raise HTTPForbidden()
767
779
768 # regular user authentication
780 # regular user authentication
769 if user.is_authenticated or user.is_default_user:
781 if user.is_authenticated or user.is_default_user:
770 log.info('user %s authenticated with regular auth @ %s', user, loc)
782 log.info('user %s authenticated with regular auth @ %s', user, loc)
771 return func(*fargs, **fkwargs)
783 return func(*fargs, **fkwargs)
772 else:
784 else:
773 log.warning('user %s NOT authenticated with regular auth @ %s', user, loc)
785 log.warning('user %s NOT authenticated with regular auth @ %s', user, loc)
774 raise _redirect_to_login()
786 raise _redirect_to_login()
775
787
776
788
777 # Use as decorator
789 # Use as decorator
778 class NotAnonymous(object):
790 class NotAnonymous(object):
779 """Ensures that client is not logged in as the "default" user, and
791 """Ensures that client is not logged in as the "default" user, and
780 redirects to the login page otherwise. Must be used together with
792 redirects to the login page otherwise. Must be used together with
781 LoginRequired."""
793 LoginRequired."""
782
794
783 def __call__(self, func):
795 def __call__(self, func):
784 return decorator(self.__wrapper, func)
796 return decorator(self.__wrapper, func)
785
797
786 def __wrapper(self, func, *fargs, **fkwargs):
798 def __wrapper(self, func, *fargs, **fkwargs):
787 cls = fargs[0]
799 cls = fargs[0]
788 user = request.authuser
800 user = request.authuser
789
801
790 log.debug('Checking that user %s is not anonymous @%s', user.username, cls)
802 log.debug('Checking that user %s is not anonymous @%s', user.username, cls)
791
803
792 if user.is_default_user:
804 if user.is_default_user:
793 raise _redirect_to_login(_('You need to be a registered user to '
805 raise _redirect_to_login(_('You need to be a registered user to '
794 'perform this action'))
806 'perform this action'))
795 else:
807 else:
796 return func(*fargs, **fkwargs)
808 return func(*fargs, **fkwargs)
797
809
798
810
799 class _PermsDecorator(object):
811 class _PermsDecorator(object):
800 """Base class for controller decorators"""
812 """Base class for controller decorators"""
801
813
802 def __init__(self, *required_perms):
814 def __init__(self, *required_perms):
803 self.required_perms = required_perms # usually very short - a list is thus fine
815 self.required_perms = required_perms # usually very short - a list is thus fine
804
816
805 def __call__(self, func):
817 def __call__(self, func):
806 return decorator(self.__wrapper, func)
818 return decorator(self.__wrapper, func)
807
819
808 def __wrapper(self, func, *fargs, **fkwargs):
820 def __wrapper(self, func, *fargs, **fkwargs):
809 cls = fargs[0]
821 cls = fargs[0]
810 user = request.authuser
822 user = request.authuser
811 log.debug('checking %s permissions %s for %s %s',
823 log.debug('checking %s permissions %s for %s %s',
812 self.__class__.__name__, self.required_perms, cls, user)
824 self.__class__.__name__, self.required_perms, cls, user)
813
825
814 if self.check_permissions(user):
826 if self.check_permissions(user):
815 log.debug('Permission granted for %s %s', cls, user)
827 log.debug('Permission granted for %s %s', cls, user)
816 return func(*fargs, **fkwargs)
828 return func(*fargs, **fkwargs)
817
829
818 else:
830 else:
819 log.debug('Permission denied for %s %s', cls, user)
831 log.debug('Permission denied for %s %s', cls, user)
820 if user.is_default_user:
832 if user.is_default_user:
821 raise _redirect_to_login(_('You need to be signed in to view this page'))
833 raise _redirect_to_login(_('You need to be signed in to view this page'))
822 else:
834 else:
823 raise HTTPForbidden()
835 raise HTTPForbidden()
824
836
825 def check_permissions(self, user):
837 def check_permissions(self, user):
826 raise NotImplementedError()
838 raise NotImplementedError()
827
839
828
840
829 class HasPermissionAnyDecorator(_PermsDecorator):
841 class HasPermissionAnyDecorator(_PermsDecorator):
830 """
842 """
831 Checks the user has any of the given global permissions.
843 Checks the user has any of the given global permissions.
832 """
844 """
833
845
834 def check_permissions(self, user):
846 def check_permissions(self, user):
835 global_permissions = user.permissions['global'] # usually very short
847 global_permissions = user.permissions['global'] # usually very short
836 return any(p in global_permissions for p in self.required_perms)
848 return any(p in global_permissions for p in self.required_perms)
837
849
838
850
839 class HasRepoPermissionAnyDecorator(_PermsDecorator):
851 class HasRepoPermissionLevelDecorator(_PermsDecorator):
840 """
852 """
841 Checks the user has any of given permissions for the requested repository.
853 Checks the user has at least the specified permission level for the requested repository.
842 """
854 """
843
855
844 def check_permissions(self, user):
856 def check_permissions(self, user):
845 repo_name = get_repo_slug(request)
857 repo_name = get_repo_slug(request)
846 try:
858 (level,) = self.required_perms
847 return user.permissions['repositories'][repo_name] in self.required_perms
859 return user.has_repository_permission_level(repo_name, level)
848 except KeyError:
849 return False
850
860
851
861
852 class HasRepoGroupPermissionAnyDecorator(_PermsDecorator):
862 class HasRepoGroupPermissionAnyDecorator(_PermsDecorator):
853 """
863 """
854 Checks the user has any of given permissions for the requested repository group.
864 Checks the user has any of given permissions for the requested repository group.
855 """
865 """
856
866
857 def check_permissions(self, user):
867 def check_permissions(self, user):
858 repo_group_name = get_repo_group_slug(request)
868 repo_group_name = get_repo_group_slug(request)
859 try:
869 try:
860 return user.permissions['repositories_groups'][repo_group_name] in self.required_perms
870 return user.permissions['repositories_groups'][repo_group_name] in self.required_perms
861 except KeyError:
871 except KeyError:
862 return False
872 return False
863
873
864
874
865 class HasUserGroupPermissionAnyDecorator(_PermsDecorator):
875 class HasUserGroupPermissionAnyDecorator(_PermsDecorator):
866 """
876 """
867 Checks for access permission for any of given predicates for specific
877 Checks for access permission for any of given predicates for specific
868 user group. In order to fulfill the request any of predicates must be meet
878 user group. In order to fulfill the request any of predicates must be meet
869 """
879 """
870
880
871 def check_permissions(self, user):
881 def check_permissions(self, user):
872 user_group_name = get_user_group_slug(request)
882 user_group_name = get_user_group_slug(request)
873 try:
883 try:
874 return user.permissions['user_groups'][user_group_name] in self.required_perms
884 return user.permissions['user_groups'][user_group_name] in self.required_perms
875 except KeyError:
885 except KeyError:
876 return False
886 return False
877
887
878
888
879 #==============================================================================
889 #==============================================================================
880 # CHECK FUNCTIONS
890 # CHECK FUNCTIONS
881 #==============================================================================
891 #==============================================================================
882
892
883 class _PermsFunction(object):
893 class _PermsFunction(object):
884 """Base function for other check functions"""
894 """Base function for other check functions"""
885
895
886 def __init__(self, *required_perms):
896 def __init__(self, *required_perms):
887 self.required_perms = required_perms # usually very short - a list is thus fine
897 self.required_perms = required_perms # usually very short - a list is thus fine
888
898
889 def __nonzero__(self):
899 def __nonzero__(self):
890 """ Defend against accidentally forgetting to call the object
900 """ Defend against accidentally forgetting to call the object
891 and instead evaluating it directly in a boolean context,
901 and instead evaluating it directly in a boolean context,
892 which could have security implications.
902 which could have security implications.
893 """
903 """
894 raise AssertionError(self.__class__.__name__ + ' is not a bool and must be called!')
904 raise AssertionError(self.__class__.__name__ + ' is not a bool and must be called!')
895
905
896 def __call__(self, *a, **b):
906 def __call__(self, *a, **b):
897 raise NotImplementedError()
907 raise NotImplementedError()
898
908
899
909
900 class HasPermissionAny(_PermsFunction):
910 class HasPermissionAny(_PermsFunction):
901
911
902 def __call__(self, purpose=None):
912 def __call__(self, purpose=None):
903 global_permissions = request.user.permissions['global'] # usually very short
913 global_permissions = request.user.permissions['global'] # usually very short
904 ok = any(p in global_permissions for p in self.required_perms)
914 ok = any(p in global_permissions for p in self.required_perms)
905
915
906 log.debug('Check %s for global %s (%s): %s' %
916 log.debug('Check %s for global %s (%s): %s' %
907 (request.user.username, self.required_perms, purpose, ok))
917 (request.user.username, self.required_perms, purpose, ok))
908 return ok
918 return ok
909
919
910
920
911 class HasRepoPermissionAny(_PermsFunction):
921 class HasRepoPermissionLevel(_PermsFunction):
912
922
913 def __call__(self, repo_name, purpose=None):
923 def __call__(self, repo_name, purpose=None):
914 try:
924 (level,) = self.required_perms
915 ok = request.user.permissions['repositories'][repo_name] in self.required_perms
925 return request.user.has_repository_permission_level(repo_name, level, purpose)
916 except KeyError:
917 ok = False
918
919 log.debug('Check %s for %s for repo %s (%s): %s' %
920 (request.user.username, self.required_perms, repo_name, purpose, ok))
921 return ok
922
926
923
927
924 class HasRepoGroupPermissionAny(_PermsFunction):
928 class HasRepoGroupPermissionAny(_PermsFunction):
925
929
926 def __call__(self, group_name, purpose=None):
930 def __call__(self, group_name, purpose=None):
927 try:
931 try:
928 ok = request.user.permissions['repositories_groups'][group_name] in self.required_perms
932 ok = request.user.permissions['repositories_groups'][group_name] in self.required_perms
929 except KeyError:
933 except KeyError:
930 ok = False
934 ok = False
931
935
932 log.debug('Check %s for %s for repo group %s (%s): %s' %
936 log.debug('Check %s for %s for repo group %s (%s): %s' %
933 (request.user.username, self.required_perms, group_name, purpose, ok))
937 (request.user.username, self.required_perms, group_name, purpose, ok))
934 return ok
938 return ok
935
939
936
940
937 class HasUserGroupPermissionAny(_PermsFunction):
941 class HasUserGroupPermissionAny(_PermsFunction):
938
942
939 def __call__(self, user_group_name, purpose=None):
943 def __call__(self, user_group_name, purpose=None):
940 try:
944 try:
941 ok = request.user.permissions['user_groups'][user_group_name] in self.required_perms
945 ok = request.user.permissions['user_groups'][user_group_name] in self.required_perms
942 except KeyError:
946 except KeyError:
943 ok = False
947 ok = False
944
948
945 log.debug('Check %s %s for user group %s (%s): %s' %
949 log.debug('Check %s %s for user group %s (%s): %s' %
946 (request.user.username, self.required_perms, user_group_name, purpose, ok))
950 (request.user.username, self.required_perms, user_group_name, purpose, ok))
947 return ok
951 return ok
948
952
949
953
950 #==============================================================================
954 #==============================================================================
951 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
955 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
952 #==============================================================================
956 #==============================================================================
953
957
954 class HasPermissionAnyMiddleware(object):
958 class HasPermissionAnyMiddleware(object):
955 def __init__(self, *perms):
959 def __init__(self, *perms):
956 self.required_perms = set(perms)
960 self.required_perms = set(perms)
957
961
958 def __call__(self, user, repo_name, purpose=None):
962 def __call__(self, user, repo_name, purpose=None):
959 # repo_name MUST be unicode, since we handle keys in ok
963 # repo_name MUST be unicode, since we handle keys in ok
960 # dict by unicode
964 # dict by unicode
961 repo_name = safe_unicode(repo_name)
965 repo_name = safe_unicode(repo_name)
962 user = AuthUser(user.user_id)
966 user = AuthUser(user.user_id)
963
967
964 try:
968 try:
965 ok = user.permissions['repositories'][repo_name] in self.required_perms
969 ok = user.permissions['repositories'][repo_name] in self.required_perms
966 except KeyError:
970 except KeyError:
967 ok = False
971 ok = False
968
972
969 log.debug('Middleware check %s for %s for repo %s (%s): %s' % (user.username, self.required_perms, repo_name, purpose, ok))
973 log.debug('Middleware check %s for %s for repo %s (%s): %s' % (user.username, self.required_perms, repo_name, purpose, ok))
970 return ok
974 return ok
971
975
972
976
973 def check_ip_access(source_ip, allowed_ips=None):
977 def check_ip_access(source_ip, allowed_ips=None):
974 """
978 """
975 Checks if source_ip is a subnet of any of allowed_ips.
979 Checks if source_ip is a subnet of any of allowed_ips.
976
980
977 :param source_ip:
981 :param source_ip:
978 :param allowed_ips: list of allowed ips together with mask
982 :param allowed_ips: list of allowed ips together with mask
979 """
983 """
980 from kallithea.lib import ipaddr
984 from kallithea.lib import ipaddr
981 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
985 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
982 if isinstance(allowed_ips, (tuple, list, set)):
986 if isinstance(allowed_ips, (tuple, list, set)):
983 for ip in allowed_ips:
987 for ip in allowed_ips:
984 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
988 if ipaddr.IPAddress(source_ip) in ipaddr.IPNetwork(ip):
985 log.debug('IP %s is network %s',
989 log.debug('IP %s is network %s',
986 ipaddr.IPAddress(source_ip), ipaddr.IPNetwork(ip))
990 ipaddr.IPAddress(source_ip), ipaddr.IPNetwork(ip))
987 return True
991 return True
988 return False
992 return False
@@ -1,1204 +1,1204 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 StringIO
21 import StringIO
22 import logging
22 import logging
23 import re
23 import re
24 import urlparse
24 import urlparse
25 import textwrap
25 import textwrap
26
26
27 from beaker.cache import cache_region
27 from beaker.cache import cache_region
28 from pygments.formatters.html import HtmlFormatter
28 from pygments.formatters.html import HtmlFormatter
29 from pygments import highlight as code_highlight
29 from pygments import highlight as code_highlight
30 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
31
31
32 from webhelpers.html import literal, HTML, escape
32 from webhelpers.html import literal, HTML, escape
33 from webhelpers.html.tags import checkbox, end_form, hidden, link_to, \
33 from webhelpers.html.tags import checkbox, end_form, hidden, link_to, \
34 select, submit, text, password, textarea, radio, form as insecure_form
34 select, submit, text, password, textarea, radio, form as insecure_form
35 from webhelpers.number import format_byte_size
35 from webhelpers.number import format_byte_size
36 from webhelpers.pylonslib import Flash as _Flash
36 from webhelpers.pylonslib import Flash as _Flash
37 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
37 from webhelpers.pylonslib.secure_form import secure_form, authentication_token
38 from webhelpers.text import chop_at, truncate, wrap_paragraphs
38 from webhelpers.text import chop_at, truncate, wrap_paragraphs
39 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
39 from webhelpers.html.tags import _set_input_attrs, _set_id_attr, \
40 convert_boolean_attrs, NotGiven, _make_safe_id_component
40 convert_boolean_attrs, NotGiven, _make_safe_id_component
41
41
42 from kallithea.config.routing import url
42 from kallithea.config.routing import url
43 from kallithea.lib.annotate import annotate_highlight
43 from kallithea.lib.annotate import annotate_highlight
44 from kallithea.lib.pygmentsutils import get_custom_lexer
44 from kallithea.lib.pygmentsutils import get_custom_lexer
45 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
45 from kallithea.lib.utils2 import str2bool, safe_unicode, safe_str, \
46 time_to_datetime, AttributeDict, safe_int, MENTIONS_REGEX
46 time_to_datetime, AttributeDict, safe_int, MENTIONS_REGEX
47 from kallithea.lib.markup_renderer import url_re
47 from kallithea.lib.markup_renderer import url_re
48 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
48 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
49 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
49 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
50
50
51 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
52
52
53
53
54 def canonical_url(*args, **kargs):
54 def canonical_url(*args, **kargs):
55 '''Like url(x, qualified=True), but returns url that not only is qualified
55 '''Like url(x, qualified=True), but returns url that not only is qualified
56 but also canonical, as configured in canonical_url'''
56 but also canonical, as configured in canonical_url'''
57 from kallithea import CONFIG
57 from kallithea import CONFIG
58 try:
58 try:
59 parts = CONFIG.get('canonical_url', '').split('://', 1)
59 parts = CONFIG.get('canonical_url', '').split('://', 1)
60 kargs['host'] = parts[1].split('/', 1)[0]
60 kargs['host'] = parts[1].split('/', 1)[0]
61 kargs['protocol'] = parts[0]
61 kargs['protocol'] = parts[0]
62 except IndexError:
62 except IndexError:
63 kargs['qualified'] = True
63 kargs['qualified'] = True
64 return url(*args, **kargs)
64 return url(*args, **kargs)
65
65
66 def canonical_hostname():
66 def canonical_hostname():
67 '''Return canonical hostname of system'''
67 '''Return canonical hostname of system'''
68 from kallithea import CONFIG
68 from kallithea import CONFIG
69 try:
69 try:
70 parts = CONFIG.get('canonical_url', '').split('://', 1)
70 parts = CONFIG.get('canonical_url', '').split('://', 1)
71 return parts[1].split('/', 1)[0]
71 return parts[1].split('/', 1)[0]
72 except IndexError:
72 except IndexError:
73 parts = url('home', qualified=True).split('://', 1)
73 parts = url('home', qualified=True).split('://', 1)
74 return parts[1].split('/', 1)[0]
74 return parts[1].split('/', 1)[0]
75
75
76 def html_escape(s):
76 def html_escape(s):
77 """Return string with all html escaped.
77 """Return string with all html escaped.
78 This is also safe for javascript in html but not necessarily correct.
78 This is also safe for javascript in html but not necessarily correct.
79 """
79 """
80 return (s
80 return (s
81 .replace('&', '&amp;')
81 .replace('&', '&amp;')
82 .replace(">", "&gt;")
82 .replace(">", "&gt;")
83 .replace("<", "&lt;")
83 .replace("<", "&lt;")
84 .replace('"', "&quot;")
84 .replace('"', "&quot;")
85 .replace("'", "&apos;")
85 .replace("'", "&apos;")
86 )
86 )
87
87
88 def shorter(s, size=20, firstline=False, postfix='...'):
88 def shorter(s, size=20, firstline=False, postfix='...'):
89 """Truncate s to size, including the postfix string if truncating.
89 """Truncate s to size, including the postfix string if truncating.
90 If firstline, truncate at newline.
90 If firstline, truncate at newline.
91 """
91 """
92 if firstline:
92 if firstline:
93 s = s.split('\n', 1)[0].rstrip()
93 s = s.split('\n', 1)[0].rstrip()
94 if len(s) > size:
94 if len(s) > size:
95 return s[:size - len(postfix)] + postfix
95 return s[:size - len(postfix)] + postfix
96 return s
96 return s
97
97
98
98
99 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
99 def _reset(name, value=None, id=NotGiven, type="reset", **attrs):
100 """
100 """
101 Reset button
101 Reset button
102 """
102 """
103 _set_input_attrs(attrs, type, name, value)
103 _set_input_attrs(attrs, type, name, value)
104 _set_id_attr(attrs, id, name)
104 _set_id_attr(attrs, id, name)
105 convert_boolean_attrs(attrs, ["disabled"])
105 convert_boolean_attrs(attrs, ["disabled"])
106 return HTML.input(**attrs)
106 return HTML.input(**attrs)
107
107
108 reset = _reset
108 reset = _reset
109 safeid = _make_safe_id_component
109 safeid = _make_safe_id_component
110
110
111
111
112 def FID(raw_id, path):
112 def FID(raw_id, path):
113 """
113 """
114 Creates a unique ID for filenode based on it's hash of path and revision
114 Creates a unique ID for filenode based on it's hash of path and revision
115 it's safe to use in urls
115 it's safe to use in urls
116
116
117 :param raw_id:
117 :param raw_id:
118 :param path:
118 :param path:
119 """
119 """
120
120
121 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
121 return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_str(path)).hexdigest()[:12])
122
122
123
123
124 class _FilesBreadCrumbs(object):
124 class _FilesBreadCrumbs(object):
125
125
126 def __call__(self, repo_name, rev, paths):
126 def __call__(self, repo_name, rev, paths):
127 if isinstance(paths, str):
127 if isinstance(paths, str):
128 paths = safe_unicode(paths)
128 paths = safe_unicode(paths)
129 url_l = [link_to(repo_name, url('files_home',
129 url_l = [link_to(repo_name, url('files_home',
130 repo_name=repo_name,
130 repo_name=repo_name,
131 revision=rev, f_path=''),
131 revision=rev, f_path=''),
132 class_='ypjax-link')]
132 class_='ypjax-link')]
133 paths_l = paths.split('/')
133 paths_l = paths.split('/')
134 for cnt, p in enumerate(paths_l):
134 for cnt, p in enumerate(paths_l):
135 if p != '':
135 if p != '':
136 url_l.append(link_to(p,
136 url_l.append(link_to(p,
137 url('files_home',
137 url('files_home',
138 repo_name=repo_name,
138 repo_name=repo_name,
139 revision=rev,
139 revision=rev,
140 f_path='/'.join(paths_l[:cnt + 1])
140 f_path='/'.join(paths_l[:cnt + 1])
141 ),
141 ),
142 class_='ypjax-link'
142 class_='ypjax-link'
143 )
143 )
144 )
144 )
145
145
146 return literal('/'.join(url_l))
146 return literal('/'.join(url_l))
147
147
148 files_breadcrumbs = _FilesBreadCrumbs()
148 files_breadcrumbs = _FilesBreadCrumbs()
149
149
150
150
151 class CodeHtmlFormatter(HtmlFormatter):
151 class CodeHtmlFormatter(HtmlFormatter):
152 """
152 """
153 My code Html Formatter for source codes
153 My code Html Formatter for source codes
154 """
154 """
155
155
156 def wrap(self, source, outfile):
156 def wrap(self, source, outfile):
157 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
157 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
158
158
159 def _wrap_code(self, source):
159 def _wrap_code(self, source):
160 for cnt, it in enumerate(source):
160 for cnt, it in enumerate(source):
161 i, t = it
161 i, t = it
162 t = '<span id="L%s">%s</span>' % (cnt + 1, t)
162 t = '<span id="L%s">%s</span>' % (cnt + 1, t)
163 yield i, t
163 yield i, t
164
164
165 def _wrap_tablelinenos(self, inner):
165 def _wrap_tablelinenos(self, inner):
166 dummyoutfile = StringIO.StringIO()
166 dummyoutfile = StringIO.StringIO()
167 lncount = 0
167 lncount = 0
168 for t, line in inner:
168 for t, line in inner:
169 if t:
169 if t:
170 lncount += 1
170 lncount += 1
171 dummyoutfile.write(line)
171 dummyoutfile.write(line)
172
172
173 fl = self.linenostart
173 fl = self.linenostart
174 mw = len(str(lncount + fl - 1))
174 mw = len(str(lncount + fl - 1))
175 sp = self.linenospecial
175 sp = self.linenospecial
176 st = self.linenostep
176 st = self.linenostep
177 la = self.lineanchors
177 la = self.lineanchors
178 aln = self.anchorlinenos
178 aln = self.anchorlinenos
179 nocls = self.noclasses
179 nocls = self.noclasses
180 if sp:
180 if sp:
181 lines = []
181 lines = []
182
182
183 for i in range(fl, fl + lncount):
183 for i in range(fl, fl + lncount):
184 if i % st == 0:
184 if i % st == 0:
185 if i % sp == 0:
185 if i % sp == 0:
186 if aln:
186 if aln:
187 lines.append('<a href="#%s%d" class="special">%*d</a>' %
187 lines.append('<a href="#%s%d" class="special">%*d</a>' %
188 (la, i, mw, i))
188 (la, i, mw, i))
189 else:
189 else:
190 lines.append('<span class="special">%*d</span>' % (mw, i))
190 lines.append('<span class="special">%*d</span>' % (mw, i))
191 else:
191 else:
192 if aln:
192 if aln:
193 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
193 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
194 else:
194 else:
195 lines.append('%*d' % (mw, i))
195 lines.append('%*d' % (mw, i))
196 else:
196 else:
197 lines.append('')
197 lines.append('')
198 ls = '\n'.join(lines)
198 ls = '\n'.join(lines)
199 else:
199 else:
200 lines = []
200 lines = []
201 for i in range(fl, fl + lncount):
201 for i in range(fl, fl + lncount):
202 if i % st == 0:
202 if i % st == 0:
203 if aln:
203 if aln:
204 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
204 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
205 else:
205 else:
206 lines.append('%*d' % (mw, i))
206 lines.append('%*d' % (mw, i))
207 else:
207 else:
208 lines.append('')
208 lines.append('')
209 ls = '\n'.join(lines)
209 ls = '\n'.join(lines)
210
210
211 # in case you wonder about the seemingly redundant <div> here: since the
211 # in case you wonder about the seemingly redundant <div> here: since the
212 # content in the other cell also is wrapped in a div, some browsers in
212 # content in the other cell also is wrapped in a div, some browsers in
213 # some configurations seem to mess up the formatting...
213 # some configurations seem to mess up the formatting...
214 if nocls:
214 if nocls:
215 yield 0, ('<table class="%stable">' % self.cssclass +
215 yield 0, ('<table class="%stable">' % self.cssclass +
216 '<tr><td><div class="linenodiv" '
216 '<tr><td><div class="linenodiv" '
217 'style="background-color: #f0f0f0; padding-right: 10px">'
217 'style="background-color: #f0f0f0; padding-right: 10px">'
218 '<pre style="line-height: 125%">' +
218 '<pre style="line-height: 125%">' +
219 ls + '</pre></div></td><td id="hlcode" class="code">')
219 ls + '</pre></div></td><td id="hlcode" class="code">')
220 else:
220 else:
221 yield 0, ('<table class="%stable">' % self.cssclass +
221 yield 0, ('<table class="%stable">' % self.cssclass +
222 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
222 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
223 ls + '</pre></div></td><td id="hlcode" class="code">')
223 ls + '</pre></div></td><td id="hlcode" class="code">')
224 yield 0, dummyoutfile.getvalue()
224 yield 0, dummyoutfile.getvalue()
225 yield 0, '</td></tr></table>'
225 yield 0, '</td></tr></table>'
226
226
227
227
228 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
228 _whitespace_re = re.compile(r'(\t)|( )(?=\n|</div>)')
229
229
230 def _markup_whitespace(m):
230 def _markup_whitespace(m):
231 groups = m.groups()
231 groups = m.groups()
232 if groups[0]:
232 if groups[0]:
233 return '<u>\t</u>'
233 return '<u>\t</u>'
234 if groups[1]:
234 if groups[1]:
235 return ' <i></i>'
235 return ' <i></i>'
236
236
237 def markup_whitespace(s):
237 def markup_whitespace(s):
238 return _whitespace_re.sub(_markup_whitespace, s)
238 return _whitespace_re.sub(_markup_whitespace, s)
239
239
240 def pygmentize(filenode, **kwargs):
240 def pygmentize(filenode, **kwargs):
241 """
241 """
242 pygmentize function using pygments
242 pygmentize function using pygments
243
243
244 :param filenode:
244 :param filenode:
245 """
245 """
246 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
246 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
247 return literal(markup_whitespace(
247 return literal(markup_whitespace(
248 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
248 code_highlight(filenode.content, lexer, CodeHtmlFormatter(**kwargs))))
249
249
250
250
251 def pygmentize_annotation(repo_name, filenode, **kwargs):
251 def pygmentize_annotation(repo_name, filenode, **kwargs):
252 """
252 """
253 pygmentize function for annotation
253 pygmentize function for annotation
254
254
255 :param filenode:
255 :param filenode:
256 """
256 """
257
257
258 color_dict = {}
258 color_dict = {}
259
259
260 def gen_color(n=10000):
260 def gen_color(n=10000):
261 """generator for getting n of evenly distributed colors using
261 """generator for getting n of evenly distributed colors using
262 hsv color and golden ratio. It always return same order of colors
262 hsv color and golden ratio. It always return same order of colors
263
263
264 :returns: RGB tuple
264 :returns: RGB tuple
265 """
265 """
266
266
267 def hsv_to_rgb(h, s, v):
267 def hsv_to_rgb(h, s, v):
268 if s == 0.0:
268 if s == 0.0:
269 return v, v, v
269 return v, v, v
270 i = int(h * 6.0) # XXX assume int() truncates!
270 i = int(h * 6.0) # XXX assume int() truncates!
271 f = (h * 6.0) - i
271 f = (h * 6.0) - i
272 p = v * (1.0 - s)
272 p = v * (1.0 - s)
273 q = v * (1.0 - s * f)
273 q = v * (1.0 - s * f)
274 t = v * (1.0 - s * (1.0 - f))
274 t = v * (1.0 - s * (1.0 - f))
275 i = i % 6
275 i = i % 6
276 if i == 0:
276 if i == 0:
277 return v, t, p
277 return v, t, p
278 if i == 1:
278 if i == 1:
279 return q, v, p
279 return q, v, p
280 if i == 2:
280 if i == 2:
281 return p, v, t
281 return p, v, t
282 if i == 3:
282 if i == 3:
283 return p, q, v
283 return p, q, v
284 if i == 4:
284 if i == 4:
285 return t, p, v
285 return t, p, v
286 if i == 5:
286 if i == 5:
287 return v, p, q
287 return v, p, q
288
288
289 golden_ratio = 0.618033988749895
289 golden_ratio = 0.618033988749895
290 h = 0.22717784590367374
290 h = 0.22717784590367374
291
291
292 for _unused in xrange(n):
292 for _unused in xrange(n):
293 h += golden_ratio
293 h += golden_ratio
294 h %= 1
294 h %= 1
295 HSV_tuple = [h, 0.95, 0.95]
295 HSV_tuple = [h, 0.95, 0.95]
296 RGB_tuple = hsv_to_rgb(*HSV_tuple)
296 RGB_tuple = hsv_to_rgb(*HSV_tuple)
297 yield map(lambda x: str(int(x * 256)), RGB_tuple)
297 yield map(lambda x: str(int(x * 256)), RGB_tuple)
298
298
299 cgenerator = gen_color()
299 cgenerator = gen_color()
300
300
301 def get_color_string(cs):
301 def get_color_string(cs):
302 if cs in color_dict:
302 if cs in color_dict:
303 col = color_dict[cs]
303 col = color_dict[cs]
304 else:
304 else:
305 col = color_dict[cs] = cgenerator.next()
305 col = color_dict[cs] = cgenerator.next()
306 return "color: rgb(%s)! important;" % (', '.join(col))
306 return "color: rgb(%s)! important;" % (', '.join(col))
307
307
308 def url_func(repo_name):
308 def url_func(repo_name):
309
309
310 def _url_func(changeset):
310 def _url_func(changeset):
311 author = escape(changeset.author)
311 author = escape(changeset.author)
312 date = changeset.date
312 date = changeset.date
313 message = escape(changeset.message)
313 message = escape(changeset.message)
314 tooltip_html = ("<b>Author:</b> %s<br/>"
314 tooltip_html = ("<b>Author:</b> %s<br/>"
315 "<b>Date:</b> %s</b><br/>"
315 "<b>Date:</b> %s</b><br/>"
316 "<b>Message:</b> %s") % (author, date, message)
316 "<b>Message:</b> %s") % (author, date, message)
317
317
318 lnk_format = show_id(changeset)
318 lnk_format = show_id(changeset)
319 uri = link_to(
319 uri = link_to(
320 lnk_format,
320 lnk_format,
321 url('changeset_home', repo_name=repo_name,
321 url('changeset_home', repo_name=repo_name,
322 revision=changeset.raw_id),
322 revision=changeset.raw_id),
323 style=get_color_string(changeset.raw_id),
323 style=get_color_string(changeset.raw_id),
324 **{'data-toggle': 'popover',
324 **{'data-toggle': 'popover',
325 'data-content': tooltip_html}
325 'data-content': tooltip_html}
326 )
326 )
327
327
328 uri += '\n'
328 uri += '\n'
329 return uri
329 return uri
330 return _url_func
330 return _url_func
331
331
332 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
332 return literal(markup_whitespace(annotate_highlight(filenode, url_func(repo_name), **kwargs)))
333
333
334
334
335 class _Message(object):
335 class _Message(object):
336 """A message returned by ``Flash.pop_messages()``.
336 """A message returned by ``Flash.pop_messages()``.
337
337
338 Converting the message to a string returns the message text. Instances
338 Converting the message to a string returns the message text. Instances
339 also have the following attributes:
339 also have the following attributes:
340
340
341 * ``message``: the message text.
341 * ``message``: the message text.
342 * ``category``: the category specified when the message was created.
342 * ``category``: the category specified when the message was created.
343 """
343 """
344
344
345 def __init__(self, category, message):
345 def __init__(self, category, message):
346 self.category = category
346 self.category = category
347 self.message = message
347 self.message = message
348
348
349 def __str__(self):
349 def __str__(self):
350 return self.message
350 return self.message
351
351
352 __unicode__ = __str__
352 __unicode__ = __str__
353
353
354 def __html__(self):
354 def __html__(self):
355 return escape(safe_unicode(self.message))
355 return escape(safe_unicode(self.message))
356
356
357 class Flash(_Flash):
357 class Flash(_Flash):
358
358
359 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
359 def __call__(self, message, category=None, ignore_duplicate=False, logf=None):
360 """
360 """
361 Show a message to the user _and_ log it through the specified function
361 Show a message to the user _and_ log it through the specified function
362
362
363 category: notice (default), warning, error, success
363 category: notice (default), warning, error, success
364 logf: a custom log function - such as log.debug
364 logf: a custom log function - such as log.debug
365
365
366 logf defaults to log.info, unless category equals 'success', in which
366 logf defaults to log.info, unless category equals 'success', in which
367 case logf defaults to log.debug.
367 case logf defaults to log.debug.
368 """
368 """
369 if logf is None:
369 if logf is None:
370 logf = log.info
370 logf = log.info
371 if category == 'success':
371 if category == 'success':
372 logf = log.debug
372 logf = log.debug
373
373
374 logf('Flash %s: %s', category, message)
374 logf('Flash %s: %s', category, message)
375
375
376 super(Flash, self).__call__(message, category, ignore_duplicate)
376 super(Flash, self).__call__(message, category, ignore_duplicate)
377
377
378 def pop_messages(self):
378 def pop_messages(self):
379 """Return all accumulated messages and delete them from the session.
379 """Return all accumulated messages and delete them from the session.
380
380
381 The return value is a list of ``Message`` objects.
381 The return value is a list of ``Message`` objects.
382 """
382 """
383 from pylons import session
383 from pylons import session
384 messages = session.pop(self.session_key, [])
384 messages = session.pop(self.session_key, [])
385 session.save()
385 session.save()
386 return [_Message(*m) for m in messages]
386 return [_Message(*m) for m in messages]
387
387
388 flash = Flash()
388 flash = Flash()
389
389
390 #==============================================================================
390 #==============================================================================
391 # SCM FILTERS available via h.
391 # SCM FILTERS available via h.
392 #==============================================================================
392 #==============================================================================
393 from kallithea.lib.vcs.utils import author_name, author_email
393 from kallithea.lib.vcs.utils import author_name, author_email
394 from kallithea.lib.utils2 import credentials_filter, age as _age
394 from kallithea.lib.utils2 import credentials_filter, age as _age
395
395
396 age = lambda x, y=False: _age(x, y)
396 age = lambda x, y=False: _age(x, y)
397 capitalize = lambda x: x.capitalize()
397 capitalize = lambda x: x.capitalize()
398 email = author_email
398 email = author_email
399 short_id = lambda x: x[:12]
399 short_id = lambda x: x[:12]
400 hide_credentials = lambda x: ''.join(credentials_filter(x))
400 hide_credentials = lambda x: ''.join(credentials_filter(x))
401
401
402
402
403 def show_id(cs):
403 def show_id(cs):
404 """
404 """
405 Configurable function that shows ID
405 Configurable function that shows ID
406 by default it's r123:fffeeefffeee
406 by default it's r123:fffeeefffeee
407
407
408 :param cs: changeset instance
408 :param cs: changeset instance
409 """
409 """
410 from kallithea import CONFIG
410 from kallithea import CONFIG
411 def_len = safe_int(CONFIG.get('show_sha_length', 12))
411 def_len = safe_int(CONFIG.get('show_sha_length', 12))
412 show_rev = str2bool(CONFIG.get('show_revision_number', False))
412 show_rev = str2bool(CONFIG.get('show_revision_number', False))
413
413
414 raw_id = cs.raw_id[:def_len]
414 raw_id = cs.raw_id[:def_len]
415 if show_rev:
415 if show_rev:
416 return 'r%s:%s' % (cs.revision, raw_id)
416 return 'r%s:%s' % (cs.revision, raw_id)
417 else:
417 else:
418 return raw_id
418 return raw_id
419
419
420
420
421 def fmt_date(date):
421 def fmt_date(date):
422 if date:
422 if date:
423 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
423 return date.strftime("%Y-%m-%d %H:%M:%S").decode('utf8')
424
424
425 return ""
425 return ""
426
426
427
427
428 def is_git(repository):
428 def is_git(repository):
429 if hasattr(repository, 'alias'):
429 if hasattr(repository, 'alias'):
430 _type = repository.alias
430 _type = repository.alias
431 elif hasattr(repository, 'repo_type'):
431 elif hasattr(repository, 'repo_type'):
432 _type = repository.repo_type
432 _type = repository.repo_type
433 else:
433 else:
434 _type = repository
434 _type = repository
435 return _type == 'git'
435 return _type == 'git'
436
436
437
437
438 def is_hg(repository):
438 def is_hg(repository):
439 if hasattr(repository, 'alias'):
439 if hasattr(repository, 'alias'):
440 _type = repository.alias
440 _type = repository.alias
441 elif hasattr(repository, 'repo_type'):
441 elif hasattr(repository, 'repo_type'):
442 _type = repository.repo_type
442 _type = repository.repo_type
443 else:
443 else:
444 _type = repository
444 _type = repository
445 return _type == 'hg'
445 return _type == 'hg'
446
446
447
447
448 @cache_region('long_term', 'user_or_none')
448 @cache_region('long_term', 'user_or_none')
449 def user_or_none(author):
449 def user_or_none(author):
450 """Try to match email part of VCS committer string with a local user - or return None"""
450 """Try to match email part of VCS committer string with a local user - or return None"""
451 from kallithea.model.db import User
451 from kallithea.model.db import User
452 email = author_email(author)
452 email = author_email(author)
453 if email:
453 if email:
454 return User.get_by_email(email, cache=True) # cache will only use sql_cache_short
454 return User.get_by_email(email, cache=True) # cache will only use sql_cache_short
455 return None
455 return None
456
456
457 def email_or_none(author):
457 def email_or_none(author):
458 """Try to match email part of VCS committer string with a local user.
458 """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."""
459 Return primary email of user, email part of the specified author name, or None."""
460 if not author:
460 if not author:
461 return None
461 return None
462 user = user_or_none(author)
462 user = user_or_none(author)
463 if user is not None:
463 if user is not None:
464 return user.email # always use main email address - not necessarily the one used to find user
464 return user.email # always use main email address - not necessarily the one used to find user
465
465
466 # extract email from the commit string
466 # extract email from the commit string
467 email = author_email(author)
467 email = author_email(author)
468 if email:
468 if email:
469 return email
469 return email
470
470
471 # No valid email, not a valid user in the system, none!
471 # No valid email, not a valid user in the system, none!
472 return None
472 return None
473
473
474 def person(author, show_attr="username"):
474 def person(author, show_attr="username"):
475 """Find the user identified by 'author', return one of the users attributes,
475 """Find the user identified by 'author', return one of the users attributes,
476 default to the username attribute, None if there is no user"""
476 default to the username attribute, None if there is no user"""
477 from kallithea.model.db import User
477 from kallithea.model.db import User
478 # attr to return from fetched user
478 # attr to return from fetched user
479 person_getter = lambda usr: getattr(usr, show_attr)
479 person_getter = lambda usr: getattr(usr, show_attr)
480
480
481 # if author is already an instance use it for extraction
481 # if author is already an instance use it for extraction
482 if isinstance(author, User):
482 if isinstance(author, User):
483 return person_getter(author)
483 return person_getter(author)
484
484
485 user = user_or_none(author)
485 user = user_or_none(author)
486 if user is not None:
486 if user is not None:
487 return person_getter(user)
487 return person_getter(user)
488
488
489 # Still nothing? Just pass back the author name if any, else the email
489 # Still nothing? Just pass back the author name if any, else the email
490 return author_name(author) or email(author)
490 return author_name(author) or email(author)
491
491
492
492
493 def person_by_id(id_, show_attr="username"):
493 def person_by_id(id_, show_attr="username"):
494 from kallithea.model.db import User
494 from kallithea.model.db import User
495 # attr to return from fetched user
495 # attr to return from fetched user
496 person_getter = lambda usr: getattr(usr, show_attr)
496 person_getter = lambda usr: getattr(usr, show_attr)
497
497
498 #maybe it's an ID ?
498 #maybe it's an ID ?
499 if str(id_).isdigit() or isinstance(id_, int):
499 if str(id_).isdigit() or isinstance(id_, int):
500 id_ = int(id_)
500 id_ = int(id_)
501 user = User.get(id_)
501 user = User.get(id_)
502 if user is not None:
502 if user is not None:
503 return person_getter(user)
503 return person_getter(user)
504 return id_
504 return id_
505
505
506
506
507 def boolicon(value):
507 def boolicon(value):
508 """Returns boolean value of a value, represented as small html image of true/false
508 """Returns boolean value of a value, represented as small html image of true/false
509 icons
509 icons
510
510
511 :param value: value
511 :param value: value
512 """
512 """
513
513
514 if value:
514 if value:
515 return HTML.tag('i', class_="icon-ok")
515 return HTML.tag('i', class_="icon-ok")
516 else:
516 else:
517 return HTML.tag('i', class_="icon-minus-circled")
517 return HTML.tag('i', class_="icon-minus-circled")
518
518
519
519
520 def action_parser(user_log, feed=False, parse_cs=False):
520 def action_parser(user_log, feed=False, parse_cs=False):
521 """
521 """
522 This helper will action_map the specified string action into translated
522 This helper will action_map the specified string action into translated
523 fancy names with icons and links
523 fancy names with icons and links
524
524
525 :param user_log: user log instance
525 :param user_log: user log instance
526 :param feed: use output for feeds (no html and fancy icons)
526 :param feed: use output for feeds (no html and fancy icons)
527 :param parse_cs: parse Changesets into VCS instances
527 :param parse_cs: parse Changesets into VCS instances
528 """
528 """
529
529
530 action = user_log.action
530 action = user_log.action
531 action_params = ' '
531 action_params = ' '
532
532
533 x = action.split(':')
533 x = action.split(':')
534
534
535 if len(x) > 1:
535 if len(x) > 1:
536 action, action_params = x
536 action, action_params = x
537
537
538 def get_cs_links():
538 def get_cs_links():
539 revs_limit = 3 # display this amount always
539 revs_limit = 3 # display this amount always
540 revs_top_limit = 50 # show upto this amount of changesets hidden
540 revs_top_limit = 50 # show upto this amount of changesets hidden
541 revs_ids = action_params.split(',')
541 revs_ids = action_params.split(',')
542 deleted = user_log.repository is None
542 deleted = user_log.repository is None
543 if deleted:
543 if deleted:
544 return ','.join(revs_ids)
544 return ','.join(revs_ids)
545
545
546 repo_name = user_log.repository.repo_name
546 repo_name = user_log.repository.repo_name
547
547
548 def lnk(rev, repo_name):
548 def lnk(rev, repo_name):
549 lazy_cs = False
549 lazy_cs = False
550 title_ = None
550 title_ = None
551 url_ = '#'
551 url_ = '#'
552 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
552 if isinstance(rev, BaseChangeset) or isinstance(rev, AttributeDict):
553 if rev.op and rev.ref_name:
553 if rev.op and rev.ref_name:
554 if rev.op == 'delete_branch':
554 if rev.op == 'delete_branch':
555 lbl = _('Deleted branch: %s') % rev.ref_name
555 lbl = _('Deleted branch: %s') % rev.ref_name
556 elif rev.op == 'tag':
556 elif rev.op == 'tag':
557 lbl = _('Created tag: %s') % rev.ref_name
557 lbl = _('Created tag: %s') % rev.ref_name
558 else:
558 else:
559 lbl = 'Unknown operation %s' % rev.op
559 lbl = 'Unknown operation %s' % rev.op
560 else:
560 else:
561 lazy_cs = True
561 lazy_cs = True
562 lbl = rev.short_id[:8]
562 lbl = rev.short_id[:8]
563 url_ = url('changeset_home', repo_name=repo_name,
563 url_ = url('changeset_home', repo_name=repo_name,
564 revision=rev.raw_id)
564 revision=rev.raw_id)
565 else:
565 else:
566 # changeset cannot be found - it might have been stripped or removed
566 # changeset cannot be found - it might have been stripped or removed
567 lbl = rev[:12]
567 lbl = rev[:12]
568 title_ = _('Changeset %s not found') % lbl
568 title_ = _('Changeset %s not found') % lbl
569 if parse_cs:
569 if parse_cs:
570 return link_to(lbl, url_, title=title_, **{'data-toggle': 'tooltip'})
570 return link_to(lbl, url_, title=title_, **{'data-toggle': 'tooltip'})
571 return link_to(lbl, url_, class_='lazy-cs' if lazy_cs else '',
571 return link_to(lbl, url_, class_='lazy-cs' if lazy_cs else '',
572 **{'data-raw_id':rev.raw_id, 'data-repo_name':repo_name})
572 **{'data-raw_id':rev.raw_id, 'data-repo_name':repo_name})
573
573
574 def _get_op(rev_txt):
574 def _get_op(rev_txt):
575 _op = None
575 _op = None
576 _name = rev_txt
576 _name = rev_txt
577 if len(rev_txt.split('=>')) == 2:
577 if len(rev_txt.split('=>')) == 2:
578 _op, _name = rev_txt.split('=>')
578 _op, _name = rev_txt.split('=>')
579 return _op, _name
579 return _op, _name
580
580
581 revs = []
581 revs = []
582 if len(filter(lambda v: v != '', revs_ids)) > 0:
582 if len(filter(lambda v: v != '', revs_ids)) > 0:
583 repo = None
583 repo = None
584 for rev in revs_ids[:revs_top_limit]:
584 for rev in revs_ids[:revs_top_limit]:
585 _op, _name = _get_op(rev)
585 _op, _name = _get_op(rev)
586
586
587 # we want parsed changesets, or new log store format is bad
587 # we want parsed changesets, or new log store format is bad
588 if parse_cs:
588 if parse_cs:
589 try:
589 try:
590 if repo is None:
590 if repo is None:
591 repo = user_log.repository.scm_instance
591 repo = user_log.repository.scm_instance
592 _rev = repo.get_changeset(rev)
592 _rev = repo.get_changeset(rev)
593 revs.append(_rev)
593 revs.append(_rev)
594 except ChangesetDoesNotExistError:
594 except ChangesetDoesNotExistError:
595 log.error('cannot find revision %s in this repo', rev)
595 log.error('cannot find revision %s in this repo', rev)
596 revs.append(rev)
596 revs.append(rev)
597 else:
597 else:
598 _rev = AttributeDict({
598 _rev = AttributeDict({
599 'short_id': rev[:12],
599 'short_id': rev[:12],
600 'raw_id': rev,
600 'raw_id': rev,
601 'message': '',
601 'message': '',
602 'op': _op,
602 'op': _op,
603 'ref_name': _name
603 'ref_name': _name
604 })
604 })
605 revs.append(_rev)
605 revs.append(_rev)
606 cs_links = [" " + ', '.join(
606 cs_links = [" " + ', '.join(
607 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
607 [lnk(rev, repo_name) for rev in revs[:revs_limit]]
608 )]
608 )]
609 _op1, _name1 = _get_op(revs_ids[0])
609 _op1, _name1 = _get_op(revs_ids[0])
610 _op2, _name2 = _get_op(revs_ids[-1])
610 _op2, _name2 = _get_op(revs_ids[-1])
611
611
612 _rev = '%s...%s' % (_name1, _name2)
612 _rev = '%s...%s' % (_name1, _name2)
613
613
614 compare_view = (
614 compare_view = (
615 ' <div class="compare_view" data-toggle="tooltip" title="%s">'
615 ' <div class="compare_view" data-toggle="tooltip" title="%s">'
616 '<a href="%s">%s</a> </div>' % (
616 '<a href="%s">%s</a> </div>' % (
617 _('Show all combined changesets %s->%s') % (
617 _('Show all combined changesets %s->%s') % (
618 revs_ids[0][:12], revs_ids[-1][:12]
618 revs_ids[0][:12], revs_ids[-1][:12]
619 ),
619 ),
620 url('changeset_home', repo_name=repo_name,
620 url('changeset_home', repo_name=repo_name,
621 revision=_rev
621 revision=_rev
622 ),
622 ),
623 _('Compare view')
623 _('Compare view')
624 )
624 )
625 )
625 )
626
626
627 # if we have exactly one more than normally displayed
627 # if we have exactly one more than normally displayed
628 # just display it, takes less space than displaying
628 # just display it, takes less space than displaying
629 # "and 1 more revisions"
629 # "and 1 more revisions"
630 if len(revs_ids) == revs_limit + 1:
630 if len(revs_ids) == revs_limit + 1:
631 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
631 cs_links.append(", " + lnk(revs[revs_limit], repo_name))
632
632
633 # hidden-by-default ones
633 # hidden-by-default ones
634 if len(revs_ids) > revs_limit + 1:
634 if len(revs_ids) > revs_limit + 1:
635 uniq_id = revs_ids[0]
635 uniq_id = revs_ids[0]
636 html_tmpl = (
636 html_tmpl = (
637 '<span> %s <a class="show_more" id="_%s" '
637 '<span> %s <a class="show_more" id="_%s" '
638 'href="#more">%s</a> %s</span>'
638 'href="#more">%s</a> %s</span>'
639 )
639 )
640 if not feed:
640 if not feed:
641 cs_links.append(html_tmpl % (
641 cs_links.append(html_tmpl % (
642 _('and'),
642 _('and'),
643 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
643 uniq_id, _('%s more') % (len(revs_ids) - revs_limit),
644 _('revisions')
644 _('revisions')
645 )
645 )
646 )
646 )
647
647
648 if not feed:
648 if not feed:
649 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
649 html_tmpl = '<span id="%s" style="display:none">, %s </span>'
650 else:
650 else:
651 html_tmpl = '<span id="%s"> %s </span>'
651 html_tmpl = '<span id="%s"> %s </span>'
652
652
653 morelinks = ', '.join(
653 morelinks = ', '.join(
654 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
654 [lnk(rev, repo_name) for rev in revs[revs_limit:]]
655 )
655 )
656
656
657 if len(revs_ids) > revs_top_limit:
657 if len(revs_ids) > revs_top_limit:
658 morelinks += ', ...'
658 morelinks += ', ...'
659
659
660 cs_links.append(html_tmpl % (uniq_id, morelinks))
660 cs_links.append(html_tmpl % (uniq_id, morelinks))
661 if len(revs) > 1:
661 if len(revs) > 1:
662 cs_links.append(compare_view)
662 cs_links.append(compare_view)
663 return ''.join(cs_links)
663 return ''.join(cs_links)
664
664
665 def get_fork_name():
665 def get_fork_name():
666 repo_name = action_params
666 repo_name = action_params
667 url_ = url('summary_home', repo_name=repo_name)
667 url_ = url('summary_home', repo_name=repo_name)
668 return _('Fork name %s') % link_to(action_params, url_)
668 return _('Fork name %s') % link_to(action_params, url_)
669
669
670 def get_user_name():
670 def get_user_name():
671 user_name = action_params
671 user_name = action_params
672 return user_name
672 return user_name
673
673
674 def get_users_group():
674 def get_users_group():
675 group_name = action_params
675 group_name = action_params
676 return group_name
676 return group_name
677
677
678 def get_pull_request():
678 def get_pull_request():
679 from kallithea.model.db import PullRequest
679 from kallithea.model.db import PullRequest
680 pull_request_id = action_params
680 pull_request_id = action_params
681 nice_id = PullRequest.make_nice_id(pull_request_id)
681 nice_id = PullRequest.make_nice_id(pull_request_id)
682
682
683 deleted = user_log.repository is None
683 deleted = user_log.repository is None
684 if deleted:
684 if deleted:
685 repo_name = user_log.repository_name
685 repo_name = user_log.repository_name
686 else:
686 else:
687 repo_name = user_log.repository.repo_name
687 repo_name = user_log.repository.repo_name
688
688
689 return link_to(_('Pull request %s') % nice_id,
689 return link_to(_('Pull request %s') % nice_id,
690 url('pullrequest_show', repo_name=repo_name,
690 url('pullrequest_show', repo_name=repo_name,
691 pull_request_id=pull_request_id))
691 pull_request_id=pull_request_id))
692
692
693 def get_archive_name():
693 def get_archive_name():
694 archive_name = action_params
694 archive_name = action_params
695 return archive_name
695 return archive_name
696
696
697 # action : translated str, callback(extractor), icon
697 # action : translated str, callback(extractor), icon
698 action_map = {
698 action_map = {
699 'user_deleted_repo': (_('[deleted] repository'),
699 'user_deleted_repo': (_('[deleted] repository'),
700 None, 'icon-trashcan'),
700 None, 'icon-trashcan'),
701 'user_created_repo': (_('[created] repository'),
701 'user_created_repo': (_('[created] repository'),
702 None, 'icon-plus'),
702 None, 'icon-plus'),
703 'user_created_fork': (_('[created] repository as fork'),
703 'user_created_fork': (_('[created] repository as fork'),
704 None, 'icon-fork'),
704 None, 'icon-fork'),
705 'user_forked_repo': (_('[forked] repository'),
705 'user_forked_repo': (_('[forked] repository'),
706 get_fork_name, 'icon-fork'),
706 get_fork_name, 'icon-fork'),
707 'user_updated_repo': (_('[updated] repository'),
707 'user_updated_repo': (_('[updated] repository'),
708 None, 'icon-pencil'),
708 None, 'icon-pencil'),
709 'user_downloaded_archive': (_('[downloaded] archive from repository'),
709 'user_downloaded_archive': (_('[downloaded] archive from repository'),
710 get_archive_name, 'icon-download-cloud'),
710 get_archive_name, 'icon-download-cloud'),
711 'admin_deleted_repo': (_('[delete] repository'),
711 'admin_deleted_repo': (_('[delete] repository'),
712 None, 'icon-trashcan'),
712 None, 'icon-trashcan'),
713 'admin_created_repo': (_('[created] repository'),
713 'admin_created_repo': (_('[created] repository'),
714 None, 'icon-plus'),
714 None, 'icon-plus'),
715 'admin_forked_repo': (_('[forked] repository'),
715 'admin_forked_repo': (_('[forked] repository'),
716 None, 'icon-fork'),
716 None, 'icon-fork'),
717 'admin_updated_repo': (_('[updated] repository'),
717 'admin_updated_repo': (_('[updated] repository'),
718 None, 'icon-pencil'),
718 None, 'icon-pencil'),
719 'admin_created_user': (_('[created] user'),
719 'admin_created_user': (_('[created] user'),
720 get_user_name, 'icon-user'),
720 get_user_name, 'icon-user'),
721 'admin_updated_user': (_('[updated] user'),
721 'admin_updated_user': (_('[updated] user'),
722 get_user_name, 'icon-user'),
722 get_user_name, 'icon-user'),
723 'admin_created_users_group': (_('[created] user group'),
723 'admin_created_users_group': (_('[created] user group'),
724 get_users_group, 'icon-pencil'),
724 get_users_group, 'icon-pencil'),
725 'admin_updated_users_group': (_('[updated] user group'),
725 'admin_updated_users_group': (_('[updated] user group'),
726 get_users_group, 'icon-pencil'),
726 get_users_group, 'icon-pencil'),
727 'user_commented_revision': (_('[commented] on revision in repository'),
727 'user_commented_revision': (_('[commented] on revision in repository'),
728 get_cs_links, 'icon-comment'),
728 get_cs_links, 'icon-comment'),
729 'user_commented_pull_request': (_('[commented] on pull request for'),
729 'user_commented_pull_request': (_('[commented] on pull request for'),
730 get_pull_request, 'icon-comment'),
730 get_pull_request, 'icon-comment'),
731 'user_closed_pull_request': (_('[closed] pull request for'),
731 'user_closed_pull_request': (_('[closed] pull request for'),
732 get_pull_request, 'icon-ok'),
732 get_pull_request, 'icon-ok'),
733 'push': (_('[pushed] into'),
733 'push': (_('[pushed] into'),
734 get_cs_links, 'icon-move-up'),
734 get_cs_links, 'icon-move-up'),
735 'push_local': (_('[committed via Kallithea] into repository'),
735 'push_local': (_('[committed via Kallithea] into repository'),
736 get_cs_links, 'icon-pencil'),
736 get_cs_links, 'icon-pencil'),
737 'push_remote': (_('[pulled from remote] into repository'),
737 'push_remote': (_('[pulled from remote] into repository'),
738 get_cs_links, 'icon-move-up'),
738 get_cs_links, 'icon-move-up'),
739 'pull': (_('[pulled] from'),
739 'pull': (_('[pulled] from'),
740 None, 'icon-move-down'),
740 None, 'icon-move-down'),
741 'started_following_repo': (_('[started following] repository'),
741 'started_following_repo': (_('[started following] repository'),
742 None, 'icon-heart'),
742 None, 'icon-heart'),
743 'stopped_following_repo': (_('[stopped following] repository'),
743 'stopped_following_repo': (_('[stopped following] repository'),
744 None, 'icon-heart-empty'),
744 None, 'icon-heart-empty'),
745 }
745 }
746
746
747 action_str = action_map.get(action, action)
747 action_str = action_map.get(action, action)
748 if feed:
748 if feed:
749 action = action_str[0].replace('[', '').replace(']', '')
749 action = action_str[0].replace('[', '').replace(']', '')
750 else:
750 else:
751 action = action_str[0] \
751 action = action_str[0] \
752 .replace('[', '<b>') \
752 .replace('[', '<b>') \
753 .replace(']', '</b>')
753 .replace(']', '</b>')
754
754
755 action_params_func = lambda: ""
755 action_params_func = lambda: ""
756
756
757 if callable(action_str[1]):
757 if callable(action_str[1]):
758 action_params_func = action_str[1]
758 action_params_func = action_str[1]
759
759
760 def action_parser_icon():
760 def action_parser_icon():
761 action = user_log.action
761 action = user_log.action
762 action_params = None
762 action_params = None
763 x = action.split(':')
763 x = action.split(':')
764
764
765 if len(x) > 1:
765 if len(x) > 1:
766 action, action_params = x
766 action, action_params = x
767
767
768 ico = action_map.get(action, ['', '', ''])[2]
768 ico = action_map.get(action, ['', '', ''])[2]
769 html = """<i class="%s"></i>""" % ico
769 html = """<i class="%s"></i>""" % ico
770 return literal(html)
770 return literal(html)
771
771
772 # returned callbacks we need to call to get
772 # returned callbacks we need to call to get
773 return [lambda: literal(action), action_params_func, action_parser_icon]
773 return [lambda: literal(action), action_params_func, action_parser_icon]
774
774
775
775
776
776
777 #==============================================================================
777 #==============================================================================
778 # PERMS
778 # PERMS
779 #==============================================================================
779 #==============================================================================
780 from kallithea.lib.auth import HasPermissionAny, \
780 from kallithea.lib.auth import HasPermissionAny, \
781 HasRepoPermissionAny, HasRepoGroupPermissionAny
781 HasRepoPermissionLevel, HasRepoGroupPermissionAny
782
782
783
783
784 #==============================================================================
784 #==============================================================================
785 # GRAVATAR URL
785 # GRAVATAR URL
786 #==============================================================================
786 #==============================================================================
787 def gravatar_div(email_address, cls='', size=30, **div_attributes):
787 def gravatar_div(email_address, cls='', size=30, **div_attributes):
788 """Return an html literal with a div around a gravatar if they are enabled.
788 """Return an html literal with a div around a gravatar if they are enabled.
789 Extra keyword parameters starting with 'div_' will get the prefix removed
789 Extra keyword parameters starting with 'div_' will get the prefix removed
790 and '_' changed to '-' and be used as attributes on the div. The default
790 and '_' changed to '-' and be used as attributes on the div. The default
791 class is 'gravatar'.
791 class is 'gravatar'.
792 """
792 """
793 from pylons import tmpl_context as c
793 from pylons import tmpl_context as c
794 if not c.visual.use_gravatar:
794 if not c.visual.use_gravatar:
795 return ''
795 return ''
796 if 'div_class' not in div_attributes:
796 if 'div_class' not in div_attributes:
797 div_attributes['div_class'] = "gravatar"
797 div_attributes['div_class'] = "gravatar"
798 attributes = []
798 attributes = []
799 for k, v in sorted(div_attributes.items()):
799 for k, v in sorted(div_attributes.items()):
800 assert k.startswith('div_'), k
800 assert k.startswith('div_'), k
801 attributes.append(' %s="%s"' % (k[4:].replace('_', '-'), escape(v)))
801 attributes.append(' %s="%s"' % (k[4:].replace('_', '-'), escape(v)))
802 return literal("""<div%s>%s</div>""" %
802 return literal("""<div%s>%s</div>""" %
803 (''.join(attributes),
803 (''.join(attributes),
804 gravatar(email_address, cls=cls, size=size)))
804 gravatar(email_address, cls=cls, size=size)))
805
805
806 def gravatar(email_address, cls='', size=30):
806 def gravatar(email_address, cls='', size=30):
807 """return html element of the gravatar
807 """return html element of the gravatar
808
808
809 This method will return an <img> with the resolution double the size (for
809 This method will return an <img> with the resolution double the size (for
810 retina screens) of the image. If the url returned from gravatar_url is
810 retina screens) of the image. If the url returned from gravatar_url is
811 empty then we fallback to using an icon.
811 empty then we fallback to using an icon.
812
812
813 """
813 """
814 from pylons import tmpl_context as c
814 from pylons import tmpl_context as c
815 if not c.visual.use_gravatar:
815 if not c.visual.use_gravatar:
816 return ''
816 return ''
817
817
818 src = gravatar_url(email_address, size * 2)
818 src = gravatar_url(email_address, size * 2)
819
819
820 if src:
820 if src:
821 # here it makes sense to use style="width: ..." (instead of, say, a
821 # here it makes sense to use style="width: ..." (instead of, say, a
822 # stylesheet) because we using this to generate a high-res (retina) size
822 # stylesheet) because we using this to generate a high-res (retina) size
823 html = ('<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
823 html = ('<img alt="" class="{cls}" style="width: {size}px; height: {size}px" src="{src}"/>'
824 .format(cls=cls, size=size, src=src))
824 .format(cls=cls, size=size, src=src))
825
825
826 else:
826 else:
827 # if src is empty then there was no gravatar, so we use a font icon
827 # if src is empty then there was no gravatar, so we use a font icon
828 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
828 html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
829 .format(cls=cls, size=size, src=src))
829 .format(cls=cls, size=size, src=src))
830
830
831 return literal(html)
831 return literal(html)
832
832
833 def gravatar_url(email_address, size=30, default=''):
833 def gravatar_url(email_address, size=30, default=''):
834 # doh, we need to re-import those to mock it later
834 # doh, we need to re-import those to mock it later
835 from kallithea.config.routing import url
835 from kallithea.config.routing import url
836 from kallithea.model.db import User
836 from kallithea.model.db import User
837 from pylons import tmpl_context as c
837 from pylons import tmpl_context as c
838 if not c.visual.use_gravatar:
838 if not c.visual.use_gravatar:
839 return ""
839 return ""
840
840
841 _def = 'anonymous@kallithea-scm.org' # default gravatar
841 _def = 'anonymous@kallithea-scm.org' # default gravatar
842 email_address = email_address or _def
842 email_address = email_address or _def
843
843
844 if email_address == _def:
844 if email_address == _def:
845 return default
845 return default
846
846
847 parsed_url = urlparse.urlparse(url.current(qualified=True))
847 parsed_url = urlparse.urlparse(url.current(qualified=True))
848 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
848 url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL ) \
849 .replace('{email}', email_address) \
849 .replace('{email}', email_address) \
850 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
850 .replace('{md5email}', hashlib.md5(safe_str(email_address).lower()).hexdigest()) \
851 .replace('{netloc}', parsed_url.netloc) \
851 .replace('{netloc}', parsed_url.netloc) \
852 .replace('{scheme}', parsed_url.scheme) \
852 .replace('{scheme}', parsed_url.scheme) \
853 .replace('{size}', safe_str(size))
853 .replace('{size}', safe_str(size))
854 return url
854 return url
855
855
856
856
857 def changed_tooltip(nodes):
857 def changed_tooltip(nodes):
858 """
858 """
859 Generates a html string for changed nodes in changeset page.
859 Generates a html string for changed nodes in changeset page.
860 It limits the output to 30 entries
860 It limits the output to 30 entries
861
861
862 :param nodes: LazyNodesGenerator
862 :param nodes: LazyNodesGenerator
863 """
863 """
864 if nodes:
864 if nodes:
865 pref = ': <br/> '
865 pref = ': <br/> '
866 suf = ''
866 suf = ''
867 if len(nodes) > 30:
867 if len(nodes) > 30:
868 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
868 suf = '<br/>' + _(' and %s more') % (len(nodes) - 30)
869 return literal(pref + '<br/> '.join([safe_unicode(x.path)
869 return literal(pref + '<br/> '.join([safe_unicode(x.path)
870 for x in nodes[:30]]) + suf)
870 for x in nodes[:30]]) + suf)
871 else:
871 else:
872 return ': ' + _('No files')
872 return ': ' + _('No files')
873
873
874
874
875 def fancy_file_stats(stats):
875 def fancy_file_stats(stats):
876 """
876 """
877 Displays a fancy two colored bar for number of added/deleted
877 Displays a fancy two colored bar for number of added/deleted
878 lines of code on file
878 lines of code on file
879
879
880 :param stats: two element list of added/deleted lines of code
880 :param stats: two element list of added/deleted lines of code
881 """
881 """
882 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
882 from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
883 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
883 MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
884
884
885 a, d = stats['added'], stats['deleted']
885 a, d = stats['added'], stats['deleted']
886 width = 100
886 width = 100
887
887
888 if stats['binary']:
888 if stats['binary']:
889 #binary mode
889 #binary mode
890 lbl = ''
890 lbl = ''
891 bin_op = 1
891 bin_op = 1
892
892
893 if BIN_FILENODE in stats['ops']:
893 if BIN_FILENODE in stats['ops']:
894 lbl = 'bin+'
894 lbl = 'bin+'
895
895
896 if NEW_FILENODE in stats['ops']:
896 if NEW_FILENODE in stats['ops']:
897 lbl += _('new file')
897 lbl += _('new file')
898 bin_op = NEW_FILENODE
898 bin_op = NEW_FILENODE
899 elif MOD_FILENODE in stats['ops']:
899 elif MOD_FILENODE in stats['ops']:
900 lbl += _('mod')
900 lbl += _('mod')
901 bin_op = MOD_FILENODE
901 bin_op = MOD_FILENODE
902 elif DEL_FILENODE in stats['ops']:
902 elif DEL_FILENODE in stats['ops']:
903 lbl += _('del')
903 lbl += _('del')
904 bin_op = DEL_FILENODE
904 bin_op = DEL_FILENODE
905 elif RENAMED_FILENODE in stats['ops']:
905 elif RENAMED_FILENODE in stats['ops']:
906 lbl += _('rename')
906 lbl += _('rename')
907 bin_op = RENAMED_FILENODE
907 bin_op = RENAMED_FILENODE
908
908
909 #chmod can go with other operations
909 #chmod can go with other operations
910 if CHMOD_FILENODE in stats['ops']:
910 if CHMOD_FILENODE in stats['ops']:
911 _org_lbl = _('chmod')
911 _org_lbl = _('chmod')
912 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
912 lbl += _org_lbl if lbl.endswith('+') else '+%s' % _org_lbl
913
913
914 #import ipdb;ipdb.set_trace()
914 #import ipdb;ipdb.set_trace()
915 b_d = '<div class="bin bin%s progress-bar" style="width:100%%">%s</div>' % (bin_op, lbl)
915 b_d = '<div class="bin bin%s progress-bar" style="width:100%%">%s</div>' % (bin_op, lbl)
916 b_a = '<div class="bin bin1" style="width:0%"></div>'
916 b_a = '<div class="bin bin1" style="width:0%"></div>'
917 return literal('<div style="width:%spx" class="progress">%s%s</div>' % (width, b_a, b_d))
917 return literal('<div style="width:%spx" class="progress">%s%s</div>' % (width, b_a, b_d))
918
918
919 t = stats['added'] + stats['deleted']
919 t = stats['added'] + stats['deleted']
920 unit = float(width) / (t or 1)
920 unit = float(width) / (t or 1)
921
921
922 # needs > 9% of width to be visible or 0 to be hidden
922 # needs > 9% of width to be visible or 0 to be hidden
923 a_p = max(9, unit * a) if a > 0 else 0
923 a_p = max(9, unit * a) if a > 0 else 0
924 d_p = max(9, unit * d) if d > 0 else 0
924 d_p = max(9, unit * d) if d > 0 else 0
925 p_sum = a_p + d_p
925 p_sum = a_p + d_p
926
926
927 if p_sum > width:
927 if p_sum > width:
928 #adjust the percentage to be == 100% since we adjusted to 9
928 #adjust the percentage to be == 100% since we adjusted to 9
929 if a_p > d_p:
929 if a_p > d_p:
930 a_p = a_p - (p_sum - width)
930 a_p = a_p - (p_sum - width)
931 else:
931 else:
932 d_p = d_p - (p_sum - width)
932 d_p = d_p - (p_sum - width)
933
933
934 a_v = a if a > 0 else ''
934 a_v = a if a > 0 else ''
935 d_v = d if d > 0 else ''
935 d_v = d if d > 0 else ''
936
936
937 d_a = '<div class="added progress-bar" style="width:%s%%">%s</div>' % (
937 d_a = '<div class="added progress-bar" style="width:%s%%">%s</div>' % (
938 a_p, a_v
938 a_p, a_v
939 )
939 )
940 d_d = '<div class="deleted progress-bar" style="width:%s%%">%s</div>' % (
940 d_d = '<div class="deleted progress-bar" style="width:%s%%">%s</div>' % (
941 d_p, d_v
941 d_p, d_v
942 )
942 )
943 return literal('<div class="pull-right progress" style="width:%spx">%s%s</div>' % (width, d_a, d_d))
943 return literal('<div class="pull-right progress" style="width:%spx">%s%s</div>' % (width, d_a, d_d))
944
944
945
945
946 _URLIFY_RE = re.compile(r'''
946 _URLIFY_RE = re.compile(r'''
947 # URL markup
947 # URL markup
948 (?P<url>%s) |
948 (?P<url>%s) |
949 # @mention markup
949 # @mention markup
950 (?P<mention>%s) |
950 (?P<mention>%s) |
951 # Changeset hash markup
951 # Changeset hash markup
952 (?<!\w|[-_])
952 (?<!\w|[-_])
953 (?P<hash>[0-9a-f]{12,40})
953 (?P<hash>[0-9a-f]{12,40})
954 (?!\w|[-_]) |
954 (?!\w|[-_]) |
955 # Markup of *bold text*
955 # Markup of *bold text*
956 (?:
956 (?:
957 (?:^|(?<=\s))
957 (?:^|(?<=\s))
958 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
958 (?P<bold> [*] (?!\s) [^*\n]* (?<!\s) [*] )
959 (?![*\w])
959 (?![*\w])
960 ) |
960 ) |
961 # "Stylize" markup
961 # "Stylize" markup
962 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
962 \[see\ \=&gt;\ *(?P<seen>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
963 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
963 \[license\ \=&gt;\ *(?P<license>[a-zA-Z0-9\/\=\?\&\ \:\/\.\-]*)\] |
964 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
964 \[(?P<tagtype>requires|recommends|conflicts|base)\ \=&gt;\ *(?P<tagvalue>[a-zA-Z0-9\-\/]*)\] |
965 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
965 \[(?:lang|language)\ \=&gt;\ *(?P<lang>[a-zA-Z\-\/\#\+]*)\] |
966 \[(?P<tag>[a-z]+)\]
966 \[(?P<tag>[a-z]+)\]
967 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
967 ''' % (url_re.pattern, MENTIONS_REGEX.pattern),
968 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
968 re.VERBOSE | re.MULTILINE | re.IGNORECASE)
969
969
970
970
971
971
972 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
972 def urlify_text(s, repo_name=None, link_=None, truncate=None, stylize=False, truncatef=truncate):
973 """
973 """
974 Parses given text message and make literal html with markup.
974 Parses given text message and make literal html with markup.
975 The text will be truncated to the specified length.
975 The text will be truncated to the specified length.
976 Hashes are turned into changeset links to specified repository.
976 Hashes are turned into changeset links to specified repository.
977 URLs links to what they say.
977 URLs links to what they say.
978 Issues are linked to given issue-server.
978 Issues are linked to given issue-server.
979 If link_ is provided, all text not already linking somewhere will link there.
979 If link_ is provided, all text not already linking somewhere will link there.
980 """
980 """
981
981
982 def _replace(match_obj):
982 def _replace(match_obj):
983 url = match_obj.group('url')
983 url = match_obj.group('url')
984 if url is not None:
984 if url is not None:
985 return '<a href="%(url)s">%(url)s</a>' % {'url': url}
985 return '<a href="%(url)s">%(url)s</a>' % {'url': url}
986 mention = match_obj.group('mention')
986 mention = match_obj.group('mention')
987 if mention is not None:
987 if mention is not None:
988 return '<b>%s</b>' % mention
988 return '<b>%s</b>' % mention
989 hash_ = match_obj.group('hash')
989 hash_ = match_obj.group('hash')
990 if hash_ is not None and repo_name is not None:
990 if hash_ is not None and repo_name is not None:
991 from kallithea.config.routing import url # doh, we need to re-import url to mock it later
991 from kallithea.config.routing import url # doh, we need to re-import url to mock it later
992 return '<a class="revision-link" href="%(url)s">%(hash)s</a>' % {
992 return '<a class="revision-link" href="%(url)s">%(hash)s</a>' % {
993 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
993 'url': url('changeset_home', repo_name=repo_name, revision=hash_),
994 'hash': hash_,
994 'hash': hash_,
995 }
995 }
996 bold = match_obj.group('bold')
996 bold = match_obj.group('bold')
997 if bold is not None:
997 if bold is not None:
998 return '<b>*%s*</b>' % _urlify(bold[1:-1])
998 return '<b>*%s*</b>' % _urlify(bold[1:-1])
999 if stylize:
999 if stylize:
1000 seen = match_obj.group('seen')
1000 seen = match_obj.group('seen')
1001 if seen:
1001 if seen:
1002 return '<div class="metatag" data-tag="see">see =&gt; %s</div>' % seen
1002 return '<div class="metatag" data-tag="see">see =&gt; %s</div>' % seen
1003 license = match_obj.group('license')
1003 license = match_obj.group('license')
1004 if license:
1004 if license:
1005 return '<div class="metatag" data-tag="license"><a href="http:\/\/www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
1005 return '<div class="metatag" data-tag="license"><a href="http:\/\/www.opensource.org/licenses/%s">%s</a></div>' % (license, license)
1006 tagtype = match_obj.group('tagtype')
1006 tagtype = match_obj.group('tagtype')
1007 if tagtype:
1007 if tagtype:
1008 tagvalue = match_obj.group('tagvalue')
1008 tagvalue = match_obj.group('tagvalue')
1009 return '<div class="metatag" data-tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
1009 return '<div class="metatag" data-tag="%s">%s =&gt; <a href="/%s">%s</a></div>' % (tagtype, tagtype, tagvalue, tagvalue)
1010 lang = match_obj.group('lang')
1010 lang = match_obj.group('lang')
1011 if lang:
1011 if lang:
1012 return '<div class="metatag" data-tag="lang">%s</div>' % lang
1012 return '<div class="metatag" data-tag="lang">%s</div>' % lang
1013 tag = match_obj.group('tag')
1013 tag = match_obj.group('tag')
1014 if tag:
1014 if tag:
1015 return '<div class="metatag" data-tag="%s">%s</div>' % (tag, tag)
1015 return '<div class="metatag" data-tag="%s">%s</div>' % (tag, tag)
1016 return match_obj.group(0)
1016 return match_obj.group(0)
1017
1017
1018 def _urlify(s):
1018 def _urlify(s):
1019 """
1019 """
1020 Extract urls from text and make html links out of them
1020 Extract urls from text and make html links out of them
1021 """
1021 """
1022 return _URLIFY_RE.sub(_replace, s)
1022 return _URLIFY_RE.sub(_replace, s)
1023
1023
1024 if truncate is None:
1024 if truncate is None:
1025 s = s.rstrip()
1025 s = s.rstrip()
1026 else:
1026 else:
1027 s = truncatef(s, truncate, whole_word=True)
1027 s = truncatef(s, truncate, whole_word=True)
1028 s = html_escape(s)
1028 s = html_escape(s)
1029 s = _urlify(s)
1029 s = _urlify(s)
1030 if repo_name is not None:
1030 if repo_name is not None:
1031 s = urlify_issues(s, repo_name)
1031 s = urlify_issues(s, repo_name)
1032 if link_ is not None:
1032 if link_ is not None:
1033 # make href around everything that isn't a href already
1033 # make href around everything that isn't a href already
1034 s = linkify_others(s, link_)
1034 s = linkify_others(s, link_)
1035 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1035 s = s.replace('\r\n', '<br/>').replace('\n', '<br/>')
1036 return literal(s)
1036 return literal(s)
1037
1037
1038
1038
1039 def linkify_others(t, l):
1039 def linkify_others(t, l):
1040 """Add a default link to html with links.
1040 """Add a default link to html with links.
1041 HTML doesn't allow nesting of links, so the outer link must be broken up
1041 HTML doesn't allow nesting of links, so the outer link must be broken up
1042 in pieces and give space for other links.
1042 in pieces and give space for other links.
1043 """
1043 """
1044 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1044 urls = re.compile(r'(\<a.*?\<\/a\>)',)
1045 links = []
1045 links = []
1046 for e in urls.split(t):
1046 for e in urls.split(t):
1047 if e.strip() and not urls.match(e):
1047 if e.strip() and not urls.match(e):
1048 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1048 links.append('<a class="message-link" href="%s">%s</a>' % (l, e))
1049 else:
1049 else:
1050 links.append(e)
1050 links.append(e)
1051
1051
1052 return ''.join(links)
1052 return ''.join(links)
1053
1053
1054
1054
1055 # Global variable that will hold the actual urlify_issues function body.
1055 # Global variable that will hold the actual urlify_issues function body.
1056 # Will be set on first use when the global configuration has been read.
1056 # Will be set on first use when the global configuration has been read.
1057 _urlify_issues_f = None
1057 _urlify_issues_f = None
1058
1058
1059
1059
1060 def urlify_issues(newtext, repo_name):
1060 def urlify_issues(newtext, repo_name):
1061 """Urlify issue references according to .ini configuration"""
1061 """Urlify issue references according to .ini configuration"""
1062 global _urlify_issues_f
1062 global _urlify_issues_f
1063 if _urlify_issues_f is None:
1063 if _urlify_issues_f is None:
1064 from kallithea import CONFIG
1064 from kallithea import CONFIG
1065 from kallithea.model.db import URL_SEP
1065 from kallithea.model.db import URL_SEP
1066 assert CONFIG['sqlalchemy.url'] # make sure config has been loaded
1066 assert CONFIG['sqlalchemy.url'] # make sure config has been loaded
1067
1067
1068 # Build chain of urlify functions, starting with not doing any transformation
1068 # Build chain of urlify functions, starting with not doing any transformation
1069 tmp_urlify_issues_f = lambda s: s
1069 tmp_urlify_issues_f = lambda s: s
1070
1070
1071 issue_pat_re = re.compile(r'issue_pat(.*)')
1071 issue_pat_re = re.compile(r'issue_pat(.*)')
1072 for k in CONFIG.keys():
1072 for k in CONFIG.keys():
1073 # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
1073 # Find all issue_pat* settings that also have corresponding server_link and prefix configuration
1074 m = issue_pat_re.match(k)
1074 m = issue_pat_re.match(k)
1075 if m is None:
1075 if m is None:
1076 continue
1076 continue
1077 suffix = m.group(1)
1077 suffix = m.group(1)
1078 issue_pat = CONFIG.get(k)
1078 issue_pat = CONFIG.get(k)
1079 issue_server_link = CONFIG.get('issue_server_link%s' % suffix)
1079 issue_server_link = CONFIG.get('issue_server_link%s' % suffix)
1080 issue_prefix = CONFIG.get('issue_prefix%s' % suffix)
1080 issue_prefix = CONFIG.get('issue_prefix%s' % suffix)
1081 if issue_pat and issue_server_link and issue_prefix:
1081 if issue_pat and issue_server_link and issue_prefix:
1082 log.debug('issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_prefix)
1082 log.debug('issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_prefix)
1083 else:
1083 else:
1084 log.error('skipping incomplete issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_prefix)
1084 log.error('skipping incomplete issue pattern %r: %r -> %r %r', suffix, issue_pat, issue_server_link, issue_prefix)
1085 continue
1085 continue
1086
1086
1087 # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
1087 # Wrap tmp_urlify_issues_f with substitution of this pattern, while making sure all loop variables (and compiled regexpes) are bound
1088 issue_re = re.compile(issue_pat)
1088 issue_re = re.compile(issue_pat)
1089 def issues_replace(match_obj,
1089 def issues_replace(match_obj,
1090 issue_server_link=issue_server_link, issue_prefix=issue_prefix):
1090 issue_server_link=issue_server_link, issue_prefix=issue_prefix):
1091 leadingspace = ' ' if match_obj.group().startswith(' ') else ''
1091 leadingspace = ' ' if match_obj.group().startswith(' ') else ''
1092 issue_id = ''.join(match_obj.groups())
1092 issue_id = ''.join(match_obj.groups())
1093 issue_url = issue_server_link.replace('{id}', issue_id)
1093 issue_url = issue_server_link.replace('{id}', issue_id)
1094 issue_url = issue_url.replace('{repo}', repo_name)
1094 issue_url = issue_url.replace('{repo}', repo_name)
1095 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1095 issue_url = issue_url.replace('{repo_name}', repo_name.split(URL_SEP)[-1])
1096 return (
1096 return (
1097 '%(leadingspace)s<a class="issue-tracker-link" href="%(url)s">'
1097 '%(leadingspace)s<a class="issue-tracker-link" href="%(url)s">'
1098 '%(issue-prefix)s%(id-repr)s'
1098 '%(issue-prefix)s%(id-repr)s'
1099 '</a>'
1099 '</a>'
1100 ) % {
1100 ) % {
1101 'leadingspace': leadingspace,
1101 'leadingspace': leadingspace,
1102 'url': issue_url,
1102 'url': issue_url,
1103 'id-repr': issue_id,
1103 'id-repr': issue_id,
1104 'issue-prefix': issue_prefix,
1104 'issue-prefix': issue_prefix,
1105 'serv': issue_server_link,
1105 'serv': issue_server_link,
1106 }
1106 }
1107 tmp_urlify_issues_f = (lambda s,
1107 tmp_urlify_issues_f = (lambda s,
1108 issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f:
1108 issue_re=issue_re, issues_replace=issues_replace, chain_f=tmp_urlify_issues_f:
1109 issue_re.sub(issues_replace, chain_f(s)))
1109 issue_re.sub(issues_replace, chain_f(s)))
1110
1110
1111 # Set tmp function globally - atomically
1111 # Set tmp function globally - atomically
1112 _urlify_issues_f = tmp_urlify_issues_f
1112 _urlify_issues_f = tmp_urlify_issues_f
1113
1113
1114 return _urlify_issues_f(newtext)
1114 return _urlify_issues_f(newtext)
1115
1115
1116
1116
1117 def render_w_mentions(source, repo_name=None):
1117 def render_w_mentions(source, repo_name=None):
1118 """
1118 """
1119 Render plain text with revision hashes and issue references urlified
1119 Render plain text with revision hashes and issue references urlified
1120 and with @mention highlighting.
1120 and with @mention highlighting.
1121 """
1121 """
1122 s = safe_unicode(source)
1122 s = safe_unicode(source)
1123 s = urlify_text(s, repo_name=repo_name)
1123 s = urlify_text(s, repo_name=repo_name)
1124 return literal('<div class="formatted-fixed">%s</div>' % s)
1124 return literal('<div class="formatted-fixed">%s</div>' % s)
1125
1125
1126
1126
1127 def short_ref(ref_type, ref_name):
1127 def short_ref(ref_type, ref_name):
1128 if ref_type == 'rev':
1128 if ref_type == 'rev':
1129 return short_id(ref_name)
1129 return short_id(ref_name)
1130 return ref_name
1130 return ref_name
1131
1131
1132 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1132 def link_to_ref(repo_name, ref_type, ref_name, rev=None):
1133 """
1133 """
1134 Return full markup for a href to changeset_home for a changeset.
1134 Return full markup for a href to changeset_home for a changeset.
1135 If ref_type is branch it will link to changelog.
1135 If ref_type is branch it will link to changelog.
1136 ref_name is shortened if ref_type is 'rev'.
1136 ref_name is shortened if ref_type is 'rev'.
1137 if rev is specified show it too, explicitly linking to that revision.
1137 if rev is specified show it too, explicitly linking to that revision.
1138 """
1138 """
1139 txt = short_ref(ref_type, ref_name)
1139 txt = short_ref(ref_type, ref_name)
1140 if ref_type == 'branch':
1140 if ref_type == 'branch':
1141 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1141 u = url('changelog_home', repo_name=repo_name, branch=ref_name)
1142 else:
1142 else:
1143 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1143 u = url('changeset_home', repo_name=repo_name, revision=ref_name)
1144 l = link_to(repo_name + '#' + txt, u)
1144 l = link_to(repo_name + '#' + txt, u)
1145 if rev and ref_type != 'rev':
1145 if rev and ref_type != 'rev':
1146 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1146 l = literal('%s (%s)' % (l, link_to(short_id(rev), url('changeset_home', repo_name=repo_name, revision=rev))))
1147 return l
1147 return l
1148
1148
1149 def changeset_status(repo, revision):
1149 def changeset_status(repo, revision):
1150 from kallithea.model.changeset_status import ChangesetStatusModel
1150 from kallithea.model.changeset_status import ChangesetStatusModel
1151 return ChangesetStatusModel().get_status(repo, revision)
1151 return ChangesetStatusModel().get_status(repo, revision)
1152
1152
1153
1153
1154 def changeset_status_lbl(changeset_status):
1154 def changeset_status_lbl(changeset_status):
1155 from kallithea.model.db import ChangesetStatus
1155 from kallithea.model.db import ChangesetStatus
1156 return ChangesetStatus.get_status_lbl(changeset_status)
1156 return ChangesetStatus.get_status_lbl(changeset_status)
1157
1157
1158
1158
1159 def get_permission_name(key):
1159 def get_permission_name(key):
1160 from kallithea.model.db import Permission
1160 from kallithea.model.db import Permission
1161 return dict(Permission.PERMS).get(key)
1161 return dict(Permission.PERMS).get(key)
1162
1162
1163
1163
1164 def journal_filter_help():
1164 def journal_filter_help():
1165 return _(textwrap.dedent('''
1165 return _(textwrap.dedent('''
1166 Example filter terms:
1166 Example filter terms:
1167 repository:vcs
1167 repository:vcs
1168 username:developer
1168 username:developer
1169 action:*push*
1169 action:*push*
1170 ip:127.0.0.1
1170 ip:127.0.0.1
1171 date:20120101
1171 date:20120101
1172 date:[20120101100000 TO 20120102]
1172 date:[20120101100000 TO 20120102]
1173
1173
1174 Generate wildcards using '*' character:
1174 Generate wildcards using '*' character:
1175 "repository:vcs*" - search everything starting with 'vcs'
1175 "repository:vcs*" - search everything starting with 'vcs'
1176 "repository:*vcs*" - search for repository containing 'vcs'
1176 "repository:*vcs*" - search for repository containing 'vcs'
1177
1177
1178 Optional AND / OR operators in queries
1178 Optional AND / OR operators in queries
1179 "repository:vcs OR repository:test"
1179 "repository:vcs OR repository:test"
1180 "username:test AND repository:test*"
1180 "username:test AND repository:test*"
1181 '''))
1181 '''))
1182
1182
1183
1183
1184 def not_mapped_error(repo_name):
1184 def not_mapped_error(repo_name):
1185 flash(_('%s repository is not mapped to db perhaps'
1185 flash(_('%s repository is not mapped to db perhaps'
1186 ' it was created or renamed from the filesystem'
1186 ' it was created or renamed from the filesystem'
1187 ' please run the application again'
1187 ' please run the application again'
1188 ' in order to rescan repositories') % repo_name, category='error')
1188 ' in order to rescan repositories') % repo_name, category='error')
1189
1189
1190
1190
1191 def ip_range(ip_addr):
1191 def ip_range(ip_addr):
1192 from kallithea.model.db import UserIpMap
1192 from kallithea.model.db import UserIpMap
1193 s, e = UserIpMap._get_ip_range(ip_addr)
1193 s, e = UserIpMap._get_ip_range(ip_addr)
1194 return '%s - %s' % (s, e)
1194 return '%s - %s' % (s, e)
1195
1195
1196
1196
1197 def form(url, method="post", **attrs):
1197 def form(url, method="post", **attrs):
1198 """Like webhelpers.html.tags.form but automatically using secure_form with
1198 """Like webhelpers.html.tags.form but automatically using secure_form with
1199 authentication_token for POST. authentication_token is thus never leaked
1199 authentication_token for POST. authentication_token is thus never leaked
1200 in the URL."""
1200 in the URL."""
1201 if method.lower() == 'get':
1201 if method.lower() == 'get':
1202 return insecure_form(url, method=method, **attrs)
1202 return insecure_form(url, method=method, **attrs)
1203 # webhelpers will turn everything but GET into POST
1203 # webhelpers will turn everything but GET into POST
1204 return secure_form(url, method=method, **attrs)
1204 return secure_form(url, method=method, **attrs)
@@ -1,759 +1,756 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.repo
15 kallithea.model.repo
16 ~~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~~
17
17
18 Repository model for kallithea
18 Repository 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: Jun 5, 2010
22 :created_on: Jun 5, 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 os
29 import os
30 import shutil
30 import shutil
31 import logging
31 import logging
32 import traceback
32 import traceback
33 from datetime import datetime
33 from datetime import datetime
34 from sqlalchemy.orm import subqueryload
34 from sqlalchemy.orm import subqueryload
35
35
36 from kallithea.lib.utils import make_ui
36 from kallithea.lib.utils import make_ui
37 from kallithea.lib.vcs.backends import get_backend
37 from kallithea.lib.vcs.backends import get_backend
38 from kallithea.lib.compat import json
38 from kallithea.lib.compat import json
39 from kallithea.lib.utils2 import LazyProperty, safe_str, safe_unicode, \
39 from kallithea.lib.utils2 import LazyProperty, safe_str, safe_unicode, \
40 remove_prefix, obfuscate_url_pw, get_current_authuser
40 remove_prefix, obfuscate_url_pw, get_current_authuser
41 from kallithea.lib.caching_query import FromCache
41 from kallithea.lib.caching_query import FromCache
42 from kallithea.lib.hooks import log_delete_repository
42 from kallithea.lib.hooks import log_delete_repository
43
43
44 from kallithea.model.base import BaseModel
44 from kallithea.model.base import BaseModel
45 from kallithea.model.db import Repository, UserRepoToPerm, UserGroupRepoToPerm, \
45 from kallithea.model.db import Repository, UserRepoToPerm, UserGroupRepoToPerm, \
46 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission, \
46 UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission, \
47 Statistics, UserGroup, Ui, RepoGroup, RepositoryField
47 Statistics, UserGroup, Ui, RepoGroup, RepositoryField
48
48
49 from kallithea.lib import helpers as h
49 from kallithea.lib import helpers as h
50 from kallithea.lib.auth import HasRepoPermissionAny, HasUserGroupPermissionAny
50 from kallithea.lib.auth import HasRepoPermissionLevel, HasUserGroupPermissionAny
51 from kallithea.lib.exceptions import AttachedForksError
51 from kallithea.lib.exceptions import AttachedForksError
52 from kallithea.model.scm import UserGroupList
52 from kallithea.model.scm import UserGroupList
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class RepoModel(BaseModel):
57 class RepoModel(BaseModel):
58
58
59 URL_SEPARATOR = Repository.url_sep()
59 URL_SEPARATOR = Repository.url_sep()
60
60
61 def _create_default_perms(self, repository, private):
61 def _create_default_perms(self, repository, private):
62 # create default permission
62 # create default permission
63 default = 'repository.read'
63 default = 'repository.read'
64 def_user = User.get_default_user()
64 def_user = User.get_default_user()
65 for p in def_user.user_perms:
65 for p in def_user.user_perms:
66 if p.permission.permission_name.startswith('repository.'):
66 if p.permission.permission_name.startswith('repository.'):
67 default = p.permission.permission_name
67 default = p.permission.permission_name
68 break
68 break
69
69
70 default_perm = 'repository.none' if private else default
70 default_perm = 'repository.none' if private else default
71
71
72 repo_to_perm = UserRepoToPerm()
72 repo_to_perm = UserRepoToPerm()
73 repo_to_perm.permission = Permission.get_by_key(default_perm)
73 repo_to_perm.permission = Permission.get_by_key(default_perm)
74
74
75 repo_to_perm.repository = repository
75 repo_to_perm.repository = repository
76 repo_to_perm.user_id = def_user.user_id
76 repo_to_perm.user_id = def_user.user_id
77
77
78 return repo_to_perm
78 return repo_to_perm
79
79
80 @LazyProperty
80 @LazyProperty
81 def repos_path(self):
81 def repos_path(self):
82 """
82 """
83 Gets the repositories root path from database
83 Gets the repositories root path from database
84 """
84 """
85
85
86 q = self.sa.query(Ui).filter(Ui.ui_key == '/').one()
86 q = self.sa.query(Ui).filter(Ui.ui_key == '/').one()
87 return q.ui_value
87 return q.ui_value
88
88
89 def get(self, repo_id, cache=False):
89 def get(self, repo_id, cache=False):
90 repo = self.sa.query(Repository) \
90 repo = self.sa.query(Repository) \
91 .filter(Repository.repo_id == repo_id)
91 .filter(Repository.repo_id == repo_id)
92
92
93 if cache:
93 if cache:
94 repo = repo.options(FromCache("sql_cache_short",
94 repo = repo.options(FromCache("sql_cache_short",
95 "get_repo_%s" % repo_id))
95 "get_repo_%s" % repo_id))
96 return repo.scalar()
96 return repo.scalar()
97
97
98 def get_repo(self, repository):
98 def get_repo(self, repository):
99 return Repository.guess_instance(repository)
99 return Repository.guess_instance(repository)
100
100
101 def get_by_repo_name(self, repo_name, cache=False):
101 def get_by_repo_name(self, repo_name, cache=False):
102 repo = self.sa.query(Repository) \
102 repo = self.sa.query(Repository) \
103 .filter(Repository.repo_name == repo_name)
103 .filter(Repository.repo_name == repo_name)
104
104
105 if cache:
105 if cache:
106 repo = repo.options(FromCache("sql_cache_short",
106 repo = repo.options(FromCache("sql_cache_short",
107 "get_repo_%s" % repo_name))
107 "get_repo_%s" % repo_name))
108 return repo.scalar()
108 return repo.scalar()
109
109
110 def get_all_user_repos(self, user):
110 def get_all_user_repos(self, user):
111 """
111 """
112 Gets all repositories that user have at least read access
112 Gets all repositories that user have at least read access
113
113
114 :param user:
114 :param user:
115 """
115 """
116 from kallithea.lib.auth import AuthUser
116 from kallithea.lib.auth import AuthUser
117 user = User.guess_instance(user)
117 user = User.guess_instance(user)
118 repos = AuthUser(dbuser=user).permissions['repositories']
118 repos = AuthUser(dbuser=user).permissions['repositories']
119 access_check = lambda r: r[1] in ['repository.read',
119 access_check = lambda r: r[1] in ['repository.read',
120 'repository.write',
120 'repository.write',
121 'repository.admin']
121 'repository.admin']
122 repos = [x[0] for x in filter(access_check, repos.items())]
122 repos = [x[0] for x in filter(access_check, repos.items())]
123 return Repository.query().filter(Repository.repo_name.in_(repos))
123 return Repository.query().filter(Repository.repo_name.in_(repos))
124
124
125 def get_users_js(self):
125 def get_users_js(self):
126 users = self.sa.query(User) \
126 users = self.sa.query(User) \
127 .filter(User.active == True) \
127 .filter(User.active == True) \
128 .order_by(User.name, User.lastname) \
128 .order_by(User.name, User.lastname) \
129 .all()
129 .all()
130 return json.dumps([
130 return json.dumps([
131 {
131 {
132 'id': u.user_id,
132 'id': u.user_id,
133 'fname': h.escape(u.name),
133 'fname': h.escape(u.name),
134 'lname': h.escape(u.lastname),
134 'lname': h.escape(u.lastname),
135 'nname': u.username,
135 'nname': u.username,
136 'gravatar_lnk': h.gravatar_url(u.email, size=28, default='default'),
136 'gravatar_lnk': h.gravatar_url(u.email, size=28, default='default'),
137 'gravatar_size': 14,
137 'gravatar_size': 14,
138 } for u in users]
138 } for u in users]
139 )
139 )
140
140
141 def get_user_groups_js(self):
141 def get_user_groups_js(self):
142 user_groups = self.sa.query(UserGroup) \
142 user_groups = self.sa.query(UserGroup) \
143 .filter(UserGroup.users_group_active == True) \
143 .filter(UserGroup.users_group_active == True) \
144 .order_by(UserGroup.users_group_name) \
144 .order_by(UserGroup.users_group_name) \
145 .options(subqueryload(UserGroup.members)) \
145 .options(subqueryload(UserGroup.members)) \
146 .all()
146 .all()
147 user_groups = UserGroupList(user_groups, perm_set=['usergroup.read',
147 user_groups = UserGroupList(user_groups, perm_set=['usergroup.read',
148 'usergroup.write',
148 'usergroup.write',
149 'usergroup.admin'])
149 'usergroup.admin'])
150 return json.dumps([
150 return json.dumps([
151 {
151 {
152 'id': gr.users_group_id,
152 'id': gr.users_group_id,
153 'grname': gr.users_group_name,
153 'grname': gr.users_group_name,
154 'grmembers': len(gr.members),
154 'grmembers': len(gr.members),
155 } for gr in user_groups]
155 } for gr in user_groups]
156 )
156 )
157
157
158 @classmethod
158 @classmethod
159 def _render_datatable(cls, tmpl, *args, **kwargs):
159 def _render_datatable(cls, tmpl, *args, **kwargs):
160 import kallithea
160 import kallithea
161 from pylons import tmpl_context as c, request
161 from pylons import tmpl_context as c, request
162 from pylons.i18n.translation import _
162 from pylons.i18n.translation import _
163
163
164 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
164 _tmpl_lookup = kallithea.CONFIG['pylons.app_globals'].mako_lookup
165 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
165 template = _tmpl_lookup.get_template('data_table/_dt_elements.html')
166
166
167 tmpl = template.get_def(tmpl)
167 tmpl = template.get_def(tmpl)
168 kwargs.update(dict(_=_, h=h, c=c, request=request))
168 kwargs.update(dict(_=_, h=h, c=c, request=request))
169 return tmpl.render(*args, **kwargs)
169 return tmpl.render(*args, **kwargs)
170
170
171 def get_repos_as_dict(self, repos_list=None, admin=False, perm_check=True,
171 def get_repos_as_dict(self, repos_list=None, admin=False, perm_check=True,
172 super_user_actions=False, short_name=False):
172 super_user_actions=False, short_name=False):
173 _render = self._render_datatable
173 _render = self._render_datatable
174 from pylons import tmpl_context as c
174 from pylons import tmpl_context as c
175
175
176 def repo_lnk(name, rtype, rstate, private, fork_of):
176 def repo_lnk(name, rtype, rstate, private, fork_of):
177 return _render('repo_name', name, rtype, rstate, private, fork_of,
177 return _render('repo_name', name, rtype, rstate, private, fork_of,
178 short_name=short_name, admin=False)
178 short_name=short_name, admin=False)
179
179
180 def last_change(last_change):
180 def last_change(last_change):
181 return _render("last_change", last_change)
181 return _render("last_change", last_change)
182
182
183 def rss_lnk(repo_name):
183 def rss_lnk(repo_name):
184 return _render("rss", repo_name)
184 return _render("rss", repo_name)
185
185
186 def atom_lnk(repo_name):
186 def atom_lnk(repo_name):
187 return _render("atom", repo_name)
187 return _render("atom", repo_name)
188
188
189 def last_rev(repo_name, cs_cache):
189 def last_rev(repo_name, cs_cache):
190 return _render('revision', repo_name, cs_cache.get('revision'),
190 return _render('revision', repo_name, cs_cache.get('revision'),
191 cs_cache.get('raw_id'), cs_cache.get('author'),
191 cs_cache.get('raw_id'), cs_cache.get('author'),
192 cs_cache.get('message'))
192 cs_cache.get('message'))
193
193
194 def desc(desc):
194 def desc(desc):
195 return h.urlify_text(desc, truncate=80, stylize=c.visual.stylify_metatags)
195 return h.urlify_text(desc, truncate=80, stylize=c.visual.stylify_metatags)
196
196
197 def state(repo_state):
197 def state(repo_state):
198 return _render("repo_state", repo_state)
198 return _render("repo_state", repo_state)
199
199
200 def repo_actions(repo_name):
200 def repo_actions(repo_name):
201 return _render('repo_actions', repo_name, super_user_actions)
201 return _render('repo_actions', repo_name, super_user_actions)
202
202
203 def owner_actions(owner_id, username):
203 def owner_actions(owner_id, username):
204 return _render('user_name', owner_id, username)
204 return _render('user_name', owner_id, username)
205
205
206 repos_data = []
206 repos_data = []
207 for repo in repos_list:
207 for repo in repos_list:
208 if perm_check:
208 if perm_check:
209 # check permission at this level
209 # check permission at this level
210 if not HasRepoPermissionAny(
210 if not HasRepoPermissionLevel('read')(repo.repo_name, 'get_repos_as_dict check'):
211 'repository.read', 'repository.write',
212 'repository.admin'
213 )(repo.repo_name, 'get_repos_as_dict check'):
214 continue
211 continue
215 cs_cache = repo.changeset_cache
212 cs_cache = repo.changeset_cache
216 row = {
213 row = {
217 "raw_name": repo.repo_name,
214 "raw_name": repo.repo_name,
218 "just_name": repo.just_name,
215 "just_name": repo.just_name,
219 "name": repo_lnk(repo.repo_name, repo.repo_type,
216 "name": repo_lnk(repo.repo_name, repo.repo_type,
220 repo.repo_state, repo.private, repo.fork),
217 repo.repo_state, repo.private, repo.fork),
221 "last_change_iso": repo.last_db_change.isoformat(),
218 "last_change_iso": repo.last_db_change.isoformat(),
222 "last_change": last_change(repo.last_db_change),
219 "last_change": last_change(repo.last_db_change),
223 "last_changeset": last_rev(repo.repo_name, cs_cache),
220 "last_changeset": last_rev(repo.repo_name, cs_cache),
224 "last_rev_raw": cs_cache.get('revision'),
221 "last_rev_raw": cs_cache.get('revision'),
225 "desc": desc(repo.description),
222 "desc": desc(repo.description),
226 "owner": h.person(repo.owner),
223 "owner": h.person(repo.owner),
227 "state": state(repo.repo_state),
224 "state": state(repo.repo_state),
228 "rss": rss_lnk(repo.repo_name),
225 "rss": rss_lnk(repo.repo_name),
229 "atom": atom_lnk(repo.repo_name),
226 "atom": atom_lnk(repo.repo_name),
230
227
231 }
228 }
232 if admin:
229 if admin:
233 row.update({
230 row.update({
234 "action": repo_actions(repo.repo_name),
231 "action": repo_actions(repo.repo_name),
235 "owner": owner_actions(repo.owner_id,
232 "owner": owner_actions(repo.owner_id,
236 h.person(repo.owner))
233 h.person(repo.owner))
237 })
234 })
238 repos_data.append(row)
235 repos_data.append(row)
239
236
240 return {
237 return {
241 "totalRecords": len(repos_list),
238 "totalRecords": len(repos_list),
242 "startIndex": 0,
239 "startIndex": 0,
243 "sort": "name",
240 "sort": "name",
244 "dir": "asc",
241 "dir": "asc",
245 "records": repos_data
242 "records": repos_data
246 }
243 }
247
244
248 def _get_defaults(self, repo_name):
245 def _get_defaults(self, repo_name):
249 """
246 """
250 Gets information about repository, and returns a dict for
247 Gets information about repository, and returns a dict for
251 usage in forms
248 usage in forms
252
249
253 :param repo_name:
250 :param repo_name:
254 """
251 """
255
252
256 repo_info = Repository.get_by_repo_name(repo_name)
253 repo_info = Repository.get_by_repo_name(repo_name)
257
254
258 if repo_info is None:
255 if repo_info is None:
259 return None
256 return None
260
257
261 defaults = repo_info.get_dict()
258 defaults = repo_info.get_dict()
262 defaults['repo_name'] = repo_info.just_name
259 defaults['repo_name'] = repo_info.just_name
263 defaults['repo_group'] = repo_info.group_id
260 defaults['repo_group'] = repo_info.group_id
264
261
265 for strip, k in [(0, 'repo_type'), (1, 'repo_enable_downloads'),
262 for strip, k in [(0, 'repo_type'), (1, 'repo_enable_downloads'),
266 (1, 'repo_description'), (1, 'repo_enable_locking'),
263 (1, 'repo_description'), (1, 'repo_enable_locking'),
267 (1, 'repo_landing_rev'), (0, 'clone_uri'),
264 (1, 'repo_landing_rev'), (0, 'clone_uri'),
268 (1, 'repo_private'), (1, 'repo_enable_statistics')]:
265 (1, 'repo_private'), (1, 'repo_enable_statistics')]:
269 attr = k
266 attr = k
270 if strip:
267 if strip:
271 attr = remove_prefix(k, 'repo_')
268 attr = remove_prefix(k, 'repo_')
272
269
273 val = defaults[attr]
270 val = defaults[attr]
274 if k == 'repo_landing_rev':
271 if k == 'repo_landing_rev':
275 val = ':'.join(defaults[attr])
272 val = ':'.join(defaults[attr])
276 defaults[k] = val
273 defaults[k] = val
277 if k == 'clone_uri':
274 if k == 'clone_uri':
278 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
275 defaults['clone_uri_hidden'] = repo_info.clone_uri_hidden
279
276
280 # fill owner
277 # fill owner
281 if repo_info.owner:
278 if repo_info.owner:
282 defaults.update({'owner': repo_info.owner.username})
279 defaults.update({'owner': repo_info.owner.username})
283 else:
280 else:
284 replacement_user = User.query().filter(User.admin ==
281 replacement_user = User.query().filter(User.admin ==
285 True).first().username
282 True).first().username
286 defaults.update({'owner': replacement_user})
283 defaults.update({'owner': replacement_user})
287
284
288 # fill repository users
285 # fill repository users
289 for p in repo_info.repo_to_perm:
286 for p in repo_info.repo_to_perm:
290 defaults.update({'u_perm_%s' % p.user.username:
287 defaults.update({'u_perm_%s' % p.user.username:
291 p.permission.permission_name})
288 p.permission.permission_name})
292
289
293 # fill repository groups
290 # fill repository groups
294 for p in repo_info.users_group_to_perm:
291 for p in repo_info.users_group_to_perm:
295 defaults.update({'g_perm_%s' % p.users_group.users_group_name:
292 defaults.update({'g_perm_%s' % p.users_group.users_group_name:
296 p.permission.permission_name})
293 p.permission.permission_name})
297
294
298 return defaults
295 return defaults
299
296
300 def update(self, repo, **kwargs):
297 def update(self, repo, **kwargs):
301 try:
298 try:
302 cur_repo = Repository.guess_instance(repo)
299 cur_repo = Repository.guess_instance(repo)
303 org_repo_name = cur_repo.repo_name
300 org_repo_name = cur_repo.repo_name
304 if 'owner' in kwargs:
301 if 'owner' in kwargs:
305 cur_repo.owner = User.get_by_username(kwargs['owner'])
302 cur_repo.owner = User.get_by_username(kwargs['owner'])
306
303
307 if 'repo_group' in kwargs:
304 if 'repo_group' in kwargs:
308 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
305 cur_repo.group = RepoGroup.get(kwargs['repo_group'])
309 cur_repo.repo_name = cur_repo.get_new_name(cur_repo.just_name)
306 cur_repo.repo_name = cur_repo.get_new_name(cur_repo.just_name)
310 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
307 log.debug('Updating repo %s with params:%s', cur_repo, kwargs)
311 for k in ['repo_enable_downloads',
308 for k in ['repo_enable_downloads',
312 'repo_description',
309 'repo_description',
313 'repo_enable_locking',
310 'repo_enable_locking',
314 'repo_landing_rev',
311 'repo_landing_rev',
315 'repo_private',
312 'repo_private',
316 'repo_enable_statistics',
313 'repo_enable_statistics',
317 ]:
314 ]:
318 if k in kwargs:
315 if k in kwargs:
319 setattr(cur_repo, remove_prefix(k, 'repo_'), kwargs[k])
316 setattr(cur_repo, remove_prefix(k, 'repo_'), kwargs[k])
320 clone_uri = kwargs.get('clone_uri')
317 clone_uri = kwargs.get('clone_uri')
321 if clone_uri is not None and clone_uri != cur_repo.clone_uri_hidden:
318 if clone_uri is not None and clone_uri != cur_repo.clone_uri_hidden:
322 cur_repo.clone_uri = clone_uri
319 cur_repo.clone_uri = clone_uri
323
320
324 if 'repo_name' in kwargs:
321 if 'repo_name' in kwargs:
325 cur_repo.repo_name = cur_repo.get_new_name(kwargs['repo_name'])
322 cur_repo.repo_name = cur_repo.get_new_name(kwargs['repo_name'])
326
323
327 #if private flag is set, reset default permission to NONE
324 #if private flag is set, reset default permission to NONE
328 if kwargs.get('repo_private'):
325 if kwargs.get('repo_private'):
329 EMPTY_PERM = 'repository.none'
326 EMPTY_PERM = 'repository.none'
330 RepoModel().grant_user_permission(
327 RepoModel().grant_user_permission(
331 repo=cur_repo, user='default', perm=EMPTY_PERM
328 repo=cur_repo, user='default', perm=EMPTY_PERM
332 )
329 )
333 #handle extra fields
330 #handle extra fields
334 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
331 for field in filter(lambda k: k.startswith(RepositoryField.PREFIX),
335 kwargs):
332 kwargs):
336 k = RepositoryField.un_prefix_key(field)
333 k = RepositoryField.un_prefix_key(field)
337 ex_field = RepositoryField.get_by_key_name(key=k, repo=cur_repo)
334 ex_field = RepositoryField.get_by_key_name(key=k, repo=cur_repo)
338 if ex_field:
335 if ex_field:
339 ex_field.field_value = kwargs[field]
336 ex_field.field_value = kwargs[field]
340 self.sa.add(ex_field)
337 self.sa.add(ex_field)
341 self.sa.add(cur_repo)
338 self.sa.add(cur_repo)
342
339
343 if org_repo_name != cur_repo.repo_name:
340 if org_repo_name != cur_repo.repo_name:
344 # rename repository
341 # rename repository
345 self._rename_filesystem_repo(old=org_repo_name, new=cur_repo.repo_name)
342 self._rename_filesystem_repo(old=org_repo_name, new=cur_repo.repo_name)
346
343
347 return cur_repo
344 return cur_repo
348 except Exception:
345 except Exception:
349 log.error(traceback.format_exc())
346 log.error(traceback.format_exc())
350 raise
347 raise
351
348
352 def _create_repo(self, repo_name, repo_type, description, owner,
349 def _create_repo(self, repo_name, repo_type, description, owner,
353 private=False, clone_uri=None, repo_group=None,
350 private=False, clone_uri=None, repo_group=None,
354 landing_rev='rev:tip', fork_of=None,
351 landing_rev='rev:tip', fork_of=None,
355 copy_fork_permissions=False, enable_statistics=False,
352 copy_fork_permissions=False, enable_statistics=False,
356 enable_locking=False, enable_downloads=False,
353 enable_locking=False, enable_downloads=False,
357 copy_group_permissions=False, state=Repository.STATE_PENDING):
354 copy_group_permissions=False, state=Repository.STATE_PENDING):
358 """
355 """
359 Create repository inside database with PENDING state. This should only be
356 Create repository inside database with PENDING state. This should only be
360 executed by create() repo, with exception of importing existing repos.
357 executed by create() repo, with exception of importing existing repos.
361
358
362 """
359 """
363 from kallithea.model.scm import ScmModel
360 from kallithea.model.scm import ScmModel
364
361
365 owner = User.guess_instance(owner)
362 owner = User.guess_instance(owner)
366 fork_of = Repository.guess_instance(fork_of)
363 fork_of = Repository.guess_instance(fork_of)
367 repo_group = RepoGroup.guess_instance(repo_group)
364 repo_group = RepoGroup.guess_instance(repo_group)
368 try:
365 try:
369 repo_name = safe_unicode(repo_name)
366 repo_name = safe_unicode(repo_name)
370 description = safe_unicode(description)
367 description = safe_unicode(description)
371 # repo name is just a name of repository
368 # repo name is just a name of repository
372 # while repo_name_full is a full qualified name that is combined
369 # while repo_name_full is a full qualified name that is combined
373 # with name and path of group
370 # with name and path of group
374 repo_name_full = repo_name
371 repo_name_full = repo_name
375 repo_name = repo_name.split(self.URL_SEPARATOR)[-1]
372 repo_name = repo_name.split(self.URL_SEPARATOR)[-1]
376
373
377 new_repo = Repository()
374 new_repo = Repository()
378 new_repo.repo_state = state
375 new_repo.repo_state = state
379 new_repo.enable_statistics = False
376 new_repo.enable_statistics = False
380 new_repo.repo_name = repo_name_full
377 new_repo.repo_name = repo_name_full
381 new_repo.repo_type = repo_type
378 new_repo.repo_type = repo_type
382 new_repo.owner = owner
379 new_repo.owner = owner
383 new_repo.group = repo_group
380 new_repo.group = repo_group
384 new_repo.description = description or repo_name
381 new_repo.description = description or repo_name
385 new_repo.private = private
382 new_repo.private = private
386 new_repo.clone_uri = clone_uri
383 new_repo.clone_uri = clone_uri
387 new_repo.landing_rev = landing_rev
384 new_repo.landing_rev = landing_rev
388
385
389 new_repo.enable_statistics = enable_statistics
386 new_repo.enable_statistics = enable_statistics
390 new_repo.enable_locking = enable_locking
387 new_repo.enable_locking = enable_locking
391 new_repo.enable_downloads = enable_downloads
388 new_repo.enable_downloads = enable_downloads
392
389
393 if repo_group:
390 if repo_group:
394 new_repo.enable_locking = repo_group.enable_locking
391 new_repo.enable_locking = repo_group.enable_locking
395
392
396 if fork_of:
393 if fork_of:
397 parent_repo = fork_of
394 parent_repo = fork_of
398 new_repo.fork = parent_repo
395 new_repo.fork = parent_repo
399
396
400 self.sa.add(new_repo)
397 self.sa.add(new_repo)
401
398
402 if fork_of and copy_fork_permissions:
399 if fork_of and copy_fork_permissions:
403 repo = fork_of
400 repo = fork_of
404 user_perms = UserRepoToPerm.query() \
401 user_perms = UserRepoToPerm.query() \
405 .filter(UserRepoToPerm.repository == repo).all()
402 .filter(UserRepoToPerm.repository == repo).all()
406 group_perms = UserGroupRepoToPerm.query() \
403 group_perms = UserGroupRepoToPerm.query() \
407 .filter(UserGroupRepoToPerm.repository == repo).all()
404 .filter(UserGroupRepoToPerm.repository == repo).all()
408
405
409 for perm in user_perms:
406 for perm in user_perms:
410 UserRepoToPerm.create(perm.user, new_repo, perm.permission)
407 UserRepoToPerm.create(perm.user, new_repo, perm.permission)
411
408
412 for perm in group_perms:
409 for perm in group_perms:
413 UserGroupRepoToPerm.create(perm.users_group, new_repo,
410 UserGroupRepoToPerm.create(perm.users_group, new_repo,
414 perm.permission)
411 perm.permission)
415
412
416 elif repo_group and copy_group_permissions:
413 elif repo_group and copy_group_permissions:
417
414
418 user_perms = UserRepoGroupToPerm.query() \
415 user_perms = UserRepoGroupToPerm.query() \
419 .filter(UserRepoGroupToPerm.group == repo_group).all()
416 .filter(UserRepoGroupToPerm.group == repo_group).all()
420
417
421 group_perms = UserGroupRepoGroupToPerm.query() \
418 group_perms = UserGroupRepoGroupToPerm.query() \
422 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
419 .filter(UserGroupRepoGroupToPerm.group == repo_group).all()
423
420
424 for perm in user_perms:
421 for perm in user_perms:
425 perm_name = perm.permission.permission_name.replace('group.', 'repository.')
422 perm_name = perm.permission.permission_name.replace('group.', 'repository.')
426 perm_obj = Permission.get_by_key(perm_name)
423 perm_obj = Permission.get_by_key(perm_name)
427 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
424 UserRepoToPerm.create(perm.user, new_repo, perm_obj)
428
425
429 for perm in group_perms:
426 for perm in group_perms:
430 perm_name = perm.permission.permission_name.replace('group.', 'repository.')
427 perm_name = perm.permission.permission_name.replace('group.', 'repository.')
431 perm_obj = Permission.get_by_key(perm_name)
428 perm_obj = Permission.get_by_key(perm_name)
432 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
429 UserGroupRepoToPerm.create(perm.users_group, new_repo, perm_obj)
433
430
434 else:
431 else:
435 perm_obj = self._create_default_perms(new_repo, private)
432 perm_obj = self._create_default_perms(new_repo, private)
436 self.sa.add(perm_obj)
433 self.sa.add(perm_obj)
437
434
438 # now automatically start following this repository as owner
435 # now automatically start following this repository as owner
439 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
436 ScmModel(self.sa).toggle_following_repo(new_repo.repo_id,
440 owner.user_id)
437 owner.user_id)
441 # we need to flush here, in order to check if database won't
438 # we need to flush here, in order to check if database won't
442 # throw any exceptions, create filesystem dirs at the very end
439 # throw any exceptions, create filesystem dirs at the very end
443 self.sa.flush()
440 self.sa.flush()
444 return new_repo
441 return new_repo
445 except Exception:
442 except Exception:
446 log.error(traceback.format_exc())
443 log.error(traceback.format_exc())
447 raise
444 raise
448
445
449 def create(self, form_data, cur_user):
446 def create(self, form_data, cur_user):
450 """
447 """
451 Create repository using celery tasks
448 Create repository using celery tasks
452
449
453 :param form_data:
450 :param form_data:
454 :param cur_user:
451 :param cur_user:
455 """
452 """
456 from kallithea.lib.celerylib import tasks
453 from kallithea.lib.celerylib import tasks
457 return tasks.create_repo(form_data, cur_user)
454 return tasks.create_repo(form_data, cur_user)
458
455
459 def _update_permissions(self, repo, perms_new=None, perms_updates=None,
456 def _update_permissions(self, repo, perms_new=None, perms_updates=None,
460 check_perms=True):
457 check_perms=True):
461 if not perms_new:
458 if not perms_new:
462 perms_new = []
459 perms_new = []
463 if not perms_updates:
460 if not perms_updates:
464 perms_updates = []
461 perms_updates = []
465
462
466 # update permissions
463 # update permissions
467 for member, perm, member_type in perms_updates:
464 for member, perm, member_type in perms_updates:
468 if member_type == 'user':
465 if member_type == 'user':
469 # this updates existing one
466 # this updates existing one
470 self.grant_user_permission(
467 self.grant_user_permission(
471 repo=repo, user=member, perm=perm
468 repo=repo, user=member, perm=perm
472 )
469 )
473 else:
470 else:
474 #check if we have permissions to alter this usergroup
471 #check if we have permissions to alter this usergroup
475 req_perms = (
472 req_perms = (
476 'usergroup.read', 'usergroup.write', 'usergroup.admin')
473 'usergroup.read', 'usergroup.write', 'usergroup.admin')
477 if not check_perms or HasUserGroupPermissionAny(*req_perms)(
474 if not check_perms or HasUserGroupPermissionAny(*req_perms)(
478 member):
475 member):
479 self.grant_user_group_permission(
476 self.grant_user_group_permission(
480 repo=repo, group_name=member, perm=perm
477 repo=repo, group_name=member, perm=perm
481 )
478 )
482 # set new permissions
479 # set new permissions
483 for member, perm, member_type in perms_new:
480 for member, perm, member_type in perms_new:
484 if member_type == 'user':
481 if member_type == 'user':
485 self.grant_user_permission(
482 self.grant_user_permission(
486 repo=repo, user=member, perm=perm
483 repo=repo, user=member, perm=perm
487 )
484 )
488 else:
485 else:
489 #check if we have permissions to alter this usergroup
486 #check if we have permissions to alter this usergroup
490 req_perms = (
487 req_perms = (
491 'usergroup.read', 'usergroup.write', 'usergroup.admin')
488 'usergroup.read', 'usergroup.write', 'usergroup.admin')
492 if not check_perms or HasUserGroupPermissionAny(*req_perms)(
489 if not check_perms or HasUserGroupPermissionAny(*req_perms)(
493 member):
490 member):
494 self.grant_user_group_permission(
491 self.grant_user_group_permission(
495 repo=repo, group_name=member, perm=perm
492 repo=repo, group_name=member, perm=perm
496 )
493 )
497
494
498 def create_fork(self, form_data, cur_user):
495 def create_fork(self, form_data, cur_user):
499 """
496 """
500 Simple wrapper into executing celery task for fork creation
497 Simple wrapper into executing celery task for fork creation
501
498
502 :param form_data:
499 :param form_data:
503 :param cur_user:
500 :param cur_user:
504 """
501 """
505 from kallithea.lib.celerylib import tasks
502 from kallithea.lib.celerylib import tasks
506 return tasks.create_repo_fork(form_data, cur_user)
503 return tasks.create_repo_fork(form_data, cur_user)
507
504
508 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
505 def delete(self, repo, forks=None, fs_remove=True, cur_user=None):
509 """
506 """
510 Delete given repository, forks parameter defines what do do with
507 Delete given repository, forks parameter defines what do do with
511 attached forks. Throws AttachedForksError if deleted repo has attached
508 attached forks. Throws AttachedForksError if deleted repo has attached
512 forks
509 forks
513
510
514 :param repo:
511 :param repo:
515 :param forks: str 'delete' or 'detach'
512 :param forks: str 'delete' or 'detach'
516 :param fs_remove: remove(archive) repo from filesystem
513 :param fs_remove: remove(archive) repo from filesystem
517 """
514 """
518 if not cur_user:
515 if not cur_user:
519 cur_user = getattr(get_current_authuser(), 'username', None)
516 cur_user = getattr(get_current_authuser(), 'username', None)
520 repo = Repository.guess_instance(repo)
517 repo = Repository.guess_instance(repo)
521 if repo is not None:
518 if repo is not None:
522 if forks == 'detach':
519 if forks == 'detach':
523 for r in repo.forks:
520 for r in repo.forks:
524 r.fork = None
521 r.fork = None
525 self.sa.add(r)
522 self.sa.add(r)
526 elif forks == 'delete':
523 elif forks == 'delete':
527 for r in repo.forks:
524 for r in repo.forks:
528 self.delete(r, forks='delete')
525 self.delete(r, forks='delete')
529 elif [f for f in repo.forks]:
526 elif [f for f in repo.forks]:
530 raise AttachedForksError()
527 raise AttachedForksError()
531
528
532 old_repo_dict = repo.get_dict()
529 old_repo_dict = repo.get_dict()
533 try:
530 try:
534 self.sa.delete(repo)
531 self.sa.delete(repo)
535 if fs_remove:
532 if fs_remove:
536 self._delete_filesystem_repo(repo)
533 self._delete_filesystem_repo(repo)
537 else:
534 else:
538 log.debug('skipping removal from filesystem')
535 log.debug('skipping removal from filesystem')
539 log_delete_repository(old_repo_dict,
536 log_delete_repository(old_repo_dict,
540 deleted_by=cur_user)
537 deleted_by=cur_user)
541 except Exception:
538 except Exception:
542 log.error(traceback.format_exc())
539 log.error(traceback.format_exc())
543 raise
540 raise
544
541
545 def grant_user_permission(self, repo, user, perm):
542 def grant_user_permission(self, repo, user, perm):
546 """
543 """
547 Grant permission for user on given repository, or update existing one
544 Grant permission for user on given repository, or update existing one
548 if found
545 if found
549
546
550 :param repo: Instance of Repository, repository_id, or repository name
547 :param repo: Instance of Repository, repository_id, or repository name
551 :param user: Instance of User, user_id or username
548 :param user: Instance of User, user_id or username
552 :param perm: Instance of Permission, or permission_name
549 :param perm: Instance of Permission, or permission_name
553 """
550 """
554 user = User.guess_instance(user)
551 user = User.guess_instance(user)
555 repo = Repository.guess_instance(repo)
552 repo = Repository.guess_instance(repo)
556 permission = Permission.guess_instance(perm)
553 permission = Permission.guess_instance(perm)
557
554
558 # check if we have that permission already
555 # check if we have that permission already
559 obj = self.sa.query(UserRepoToPerm) \
556 obj = self.sa.query(UserRepoToPerm) \
560 .filter(UserRepoToPerm.user == user) \
557 .filter(UserRepoToPerm.user == user) \
561 .filter(UserRepoToPerm.repository == repo) \
558 .filter(UserRepoToPerm.repository == repo) \
562 .scalar()
559 .scalar()
563 if obj is None:
560 if obj is None:
564 # create new !
561 # create new !
565 obj = UserRepoToPerm()
562 obj = UserRepoToPerm()
566 obj.repository = repo
563 obj.repository = repo
567 obj.user = user
564 obj.user = user
568 obj.permission = permission
565 obj.permission = permission
569 self.sa.add(obj)
566 self.sa.add(obj)
570 log.debug('Granted perm %s to %s on %s', perm, user, repo)
567 log.debug('Granted perm %s to %s on %s', perm, user, repo)
571 return obj
568 return obj
572
569
573 def revoke_user_permission(self, repo, user):
570 def revoke_user_permission(self, repo, user):
574 """
571 """
575 Revoke permission for user on given repository
572 Revoke permission for user on given repository
576
573
577 :param repo: Instance of Repository, repository_id, or repository name
574 :param repo: Instance of Repository, repository_id, or repository name
578 :param user: Instance of User, user_id or username
575 :param user: Instance of User, user_id or username
579 """
576 """
580
577
581 user = User.guess_instance(user)
578 user = User.guess_instance(user)
582 repo = Repository.guess_instance(repo)
579 repo = Repository.guess_instance(repo)
583
580
584 obj = self.sa.query(UserRepoToPerm) \
581 obj = self.sa.query(UserRepoToPerm) \
585 .filter(UserRepoToPerm.repository == repo) \
582 .filter(UserRepoToPerm.repository == repo) \
586 .filter(UserRepoToPerm.user == user) \
583 .filter(UserRepoToPerm.user == user) \
587 .scalar()
584 .scalar()
588 if obj is not None:
585 if obj is not None:
589 self.sa.delete(obj)
586 self.sa.delete(obj)
590 log.debug('Revoked perm on %s on %s', repo, user)
587 log.debug('Revoked perm on %s on %s', repo, user)
591
588
592 def grant_user_group_permission(self, repo, group_name, perm):
589 def grant_user_group_permission(self, repo, group_name, perm):
593 """
590 """
594 Grant permission for user group on given repository, or update
591 Grant permission for user group on given repository, or update
595 existing one if found
592 existing one if found
596
593
597 :param repo: Instance of Repository, repository_id, or repository name
594 :param repo: Instance of Repository, repository_id, or repository name
598 :param group_name: Instance of UserGroup, users_group_id,
595 :param group_name: Instance of UserGroup, users_group_id,
599 or user group name
596 or user group name
600 :param perm: Instance of Permission, or permission_name
597 :param perm: Instance of Permission, or permission_name
601 """
598 """
602 repo = Repository.guess_instance(repo)
599 repo = Repository.guess_instance(repo)
603 group_name = UserGroup.guess_instance(group_name)
600 group_name = UserGroup.guess_instance(group_name)
604 permission = Permission.guess_instance(perm)
601 permission = Permission.guess_instance(perm)
605
602
606 # check if we have that permission already
603 # check if we have that permission already
607 obj = self.sa.query(UserGroupRepoToPerm) \
604 obj = self.sa.query(UserGroupRepoToPerm) \
608 .filter(UserGroupRepoToPerm.users_group == group_name) \
605 .filter(UserGroupRepoToPerm.users_group == group_name) \
609 .filter(UserGroupRepoToPerm.repository == repo) \
606 .filter(UserGroupRepoToPerm.repository == repo) \
610 .scalar()
607 .scalar()
611
608
612 if obj is None:
609 if obj is None:
613 # create new
610 # create new
614 obj = UserGroupRepoToPerm()
611 obj = UserGroupRepoToPerm()
615
612
616 obj.repository = repo
613 obj.repository = repo
617 obj.users_group = group_name
614 obj.users_group = group_name
618 obj.permission = permission
615 obj.permission = permission
619 self.sa.add(obj)
616 self.sa.add(obj)
620 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
617 log.debug('Granted perm %s to %s on %s', perm, group_name, repo)
621 return obj
618 return obj
622
619
623 def revoke_user_group_permission(self, repo, group_name):
620 def revoke_user_group_permission(self, repo, group_name):
624 """
621 """
625 Revoke permission for user group on given repository
622 Revoke permission for user group on given repository
626
623
627 :param repo: Instance of Repository, repository_id, or repository name
624 :param repo: Instance of Repository, repository_id, or repository name
628 :param group_name: Instance of UserGroup, users_group_id,
625 :param group_name: Instance of UserGroup, users_group_id,
629 or user group name
626 or user group name
630 """
627 """
631 repo = Repository.guess_instance(repo)
628 repo = Repository.guess_instance(repo)
632 group_name = UserGroup.guess_instance(group_name)
629 group_name = UserGroup.guess_instance(group_name)
633
630
634 obj = self.sa.query(UserGroupRepoToPerm) \
631 obj = self.sa.query(UserGroupRepoToPerm) \
635 .filter(UserGroupRepoToPerm.repository == repo) \
632 .filter(UserGroupRepoToPerm.repository == repo) \
636 .filter(UserGroupRepoToPerm.users_group == group_name) \
633 .filter(UserGroupRepoToPerm.users_group == group_name) \
637 .scalar()
634 .scalar()
638 if obj is not None:
635 if obj is not None:
639 self.sa.delete(obj)
636 self.sa.delete(obj)
640 log.debug('Revoked perm to %s on %s', repo, group_name)
637 log.debug('Revoked perm to %s on %s', repo, group_name)
641
638
642 def delete_stats(self, repo_name):
639 def delete_stats(self, repo_name):
643 """
640 """
644 removes stats for given repo
641 removes stats for given repo
645
642
646 :param repo_name:
643 :param repo_name:
647 """
644 """
648 repo = Repository.guess_instance(repo_name)
645 repo = Repository.guess_instance(repo_name)
649 try:
646 try:
650 obj = self.sa.query(Statistics) \
647 obj = self.sa.query(Statistics) \
651 .filter(Statistics.repository == repo).scalar()
648 .filter(Statistics.repository == repo).scalar()
652 if obj is not None:
649 if obj is not None:
653 self.sa.delete(obj)
650 self.sa.delete(obj)
654 except Exception:
651 except Exception:
655 log.error(traceback.format_exc())
652 log.error(traceback.format_exc())
656 raise
653 raise
657
654
658 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
655 def _create_filesystem_repo(self, repo_name, repo_type, repo_group,
659 clone_uri=None, repo_store_location=None):
656 clone_uri=None, repo_store_location=None):
660 """
657 """
661 Makes repository on filesystem. Operation is group aware, meaning that it will create
658 Makes repository on filesystem. Operation is group aware, meaning that it will create
662 a repository within a group, and alter the paths accordingly to the group location.
659 a repository within a group, and alter the paths accordingly to the group location.
663
660
664 :param repo_name:
661 :param repo_name:
665 :param alias:
662 :param alias:
666 :param parent:
663 :param parent:
667 :param clone_uri:
664 :param clone_uri:
668 :param repo_store_location:
665 :param repo_store_location:
669 """
666 """
670 from kallithea.lib.utils import is_valid_repo, is_valid_repo_group
667 from kallithea.lib.utils import is_valid_repo, is_valid_repo_group
671 from kallithea.model.scm import ScmModel
668 from kallithea.model.scm import ScmModel
672
669
673 if '/' in repo_name:
670 if '/' in repo_name:
674 raise ValueError('repo_name must not contain groups got `%s`' % repo_name)
671 raise ValueError('repo_name must not contain groups got `%s`' % repo_name)
675
672
676 if isinstance(repo_group, RepoGroup):
673 if isinstance(repo_group, RepoGroup):
677 new_parent_path = os.sep.join(repo_group.full_path_splitted)
674 new_parent_path = os.sep.join(repo_group.full_path_splitted)
678 else:
675 else:
679 new_parent_path = repo_group or ''
676 new_parent_path = repo_group or ''
680
677
681 if repo_store_location:
678 if repo_store_location:
682 _paths = [repo_store_location]
679 _paths = [repo_store_location]
683 else:
680 else:
684 _paths = [self.repos_path, new_parent_path, repo_name]
681 _paths = [self.repos_path, new_parent_path, repo_name]
685 # we need to make it str for mercurial
682 # we need to make it str for mercurial
686 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
683 repo_path = os.path.join(*map(lambda x: safe_str(x), _paths))
687
684
688 # check if this path is not a repository
685 # check if this path is not a repository
689 if is_valid_repo(repo_path, self.repos_path):
686 if is_valid_repo(repo_path, self.repos_path):
690 raise Exception('This path %s is a valid repository' % repo_path)
687 raise Exception('This path %s is a valid repository' % repo_path)
691
688
692 # check if this path is a group
689 # check if this path is a group
693 if is_valid_repo_group(repo_path, self.repos_path):
690 if is_valid_repo_group(repo_path, self.repos_path):
694 raise Exception('This path %s is a valid group' % repo_path)
691 raise Exception('This path %s is a valid group' % repo_path)
695
692
696 log.info('creating repo %s in %s from url: `%s`',
693 log.info('creating repo %s in %s from url: `%s`',
697 repo_name, safe_unicode(repo_path),
694 repo_name, safe_unicode(repo_path),
698 obfuscate_url_pw(clone_uri))
695 obfuscate_url_pw(clone_uri))
699
696
700 backend = get_backend(repo_type)
697 backend = get_backend(repo_type)
701
698
702 if repo_type == 'hg':
699 if repo_type == 'hg':
703 baseui = make_ui('db', clear_session=False)
700 baseui = make_ui('db', clear_session=False)
704 # patch and reset hooks section of UI config to not run any
701 # patch and reset hooks section of UI config to not run any
705 # hooks on creating remote repo
702 # hooks on creating remote repo
706 for k, v in baseui.configitems('hooks'):
703 for k, v in baseui.configitems('hooks'):
707 baseui.setconfig('hooks', k, None)
704 baseui.setconfig('hooks', k, None)
708
705
709 repo = backend(repo_path, create=True, src_url=clone_uri, baseui=baseui)
706 repo = backend(repo_path, create=True, src_url=clone_uri, baseui=baseui)
710 elif repo_type == 'git':
707 elif repo_type == 'git':
711 repo = backend(repo_path, create=True, src_url=clone_uri, bare=True)
708 repo = backend(repo_path, create=True, src_url=clone_uri, bare=True)
712 # add kallithea hook into this repo
709 # add kallithea hook into this repo
713 ScmModel().install_git_hooks(repo=repo)
710 ScmModel().install_git_hooks(repo=repo)
714 else:
711 else:
715 raise Exception('Not supported repo_type %s expected hg/git' % repo_type)
712 raise Exception('Not supported repo_type %s expected hg/git' % repo_type)
716
713
717 log.debug('Created repo %s with %s backend',
714 log.debug('Created repo %s with %s backend',
718 safe_unicode(repo_name), safe_unicode(repo_type))
715 safe_unicode(repo_name), safe_unicode(repo_type))
719 return repo
716 return repo
720
717
721 def _rename_filesystem_repo(self, old, new):
718 def _rename_filesystem_repo(self, old, new):
722 """
719 """
723 renames repository on filesystem
720 renames repository on filesystem
724
721
725 :param old: old name
722 :param old: old name
726 :param new: new name
723 :param new: new name
727 """
724 """
728 log.info('renaming repo from %s to %s', old, new)
725 log.info('renaming repo from %s to %s', old, new)
729
726
730 old_path = safe_str(os.path.join(self.repos_path, old))
727 old_path = safe_str(os.path.join(self.repos_path, old))
731 new_path = safe_str(os.path.join(self.repos_path, new))
728 new_path = safe_str(os.path.join(self.repos_path, new))
732 if os.path.isdir(new_path):
729 if os.path.isdir(new_path):
733 raise Exception(
730 raise Exception(
734 'Was trying to rename to already existing dir %s' % new_path
731 'Was trying to rename to already existing dir %s' % new_path
735 )
732 )
736 shutil.move(old_path, new_path)
733 shutil.move(old_path, new_path)
737
734
738 def _delete_filesystem_repo(self, repo):
735 def _delete_filesystem_repo(self, repo):
739 """
736 """
740 removes repo from filesystem, the removal is actually done by
737 removes repo from filesystem, the removal is actually done by
741 renaming dir to a 'rm__*' prefix which Kallithea will skip.
738 renaming dir to a 'rm__*' prefix which Kallithea will skip.
742 It can be undeleted later by reverting the rename.
739 It can be undeleted later by reverting the rename.
743
740
744 :param repo: repo object
741 :param repo: repo object
745 """
742 """
746 rm_path = safe_str(os.path.join(self.repos_path, repo.repo_name))
743 rm_path = safe_str(os.path.join(self.repos_path, repo.repo_name))
747 log.info("Removing %s", rm_path)
744 log.info("Removing %s", rm_path)
748
745
749 _now = datetime.now()
746 _now = datetime.now()
750 _ms = str(_now.microsecond).rjust(6, '0')
747 _ms = str(_now.microsecond).rjust(6, '0')
751 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
748 _d = 'rm__%s__%s' % (_now.strftime('%Y%m%d_%H%M%S_' + _ms),
752 repo.just_name)
749 repo.just_name)
753 if repo.group:
750 if repo.group:
754 args = repo.group.full_path_splitted + [_d]
751 args = repo.group.full_path_splitted + [_d]
755 _d = os.path.join(*args)
752 _d = os.path.join(*args)
756 if os.path.exists(rm_path):
753 if os.path.exists(rm_path):
757 shutil.move(rm_path, safe_str(os.path.join(self.repos_path, _d)))
754 shutil.move(rm_path, safe_str(os.path.join(self.repos_path, _d)))
758 else:
755 else:
759 log.error("Can't find repo to delete in %r", rm_path)
756 log.error("Can't find repo to delete in %r", rm_path)
@@ -1,806 +1,803 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.scm
15 kallithea.model.scm
16 ~~~~~~~~~~~~~~~~~~~
16 ~~~~~~~~~~~~~~~~~~~
17
17
18 Scm model for Kallithea
18 Scm 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 import os
28 import os
29 import sys
29 import sys
30 import posixpath
30 import posixpath
31 import re
31 import re
32 import time
32 import time
33 import traceback
33 import traceback
34 import logging
34 import logging
35 import cStringIO
35 import cStringIO
36 import pkg_resources
36 import pkg_resources
37
37
38 from sqlalchemy import func
38 from sqlalchemy import func
39 from pylons.i18n.translation import _
39 from pylons.i18n.translation import _
40
40
41 import kallithea
41 import kallithea
42 from kallithea.lib.vcs import get_backend
42 from kallithea.lib.vcs import get_backend
43 from kallithea.lib.vcs.exceptions import RepositoryError
43 from kallithea.lib.vcs.exceptions import RepositoryError
44 from kallithea.lib.vcs.utils.lazy import LazyProperty
44 from kallithea.lib.vcs.utils.lazy import LazyProperty
45 from kallithea.lib.vcs.nodes import FileNode
45 from kallithea.lib.vcs.nodes import FileNode
46 from kallithea.lib.vcs.backends.base import EmptyChangeset
46 from kallithea.lib.vcs.backends.base import EmptyChangeset
47
47
48 from kallithea import BACKENDS
48 from kallithea import BACKENDS
49 from kallithea.lib import helpers as h
49 from kallithea.lib import helpers as h
50 from kallithea.lib.utils2 import safe_str, safe_unicode, get_server_url, \
50 from kallithea.lib.utils2 import safe_str, safe_unicode, get_server_url, \
51 _set_extras
51 _set_extras
52 from kallithea.lib.auth import HasRepoPermissionAny, HasRepoGroupPermissionAny, \
52 from kallithea.lib.auth import HasRepoPermissionLevel, HasRepoGroupPermissionAny, \
53 HasUserGroupPermissionAny, HasPermissionAny, HasPermissionAny
53 HasUserGroupPermissionAny, HasPermissionAny, HasPermissionAny
54 from kallithea.lib.utils import get_filesystem_repos, make_ui, \
54 from kallithea.lib.utils import get_filesystem_repos, make_ui, \
55 action_logger
55 action_logger
56 from kallithea.model.base import BaseModel
56 from kallithea.model.base import BaseModel
57 from kallithea.model.db import Repository, Ui, CacheInvalidation, \
57 from kallithea.model.db import Repository, Ui, CacheInvalidation, \
58 UserFollowing, UserLog, User, RepoGroup, PullRequest
58 UserFollowing, UserLog, User, RepoGroup, PullRequest
59 from kallithea.lib.hooks import log_push_action
59 from kallithea.lib.hooks import log_push_action
60 from kallithea.lib.exceptions import NonRelativePathError, IMCCommitError
60 from kallithea.lib.exceptions import NonRelativePathError, IMCCommitError
61
61
62 log = logging.getLogger(__name__)
62 log = logging.getLogger(__name__)
63
63
64
64
65 class UserTemp(object):
65 class UserTemp(object):
66 def __init__(self, user_id):
66 def __init__(self, user_id):
67 self.user_id = user_id
67 self.user_id = user_id
68
68
69 def __repr__(self):
69 def __repr__(self):
70 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
70 return "<%s('id:%s')>" % (self.__class__.__name__, self.user_id)
71
71
72
72
73 class RepoTemp(object):
73 class RepoTemp(object):
74 def __init__(self, repo_id):
74 def __init__(self, repo_id):
75 self.repo_id = repo_id
75 self.repo_id = repo_id
76
76
77 def __repr__(self):
77 def __repr__(self):
78 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
78 return "<%s('id:%s')>" % (self.__class__.__name__, self.repo_id)
79
79
80
80
81 class _PermCheckIterator(object):
81 class _PermCheckIterator(object):
82 def __init__(self, obj_list, obj_attr, perm_set, perm_checker, extra_kwargs=None):
82 def __init__(self, obj_list, obj_attr, perm_set, perm_checker, extra_kwargs=None):
83 """
83 """
84 Creates iterator from given list of objects, additionally
84 Creates iterator from given list of objects, additionally
85 checking permission for them from perm_set var
85 checking permission for them from perm_set var
86
86
87 :param obj_list: list of db objects
87 :param obj_list: list of db objects
88 :param obj_attr: attribute of object to pass into perm_checker
88 :param obj_attr: attribute of object to pass into perm_checker
89 :param perm_set: list of permissions to check
89 :param perm_set: list of permissions to check
90 :param perm_checker: callable to check permissions against
90 :param perm_checker: callable to check permissions against
91 """
91 """
92 self.obj_list = obj_list
92 self.obj_list = obj_list
93 self.obj_attr = obj_attr
93 self.obj_attr = obj_attr
94 self.perm_set = perm_set
94 self.perm_set = perm_set
95 self.perm_checker = perm_checker
95 self.perm_checker = perm_checker
96 self.extra_kwargs = extra_kwargs or {}
96 self.extra_kwargs = extra_kwargs or {}
97
97
98 def __len__(self):
98 def __len__(self):
99 return len(self.obj_list)
99 return len(self.obj_list)
100
100
101 def __repr__(self):
101 def __repr__(self):
102 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
102 return '<%s (%s)>' % (self.__class__.__name__, self.__len__())
103
103
104 def __iter__(self):
104 def __iter__(self):
105 for db_obj in self.obj_list:
105 for db_obj in self.obj_list:
106 # check permission at this level
106 # check permission at this level
107 name = getattr(db_obj, self.obj_attr, None)
107 name = getattr(db_obj, self.obj_attr, None)
108 if not self.perm_checker(*self.perm_set)(
108 if not self.perm_checker(*self.perm_set)(
109 name, self.__class__.__name__, **self.extra_kwargs):
109 name, self.__class__.__name__, **self.extra_kwargs):
110 continue
110 continue
111
111
112 yield db_obj
112 yield db_obj
113
113
114
114
115 class RepoList(_PermCheckIterator):
115 class RepoList(_PermCheckIterator):
116
116
117 def __init__(self, db_repo_list, perm_set=None, extra_kwargs=None):
117 def __init__(self, db_repo_list, perm_level, extra_kwargs=None):
118 if not perm_set:
119 perm_set = ['repository.read', 'repository.write', 'repository.admin']
120
121 super(RepoList, self).__init__(obj_list=db_repo_list,
118 super(RepoList, self).__init__(obj_list=db_repo_list,
122 obj_attr='repo_name', perm_set=perm_set,
119 obj_attr='repo_name', perm_set=[perm_level],
123 perm_checker=HasRepoPermissionAny,
120 perm_checker=HasRepoPermissionLevel,
124 extra_kwargs=extra_kwargs)
121 extra_kwargs=extra_kwargs)
125
122
126
123
127 class RepoGroupList(_PermCheckIterator):
124 class RepoGroupList(_PermCheckIterator):
128
125
129 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
126 def __init__(self, db_repo_group_list, perm_set=None, extra_kwargs=None):
130 if not perm_set:
127 if not perm_set:
131 perm_set = ['group.read', 'group.write', 'group.admin']
128 perm_set = ['group.read', 'group.write', 'group.admin']
132
129
133 super(RepoGroupList, self).__init__(obj_list=db_repo_group_list,
130 super(RepoGroupList, self).__init__(obj_list=db_repo_group_list,
134 obj_attr='group_name', perm_set=perm_set,
131 obj_attr='group_name', perm_set=perm_set,
135 perm_checker=HasRepoGroupPermissionAny,
132 perm_checker=HasRepoGroupPermissionAny,
136 extra_kwargs=extra_kwargs)
133 extra_kwargs=extra_kwargs)
137
134
138
135
139 class UserGroupList(_PermCheckIterator):
136 class UserGroupList(_PermCheckIterator):
140
137
141 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
138 def __init__(self, db_user_group_list, perm_set=None, extra_kwargs=None):
142 if not perm_set:
139 if not perm_set:
143 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
140 perm_set = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
144
141
145 super(UserGroupList, self).__init__(obj_list=db_user_group_list,
142 super(UserGroupList, self).__init__(obj_list=db_user_group_list,
146 obj_attr='users_group_name', perm_set=perm_set,
143 obj_attr='users_group_name', perm_set=perm_set,
147 perm_checker=HasUserGroupPermissionAny,
144 perm_checker=HasUserGroupPermissionAny,
148 extra_kwargs=extra_kwargs)
145 extra_kwargs=extra_kwargs)
149
146
150
147
151 class ScmModel(BaseModel):
148 class ScmModel(BaseModel):
152 """
149 """
153 Generic Scm Model
150 Generic Scm Model
154 """
151 """
155
152
156 def __get_repo(self, instance):
153 def __get_repo(self, instance):
157 cls = Repository
154 cls = Repository
158 if isinstance(instance, cls):
155 if isinstance(instance, cls):
159 return instance
156 return instance
160 elif isinstance(instance, int) or safe_str(instance).isdigit():
157 elif isinstance(instance, int) or safe_str(instance).isdigit():
161 return cls.get(instance)
158 return cls.get(instance)
162 elif isinstance(instance, basestring):
159 elif isinstance(instance, basestring):
163 return cls.get_by_repo_name(instance)
160 return cls.get_by_repo_name(instance)
164 elif instance is not None:
161 elif instance is not None:
165 raise Exception('given object must be int, basestr or Instance'
162 raise Exception('given object must be int, basestr or Instance'
166 ' of %s got %s' % (type(cls), type(instance)))
163 ' of %s got %s' % (type(cls), type(instance)))
167
164
168 @LazyProperty
165 @LazyProperty
169 def repos_path(self):
166 def repos_path(self):
170 """
167 """
171 Gets the repositories root path from database
168 Gets the repositories root path from database
172 """
169 """
173
170
174 q = self.sa.query(Ui).filter(Ui.ui_key == '/').one()
171 q = self.sa.query(Ui).filter(Ui.ui_key == '/').one()
175
172
176 return q.ui_value
173 return q.ui_value
177
174
178 def repo_scan(self, repos_path=None):
175 def repo_scan(self, repos_path=None):
179 """
176 """
180 Listing of repositories in given path. This path should not be a
177 Listing of repositories in given path. This path should not be a
181 repository itself. Return a dictionary of repository objects
178 repository itself. Return a dictionary of repository objects
182
179
183 :param repos_path: path to directory containing repositories
180 :param repos_path: path to directory containing repositories
184 """
181 """
185
182
186 if repos_path is None:
183 if repos_path is None:
187 repos_path = self.repos_path
184 repos_path = self.repos_path
188
185
189 log.info('scanning for repositories in %s', repos_path)
186 log.info('scanning for repositories in %s', repos_path)
190
187
191 baseui = make_ui('db')
188 baseui = make_ui('db')
192 repos = {}
189 repos = {}
193
190
194 for name, path in get_filesystem_repos(repos_path):
191 for name, path in get_filesystem_repos(repos_path):
195 # name need to be decomposed and put back together using the /
192 # name need to be decomposed and put back together using the /
196 # since this is internal storage separator for kallithea
193 # since this is internal storage separator for kallithea
197 name = Repository.normalize_repo_name(name)
194 name = Repository.normalize_repo_name(name)
198
195
199 try:
196 try:
200 if name in repos:
197 if name in repos:
201 raise RepositoryError('Duplicate repository name %s '
198 raise RepositoryError('Duplicate repository name %s '
202 'found in %s' % (name, path))
199 'found in %s' % (name, path))
203 else:
200 else:
204
201
205 klass = get_backend(path[0])
202 klass = get_backend(path[0])
206
203
207 if path[0] == 'hg' and path[0] in BACKENDS.keys():
204 if path[0] == 'hg' and path[0] in BACKENDS.keys():
208 repos[name] = klass(safe_str(path[1]), baseui=baseui)
205 repos[name] = klass(safe_str(path[1]), baseui=baseui)
209
206
210 if path[0] == 'git' and path[0] in BACKENDS.keys():
207 if path[0] == 'git' and path[0] in BACKENDS.keys():
211 repos[name] = klass(path[1])
208 repos[name] = klass(path[1])
212 except OSError:
209 except OSError:
213 continue
210 continue
214 log.debug('found %s paths with repositories', len(repos))
211 log.debug('found %s paths with repositories', len(repos))
215 return repos
212 return repos
216
213
217 def get_repos(self, repos):
214 def get_repos(self, repos):
218 """Return the repos the user has access to"""
215 """Return the repos the user has access to"""
219 return RepoList(repos)
216 return RepoList(repos, perm_level='read')
220
217
221 def get_repo_groups(self, groups=None):
218 def get_repo_groups(self, groups=None):
222 """Return the repo groups the user has access to
219 """Return the repo groups the user has access to
223 If no groups are specified, use top level groups.
220 If no groups are specified, use top level groups.
224 """
221 """
225 if groups is None:
222 if groups is None:
226 groups = RepoGroup.query() \
223 groups = RepoGroup.query() \
227 .filter(RepoGroup.parent_group_id == None).all()
224 .filter(RepoGroup.parent_group_id == None).all()
228 return RepoGroupList(groups)
225 return RepoGroupList(groups)
229
226
230 def mark_for_invalidation(self, repo_name):
227 def mark_for_invalidation(self, repo_name):
231 """
228 """
232 Mark caches of this repo invalid in the database.
229 Mark caches of this repo invalid in the database.
233
230
234 :param repo_name: the repo for which caches should be marked invalid
231 :param repo_name: the repo for which caches should be marked invalid
235 """
232 """
236 CacheInvalidation.set_invalidate(repo_name)
233 CacheInvalidation.set_invalidate(repo_name)
237 repo = Repository.get_by_repo_name(repo_name)
234 repo = Repository.get_by_repo_name(repo_name)
238 if repo is not None:
235 if repo is not None:
239 repo.update_changeset_cache()
236 repo.update_changeset_cache()
240
237
241 def toggle_following_repo(self, follow_repo_id, user_id):
238 def toggle_following_repo(self, follow_repo_id, user_id):
242
239
243 f = self.sa.query(UserFollowing) \
240 f = self.sa.query(UserFollowing) \
244 .filter(UserFollowing.follows_repository_id == follow_repo_id) \
241 .filter(UserFollowing.follows_repository_id == follow_repo_id) \
245 .filter(UserFollowing.user_id == user_id).scalar()
242 .filter(UserFollowing.user_id == user_id).scalar()
246
243
247 if f is not None:
244 if f is not None:
248 try:
245 try:
249 self.sa.delete(f)
246 self.sa.delete(f)
250 action_logger(UserTemp(user_id),
247 action_logger(UserTemp(user_id),
251 'stopped_following_repo',
248 'stopped_following_repo',
252 RepoTemp(follow_repo_id))
249 RepoTemp(follow_repo_id))
253 return
250 return
254 except Exception:
251 except Exception:
255 log.error(traceback.format_exc())
252 log.error(traceback.format_exc())
256 raise
253 raise
257
254
258 try:
255 try:
259 f = UserFollowing()
256 f = UserFollowing()
260 f.user_id = user_id
257 f.user_id = user_id
261 f.follows_repository_id = follow_repo_id
258 f.follows_repository_id = follow_repo_id
262 self.sa.add(f)
259 self.sa.add(f)
263
260
264 action_logger(UserTemp(user_id),
261 action_logger(UserTemp(user_id),
265 'started_following_repo',
262 'started_following_repo',
266 RepoTemp(follow_repo_id))
263 RepoTemp(follow_repo_id))
267 except Exception:
264 except Exception:
268 log.error(traceback.format_exc())
265 log.error(traceback.format_exc())
269 raise
266 raise
270
267
271 def toggle_following_user(self, follow_user_id, user_id):
268 def toggle_following_user(self, follow_user_id, user_id):
272 f = self.sa.query(UserFollowing) \
269 f = self.sa.query(UserFollowing) \
273 .filter(UserFollowing.follows_user_id == follow_user_id) \
270 .filter(UserFollowing.follows_user_id == follow_user_id) \
274 .filter(UserFollowing.user_id == user_id).scalar()
271 .filter(UserFollowing.user_id == user_id).scalar()
275
272
276 if f is not None:
273 if f is not None:
277 try:
274 try:
278 self.sa.delete(f)
275 self.sa.delete(f)
279 return
276 return
280 except Exception:
277 except Exception:
281 log.error(traceback.format_exc())
278 log.error(traceback.format_exc())
282 raise
279 raise
283
280
284 try:
281 try:
285 f = UserFollowing()
282 f = UserFollowing()
286 f.user_id = user_id
283 f.user_id = user_id
287 f.follows_user_id = follow_user_id
284 f.follows_user_id = follow_user_id
288 self.sa.add(f)
285 self.sa.add(f)
289 except Exception:
286 except Exception:
290 log.error(traceback.format_exc())
287 log.error(traceback.format_exc())
291 raise
288 raise
292
289
293 def is_following_repo(self, repo_name, user_id, cache=False):
290 def is_following_repo(self, repo_name, user_id, cache=False):
294 r = self.sa.query(Repository) \
291 r = self.sa.query(Repository) \
295 .filter(Repository.repo_name == repo_name).scalar()
292 .filter(Repository.repo_name == repo_name).scalar()
296
293
297 f = self.sa.query(UserFollowing) \
294 f = self.sa.query(UserFollowing) \
298 .filter(UserFollowing.follows_repository == r) \
295 .filter(UserFollowing.follows_repository == r) \
299 .filter(UserFollowing.user_id == user_id).scalar()
296 .filter(UserFollowing.user_id == user_id).scalar()
300
297
301 return f is not None
298 return f is not None
302
299
303 def is_following_user(self, username, user_id, cache=False):
300 def is_following_user(self, username, user_id, cache=False):
304 u = User.get_by_username(username)
301 u = User.get_by_username(username)
305
302
306 f = self.sa.query(UserFollowing) \
303 f = self.sa.query(UserFollowing) \
307 .filter(UserFollowing.follows_user == u) \
304 .filter(UserFollowing.follows_user == u) \
308 .filter(UserFollowing.user_id == user_id).scalar()
305 .filter(UserFollowing.user_id == user_id).scalar()
309
306
310 return f is not None
307 return f is not None
311
308
312 def get_followers(self, repo):
309 def get_followers(self, repo):
313 repo = Repository.guess_instance(repo)
310 repo = Repository.guess_instance(repo)
314
311
315 return self.sa.query(UserFollowing) \
312 return self.sa.query(UserFollowing) \
316 .filter(UserFollowing.follows_repository == repo).count()
313 .filter(UserFollowing.follows_repository == repo).count()
317
314
318 def get_forks(self, repo):
315 def get_forks(self, repo):
319 repo = Repository.guess_instance(repo)
316 repo = Repository.guess_instance(repo)
320 return self.sa.query(Repository) \
317 return self.sa.query(Repository) \
321 .filter(Repository.fork == repo).count()
318 .filter(Repository.fork == repo).count()
322
319
323 def get_pull_requests(self, repo):
320 def get_pull_requests(self, repo):
324 repo = Repository.guess_instance(repo)
321 repo = Repository.guess_instance(repo)
325 return self.sa.query(PullRequest) \
322 return self.sa.query(PullRequest) \
326 .filter(PullRequest.other_repo == repo) \
323 .filter(PullRequest.other_repo == repo) \
327 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
324 .filter(PullRequest.status != PullRequest.STATUS_CLOSED).count()
328
325
329 def mark_as_fork(self, repo, fork, user):
326 def mark_as_fork(self, repo, fork, user):
330 repo = self.__get_repo(repo)
327 repo = self.__get_repo(repo)
331 fork = self.__get_repo(fork)
328 fork = self.__get_repo(fork)
332 if fork and repo.repo_id == fork.repo_id:
329 if fork and repo.repo_id == fork.repo_id:
333 raise Exception("Cannot set repository as fork of itself")
330 raise Exception("Cannot set repository as fork of itself")
334
331
335 if fork and repo.repo_type != fork.repo_type:
332 if fork and repo.repo_type != fork.repo_type:
336 raise RepositoryError("Cannot set repository as fork of repository with other type")
333 raise RepositoryError("Cannot set repository as fork of repository with other type")
337
334
338 repo.fork = fork
335 repo.fork = fork
339 self.sa.add(repo)
336 self.sa.add(repo)
340 return repo
337 return repo
341
338
342 def _handle_rc_scm_extras(self, username, repo_name, repo_alias,
339 def _handle_rc_scm_extras(self, username, repo_name, repo_alias,
343 action=None):
340 action=None):
344 from kallithea import CONFIG
341 from kallithea import CONFIG
345 from kallithea.lib.base import _get_ip_addr
342 from kallithea.lib.base import _get_ip_addr
346 try:
343 try:
347 from pylons import request
344 from pylons import request
348 environ = request.environ
345 environ = request.environ
349 except TypeError:
346 except TypeError:
350 # we might use this outside of request context, let's fake the
347 # we might use this outside of request context, let's fake the
351 # environ data
348 # environ data
352 from webob import Request
349 from webob import Request
353 environ = Request.blank('').environ
350 environ = Request.blank('').environ
354 extras = {
351 extras = {
355 'ip': _get_ip_addr(environ),
352 'ip': _get_ip_addr(environ),
356 'username': username,
353 'username': username,
357 'action': action or 'push_local',
354 'action': action or 'push_local',
358 'repository': repo_name,
355 'repository': repo_name,
359 'scm': repo_alias,
356 'scm': repo_alias,
360 'config': CONFIG['__file__'],
357 'config': CONFIG['__file__'],
361 'server_url': get_server_url(environ),
358 'server_url': get_server_url(environ),
362 'make_lock': None,
359 'make_lock': None,
363 'locked_by': [None, None]
360 'locked_by': [None, None]
364 }
361 }
365 _set_extras(extras)
362 _set_extras(extras)
366
363
367 def _handle_push(self, repo, username, action, repo_name, revisions):
364 def _handle_push(self, repo, username, action, repo_name, revisions):
368 """
365 """
369 Triggers push action hooks
366 Triggers push action hooks
370
367
371 :param repo: SCM repo
368 :param repo: SCM repo
372 :param username: username who pushes
369 :param username: username who pushes
373 :param action: push/push_local/push_remote
370 :param action: push/push_local/push_remote
374 :param repo_name: name of repo
371 :param repo_name: name of repo
375 :param revisions: list of revisions that we pushed
372 :param revisions: list of revisions that we pushed
376 """
373 """
377 self._handle_rc_scm_extras(username, repo_name, repo_alias=repo.alias)
374 self._handle_rc_scm_extras(username, repo_name, repo_alias=repo.alias)
378 _scm_repo = repo._repo
375 _scm_repo = repo._repo
379 # trigger push hook
376 # trigger push hook
380 if repo.alias == 'hg':
377 if repo.alias == 'hg':
381 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
378 log_push_action(_scm_repo.ui, _scm_repo, node=revisions[0])
382 elif repo.alias == 'git':
379 elif repo.alias == 'git':
383 log_push_action(None, _scm_repo, _git_revs=revisions)
380 log_push_action(None, _scm_repo, _git_revs=revisions)
384
381
385 def _get_IMC_module(self, scm_type):
382 def _get_IMC_module(self, scm_type):
386 """
383 """
387 Returns InMemoryCommit class based on scm_type
384 Returns InMemoryCommit class based on scm_type
388
385
389 :param scm_type:
386 :param scm_type:
390 """
387 """
391 if scm_type == 'hg':
388 if scm_type == 'hg':
392 from kallithea.lib.vcs.backends.hg import MercurialInMemoryChangeset
389 from kallithea.lib.vcs.backends.hg import MercurialInMemoryChangeset
393 return MercurialInMemoryChangeset
390 return MercurialInMemoryChangeset
394
391
395 if scm_type == 'git':
392 if scm_type == 'git':
396 from kallithea.lib.vcs.backends.git import GitInMemoryChangeset
393 from kallithea.lib.vcs.backends.git import GitInMemoryChangeset
397 return GitInMemoryChangeset
394 return GitInMemoryChangeset
398
395
399 raise Exception('Invalid scm_type, must be one of hg,git got %s'
396 raise Exception('Invalid scm_type, must be one of hg,git got %s'
400 % (scm_type,))
397 % (scm_type,))
401
398
402 def pull_changes(self, repo, username):
399 def pull_changes(self, repo, username):
403 """
400 """
404 Pull from "clone URL".
401 Pull from "clone URL".
405 """
402 """
406 dbrepo = self.__get_repo(repo)
403 dbrepo = self.__get_repo(repo)
407 clone_uri = dbrepo.clone_uri
404 clone_uri = dbrepo.clone_uri
408 if not clone_uri:
405 if not clone_uri:
409 raise Exception("This repository doesn't have a clone uri")
406 raise Exception("This repository doesn't have a clone uri")
410
407
411 repo = dbrepo.scm_instance
408 repo = dbrepo.scm_instance
412 repo_name = dbrepo.repo_name
409 repo_name = dbrepo.repo_name
413 try:
410 try:
414 if repo.alias == 'git':
411 if repo.alias == 'git':
415 repo.fetch(clone_uri)
412 repo.fetch(clone_uri)
416 # git doesn't really have something like post-fetch action
413 # git doesn't really have something like post-fetch action
417 # we fake that now. #TODO: extract fetched revisions somehow
414 # we fake that now. #TODO: extract fetched revisions somehow
418 # here
415 # here
419 self._handle_push(repo,
416 self._handle_push(repo,
420 username=username,
417 username=username,
421 action='push_remote',
418 action='push_remote',
422 repo_name=repo_name,
419 repo_name=repo_name,
423 revisions=[])
420 revisions=[])
424 else:
421 else:
425 self._handle_rc_scm_extras(username, dbrepo.repo_name,
422 self._handle_rc_scm_extras(username, dbrepo.repo_name,
426 repo.alias, action='push_remote')
423 repo.alias, action='push_remote')
427 repo.pull(clone_uri)
424 repo.pull(clone_uri)
428
425
429 self.mark_for_invalidation(repo_name)
426 self.mark_for_invalidation(repo_name)
430 except Exception:
427 except Exception:
431 log.error(traceback.format_exc())
428 log.error(traceback.format_exc())
432 raise
429 raise
433
430
434 def commit_change(self, repo, repo_name, cs, user, author, message,
431 def commit_change(self, repo, repo_name, cs, user, author, message,
435 content, f_path):
432 content, f_path):
436 """
433 """
437 Commit a change to a single file
434 Commit a change to a single file
438
435
439 :param repo: a db_repo.scm_instance
436 :param repo: a db_repo.scm_instance
440 """
437 """
441 user = User.guess_instance(user)
438 user = User.guess_instance(user)
442 IMC = self._get_IMC_module(repo.alias)
439 IMC = self._get_IMC_module(repo.alias)
443
440
444 # decoding here will force that we have proper encoded values
441 # decoding here will force that we have proper encoded values
445 # in any other case this will throw exceptions and deny commit
442 # in any other case this will throw exceptions and deny commit
446 content = safe_str(content)
443 content = safe_str(content)
447 path = safe_str(f_path)
444 path = safe_str(f_path)
448 # message and author needs to be unicode
445 # message and author needs to be unicode
449 # proper backend should then translate that into required type
446 # proper backend should then translate that into required type
450 message = safe_unicode(message)
447 message = safe_unicode(message)
451 author = safe_unicode(author)
448 author = safe_unicode(author)
452 imc = IMC(repo)
449 imc = IMC(repo)
453 imc.change(FileNode(path, content, mode=cs.get_file_mode(f_path)))
450 imc.change(FileNode(path, content, mode=cs.get_file_mode(f_path)))
454 try:
451 try:
455 tip = imc.commit(message=message, author=author,
452 tip = imc.commit(message=message, author=author,
456 parents=[cs], branch=cs.branch)
453 parents=[cs], branch=cs.branch)
457 except Exception as e:
454 except Exception as e:
458 log.error(traceback.format_exc())
455 log.error(traceback.format_exc())
459 raise IMCCommitError(str(e))
456 raise IMCCommitError(str(e))
460 finally:
457 finally:
461 # always clear caches, if commit fails we want fresh object also
458 # always clear caches, if commit fails we want fresh object also
462 self.mark_for_invalidation(repo_name)
459 self.mark_for_invalidation(repo_name)
463 self._handle_push(repo,
460 self._handle_push(repo,
464 username=user.username,
461 username=user.username,
465 action='push_local',
462 action='push_local',
466 repo_name=repo_name,
463 repo_name=repo_name,
467 revisions=[tip.raw_id])
464 revisions=[tip.raw_id])
468 return tip
465 return tip
469
466
470 def _sanitize_path(self, f_path):
467 def _sanitize_path(self, f_path):
471 if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path:
468 if f_path.startswith('/') or f_path.startswith('.') or '../' in f_path:
472 raise NonRelativePathError('%s is not an relative path' % f_path)
469 raise NonRelativePathError('%s is not an relative path' % f_path)
473 if f_path:
470 if f_path:
474 f_path = posixpath.normpath(f_path)
471 f_path = posixpath.normpath(f_path)
475 return f_path
472 return f_path
476
473
477 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
474 def get_nodes(self, repo_name, revision, root_path='/', flat=True):
478 """
475 """
479 Recursively walk root dir and return a set of all paths found.
476 Recursively walk root dir and return a set of all paths found.
480
477
481 :param repo_name: name of repository
478 :param repo_name: name of repository
482 :param revision: revision for which to list nodes
479 :param revision: revision for which to list nodes
483 :param root_path: root path to list
480 :param root_path: root path to list
484 :param flat: return as a list, if False returns a dict with description
481 :param flat: return as a list, if False returns a dict with description
485
482
486 """
483 """
487 _files = list()
484 _files = list()
488 _dirs = list()
485 _dirs = list()
489 try:
486 try:
490 _repo = self.__get_repo(repo_name)
487 _repo = self.__get_repo(repo_name)
491 changeset = _repo.scm_instance.get_changeset(revision)
488 changeset = _repo.scm_instance.get_changeset(revision)
492 root_path = root_path.lstrip('/')
489 root_path = root_path.lstrip('/')
493 for topnode, dirs, files in changeset.walk(root_path):
490 for topnode, dirs, files in changeset.walk(root_path):
494 for f in files:
491 for f in files:
495 _files.append(f.path if flat else {"name": f.path,
492 _files.append(f.path if flat else {"name": f.path,
496 "type": "file"})
493 "type": "file"})
497 for d in dirs:
494 for d in dirs:
498 _dirs.append(d.path if flat else {"name": d.path,
495 _dirs.append(d.path if flat else {"name": d.path,
499 "type": "dir"})
496 "type": "dir"})
500 except RepositoryError:
497 except RepositoryError:
501 log.debug(traceback.format_exc())
498 log.debug(traceback.format_exc())
502 raise
499 raise
503
500
504 return _dirs, _files
501 return _dirs, _files
505
502
506 def create_nodes(self, user, repo, message, nodes, parent_cs=None,
503 def create_nodes(self, user, repo, message, nodes, parent_cs=None,
507 author=None, trigger_push_hook=True):
504 author=None, trigger_push_hook=True):
508 """
505 """
509 Commits specified nodes to repo.
506 Commits specified nodes to repo.
510
507
511 :param user: Kallithea User object or user_id, the committer
508 :param user: Kallithea User object or user_id, the committer
512 :param repo: Kallithea Repository object
509 :param repo: Kallithea Repository object
513 :param message: commit message
510 :param message: commit message
514 :param nodes: mapping {filename:{'content':content},...}
511 :param nodes: mapping {filename:{'content':content},...}
515 :param parent_cs: parent changeset, can be empty than it's initial commit
512 :param parent_cs: parent changeset, can be empty than it's initial commit
516 :param author: author of commit, cna be different that committer only for git
513 :param author: author of commit, cna be different that committer only for git
517 :param trigger_push_hook: trigger push hooks
514 :param trigger_push_hook: trigger push hooks
518
515
519 :returns: new committed changeset
516 :returns: new committed changeset
520 """
517 """
521
518
522 user = User.guess_instance(user)
519 user = User.guess_instance(user)
523 scm_instance = repo.scm_instance_no_cache()
520 scm_instance = repo.scm_instance_no_cache()
524
521
525 processed_nodes = []
522 processed_nodes = []
526 for f_path in nodes:
523 for f_path in nodes:
527 content = nodes[f_path]['content']
524 content = nodes[f_path]['content']
528 f_path = self._sanitize_path(f_path)
525 f_path = self._sanitize_path(f_path)
529 f_path = safe_str(f_path)
526 f_path = safe_str(f_path)
530 # decoding here will force that we have proper encoded values
527 # decoding here will force that we have proper encoded values
531 # in any other case this will throw exceptions and deny commit
528 # in any other case this will throw exceptions and deny commit
532 if isinstance(content, (basestring,)):
529 if isinstance(content, (basestring,)):
533 content = safe_str(content)
530 content = safe_str(content)
534 elif isinstance(content, (file, cStringIO.OutputType,)):
531 elif isinstance(content, (file, cStringIO.OutputType,)):
535 content = content.read()
532 content = content.read()
536 else:
533 else:
537 raise Exception('Content is of unrecognized type %s' % (
534 raise Exception('Content is of unrecognized type %s' % (
538 type(content)
535 type(content)
539 ))
536 ))
540 processed_nodes.append((f_path, content))
537 processed_nodes.append((f_path, content))
541
538
542 message = safe_unicode(message)
539 message = safe_unicode(message)
543 committer = user.full_contact
540 committer = user.full_contact
544 author = safe_unicode(author) if author else committer
541 author = safe_unicode(author) if author else committer
545
542
546 IMC = self._get_IMC_module(scm_instance.alias)
543 IMC = self._get_IMC_module(scm_instance.alias)
547 imc = IMC(scm_instance)
544 imc = IMC(scm_instance)
548
545
549 if not parent_cs:
546 if not parent_cs:
550 parent_cs = EmptyChangeset(alias=scm_instance.alias)
547 parent_cs = EmptyChangeset(alias=scm_instance.alias)
551
548
552 if isinstance(parent_cs, EmptyChangeset):
549 if isinstance(parent_cs, EmptyChangeset):
553 # EmptyChangeset means we we're editing empty repository
550 # EmptyChangeset means we we're editing empty repository
554 parents = None
551 parents = None
555 else:
552 else:
556 parents = [parent_cs]
553 parents = [parent_cs]
557 # add multiple nodes
554 # add multiple nodes
558 for path, content in processed_nodes:
555 for path, content in processed_nodes:
559 imc.add(FileNode(path, content=content))
556 imc.add(FileNode(path, content=content))
560
557
561 tip = imc.commit(message=message,
558 tip = imc.commit(message=message,
562 author=author,
559 author=author,
563 parents=parents,
560 parents=parents,
564 branch=parent_cs.branch)
561 branch=parent_cs.branch)
565
562
566 self.mark_for_invalidation(repo.repo_name)
563 self.mark_for_invalidation(repo.repo_name)
567 if trigger_push_hook:
564 if trigger_push_hook:
568 self._handle_push(scm_instance,
565 self._handle_push(scm_instance,
569 username=user.username,
566 username=user.username,
570 action='push_local',
567 action='push_local',
571 repo_name=repo.repo_name,
568 repo_name=repo.repo_name,
572 revisions=[tip.raw_id])
569 revisions=[tip.raw_id])
573 return tip
570 return tip
574
571
575 def update_nodes(self, user, repo, message, nodes, parent_cs=None,
572 def update_nodes(self, user, repo, message, nodes, parent_cs=None,
576 author=None, trigger_push_hook=True):
573 author=None, trigger_push_hook=True):
577 """
574 """
578 Commits specified nodes to repo. Again.
575 Commits specified nodes to repo. Again.
579 """
576 """
580 user = User.guess_instance(user)
577 user = User.guess_instance(user)
581 scm_instance = repo.scm_instance_no_cache()
578 scm_instance = repo.scm_instance_no_cache()
582
579
583 message = safe_unicode(message)
580 message = safe_unicode(message)
584 committer = user.full_contact
581 committer = user.full_contact
585 author = safe_unicode(author) if author else committer
582 author = safe_unicode(author) if author else committer
586
583
587 imc_class = self._get_IMC_module(scm_instance.alias)
584 imc_class = self._get_IMC_module(scm_instance.alias)
588 imc = imc_class(scm_instance)
585 imc = imc_class(scm_instance)
589
586
590 if not parent_cs:
587 if not parent_cs:
591 parent_cs = EmptyChangeset(alias=scm_instance.alias)
588 parent_cs = EmptyChangeset(alias=scm_instance.alias)
592
589
593 if isinstance(parent_cs, EmptyChangeset):
590 if isinstance(parent_cs, EmptyChangeset):
594 # EmptyChangeset means we we're editing empty repository
591 # EmptyChangeset means we we're editing empty repository
595 parents = None
592 parents = None
596 else:
593 else:
597 parents = [parent_cs]
594 parents = [parent_cs]
598
595
599 # add multiple nodes
596 # add multiple nodes
600 for _filename, data in nodes.items():
597 for _filename, data in nodes.items():
601 # new filename, can be renamed from the old one
598 # new filename, can be renamed from the old one
602 filename = self._sanitize_path(data['filename'])
599 filename = self._sanitize_path(data['filename'])
603 old_filename = self._sanitize_path(_filename)
600 old_filename = self._sanitize_path(_filename)
604 content = data['content']
601 content = data['content']
605
602
606 filenode = FileNode(old_filename, content=content)
603 filenode = FileNode(old_filename, content=content)
607 op = data['op']
604 op = data['op']
608 if op == 'add':
605 if op == 'add':
609 imc.add(filenode)
606 imc.add(filenode)
610 elif op == 'del':
607 elif op == 'del':
611 imc.remove(filenode)
608 imc.remove(filenode)
612 elif op == 'mod':
609 elif op == 'mod':
613 if filename != old_filename:
610 if filename != old_filename:
614 #TODO: handle renames, needs vcs lib changes
611 #TODO: handle renames, needs vcs lib changes
615 imc.remove(filenode)
612 imc.remove(filenode)
616 imc.add(FileNode(filename, content=content))
613 imc.add(FileNode(filename, content=content))
617 else:
614 else:
618 imc.change(filenode)
615 imc.change(filenode)
619
616
620 # commit changes
617 # commit changes
621 tip = imc.commit(message=message,
618 tip = imc.commit(message=message,
622 author=author,
619 author=author,
623 parents=parents,
620 parents=parents,
624 branch=parent_cs.branch)
621 branch=parent_cs.branch)
625
622
626 self.mark_for_invalidation(repo.repo_name)
623 self.mark_for_invalidation(repo.repo_name)
627 if trigger_push_hook:
624 if trigger_push_hook:
628 self._handle_push(scm_instance,
625 self._handle_push(scm_instance,
629 username=user.username,
626 username=user.username,
630 action='push_local',
627 action='push_local',
631 repo_name=repo.repo_name,
628 repo_name=repo.repo_name,
632 revisions=[tip.raw_id])
629 revisions=[tip.raw_id])
633
630
634 def delete_nodes(self, user, repo, message, nodes, parent_cs=None,
631 def delete_nodes(self, user, repo, message, nodes, parent_cs=None,
635 author=None, trigger_push_hook=True):
632 author=None, trigger_push_hook=True):
636 """
633 """
637 Deletes specified nodes from repo.
634 Deletes specified nodes from repo.
638
635
639 :param user: Kallithea User object or user_id, the committer
636 :param user: Kallithea User object or user_id, the committer
640 :param repo: Kallithea Repository object
637 :param repo: Kallithea Repository object
641 :param message: commit message
638 :param message: commit message
642 :param nodes: mapping {filename:{'content':content},...}
639 :param nodes: mapping {filename:{'content':content},...}
643 :param parent_cs: parent changeset, can be empty than it's initial commit
640 :param parent_cs: parent changeset, can be empty than it's initial commit
644 :param author: author of commit, cna be different that committer only for git
641 :param author: author of commit, cna be different that committer only for git
645 :param trigger_push_hook: trigger push hooks
642 :param trigger_push_hook: trigger push hooks
646
643
647 :returns: new committed changeset after deletion
644 :returns: new committed changeset after deletion
648 """
645 """
649
646
650 user = User.guess_instance(user)
647 user = User.guess_instance(user)
651 scm_instance = repo.scm_instance_no_cache()
648 scm_instance = repo.scm_instance_no_cache()
652
649
653 processed_nodes = []
650 processed_nodes = []
654 for f_path in nodes:
651 for f_path in nodes:
655 f_path = self._sanitize_path(f_path)
652 f_path = self._sanitize_path(f_path)
656 # content can be empty but for compatibility it allows same dicts
653 # content can be empty but for compatibility it allows same dicts
657 # structure as add_nodes
654 # structure as add_nodes
658 content = nodes[f_path].get('content')
655 content = nodes[f_path].get('content')
659 processed_nodes.append((f_path, content))
656 processed_nodes.append((f_path, content))
660
657
661 message = safe_unicode(message)
658 message = safe_unicode(message)
662 committer = user.full_contact
659 committer = user.full_contact
663 author = safe_unicode(author) if author else committer
660 author = safe_unicode(author) if author else committer
664
661
665 IMC = self._get_IMC_module(scm_instance.alias)
662 IMC = self._get_IMC_module(scm_instance.alias)
666 imc = IMC(scm_instance)
663 imc = IMC(scm_instance)
667
664
668 if not parent_cs:
665 if not parent_cs:
669 parent_cs = EmptyChangeset(alias=scm_instance.alias)
666 parent_cs = EmptyChangeset(alias=scm_instance.alias)
670
667
671 if isinstance(parent_cs, EmptyChangeset):
668 if isinstance(parent_cs, EmptyChangeset):
672 # EmptyChangeset means we we're editing empty repository
669 # EmptyChangeset means we we're editing empty repository
673 parents = None
670 parents = None
674 else:
671 else:
675 parents = [parent_cs]
672 parents = [parent_cs]
676 # add multiple nodes
673 # add multiple nodes
677 for path, content in processed_nodes:
674 for path, content in processed_nodes:
678 imc.remove(FileNode(path, content=content))
675 imc.remove(FileNode(path, content=content))
679
676
680 tip = imc.commit(message=message,
677 tip = imc.commit(message=message,
681 author=author,
678 author=author,
682 parents=parents,
679 parents=parents,
683 branch=parent_cs.branch)
680 branch=parent_cs.branch)
684
681
685 self.mark_for_invalidation(repo.repo_name)
682 self.mark_for_invalidation(repo.repo_name)
686 if trigger_push_hook:
683 if trigger_push_hook:
687 self._handle_push(scm_instance,
684 self._handle_push(scm_instance,
688 username=user.username,
685 username=user.username,
689 action='push_local',
686 action='push_local',
690 repo_name=repo.repo_name,
687 repo_name=repo.repo_name,
691 revisions=[tip.raw_id])
688 revisions=[tip.raw_id])
692 return tip
689 return tip
693
690
694 def get_unread_journal(self):
691 def get_unread_journal(self):
695 return self.sa.query(UserLog).count()
692 return self.sa.query(UserLog).count()
696
693
697 def get_repo_landing_revs(self, repo=None):
694 def get_repo_landing_revs(self, repo=None):
698 """
695 """
699 Generates select option with tags branches and bookmarks (for hg only)
696 Generates select option with tags branches and bookmarks (for hg only)
700 grouped by type
697 grouped by type
701
698
702 :param repo:
699 :param repo:
703 """
700 """
704
701
705 hist_l = []
702 hist_l = []
706 choices = []
703 choices = []
707 repo = self.__get_repo(repo)
704 repo = self.__get_repo(repo)
708 hist_l.append(['rev:tip', _('latest tip')])
705 hist_l.append(['rev:tip', _('latest tip')])
709 choices.append('rev:tip')
706 choices.append('rev:tip')
710 if repo is None:
707 if repo is None:
711 return choices, hist_l
708 return choices, hist_l
712
709
713 repo = repo.scm_instance
710 repo = repo.scm_instance
714
711
715 branches_group = ([(u'branch:%s' % k, k) for k, v in
712 branches_group = ([(u'branch:%s' % k, k) for k, v in
716 repo.branches.iteritems()], _("Branches"))
713 repo.branches.iteritems()], _("Branches"))
717 hist_l.append(branches_group)
714 hist_l.append(branches_group)
718 choices.extend([x[0] for x in branches_group[0]])
715 choices.extend([x[0] for x in branches_group[0]])
719
716
720 if repo.alias == 'hg':
717 if repo.alias == 'hg':
721 bookmarks_group = ([(u'book:%s' % k, k) for k, v in
718 bookmarks_group = ([(u'book:%s' % k, k) for k, v in
722 repo.bookmarks.iteritems()], _("Bookmarks"))
719 repo.bookmarks.iteritems()], _("Bookmarks"))
723 hist_l.append(bookmarks_group)
720 hist_l.append(bookmarks_group)
724 choices.extend([x[0] for x in bookmarks_group[0]])
721 choices.extend([x[0] for x in bookmarks_group[0]])
725
722
726 tags_group = ([(u'tag:%s' % k, k) for k, v in
723 tags_group = ([(u'tag:%s' % k, k) for k, v in
727 repo.tags.iteritems()], _("Tags"))
724 repo.tags.iteritems()], _("Tags"))
728 hist_l.append(tags_group)
725 hist_l.append(tags_group)
729 choices.extend([x[0] for x in tags_group[0]])
726 choices.extend([x[0] for x in tags_group[0]])
730
727
731 return choices, hist_l
728 return choices, hist_l
732
729
733 def install_git_hooks(self, repo, force_create=False):
730 def install_git_hooks(self, repo, force_create=False):
734 """
731 """
735 Creates a kallithea hook inside a git repository
732 Creates a kallithea hook inside a git repository
736
733
737 :param repo: Instance of VCS repo
734 :param repo: Instance of VCS repo
738 :param force_create: Create even if same name hook exists
735 :param force_create: Create even if same name hook exists
739 """
736 """
740
737
741 loc = os.path.join(repo.path, 'hooks')
738 loc = os.path.join(repo.path, 'hooks')
742 if not repo.bare:
739 if not repo.bare:
743 loc = os.path.join(repo.path, '.git', 'hooks')
740 loc = os.path.join(repo.path, '.git', 'hooks')
744 if not os.path.isdir(loc):
741 if not os.path.isdir(loc):
745 os.makedirs(loc)
742 os.makedirs(loc)
746
743
747 tmpl_post = "#!/usr/bin/env %s\n" % sys.executable or 'python2'
744 tmpl_post = "#!/usr/bin/env %s\n" % sys.executable or 'python2'
748 tmpl_post += pkg_resources.resource_string(
745 tmpl_post += pkg_resources.resource_string(
749 'kallithea', os.path.join('config', 'post_receive_tmpl.py')
746 'kallithea', os.path.join('config', 'post_receive_tmpl.py')
750 )
747 )
751 tmpl_pre = "#!/usr/bin/env %s\n" % sys.executable or 'python2'
748 tmpl_pre = "#!/usr/bin/env %s\n" % sys.executable or 'python2'
752 tmpl_pre += pkg_resources.resource_string(
749 tmpl_pre += pkg_resources.resource_string(
753 'kallithea', os.path.join('config', 'pre_receive_tmpl.py')
750 'kallithea', os.path.join('config', 'pre_receive_tmpl.py')
754 )
751 )
755
752
756 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
753 for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
757 _hook_file = os.path.join(loc, '%s-receive' % h_type)
754 _hook_file = os.path.join(loc, '%s-receive' % h_type)
758 has_hook = False
755 has_hook = False
759 log.debug('Installing git hook in repo %s', repo)
756 log.debug('Installing git hook in repo %s', repo)
760 if os.path.exists(_hook_file):
757 if os.path.exists(_hook_file):
761 # let's take a look at this hook, maybe it's kallithea ?
758 # let's take a look at this hook, maybe it's kallithea ?
762 log.debug('hook exists, checking if it is from kallithea')
759 log.debug('hook exists, checking if it is from kallithea')
763 with open(_hook_file, 'rb') as f:
760 with open(_hook_file, 'rb') as f:
764 data = f.read()
761 data = f.read()
765 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
762 matches = re.compile(r'(?:%s)\s*=\s*(.*)'
766 % 'KALLITHEA_HOOK_VER').search(data)
763 % 'KALLITHEA_HOOK_VER').search(data)
767 if matches:
764 if matches:
768 try:
765 try:
769 ver = matches.groups()[0]
766 ver = matches.groups()[0]
770 log.debug('got %s it is kallithea', ver)
767 log.debug('got %s it is kallithea', ver)
771 has_hook = True
768 has_hook = True
772 except Exception:
769 except Exception:
773 log.error(traceback.format_exc())
770 log.error(traceback.format_exc())
774 else:
771 else:
775 # there is no hook in this dir, so we want to create one
772 # there is no hook in this dir, so we want to create one
776 has_hook = True
773 has_hook = True
777
774
778 if has_hook or force_create:
775 if has_hook or force_create:
779 log.debug('writing %s hook file !', h_type)
776 log.debug('writing %s hook file !', h_type)
780 try:
777 try:
781 with open(_hook_file, 'wb') as f:
778 with open(_hook_file, 'wb') as f:
782 tmpl = tmpl.replace('_TMPL_', kallithea.__version__)
779 tmpl = tmpl.replace('_TMPL_', kallithea.__version__)
783 f.write(tmpl)
780 f.write(tmpl)
784 os.chmod(_hook_file, 0755)
781 os.chmod(_hook_file, 0755)
785 except IOError as e:
782 except IOError as e:
786 log.error('error writing %s: %s', _hook_file, e)
783 log.error('error writing %s: %s', _hook_file, e)
787 else:
784 else:
788 log.debug('skipping writing hook file')
785 log.debug('skipping writing hook file')
789
786
790 def AvailableRepoGroupChoices(top_perms, repo_group_perms, extras=()):
787 def AvailableRepoGroupChoices(top_perms, repo_group_perms, extras=()):
791 """Return group_id,string tuples with choices for all the repo groups where
788 """Return group_id,string tuples with choices for all the repo groups where
792 the user has the necessary permissions.
789 the user has the necessary permissions.
793
790
794 Top level is -1.
791 Top level is -1.
795 """
792 """
796 groups = RepoGroup.query().all()
793 groups = RepoGroup.query().all()
797 if HasPermissionAny('hg.admin')('available repo groups'):
794 if HasPermissionAny('hg.admin')('available repo groups'):
798 groups.append(None)
795 groups.append(None)
799 else:
796 else:
800 groups = list(RepoGroupList(groups, perm_set=repo_group_perms))
797 groups = list(RepoGroupList(groups, perm_set=repo_group_perms))
801 if top_perms and HasPermissionAny(*top_perms)('available repo groups'):
798 if top_perms and HasPermissionAny(*top_perms)('available repo groups'):
802 groups.append(None)
799 groups.append(None)
803 for extra in extras:
800 for extra in extras:
804 if not any(rg == extra for rg in groups):
801 if not any(rg == extra for rg in groups):
805 groups.append(extra)
802 groups.append(extra)
806 return RepoGroup.groups_choices(groups=groups)
803 return RepoGroup.groups_choices(groups=groups)
@@ -1,538 +1,538 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 <%inherit file="root.html"/>
2 <%inherit file="root.html"/>
3
3
4 <!-- CONTENT -->
4 <!-- CONTENT -->
5 <div id="content">
5 <div id="content">
6 ${self.flash_msg()}
6 ${self.flash_msg()}
7 <div id="main">
7 <div id="main">
8 ${next.main()}
8 ${next.main()}
9 </div>
9 </div>
10 </div>
10 </div>
11 <!-- END CONTENT -->
11 <!-- END CONTENT -->
12
12
13 <!-- FOOTER -->
13 <!-- FOOTER -->
14 <div id="footer" class="footer navbar navbar-inverse">
14 <div id="footer" class="footer navbar navbar-inverse">
15 <span class="navbar-text pull-left">
15 <span class="navbar-text pull-left">
16 ${_('Server instance: %s') % c.instance_id if c.instance_id else ''}
16 ${_('Server instance: %s') % c.instance_id if c.instance_id else ''}
17 </span>
17 </span>
18 <span class="navbar-text pull-right">
18 <span class="navbar-text pull-right">
19 This site is powered by
19 This site is powered by
20 %if c.visual.show_version:
20 %if c.visual.show_version:
21 <a class="navbar-link" href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a> ${c.kallithea_version},
21 <a class="navbar-link" href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a> ${c.kallithea_version},
22 %else:
22 %else:
23 <a class="navbar-link" href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a>,
23 <a class="navbar-link" href="${h.url('kallithea_project_url')}" target="_blank">Kallithea</a>,
24 %endif
24 %endif
25 which is
25 which is
26 <a class="navbar-link" href="${h.canonical_url('about')}#copyright">&copy; 2010&ndash;2017 by various authors &amp; licensed under GPLv3</a>.
26 <a class="navbar-link" href="${h.canonical_url('about')}#copyright">&copy; 2010&ndash;2017 by various authors &amp; licensed under GPLv3</a>.
27 %if c.issues_url:
27 %if c.issues_url:
28 &ndash; <a class="navbar-link" href="${c.issues_url}" target="_blank">${_('Support')}</a>
28 &ndash; <a class="navbar-link" href="${c.issues_url}" target="_blank">${_('Support')}</a>
29 %endif
29 %endif
30 </span>
30 </span>
31 </div>
31 </div>
32
32
33 <!-- END FOOTER -->
33 <!-- END FOOTER -->
34
34
35 ### MAKO DEFS ###
35 ### MAKO DEFS ###
36
36
37 <%block name="branding_title">
37 <%block name="branding_title">
38 %if c.site_name:
38 %if c.site_name:
39 &middot; ${c.site_name}
39 &middot; ${c.site_name}
40 %endif
40 %endif
41 </%block>
41 </%block>
42
42
43 <%def name="flash_msg()">
43 <%def name="flash_msg()">
44 <%include file="/base/flash_msg.html"/>
44 <%include file="/base/flash_msg.html"/>
45 </%def>
45 </%def>
46
46
47 <%def name="breadcrumbs()">
47 <%def name="breadcrumbs()">
48 <div class="breadcrumbs panel-title">
48 <div class="breadcrumbs panel-title">
49 ${self.breadcrumbs_links()}
49 ${self.breadcrumbs_links()}
50 </div>
50 </div>
51 </%def>
51 </%def>
52
52
53 <%def name="admin_menu()">
53 <%def name="admin_menu()">
54 <ul class="dropdown-menu" role="menu">
54 <ul class="dropdown-menu" role="menu">
55 <li><a href="${h.url('admin_home')}"><i class="icon-book"></i> ${_('Admin Journal')}</a></li>
55 <li><a href="${h.url('admin_home')}"><i class="icon-book"></i> ${_('Admin Journal')}</a></li>
56 <li><a href="${h.url('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
56 <li><a href="${h.url('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
57 <li><a href="${h.url('repos_groups')}"><i class="icon-folder"></i> ${_('Repository Groups')}</a></li>
57 <li><a href="${h.url('repos_groups')}"><i class="icon-folder"></i> ${_('Repository Groups')}</a></li>
58 <li><a href="${h.url('users')}"><i class="icon-user"></i> ${_('Users')}</a></li>
58 <li><a href="${h.url('users')}"><i class="icon-user"></i> ${_('Users')}</a></li>
59 <li><a href="${h.url('users_groups')}"><i class="icon-users"></i> ${_('User Groups')}</a></li>
59 <li><a href="${h.url('users_groups')}"><i class="icon-users"></i> ${_('User Groups')}</a></li>
60 <li><a href="${h.url('admin_permissions')}"><i class="icon-block"></i> ${_('Default Permissions')}</a></li>
60 <li><a href="${h.url('admin_permissions')}"><i class="icon-block"></i> ${_('Default Permissions')}</a></li>
61 <li><a href="${h.url('auth_home')}"><i class="icon-key"></i> ${_('Authentication')}</a></li>
61 <li><a href="${h.url('auth_home')}"><i class="icon-key"></i> ${_('Authentication')}</a></li>
62 <li><a href="${h.url('defaults')}"><i class="icon-wrench"></i> ${_('Repository Defaults')}</a></li>
62 <li><a href="${h.url('defaults')}"><i class="icon-wrench"></i> ${_('Repository Defaults')}</a></li>
63 <li class="last"><a href="${h.url('admin_settings')}"><i class="icon-gear"></i> ${_('Settings')}</a></li>
63 <li class="last"><a href="${h.url('admin_settings')}"><i class="icon-gear"></i> ${_('Settings')}</a></li>
64 </ul>
64 </ul>
65
65
66 </%def>
66 </%def>
67
67
68
68
69 ## admin menu used for people that have some admin resources
69 ## admin menu used for people that have some admin resources
70 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
70 <%def name="admin_menu_simple(repositories=None, repository_groups=None, user_groups=None)">
71 <ul class="dropdown-menu" role="menu">
71 <ul class="dropdown-menu" role="menu">
72 %if repositories:
72 %if repositories:
73 <li><a href="${h.url('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
73 <li><a href="${h.url('repos')}"><i class="icon-database"></i> ${_('Repositories')}</a></li>
74 %endif
74 %endif
75 %if repository_groups:
75 %if repository_groups:
76 <li><a href="${h.url('repos_groups')}"><i class="icon-folder"></i> ${_('Repository Groups')}</a></li>
76 <li><a href="${h.url('repos_groups')}"><i class="icon-folder"></i> ${_('Repository Groups')}</a></li>
77 %endif
77 %endif
78 %if user_groups:
78 %if user_groups:
79 <li><a href="${h.url('users_groups')}"><i class="icon-users"></i> ${_('User Groups')}</a></li>
79 <li><a href="${h.url('users_groups')}"><i class="icon-users"></i> ${_('User Groups')}</a></li>
80 %endif
80 %endif
81 </ul>
81 </ul>
82 </%def>
82 </%def>
83
83
84 <%def name="repotag(repo)">
84 <%def name="repotag(repo)">
85 %if h.is_hg(repo):
85 %if h.is_hg(repo):
86 <span class="repotag" title="${_('Mercurial repository')}">hg</span>
86 <span class="repotag" title="${_('Mercurial repository')}">hg</span>
87 %endif
87 %endif
88 %if h.is_git(repo):
88 %if h.is_git(repo):
89 <span class="repotag" title="${_('Git repository')}">git</span>
89 <span class="repotag" title="${_('Git repository')}">git</span>
90 %endif
90 %endif
91 </%def>
91 </%def>
92
92
93 <%def name="repo_context_bar(current=None, rev=None)">
93 <%def name="repo_context_bar(current=None, rev=None)">
94 <% rev = None if rev == 'tip' else rev %>
94 <% rev = None if rev == 'tip' else rev %>
95 <!--- CONTEXT BAR -->
95 <!--- CONTEXT BAR -->
96 <nav id="context-bar" class="navbar navbar-inverse">
96 <nav id="context-bar" class="navbar navbar-inverse">
97 <div class="navbar-header">
97 <div class="navbar-header">
98 <div class="navbar-brand">
98 <div class="navbar-brand">
99 ${repotag(c.db_repo)}
99 ${repotag(c.db_repo)}
100
100
101 ## public/private
101 ## public/private
102 %if c.db_repo.private:
102 %if c.db_repo.private:
103 <i class="icon-keyhole-circled"></i>
103 <i class="icon-keyhole-circled"></i>
104 %else:
104 %else:
105 <i class="icon-globe"></i>
105 <i class="icon-globe"></i>
106 %endif
106 %endif
107 %for group in c.db_repo.groups_with_parents:
107 %for group in c.db_repo.groups_with_parents:
108 ${h.link_to(group.name, url('repos_group_home', group_name=group.group_name), class_='navbar-link')}
108 ${h.link_to(group.name, url('repos_group_home', group_name=group.group_name), class_='navbar-link')}
109 &raquo;
109 &raquo;
110 %endfor
110 %endfor
111 ${h.link_to(c.db_repo.just_name, url('summary_home', repo_name=c.db_repo.repo_name), class_='navbar-link')}
111 ${h.link_to(c.db_repo.just_name, url('summary_home', repo_name=c.db_repo.repo_name), class_='navbar-link')}
112
112
113 %if current == 'createfork':
113 %if current == 'createfork':
114 - ${_('Create Fork')}
114 - ${_('Create Fork')}
115 %endif
115 %endif
116 </div>
116 </div>
117 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#context-pages" aria-expanded="false">
117 <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#context-pages" aria-expanded="false">
118 <span class="sr-only">Toggle navigation</span>
118 <span class="sr-only">Toggle navigation</span>
119 <span class="icon-bar"></span>
119 <span class="icon-bar"></span>
120 <span class="icon-bar"></span>
120 <span class="icon-bar"></span>
121 <span class="icon-bar"></span>
121 <span class="icon-bar"></span>
122 </button>
122 </button>
123 </div>
123 </div>
124 <ul id="context-pages" class="nav navbar-nav navbar-right navbar-collapse collapse">
124 <ul id="context-pages" class="nav navbar-nav navbar-right navbar-collapse collapse">
125 <li class="${'active' if current == 'summary' else ''}" data-context="summary"><a href="${h.url('summary_home', repo_name=c.repo_name)}"><i class="icon-doc-text"></i> ${_('Summary')}</a></li>
125 <li class="${'active' if current == 'summary' else ''}" data-context="summary"><a href="${h.url('summary_home', repo_name=c.repo_name)}"><i class="icon-doc-text"></i> ${_('Summary')}</a></li>
126 %if rev:
126 %if rev:
127 <li class="${'active' if current == 'changelog' else ''}" data-context="changelog"><a href="${h.url('changelog_file_home', repo_name=c.repo_name, revision=rev, f_path='')}"><i class="icon-clock"></i> ${_('Changelog')}</a></li>
127 <li class="${'active' if current == 'changelog' else ''}" data-context="changelog"><a href="${h.url('changelog_file_home', repo_name=c.repo_name, revision=rev, f_path='')}"><i class="icon-clock"></i> ${_('Changelog')}</a></li>
128 %else:
128 %else:
129 <li class="${'active' if current == 'changelog' else ''}" data-context="changelog"><a href="${h.url('changelog_home', repo_name=c.repo_name)}"><i class="icon-clock"></i> ${_('Changelog')}</a></li>
129 <li class="${'active' if current == 'changelog' else ''}" data-context="changelog"><a href="${h.url('changelog_home', repo_name=c.repo_name)}"><i class="icon-clock"></i> ${_('Changelog')}</a></li>
130 %endif
130 %endif
131 <li class="${'active' if current == 'files' else ''}" data-context="files"><a href="${h.url('files_home', repo_name=c.repo_name, revision=rev or 'tip')}"><i class="icon-doc-inv"></i> ${_('Files')}</a></li>
131 <li class="${'active' if current == 'files' else ''}" data-context="files"><a href="${h.url('files_home', repo_name=c.repo_name, revision=rev or 'tip')}"><i class="icon-doc-inv"></i> ${_('Files')}</a></li>
132 <li class="${'active' if current == 'switch-to' else ''}" data-context="switch-to">
132 <li class="${'active' if current == 'switch-to' else ''}" data-context="switch-to">
133 <input id="branch_switcher" name="branch_switcher" type="hidden">
133 <input id="branch_switcher" name="branch_switcher" type="hidden">
134 </li>
134 </li>
135 <li class="${'active' if current == 'options' else ''} dropdown" data-context="options">
135 <li class="${'active' if current == 'options' else ''} dropdown" data-context="options">
136 %if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
136 %if h.HasRepoPermissionLevel('admin')(c.repo_name):
137 <a href="${h.url('edit_repo',repo_name=c.repo_name)}" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" aria-haspopup="true"><i class="icon-wrench"></i> ${_('Options')} <i class="caret"></i></a>
137 <a href="${h.url('edit_repo',repo_name=c.repo_name)}" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" aria-haspopup="true"><i class="icon-wrench"></i> ${_('Options')} <i class="caret"></i></a>
138 %else:
138 %else:
139 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" aria-haspopup="true"><i class="icon-wrench"></i> ${_('Options')} <i class="caret"></i></a>
139 <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false" aria-haspopup="true"><i class="icon-wrench"></i> ${_('Options')} <i class="caret"></i></a>
140 %endif
140 %endif
141 <ul class="dropdown-menu" role="menu" aria-hidden="true">
141 <ul class="dropdown-menu" role="menu" aria-hidden="true">
142 %if h.HasRepoPermissionAny('repository.admin')(c.repo_name):
142 %if h.HasRepoPermissionLevel('admin')(c.repo_name):
143 <li><a href="${h.url('edit_repo',repo_name=c.repo_name)}"><i class="icon-gear"></i> ${_('Settings')}</a></li>
143 <li><a href="${h.url('edit_repo',repo_name=c.repo_name)}"><i class="icon-gear"></i> ${_('Settings')}</a></li>
144 %endif
144 %endif
145 %if c.db_repo.fork:
145 %if c.db_repo.fork:
146 <li><a href="${h.url('compare_url',repo_name=c.db_repo.fork.repo_name,org_ref_type=c.db_repo.landing_rev[0],org_ref_name=c.db_repo.landing_rev[1], other_repo=c.repo_name,other_ref_type='branch' if request.GET.get('branch') else c.db_repo.landing_rev[0],other_ref_name=request.GET.get('branch') or c.db_repo.landing_rev[1], merge=1)}">
146 <li><a href="${h.url('compare_url',repo_name=c.db_repo.fork.repo_name,org_ref_type=c.db_repo.landing_rev[0],org_ref_name=c.db_repo.landing_rev[1], other_repo=c.repo_name,other_ref_type='branch' if request.GET.get('branch') else c.db_repo.landing_rev[0],other_ref_name=request.GET.get('branch') or c.db_repo.landing_rev[1], merge=1)}">
147 <i class="icon-git-compare"></i> ${_('Compare Fork')}</a></li>
147 <i class="icon-git-compare"></i> ${_('Compare Fork')}</a></li>
148 %endif
148 %endif
149 <li><a href="${h.url('compare_home',repo_name=c.repo_name)}"><i class="icon-git-compare"></i> ${_('Compare')}</a></li>
149 <li><a href="${h.url('compare_home',repo_name=c.repo_name)}"><i class="icon-git-compare"></i> ${_('Compare')}</a></li>
150
150
151 <li><a href="${h.url('search_repo',repo_name=c.repo_name)}"><i class="icon-search"></i> ${_('Search')}</a></li>
151 <li><a href="${h.url('search_repo',repo_name=c.repo_name)}"><i class="icon-search"></i> ${_('Search')}</a></li>
152
152
153 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.db_repo.enable_locking:
153 %if h.HasRepoPermissionLevel('write')(c.repo_name) and c.db_repo.enable_locking:
154 %if c.db_repo.locked[0]:
154 %if c.db_repo.locked[0]:
155 <li><a href="${h.url('toggle_locking', repo_name=c.repo_name)}"><i class="icon-lock"></i> ${_('Unlock')}</a></li>
155 <li><a href="${h.url('toggle_locking', repo_name=c.repo_name)}"><i class="icon-lock"></i> ${_('Unlock')}</a></li>
156 %else:
156 %else:
157 <li><a href="${h.url('toggle_locking', repo_name=c.repo_name)}"><i class="icon-lock-open-alt"></i> ${_('Lock')}</li>
157 <li><a href="${h.url('toggle_locking', repo_name=c.repo_name)}"><i class="icon-lock-open-alt"></i> ${_('Lock')}</li>
158 %endif
158 %endif
159 %endif
159 %endif
160 ## TODO: this check feels wrong, it would be better to have a check for permissions
160 ## TODO: this check feels wrong, it would be better to have a check for permissions
161 ## also it feels like a job for the controller
161 ## also it feels like a job for the controller
162 %if request.authuser.username != 'default':
162 %if request.authuser.username != 'default':
163 <li>
163 <li>
164 <a href="#" class="${'following' if c.repository_following else 'follow'}" onclick="toggleFollowingRepo(this, ${c.db_repo.repo_id});">
164 <a href="#" class="${'following' if c.repository_following else 'follow'}" onclick="toggleFollowingRepo(this, ${c.db_repo.repo_id});">
165 <span class="show-follow ${'hidden' if c.repository_following else ''}"><i class="icon-heart-empty"></i> ${_('Follow')}</span>
165 <span class="show-follow ${'hidden' if c.repository_following else ''}"><i class="icon-heart-empty"></i> ${_('Follow')}</span>
166 <span class="show-following ${'' if c.repository_following else 'hidden'}"><i class="icon-heart"></i> ${_('Unfollow')}</span>
166 <span class="show-following ${'' if c.repository_following else 'hidden'}"><i class="icon-heart"></i> ${_('Unfollow')}</span>
167 </a>
167 </a>
168 </li>
168 </li>
169 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i> ${_('Fork')}</a></li>
169 <li><a href="${h.url('repo_fork_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i> ${_('Fork')}</a></li>
170 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i> ${_('Create Pull Request')}</a></li>
170 <li><a href="${h.url('pullrequest_home',repo_name=c.repo_name)}"><i class="icon-git-pull-request"></i> ${_('Create Pull Request')}</a></li>
171 %endif
171 %endif
172 </ul>
172 </ul>
173 </li>
173 </li>
174 <li class="${'active' if current == 'showpullrequest' else ''}" data-context="showpullrequest">
174 <li class="${'active' if current == 'showpullrequest' else ''}" data-context="showpullrequest">
175 <a href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests for %s') % c.repo_name}"> <i class="icon-git-pull-request"></i> ${_('Pull Requests')}
175 <a href="${h.url('pullrequest_show_all',repo_name=c.repo_name)}" title="${_('Show Pull Requests for %s') % c.repo_name}"> <i class="icon-git-pull-request"></i> ${_('Pull Requests')}
176 %if c.repository_pull_requests:
176 %if c.repository_pull_requests:
177 <span class="badge">${c.repository_pull_requests}</span>
177 <span class="badge">${c.repository_pull_requests}</span>
178 %endif
178 %endif
179 </a>
179 </a>
180 </li>
180 </li>
181 </ul>
181 </ul>
182 </nav>
182 </nav>
183 <script type="text/javascript">
183 <script type="text/javascript">
184 $(document).ready(function() {
184 $(document).ready(function() {
185 var bcache = {};
185 var bcache = {};
186
186
187 $("#branch_switcher").select2({
187 $("#branch_switcher").select2({
188 placeholder: '<span class="navbar-text"> <i class="icon-exchange"></i> ${_('Switch To')} <span class="caret"></span></span>',
188 placeholder: '<span class="navbar-text"> <i class="icon-exchange"></i> ${_('Switch To')} <span class="caret"></span></span>',
189 dropdownAutoWidth: true,
189 dropdownAutoWidth: true,
190 sortResults: prefixFirstSort,
190 sortResults: prefixFirstSort,
191 formatResult: function(obj) {
191 formatResult: function(obj) {
192 return obj.text;
192 return obj.text;
193 },
193 },
194 formatSelection: function(obj) {
194 formatSelection: function(obj) {
195 return obj.text;
195 return obj.text;
196 },
196 },
197 formatNoMatches: function(term) {
197 formatNoMatches: function(term) {
198 return "${_('No matches found')}";
198 return "${_('No matches found')}";
199 },
199 },
200 escapeMarkup: function(m) {
200 escapeMarkup: function(m) {
201 // don't escape our custom placeholder
201 // don't escape our custom placeholder
202 if (m.substr(0, 25) == '<span class="navbar-text"') {
202 if (m.substr(0, 25) == '<span class="navbar-text"') {
203 return m;
203 return m;
204 }
204 }
205
205
206 return Select2.util.escapeMarkup(m);
206 return Select2.util.escapeMarkup(m);
207 },
207 },
208 containerCssClass: "branch-switcher",
208 containerCssClass: "branch-switcher",
209 dropdownCssClass: "repo-switcher-dropdown",
209 dropdownCssClass: "repo-switcher-dropdown",
210 query: function(query) {
210 query: function(query) {
211 var key = 'cache';
211 var key = 'cache';
212 var cached = bcache[key];
212 var cached = bcache[key];
213 if (cached) {
213 if (cached) {
214 var data = {
214 var data = {
215 results: []
215 results: []
216 };
216 };
217 // filter results
217 // filter results
218 $.each(cached.results, function() {
218 $.each(cached.results, function() {
219 var section = this.text;
219 var section = this.text;
220 var children = [];
220 var children = [];
221 $.each(this.children, function() {
221 $.each(this.children, function() {
222 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
222 if (query.term.length === 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
223 children.push({
223 children.push({
224 'id': this.id,
224 'id': this.id,
225 'text': this.text,
225 'text': this.text,
226 'type': this.type,
226 'type': this.type,
227 'obj': this.obj
227 'obj': this.obj
228 });
228 });
229 }
229 }
230 });
230 });
231 if (children.length !== 0) {
231 if (children.length !== 0) {
232 data.results.push({
232 data.results.push({
233 'text': section,
233 'text': section,
234 'children': children
234 'children': children
235 });
235 });
236 }
236 }
237
237
238 });
238 });
239 query.callback(data);
239 query.callback(data);
240 } else {
240 } else {
241 $.ajax({
241 $.ajax({
242 url: pyroutes.url('repo_refs_data', {
242 url: pyroutes.url('repo_refs_data', {
243 'repo_name': '${c.repo_name}'
243 'repo_name': '${c.repo_name}'
244 }),
244 }),
245 data: {},
245 data: {},
246 dataType: 'json',
246 dataType: 'json',
247 type: 'GET',
247 type: 'GET',
248 success: function(data) {
248 success: function(data) {
249 bcache[key] = data;
249 bcache[key] = data;
250 query.callback(data);
250 query.callback(data);
251 }
251 }
252 });
252 });
253 }
253 }
254 }
254 }
255 });
255 });
256
256
257 $("#branch_switcher").on('select2-selecting', function(e) {
257 $("#branch_switcher").on('select2-selecting', function(e) {
258 e.preventDefault();
258 e.preventDefault();
259 var context = $('#context-bar .active').data('context');
259 var context = $('#context-bar .active').data('context');
260 if (context == 'files') {
260 if (context == 'files') {
261 window.location = pyroutes.url('files_home', {
261 window.location = pyroutes.url('files_home', {
262 'repo_name': REPO_NAME,
262 'repo_name': REPO_NAME,
263 'revision': e.choice.id,
263 'revision': e.choice.id,
264 'f_path': '',
264 'f_path': '',
265 'at': e.choice.text
265 'at': e.choice.text
266 });
266 });
267 } else if (context == 'changelog') {
267 } else if (context == 'changelog') {
268 if (e.choice.type == 'tag' || e.choice.type == 'book') {
268 if (e.choice.type == 'tag' || e.choice.type == 'book') {
269 $("#branch_filter").append($('<'+'option/>').val(e.choice.text));
269 $("#branch_filter").append($('<'+'option/>').val(e.choice.text));
270 }
270 }
271 $("#branch_filter").val(e.choice.text).change();
271 $("#branch_filter").val(e.choice.text).change();
272 } else {
272 } else {
273 window.location = pyroutes.url('changelog_home', {
273 window.location = pyroutes.url('changelog_home', {
274 'repo_name': '${c.repo_name}',
274 'repo_name': '${c.repo_name}',
275 'branch': e.choice.text
275 'branch': e.choice.text
276 });
276 });
277 }
277 }
278 });
278 });
279 });
279 });
280 </script>
280 </script>
281 <!--- END CONTEXT BAR -->
281 <!--- END CONTEXT BAR -->
282 </%def>
282 </%def>
283
283
284 <%def name="menu(current=None)">
284 <%def name="menu(current=None)">
285 <ul id="quick" class="nav navbar-nav navbar-right">
285 <ul id="quick" class="nav navbar-nav navbar-right">
286 <!-- repo switcher -->
286 <!-- repo switcher -->
287 <li class="${'active' if current == 'repositories' else ''}">
287 <li class="${'active' if current == 'repositories' else ''}">
288 <input id="repo_switcher" name="repo_switcher" type="hidden">
288 <input id="repo_switcher" name="repo_switcher" type="hidden">
289 </li>
289 </li>
290
290
291 ##ROOT MENU
291 ##ROOT MENU
292 %if request.authuser.username != 'default':
292 %if request.authuser.username != 'default':
293 <li class="${'active' if current == 'journal' else ''}">
293 <li class="${'active' if current == 'journal' else ''}">
294 <a class="menu_link" title="${_('Show recent activity')}" href="${h.url('journal')}">
294 <a class="menu_link" title="${_('Show recent activity')}" href="${h.url('journal')}">
295 <i class="icon-book"></i> ${_('Journal')}
295 <i class="icon-book"></i> ${_('Journal')}
296 </a>
296 </a>
297 </li>
297 </li>
298 %else:
298 %else:
299 <li class="${'active' if current == 'journal' else ''}">
299 <li class="${'active' if current == 'journal' else ''}">
300 <a class="menu_link" title="${_('Public journal')}" href="${h.url('public_journal')}">
300 <a class="menu_link" title="${_('Public journal')}" href="${h.url('public_journal')}">
301 <i class="icon-book"></i> ${_('Public journal')}
301 <i class="icon-book"></i> ${_('Public journal')}
302 </a>
302 </a>
303 </li>
303 </li>
304 %endif
304 %endif
305 <li class="${'active' if current == 'gists' else ''} dropdown">
305 <li class="${'active' if current == 'gists' else ''} dropdown">
306 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Show public gists')}" href="${h.url('gists')}">
306 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Show public gists')}" href="${h.url('gists')}">
307 <i class="icon-clippy"></i> ${_('Gists')} <span class="caret"></span>
307 <i class="icon-clippy"></i> ${_('Gists')} <span class="caret"></span>
308 </a>
308 </a>
309 <ul class="dropdown-menu" role="menu">
309 <ul class="dropdown-menu" role="menu">
310 <li><a href="${h.url('new_gist', public=1)}"><i class="icon-paste"></i> ${_('Create New Gist')}</a></li>
310 <li><a href="${h.url('new_gist', public=1)}"><i class="icon-paste"></i> ${_('Create New Gist')}</a></li>
311 <li><a href="${h.url('gists')}"><i class="icon-globe"></i> ${_('All Public Gists')}</a></li>
311 <li><a href="${h.url('gists')}"><i class="icon-globe"></i> ${_('All Public Gists')}</a></li>
312 %if request.authuser.username != 'default':
312 %if request.authuser.username != 'default':
313 <li><a href="${h.url('gists', public=1)}"><i class="icon-user"></i> ${_('My Public Gists')}</a></li>
313 <li><a href="${h.url('gists', public=1)}"><i class="icon-user"></i> ${_('My Public Gists')}</a></li>
314 <li><a href="${h.url('gists', private=1)}"><i class="icon-keyhole-circled"></i> ${_('My Private Gists')}</a></li>
314 <li><a href="${h.url('gists', private=1)}"><i class="icon-keyhole-circled"></i> ${_('My Private Gists')}</a></li>
315 %endif
315 %endif
316 </ul>
316 </ul>
317 </li>
317 </li>
318 <li class="${'active' if current == 'search' else ''}">
318 <li class="${'active' if current == 'search' else ''}">
319 <a class="menu_link" title="${_('Search in repositories')}" href="${h.url('search')}">
319 <a class="menu_link" title="${_('Search in repositories')}" href="${h.url('search')}">
320 <i class="icon-search"></i> ${_('Search')}
320 <i class="icon-search"></i> ${_('Search')}
321 </a>
321 </a>
322 </li>
322 </li>
323 % if h.HasPermissionAny('hg.admin')('access admin main page'):
323 % if h.HasPermissionAny('hg.admin')('access admin main page'):
324 <li class="${'active' if current == 'admin' else ''} dropdown">
324 <li class="${'active' if current == 'admin' else ''} dropdown">
325 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Admin')}" href="${h.url('admin_home')}">
325 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Admin')}" href="${h.url('admin_home')}">
326 <i class="icon-gear"></i> ${_('Admin')} <span class="caret"></span>
326 <i class="icon-gear"></i> ${_('Admin')} <span class="caret"></span>
327 </a>
327 </a>
328 ${admin_menu()}
328 ${admin_menu()}
329 </li>
329 </li>
330 % elif request.authuser.repositories_admin or request.authuser.repository_groups_admin or request.authuser.user_groups_admin:
330 % elif request.authuser.repositories_admin or request.authuser.repository_groups_admin or request.authuser.user_groups_admin:
331 <li class="${'active' if current == 'admin' else ''} dropdown">
331 <li class="${'active' if current == 'admin' else ''} dropdown">
332 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Admin')}">
332 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" title="${_('Admin')}">
333 <i class="icon-gear"></i> ${_('Admin')}
333 <i class="icon-gear"></i> ${_('Admin')}
334 </a>
334 </a>
335 ${admin_menu_simple(request.authuser.repositories_admin,
335 ${admin_menu_simple(request.authuser.repositories_admin,
336 request.authuser.repository_groups_admin,
336 request.authuser.repository_groups_admin,
337 request.authuser.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
337 request.authuser.user_groups_admin or h.HasPermissionAny('hg.usergroup.create.true')())}
338 </li>
338 </li>
339 % endif
339 % endif
340
340
341 <li class="${'active' if current == 'my_pullrequests' else ''}">
341 <li class="${'active' if current == 'my_pullrequests' else ''}">
342 <a class="menu_link" title="${_('My Pull Requests')}" href="${h.url('my_pullrequests')}">
342 <a class="menu_link" title="${_('My Pull Requests')}" href="${h.url('my_pullrequests')}">
343 <i class="icon-git-pull-request"></i> ${_('My Pull Requests')}
343 <i class="icon-git-pull-request"></i> ${_('My Pull Requests')}
344 %if c.my_pr_count != 0:
344 %if c.my_pr_count != 0:
345 <span class="badge">${c.my_pr_count}</span>
345 <span class="badge">${c.my_pr_count}</span>
346 %endif
346 %endif
347 </a>
347 </a>
348 </li>
348 </li>
349
349
350 ## USER MENU
350 ## USER MENU
351 <li class="dropdown">
351 <li class="dropdown">
352 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" id="quick_login_link"
352 <a class="menu_link dropdown-toggle" data-toggle="dropdown" role="button" id="quick_login_link"
353 aria-expanded="false" aria-controls="quick_login"
353 aria-expanded="false" aria-controls="quick_login"
354 %if request.authuser.username != 'default':
354 %if request.authuser.username != 'default':
355 href="${h.url('notifications')}"
355 href="${h.url('notifications')}"
356 %else:
356 %else:
357 href="#"
357 href="#"
358 %endif
358 %endif
359 >
359 >
360 ${h.gravatar_div(request.authuser.email, size=20, div_class="icon")}
360 ${h.gravatar_div(request.authuser.email, size=20, div_class="icon")}
361 %if request.authuser.username != 'default':
361 %if request.authuser.username != 'default':
362 <span class="menu_link_user">${request.authuser.username}</span>
362 <span class="menu_link_user">${request.authuser.username}</span>
363 %if c.unread_notifications != 0:
363 %if c.unread_notifications != 0:
364 <span class="badge">${c.unread_notifications}</span>
364 <span class="badge">${c.unread_notifications}</span>
365 %endif
365 %endif
366 %else:
366 %else:
367 <span>${_('Not Logged In')}</span>
367 <span>${_('Not Logged In')}</span>
368 %endif
368 %endif
369 </a>
369 </a>
370
370
371 <div class="dropdown-menu user-menu" role="menu">
371 <div class="dropdown-menu user-menu" role="menu">
372 <div id="quick_login" role="form" aria-describedby="quick_login_h" aria-hidden="true" class="container-fluid">
372 <div id="quick_login" role="form" aria-describedby="quick_login_h" aria-hidden="true" class="container-fluid">
373 %if request.authuser.username == 'default' or request.authuser.user_id is None:
373 %if request.authuser.username == 'default' or request.authuser.user_id is None:
374 <h4 id="quick_login_h">${_('Login to Your Account')}</h4>
374 <h4 id="quick_login_h">${_('Login to Your Account')}</h4>
375 ${h.form(h.url('login_home', came_from=request.path_qs))}
375 ${h.form(h.url('login_home', came_from=request.path_qs))}
376 <div class="form">
376 <div class="form">
377 <div>
377 <div>
378 <div>
378 <div>
379 <div>
379 <div>
380 <label for="username">${_('Username')}:</label>
380 <label for="username">${_('Username')}:</label>
381 </div>
381 </div>
382 <div>
382 <div>
383 ${h.text('username',class_='form-control')}
383 ${h.text('username',class_='form-control')}
384 </div>
384 </div>
385 </div>
385 </div>
386 <div>
386 <div>
387 <div>
387 <div>
388 <label for="password">${_('Password')}:</label>
388 <label for="password">${_('Password')}:</label>
389 </div>
389 </div>
390 <div>
390 <div>
391 ${h.password('password',class_='form-control')}
391 ${h.password('password',class_='form-control')}
392 </div>
392 </div>
393
393
394 </div>
394 </div>
395 <div class="buttons">
395 <div class="buttons">
396 <div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
396 <div class="password_forgoten">${h.link_to(_('Forgot password ?'),h.url('reset_password'))}</div>
397 <div class="register">
397 <div class="register">
398 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
398 %if h.HasPermissionAny('hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')():
399 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
399 ${h.link_to(_("Don't have an account ?"),h.url('register'))}
400 %endif
400 %endif
401 </div>
401 </div>
402 <div class="submit">
402 <div class="submit">
403 ${h.submit('sign_in',_('Log In'),class_="btn btn-default btn-xs")}
403 ${h.submit('sign_in',_('Log In'),class_="btn btn-default btn-xs")}
404 </div>
404 </div>
405 </div>
405 </div>
406 </div>
406 </div>
407 </div>
407 </div>
408 ${h.end_form()}
408 ${h.end_form()}
409 %else:
409 %else:
410 <div class="pull-left">
410 <div class="pull-left">
411 ${h.gravatar_div(request.authuser.email, size=48, div_class="big_gravatar")}
411 ${h.gravatar_div(request.authuser.email, size=48, div_class="big_gravatar")}
412 <b class="full_name">${request.authuser.full_name_or_username}</b>
412 <b class="full_name">${request.authuser.full_name_or_username}</b>
413 <div class="email">${request.authuser.email}</div>
413 <div class="email">${request.authuser.email}</div>
414 </div>
414 </div>
415 <div id="quick_login_h" class="pull-right list-group text-right">
415 <div id="quick_login_h" class="pull-right list-group text-right">
416 <a class="list-group-item" href="${h.url('notifications')}">${_('Notifications')}: ${c.unread_notifications}</a>
416 <a class="list-group-item" href="${h.url('notifications')}">${_('Notifications')}: ${c.unread_notifications}</a>
417 ${h.link_to(_('My Account'),h.url('my_account'),class_='list-group-item')}
417 ${h.link_to(_('My Account'),h.url('my_account'),class_='list-group-item')}
418 %if not request.authuser.is_external_auth:
418 %if not request.authuser.is_external_auth:
419 ## Cannot log out if using external (container) authentication.
419 ## Cannot log out if using external (container) authentication.
420 ${h.link_to(_('Log Out'), h.url('logout_home'),class_='list-group-item')}
420 ${h.link_to(_('Log Out'), h.url('logout_home'),class_='list-group-item')}
421 %endif
421 %endif
422 </div>
422 </div>
423 %endif
423 %endif
424 </div>
424 </div>
425 </div>
425 </div>
426 </li>
426 </li>
427 </ul>
427 </ul>
428
428
429 <script type="text/javascript">
429 <script type="text/javascript">
430 $(document).ready(function(){
430 $(document).ready(function(){
431 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
431 var visual_show_public_icon = "${c.visual.show_public_icon}" == "True";
432 var cache = {}
432 var cache = {}
433 /*format the look of items in the list*/
433 /*format the look of items in the list*/
434 var format = function(state){
434 var format = function(state){
435 if (!state.id){
435 if (!state.id){
436 return state.text; // optgroup
436 return state.text; // optgroup
437 }
437 }
438 var obj_dict = state.obj;
438 var obj_dict = state.obj;
439 var tmpl = '';
439 var tmpl = '';
440
440
441 if(obj_dict && state.type == 'repo'){
441 if(obj_dict && state.type == 'repo'){
442 tmpl += '<span class="repo-icons">';
442 tmpl += '<span class="repo-icons">';
443 if(obj_dict['repo_type'] === 'hg'){
443 if(obj_dict['repo_type'] === 'hg'){
444 tmpl += '<span class="repotag">hg</span> ';
444 tmpl += '<span class="repotag">hg</span> ';
445 }
445 }
446 else if(obj_dict['repo_type'] === 'git'){
446 else if(obj_dict['repo_type'] === 'git'){
447 tmpl += '<span class="repotag">git</span> ';
447 tmpl += '<span class="repotag">git</span> ';
448 }
448 }
449 if(obj_dict['private']){
449 if(obj_dict['private']){
450 tmpl += '<i class="icon-keyhole-circled"></i> ';
450 tmpl += '<i class="icon-keyhole-circled"></i> ';
451 }
451 }
452 else if(visual_show_public_icon){
452 else if(visual_show_public_icon){
453 tmpl += '<i class="icon-globe"></i> ';
453 tmpl += '<i class="icon-globe"></i> ';
454 }
454 }
455 tmpl += '</span>';
455 tmpl += '</span>';
456 }
456 }
457 if(obj_dict && state.type == 'group'){
457 if(obj_dict && state.type == 'group'){
458 tmpl += '<i class="icon-folder"></i> ';
458 tmpl += '<i class="icon-folder"></i> ';
459 }
459 }
460 tmpl += state.text;
460 tmpl += state.text;
461 return tmpl;
461 return tmpl;
462 }
462 }
463
463
464 $("#repo_switcher").select2({
464 $("#repo_switcher").select2({
465 placeholder: '<span class="navbar-text"><i class="icon-database"></i> ${_('Repositories')} <span class="caret"></span></span>',
465 placeholder: '<span class="navbar-text"><i class="icon-database"></i> ${_('Repositories')} <span class="caret"></span></span>',
466 dropdownAutoWidth: true,
466 dropdownAutoWidth: true,
467 sortResults: prefixFirstSort,
467 sortResults: prefixFirstSort,
468 formatResult: format,
468 formatResult: format,
469 formatSelection: format,
469 formatSelection: format,
470 formatNoMatches: function(term){
470 formatNoMatches: function(term){
471 return "${_('No matches found')}";
471 return "${_('No matches found')}";
472 },
472 },
473 containerCssClass: "repo-switcher",
473 containerCssClass: "repo-switcher",
474 dropdownCssClass: "repo-switcher-dropdown",
474 dropdownCssClass: "repo-switcher-dropdown",
475 escapeMarkup: function(m){
475 escapeMarkup: function(m){
476 // don't escape our custom placeholder
476 // don't escape our custom placeholder
477 if(m.substr(0,55) == '<span class="navbar-text"><i class="icon-database"></i>'){
477 if(m.substr(0,55) == '<span class="navbar-text"><i class="icon-database"></i>'){
478 return m;
478 return m;
479 }
479 }
480
480
481 return Select2.util.escapeMarkup(m);
481 return Select2.util.escapeMarkup(m);
482 },
482 },
483 query: function(query){
483 query: function(query){
484 var key = 'cache';
484 var key = 'cache';
485 var cached = cache[key] ;
485 var cached = cache[key] ;
486 if(cached) {
486 if(cached) {
487 var data = {results: []};
487 var data = {results: []};
488 //filter results
488 //filter results
489 $.each(cached.results, function(){
489 $.each(cached.results, function(){
490 var section = this.text;
490 var section = this.text;
491 var children = [];
491 var children = [];
492 $.each(this.children, function(){
492 $.each(this.children, function(){
493 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
493 if(query.term.length == 0 || this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ){
494 children.push({'id': this.id, 'text': this.text, 'type': this.type, 'obj': this.obj});
494 children.push({'id': this.id, 'text': this.text, 'type': this.type, 'obj': this.obj});
495 }
495 }
496 });
496 });
497 if(children.length !== 0){
497 if(children.length !== 0){
498 data.results.push({'text': section, 'children': children});
498 data.results.push({'text': section, 'children': children});
499 }
499 }
500
500
501 });
501 });
502 query.callback(data);
502 query.callback(data);
503 }else{
503 }else{
504 $.ajax({
504 $.ajax({
505 url: "${h.url('repo_switcher_data')}",
505 url: "${h.url('repo_switcher_data')}",
506 data: {},
506 data: {},
507 dataType: 'json',
507 dataType: 'json',
508 type: 'GET',
508 type: 'GET',
509 success: function(data) {
509 success: function(data) {
510 cache[key] = data;
510 cache[key] = data;
511 query.callback({results: data.results});
511 query.callback({results: data.results});
512 }
512 }
513 });
513 });
514 }
514 }
515 }
515 }
516 });
516 });
517
517
518 $("#repo_switcher").on('select2-selecting', function(e){
518 $("#repo_switcher").on('select2-selecting', function(e){
519 e.preventDefault();
519 e.preventDefault();
520 window.location = pyroutes.url('summary_home', {'repo_name': e.val});
520 window.location = pyroutes.url('summary_home', {'repo_name': e.val});
521 });
521 });
522
522
523 $(document).on('shown.bs.dropdown', function(event) {
523 $(document).on('shown.bs.dropdown', function(event) {
524 var dropdown = $(event.target);
524 var dropdown = $(event.target);
525
525
526 dropdown.attr('aria-expanded', true);
526 dropdown.attr('aria-expanded', true);
527 dropdown.find('.dropdown-menu').attr('aria-hidden', false);
527 dropdown.find('.dropdown-menu').attr('aria-hidden', false);
528 });
528 });
529
529
530 $(document).on('hidden.bs.dropdown', function(event) {
530 $(document).on('hidden.bs.dropdown', function(event) {
531 var dropdown = $(event.target);
531 var dropdown = $(event.target);
532
532
533 dropdown.attr('aria-expanded', false);
533 dropdown.attr('aria-expanded', false);
534 dropdown.find('.dropdown-menu').attr('aria-hidden', true);
534 dropdown.find('.dropdown-menu').attr('aria-hidden', true);
535 });
535 });
536 });
536 });
537 </script>
537 </script>
538 </%def>
538 </%def>
@@ -1,110 +1,110 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 %if c.repo_changesets:
2 %if c.repo_changesets:
3 <table class="table">
3 <table class="table">
4 <tr>
4 <tr>
5 <th class="left"></th>
5 <th class="left"></th>
6 <th class="left"></th>
6 <th class="left"></th>
7 <th class="left">${_('Revision')}</th>
7 <th class="left">${_('Revision')}</th>
8 <th class="left">${_('Commit Message')}</th>
8 <th class="left">${_('Commit Message')}</th>
9 <th class="left">${_('Age')}</th>
9 <th class="left">${_('Age')}</th>
10 <th class="left">${_('Author')}</th>
10 <th class="left">${_('Author')}</th>
11 <th class="left">${_('Refs')}</th>
11 <th class="left">${_('Refs')}</th>
12 </tr>
12 </tr>
13 %for cnt,cs in enumerate(c.repo_changesets):
13 %for cnt,cs in enumerate(c.repo_changesets):
14 <tr class="parity${cnt%2} ${'mergerow' if len(cs.parents) > 1 else ''}">
14 <tr class="parity${cnt%2} ${'mergerow' if len(cs.parents) > 1 else ''}">
15 <td class="compact">
15 <td class="compact">
16 <div class="changeset-status-container">
16 <div class="changeset-status-container">
17 %if c.statuses.get(cs.raw_id):
17 %if c.statuses.get(cs.raw_id):
18 <span class="changeset-status-ico shortlog">
18 <span class="changeset-status-ico shortlog">
19 %if c.statuses.get(cs.raw_id)[2]:
19 %if c.statuses.get(cs.raw_id)[2]:
20 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s\nClick to open associated pull request %s') % (c.statuses.get(cs.raw_id)[1], c.statuses.get(cs.raw_id)[5].username, c.statuses.get(cs.raw_id)[4])}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}">
20 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s\nClick to open associated pull request %s') % (c.statuses.get(cs.raw_id)[1], c.statuses.get(cs.raw_id)[5].username, c.statuses.get(cs.raw_id)[4])}" href="${h.url('pullrequest_show',repo_name=c.statuses.get(cs.raw_id)[3],pull_request_id=c.statuses.get(cs.raw_id)[2])}">
21 <i class="icon-circle changeset-status-${c.statuses.get(cs.raw_id)[0]}"></i>
21 <i class="icon-circle changeset-status-${c.statuses.get(cs.raw_id)[0]}"></i>
22 </a>
22 </a>
23 %else:
23 %else:
24 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s') % (c.statuses.get(cs.raw_id)[1], c.statuses.get(cs.raw_id)[5].username)}"
24 <a data-toggle="tooltip" title="${_('Changeset status: %s by %s') % (c.statuses.get(cs.raw_id)[1], c.statuses.get(cs.raw_id)[5].username)}"
25 href="${c.comments[cs.raw_id][0].url()}">
25 href="${c.comments[cs.raw_id][0].url()}">
26 <i class="icon-circle changeset-status-${c.statuses.get(cs.raw_id)[0]}"></i>
26 <i class="icon-circle changeset-status-${c.statuses.get(cs.raw_id)[0]}"></i>
27 </a>
27 </a>
28 %endif
28 %endif
29 </span>
29 </span>
30 %endif
30 %endif
31 </div>
31 </div>
32 </td>
32 </td>
33 <td class="compact">
33 <td class="compact">
34 %if c.comments.get(cs.raw_id,[]):
34 %if c.comments.get(cs.raw_id,[]):
35 <div class="comments-container">
35 <div class="comments-container">
36 <div title="${('comments')}">
36 <div title="${('comments')}">
37 <a href="${c.comments[cs.raw_id][0].url()}">
37 <a href="${c.comments[cs.raw_id][0].url()}">
38 <i class="icon-comment"></i>${len(c.comments[cs.raw_id])}
38 <i class="icon-comment"></i>${len(c.comments[cs.raw_id])}
39 </a>
39 </a>
40 </div>
40 </div>
41 </div>
41 </div>
42 %endif
42 %endif
43 </td>
43 </td>
44 <td>
44 <td>
45 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}" class="revision-link">${h.show_id(cs)}</a>
45 <a href="${h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id)}" class="revision-link">${h.show_id(cs)}</a>
46 </td>
46 </td>
47 <td>
47 <td>
48 ${h.urlify_text(h.chop_at(cs.message,'\n'),c.repo_name, h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
48 ${h.urlify_text(h.chop_at(cs.message,'\n'),c.repo_name, h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
49 </td>
49 </td>
50 <td><span data-toggle="tooltip" title="${h.fmt_date(cs.date)}">
50 <td><span data-toggle="tooltip" title="${h.fmt_date(cs.date)}">
51 ${h.age(cs.date)}</span>
51 ${h.age(cs.date)}</span>
52 </td>
52 </td>
53 <td title="${cs.author}">${h.person(cs.author)}</td>
53 <td title="${cs.author}">${h.person(cs.author)}</td>
54 <td>
54 <td>
55 %if h.is_hg(c.db_repo_scm_instance):
55 %if h.is_hg(c.db_repo_scm_instance):
56 %for book in cs.bookmarks:
56 %for book in cs.bookmarks:
57 <span class="booktag" title="${_('Bookmark %s') % book}">
57 <span class="booktag" title="${_('Bookmark %s') % book}">
58 ${h.link_to(book,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
58 ${h.link_to(book,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
59 </span>
59 </span>
60 %endfor
60 %endfor
61 %endif
61 %endif
62 %for tag in cs.tags:
62 %for tag in cs.tags:
63 <span class="tagtag" title="${_('Tag %s') % tag}">
63 <span class="tagtag" title="${_('Tag %s') % tag}">
64 ${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
64 ${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
65 </span>
65 </span>
66 %endfor
66 %endfor
67 %if cs.branch:
67 %if cs.branch:
68 <span class="branchtag" title="${_('Branch %s' % cs.branch)}">
68 <span class="branchtag" title="${_('Branch %s' % cs.branch)}">
69 ${h.link_to(cs.branch,h.url('changelog_home',repo_name=c.repo_name,branch=cs.branch))}
69 ${h.link_to(cs.branch,h.url('changelog_home',repo_name=c.repo_name,branch=cs.branch))}
70 </span>
70 </span>
71 %endif
71 %endif
72 </td>
72 </td>
73 </tr>
73 </tr>
74 %endfor
74 %endfor
75
75
76 </table>
76 </table>
77
77
78 <ul class="pagination">
78 <ul class="pagination">
79 ${c.repo_changesets.pager()}
79 ${c.repo_changesets.pager()}
80 </ul>
80 </ul>
81 %else:
81 %else:
82
82
83 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
83 %if h.HasRepoPermissionLevel('write')(c.repo_name):
84 <h4>${_('Add or upload files directly via Kallithea')}</h4>
84 <h4>${_('Add or upload files directly via Kallithea')}</h4>
85 <div style="margin: 20px 30px;">
85 <div style="margin: 20px 30px;">
86 <div id="add_node_id" class="add_node">
86 <div id="add_node_id" class="add_node">
87 <a class="btn btn-default btn-xs" href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='', anchor='edit')}">${_('Add New File')}</a>
87 <a class="btn btn-default btn-xs" href="${h.url('files_add_home',repo_name=c.repo_name,revision=0,f_path='', anchor='edit')}">${_('Add New File')}</a>
88 </div>
88 </div>
89 </div>
89 </div>
90 %endif
90 %endif
91
91
92
92
93 <h4>${_('Push new repository')}</h4>
93 <h4>${_('Push new repository')}</h4>
94 <pre>
94 <pre>
95 ${c.db_repo_scm_instance.alias} clone ${c.clone_repo_url}
95 ${c.db_repo_scm_instance.alias} clone ${c.clone_repo_url}
96 ${c.db_repo_scm_instance.alias} add README # add first file
96 ${c.db_repo_scm_instance.alias} add README # add first file
97 ${c.db_repo_scm_instance.alias} commit -m "Initial" # commit with message
97 ${c.db_repo_scm_instance.alias} commit -m "Initial" # commit with message
98 ${c.db_repo_scm_instance.alias} push ${'origin master' if h.is_git(c.db_repo_scm_instance) else ''} # push changes back
98 ${c.db_repo_scm_instance.alias} push ${'origin master' if h.is_git(c.db_repo_scm_instance) else ''} # push changes back
99 </pre>
99 </pre>
100
100
101 <h4>${_('Existing repository?')}</h4>
101 <h4>${_('Existing repository?')}</h4>
102 <pre>
102 <pre>
103 %if h.is_git(c.db_repo_scm_instance):
103 %if h.is_git(c.db_repo_scm_instance):
104 git remote add origin ${c.clone_repo_url}
104 git remote add origin ${c.clone_repo_url}
105 git push -u origin master
105 git push -u origin master
106 %else:
106 %else:
107 hg push ${c.clone_repo_url}
107 hg push ${c.clone_repo_url}
108 %endif
108 %endif
109 </pre>
109 </pre>
110 %endif
110 %endif
@@ -1,188 +1,188 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4 ## ${comment.comment_block(co)}
4 ## ${comment.comment_block(co)}
5 ##
5 ##
6 <%def name="comment_block(co)">
6 <%def name="comment_block(co)">
7 <div class="comment" id="comment-${co.comment_id}">
7 <div class="comment" id="comment-${co.comment_id}">
8 <div class="comment-prev-next-links"></div>
8 <div class="comment-prev-next-links"></div>
9 <div class="comment-wrapp">
9 <div class="comment-wrapp">
10 <div class="meta">
10 <div class="meta">
11 ${h.gravatar_div(co.author.email, size=20, div_style="float:left")}
11 ${h.gravatar_div(co.author.email, size=20, div_style="float:left")}
12 <div class="user">
12 <div class="user">
13 ${co.author.full_name_and_username}
13 ${co.author.full_name_and_username}
14 </div>
14 </div>
15
15
16 <span>
16 <span>
17 ${h.age(co.modified_at)}
17 ${h.age(co.modified_at)}
18 %if co.pull_request:
18 %if co.pull_request:
19 ${_('on pull request')}
19 ${_('on pull request')}
20 <a href="${co.url()}">"${co.pull_request.title or _("No title")}"</a>
20 <a href="${co.url()}">"${co.pull_request.title or _("No title")}"</a>
21 %else:
21 %else:
22 ${_('on this changeset')}
22 ${_('on this changeset')}
23 %endif
23 %endif
24 <a class="permalink" href="${co.url()}">&para;</a>
24 <a class="permalink" href="${co.url()}">&para;</a>
25 </span>
25 </span>
26
26
27 %if co.author_id == request.authuser.user_id or h.HasRepoPermissionAny('repository.admin')(c.repo_name):
27 %if co.author_id == request.authuser.user_id or h.HasRepoPermissionLevel('admin')(c.repo_name):
28 %if co.deletable():
28 %if co.deletable():
29 <div onClick="confirm('${_('Delete comment?')}') && deleteComment(${co.comment_id})" class="buttons delete-comment btn btn-default btn-xs" style="margin:0 5px">${_('Delete')}</div>
29 <div onClick="confirm('${_('Delete comment?')}') && deleteComment(${co.comment_id})" class="buttons delete-comment btn btn-default btn-xs" style="margin:0 5px">${_('Delete')}</div>
30 %endif
30 %endif
31 %endif
31 %endif
32 </div>
32 </div>
33 <div class="text">
33 <div class="text">
34 %if co.status_change:
34 %if co.status_change:
35 <div class="automatic-comment">
35 <div class="automatic-comment">
36 <p>
36 <p>
37 <span title="${_('Changeset status')}" class="changeset-status-lbl">${_("Status change")}: ${co.status_change[0].status_lbl}</span>
37 <span title="${_('Changeset status')}" class="changeset-status-lbl">${_("Status change")}: ${co.status_change[0].status_lbl}</span>
38 <span class="changeset-status-ico"><i class="icon-circle changeset-status-${co.status_change[0].status}"></i></span>
38 <span class="changeset-status-ico"><i class="icon-circle changeset-status-${co.status_change[0].status}"></i></span>
39 </p>
39 </p>
40 </div>
40 </div>
41 %endif
41 %endif
42 %if co.text:
42 %if co.text:
43 ${h.render_w_mentions(co.text, c.repo_name)|n}
43 ${h.render_w_mentions(co.text, c.repo_name)|n}
44 %endif
44 %endif
45 </div>
45 </div>
46 </div>
46 </div>
47 </div>
47 </div>
48 </%def>
48 </%def>
49
49
50
50
51 <%def name="comment_inline_form()">
51 <%def name="comment_inline_form()">
52 <div id='comment-inline-form-template' style="display:none">
52 <div id='comment-inline-form-template' style="display:none">
53 <div class="ac">
53 <div class="ac">
54 %if request.authuser.username != 'default':
54 %if request.authuser.username != 'default':
55 ${h.form('#', class_='inline-form')}
55 ${h.form('#', class_='inline-form')}
56 <div class="well well-sm clearfix">
56 <div class="well well-sm clearfix">
57 <div class="comment-help">${_('Commenting on line.')}
57 <div class="comment-help">${_('Commenting on line.')}
58 <span class="text-muted">${_('Comments are in plain text. Use @username inside this text to notify another user.')|n}</span>
58 <span class="text-muted">${_('Comments are in plain text. Use @username inside this text to notify another user.')|n}</span>
59 </div>
59 </div>
60 <div class="mentions-container"></div>
60 <div class="mentions-container"></div>
61 <textarea name="text" class="form-control comment-block-ta yui-ac-input"></textarea>
61 <textarea name="text" class="form-control comment-block-ta yui-ac-input"></textarea>
62
62
63 <div id="status_block_container" class="status-block general-only hidden">
63 <div id="status_block_container" class="status-block general-only hidden">
64 %if c.pull_request is None:
64 %if c.pull_request is None:
65 ${_('Set changeset status')}:
65 ${_('Set changeset status')}:
66 %else:
66 %else:
67 ${_('Vote for pull request status')}:
67 ${_('Vote for pull request status')}:
68 %endif
68 %endif
69 <span class="general-only cs-only">
69 <span class="general-only cs-only">
70 </span>
70 </span>
71 <label class="radio-inline">
71 <label class="radio-inline">
72 <input type="radio" class="status_change_radio" name="changeset_status" id="changeset_status_unchanged" value="" checked="checked" />
72 <input type="radio" class="status_change_radio" name="changeset_status" id="changeset_status_unchanged" value="" checked="checked" />
73 ${_('No change')}
73 ${_('No change')}
74 </label>
74 </label>
75 %for status, lbl in c.changeset_statuses:
75 %for status, lbl in c.changeset_statuses:
76 <label class="radio-inline">
76 <label class="radio-inline">
77 <input type="radio" class="status_change_radio" name="changeset_status" id="${status}" value="${status}">
77 <input type="radio" class="status_change_radio" name="changeset_status" id="${status}" value="${status}">
78 ${lbl}<i class="icon-circle changeset-status-${status}"></i>
78 ${lbl}<i class="icon-circle changeset-status-${status}"></i>
79 </label>
79 </label>
80 %endfor
80 %endfor
81
81
82 %if c.pull_request is not None and ( \
82 %if c.pull_request is not None and ( \
83 h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) \
83 h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionLevel('admin')(c.repo_name) \
84 or c.pull_request.owner_id == request.authuser.user_id):
84 or c.pull_request.owner_id == request.authuser.user_id):
85 <div>
85 <div>
86 ${_('Finish pull request')}:
86 ${_('Finish pull request')}:
87 <label class="checkbox-inline">
87 <label class="checkbox-inline">
88 <input id="save_close" type="checkbox" name="save_close">
88 <input id="save_close" type="checkbox" name="save_close">
89 ${_("Close")}
89 ${_("Close")}
90 </label>
90 </label>
91 <label class="checkbox-inline">
91 <label class="checkbox-inline">
92 <input id="save_delete" type="checkbox" name="save_delete" value="delete">
92 <input id="save_delete" type="checkbox" name="save_delete" value="delete">
93 ${_("Delete")}
93 ${_("Delete")}
94 </label>
94 </label>
95 </div>
95 </div>
96 %endif
96 %endif
97 </div>
97 </div>
98
98
99 </div>
99 </div>
100 <div class="comment-button">
100 <div class="comment-button">
101 <div class="submitting-overlay">${_('Submitting ...')}</div>
101 <div class="submitting-overlay">${_('Submitting ...')}</div>
102 ${h.submit('save', _('Comment'), class_='btn btn-default btn-sm save-inline-form')}
102 ${h.submit('save', _('Comment'), class_='btn btn-default btn-sm save-inline-form')}
103 ${h.reset('hide-inline-form', _('Cancel'), class_='btn btn-default btn-sm hide-inline-form')}
103 ${h.reset('hide-inline-form', _('Cancel'), class_='btn btn-default btn-sm hide-inline-form')}
104 </div>
104 </div>
105 ${h.end_form()}
105 ${h.end_form()}
106 %else:
106 %else:
107 ${h.form('')}
107 ${h.form('')}
108 <div class="clearfix">
108 <div class="clearfix">
109 <div class="comment-help">
109 <div class="comment-help">
110 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home', came_from=request.path_qs)}">${_('Login now')}</a>
110 ${_('You need to be logged in to comment.')} <a href="${h.url('login_home', came_from=request.path_qs)}">${_('Login now')}</a>
111 </div>
111 </div>
112 </div>
112 </div>
113 <div class="comment-button">
113 <div class="comment-button">
114 ${h.reset('hide-inline-form', _('Hide'), class_='btn btn-default btn-sm hide-inline-form')}
114 ${h.reset('hide-inline-form', _('Hide'), class_='btn btn-default btn-sm hide-inline-form')}
115 </div>
115 </div>
116 ${h.end_form()}
116 ${h.end_form()}
117 %endif
117 %endif
118 </div>
118 </div>
119 </div>
119 </div>
120 </%def>
120 </%def>
121
121
122
122
123 ## show comment count as "x comments (y inline, z general)"
123 ## show comment count as "x comments (y inline, z general)"
124 <%def name="comment_count(inline_cnt, general_cnt)">
124 <%def name="comment_count(inline_cnt, general_cnt)">
125 ${'%s (%s, %s)' % (
125 ${'%s (%s, %s)' % (
126 ungettext("%d comment", "%d comments", inline_cnt + general_cnt) % (inline_cnt + general_cnt),
126 ungettext("%d comment", "%d comments", inline_cnt + general_cnt) % (inline_cnt + general_cnt),
127 ungettext("%d inline", "%d inline", inline_cnt) % inline_cnt,
127 ungettext("%d inline", "%d inline", inline_cnt) % inline_cnt,
128 ungettext("%d general", "%d general", general_cnt) % general_cnt
128 ungettext("%d general", "%d general", general_cnt) % general_cnt
129 )}
129 )}
130 <span class="firstlink"></span>
130 <span class="firstlink"></span>
131 </%def>
131 </%def>
132
132
133
133
134 ## generate inline comments and the main ones
134 ## generate inline comments and the main ones
135 <%def name="generate_comments()">
135 <%def name="generate_comments()">
136 ## original location of comments ... but the ones outside diff context remains here
136 ## original location of comments ... but the ones outside diff context remains here
137 <div class="comments inline-comments">
137 <div class="comments inline-comments">
138 %for f_path, lines in c.inline_comments:
138 %for f_path, lines in c.inline_comments:
139 %for line_no, comments in lines.iteritems():
139 %for line_no, comments in lines.iteritems():
140 <div class="comments-list-chunk" data-f_path="${f_path}" data-line_no="${line_no}" data-target-id="${h.safeid(h.safe_unicode(f_path))}_${line_no}">
140 <div class="comments-list-chunk" data-f_path="${f_path}" data-line_no="${line_no}" data-target-id="${h.safeid(h.safe_unicode(f_path))}_${line_no}">
141 %for co in comments:
141 %for co in comments:
142 ${comment_block(co)}
142 ${comment_block(co)}
143 %endfor
143 %endfor
144 </div>
144 </div>
145 %endfor
145 %endfor
146 %endfor
146 %endfor
147
147
148 <div class="comments-list-chunk" data-f_path="" data-line_no="" data-target-id="general-comments">
148 <div class="comments-list-chunk" data-f_path="" data-line_no="" data-target-id="general-comments">
149 %for co in c.comments:
149 %for co in c.comments:
150 ${comment_block(co)}
150 ${comment_block(co)}
151 %endfor
151 %endfor
152 </div>
152 </div>
153 </div>
153 </div>
154 <div class="comments-number">
154 <div class="comments-number">
155 ${comment_count(c.inline_cnt, len(c.comments))}
155 ${comment_count(c.inline_cnt, len(c.comments))}
156 </div>
156 </div>
157 </%def>
157 </%def>
158
158
159 ## MAIN COMMENT FORM
159 ## MAIN COMMENT FORM
160 <%def name="comments(change_status=True)">
160 <%def name="comments(change_status=True)">
161
161
162 ## global, shared for all edit boxes
162 ## global, shared for all edit boxes
163 <div class="mentions-container" id="mentions_container"></div>
163 <div class="mentions-container" id="mentions_container"></div>
164
164
165 <div class="inline-comments inline-comments-general
165 <div class="inline-comments inline-comments-general
166 ${'show-general-status' if change_status else ''}">
166 ${'show-general-status' if change_status else ''}">
167 <div id="comments-general-comments" class="">
167 <div id="comments-general-comments" class="">
168 ## comment_div for general comments
168 ## comment_div for general comments
169 </div>
169 </div>
170 </div>
170 </div>
171
171
172 <script>
172 <script>
173
173
174 $(document).ready(function () {
174 $(document).ready(function () {
175
175
176 $(window).on('beforeunload', function(){
176 $(window).on('beforeunload', function(){
177 var $textareas = $('.comment-inline-form textarea[name=text]');
177 var $textareas = $('.comment-inline-form textarea[name=text]');
178 if($textareas.size() > 1 ||
178 if($textareas.size() > 1 ||
179 $textareas.val()) {
179 $textareas.val()) {
180 // this message will not be displayed on all browsers
180 // this message will not be displayed on all browsers
181 // (e.g. some versions of Firefox), but the user will still be warned
181 // (e.g. some versions of Firefox), but the user will still be warned
182 return 'There are uncommitted comments.';
182 return 'There are uncommitted comments.';
183 }
183 }
184 });
184 });
185
185
186 });
186 });
187 </script>
187 </script>
188 </%def>
188 </%def>
@@ -1,116 +1,116 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%block name="title">
3 <%block name="title">
4 ${_('%s File Edit') % c.repo_name}
4 ${_('%s File Edit') % c.repo_name}
5 </%block>
5 </%block>
6
6
7 <%block name="js_extra">
7 <%block name="js_extra">
8 <script type="text/javascript" src="${h.url('/codemirror/lib/codemirror.js')}"></script>
8 <script type="text/javascript" src="${h.url('/codemirror/lib/codemirror.js')}"></script>
9 <script type="text/javascript" src="${h.url('/js/codemirror_loadmode.js')}"></script>
9 <script type="text/javascript" src="${h.url('/js/codemirror_loadmode.js')}"></script>
10 <script type="text/javascript" src="${h.url('/codemirror/mode/meta.js')}"></script>
10 <script type="text/javascript" src="${h.url('/codemirror/mode/meta.js')}"></script>
11 </%block>
11 </%block>
12 <%block name="css_extra">
12 <%block name="css_extra">
13 <link rel="stylesheet" type="text/css" href="${h.url('/codemirror/lib/codemirror.css')}"/>
13 <link rel="stylesheet" type="text/css" href="${h.url('/codemirror/lib/codemirror.css')}"/>
14 </%block>
14 </%block>
15
15
16 <%block name="header_menu">
16 <%block name="header_menu">
17 ${self.menu('repositories')}
17 ${self.menu('repositories')}
18 </%block>
18 </%block>
19
19
20 <%def name="breadcrumbs_links()">
20 <%def name="breadcrumbs_links()">
21 ${_('Edit file')} @ ${h.show_id(c.cs)}
21 ${_('Edit file')} @ ${h.show_id(c.cs)}
22 </%def>
22 </%def>
23
23
24 <%def name="main()">
24 <%def name="main()">
25 ${self.repo_context_bar('files')}
25 ${self.repo_context_bar('files')}
26 <div class="panel panel-primary">
26 <div class="panel panel-primary">
27 <div class="panel-heading clearfix">
27 <div class="panel-heading clearfix">
28 <div class="pull-left">
28 <div class="pull-left">
29 ${self.breadcrumbs()}
29 ${self.breadcrumbs()}
30 </div>
30 </div>
31 <div class="pull-right">
31 <div class="pull-right">
32 <a href="#">${_('Branch')}: ${c.cs.branch}</a>
32 <a href="#">${_('Branch')}: ${c.cs.branch}</a>
33 </div>
33 </div>
34 </div>
34 </div>
35 <div class="panel-body" id="edit">
35 <div class="panel-body" id="edit">
36 <div id="files_data">
36 <div id="files_data">
37 <h3 class="files_location">${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cs.raw_id,c.file.path)}</h3>
37 <h3 class="files_location">${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.cs.raw_id,c.file.path)}</h3>
38 ${h.form(h.url.current(),method='post',id='eform',class_='form-inline')}
38 ${h.form(h.url.current(),method='post',id='eform',class_='form-inline')}
39 <div id="body" class="codeblock">
39 <div id="body" class="codeblock">
40 <div class="code-header">
40 <div class="code-header">
41 <div class="stats">
41 <div class="stats">
42 <div class="pull-left">
42 <div class="pull-left">
43 <div class="left"><i class="icon-doc-inv"></i></div>
43 <div class="left"><i class="icon-doc-inv"></i></div>
44 <div class="left item">${h.link_to(h.show_id(c.file.changeset),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id))}</div>
44 <div class="left item">${h.link_to(h.show_id(c.file.changeset),h.url('changeset_home',repo_name=c.repo_name,revision=c.file.changeset.raw_id))}</div>
45 <div class="left item">${h.format_byte_size(c.file.size,binary=True)}</div>
45 <div class="left item">${h.format_byte_size(c.file.size,binary=True)}</div>
46 <div class="left item last">${c.file.mimetype}</div>
46 <div class="left item last">${c.file.mimetype}</div>
47 <div class="pull-right buttons">
47 <div class="pull-right buttons">
48 ${h.link_to(_('Show Annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
48 ${h.link_to(_('Show Annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
49 ${h.link_to(_('Show as Raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
49 ${h.link_to(_('Show as Raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
50 ${h.link_to(_('Download as Raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
50 ${h.link_to(_('Download as Raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
51 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
51 % if h.HasRepoPermissionLevel('write')(c.repo_name):
52 % if not c.file.is_binary:
52 % if not c.file.is_binary:
53 ${h.link_to(_('Source'),h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
53 ${h.link_to(_('Source'),h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
54 % endif
54 % endif
55 % endif
55 % endif
56 </div>
56 </div>
57 </div>
57 </div>
58 </div>
58 </div>
59 <label class="editing-files">
59 <label class="editing-files">
60 ${_('Editing file')}: ${c.file.unicode_path}
60 ${_('Editing file')}: ${c.file.unicode_path}
61 <select class="form-control" id="mimetype" name="mimetype"></select>
61 <select class="form-control" id="mimetype" name="mimetype"></select>
62 </label>
62 </label>
63 </div>
63 </div>
64 <pre id="editor_pre"></pre>
64 <pre id="editor_pre"></pre>
65 <textarea id="editor" name="content" style="display:none">${h.escape(c.file.content)|n}</textarea>
65 <textarea id="editor" name="content" style="display:none">${h.escape(c.file.content)|n}</textarea>
66 <div class="text-muted" style="padding: 10px">${_('Commit Message')}</div>
66 <div class="text-muted" style="padding: 10px">${_('Commit Message')}</div>
67 <textarea class="form-control" id="commit" name="message" style="height: 60px;width: 99%;margin-left:4px" placeholder="${c.default_message}"></textarea>
67 <textarea class="form-control" id="commit" name="message" style="height: 60px;width: 99%;margin-left:4px" placeholder="${c.default_message}"></textarea>
68 </div>
68 </div>
69 <div style="text-align: left;padding-top: 5px">
69 <div style="text-align: left;padding-top: 5px">
70 ${h.submit('commit',_('Commit Changes'),class_="btn btn-success")}
70 ${h.submit('commit',_('Commit Changes'),class_="btn btn-success")}
71 ${h.reset('reset',_('Reset'),class_="btn btn-default")}
71 ${h.reset('reset',_('Reset'),class_="btn btn-default")}
72 </div>
72 </div>
73 ${h.end_form()}
73 ${h.end_form()}
74 </div>
74 </div>
75 </div>
75 </div>
76 </div>
76 </div>
77
77
78 <script type="text/javascript">
78 <script type="text/javascript">
79 $(document).ready(function(){
79 $(document).ready(function(){
80 var reset_url = "${h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.file.path)}";
80 var reset_url = "${h.url('files_home',repo_name=c.repo_name,revision=c.cs.raw_id,f_path=c.file.path)}";
81 var myCodeMirror = initCodeMirror('editor', "${request.script_name}", reset_url);
81 var myCodeMirror = initCodeMirror('editor', "${request.script_name}", reset_url);
82
82
83 //inject new modes, based on codeMirrors modeInfo object
83 //inject new modes, based on codeMirrors modeInfo object
84 var $mimetype_select = $('#mimetype');
84 var $mimetype_select = $('#mimetype');
85 $mimetype_select.each(function(){
85 $mimetype_select.each(function(){
86 var modes_select = this;
86 var modes_select = this;
87 var index = 1;
87 var index = 1;
88 for(var i=0;i<CodeMirror.modeInfo.length;i++){
88 for(var i=0;i<CodeMirror.modeInfo.length;i++){
89 var m = CodeMirror.modeInfo[i];
89 var m = CodeMirror.modeInfo[i];
90 var opt = new Option(m.name, m.mime);
90 var opt = new Option(m.name, m.mime);
91 $(opt).attr('mode', m.mode);
91 $(opt).attr('mode', m.mode);
92 if (m.mime == 'text/plain') {
92 if (m.mime == 'text/plain') {
93 // default plain text
93 // default plain text
94 $(opt).prop('selected', true);
94 $(opt).prop('selected', true);
95 modes_select.options[0] = opt;
95 modes_select.options[0] = opt;
96 } else {
96 } else {
97 modes_select.options[index++] = opt;
97 modes_select.options[index++] = opt;
98 }
98 }
99 }
99 }
100 });
100 });
101 // try to detect the mode based on the file we edit
101 // try to detect the mode based on the file we edit
102 var detected_mode = CodeMirror.findModeByExtension("${c.file.extension}");
102 var detected_mode = CodeMirror.findModeByExtension("${c.file.extension}");
103 if(detected_mode){
103 if(detected_mode){
104 setCodeMirrorMode(myCodeMirror, detected_mode);
104 setCodeMirrorMode(myCodeMirror, detected_mode);
105 $($mimetype_select.find('option[value="'+detected_mode.mime+'"]')[0]).prop('selected', true);
105 $($mimetype_select.find('option[value="'+detected_mode.mime+'"]')[0]).prop('selected', true);
106 }
106 }
107
107
108 $mimetype_select.on('change', function(e){
108 $mimetype_select.on('change', function(e){
109 var selected = e.currentTarget;
109 var selected = e.currentTarget;
110 var node = selected.options[selected.selectedIndex];
110 var node = selected.options[selected.selectedIndex];
111 var detected_mode = CodeMirror.findModeByMIME(node.value);
111 var detected_mode = CodeMirror.findModeByMIME(node.value);
112 setCodeMirrorMode(myCodeMirror, detected_mode);
112 setCodeMirrorMode(myCodeMirror, detected_mode);
113 });
113 });
114 });
114 });
115 </script>
115 </script>
116 </%def>
116 </%def>
@@ -1,101 +1,101 b''
1 <div id="node_history" style="padding: 0px 0px 10px 0px">
1 <div id="node_history" style="padding: 0px 0px 10px 0px">
2 <div>
2 <div>
3 <div class="pull-left">
3 <div class="pull-left">
4 ${h.form(h.url('files_diff_home',repo_name=c.repo_name,f_path=c.f_path),method='get')}
4 ${h.form(h.url('files_diff_home',repo_name=c.repo_name,f_path=c.f_path),method='get')}
5 ${h.hidden('diff2',c.changeset.raw_id)}
5 ${h.hidden('diff2',c.changeset.raw_id)}
6 ${h.hidden('diff1')}
6 ${h.hidden('diff1')}
7 ${h.submit('diff',_('Diff to Revision'),class_="btn btn-default btn-sm")}
7 ${h.submit('diff',_('Diff to Revision'),class_="btn btn-default btn-sm")}
8 ${h.submit('show_rev',_('Show at Revision'),class_="btn btn-default btn-sm")}
8 ${h.submit('show_rev',_('Show at Revision'),class_="btn btn-default btn-sm")}
9 ${h.hidden('annotate', c.annotate)}
9 ${h.hidden('annotate', c.annotate)}
10 ${h.link_to(_('Show Full History'),h.url('changelog_file_home',repo_name=c.repo_name, revision=c.changeset.raw_id, f_path=c.f_path),class_="btn btn-default btn-sm")}
10 ${h.link_to(_('Show Full History'),h.url('changelog_file_home',repo_name=c.repo_name, revision=c.changeset.raw_id, f_path=c.f_path),class_="btn btn-default btn-sm")}
11 ${h.link_to(_('Show Authors'),'#',class_="btn btn-default btn-sm" ,id="show_authors")}
11 ${h.link_to(_('Show Authors'),'#',class_="btn btn-default btn-sm" ,id="show_authors")}
12 ${h.end_form()}
12 ${h.end_form()}
13 </div>
13 </div>
14 <div id="file_authors" class="file_author" style="clear:both; display: none"></div>
14 <div id="file_authors" class="file_author" style="clear:both; display: none"></div>
15 </div>
15 </div>
16 <div style="clear:both"></div>
16 <div style="clear:both"></div>
17 </div>
17 </div>
18
18
19
19
20 <div id="body" class="codeblock">
20 <div id="body" class="codeblock">
21 <div class="code-header">
21 <div class="code-header">
22 <div class="stats">
22 <div class="stats">
23 <div class="pull-left">
23 <div class="pull-left">
24 <div class="left img"><i class="icon-doc-inv"></i></div>
24 <div class="left img"><i class="icon-doc-inv"></i></div>
25 <div class="left item"><pre data-toggle="tooltip" title="${h.fmt_date(c.changeset.date)}">${h.link_to(h.show_id(c.changeset),h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</pre></div>
25 <div class="left item"><pre data-toggle="tooltip" title="${h.fmt_date(c.changeset.date)}">${h.link_to(h.show_id(c.changeset),h.url('changeset_home',repo_name=c.repo_name,revision=c.changeset.raw_id))}</pre></div>
26 <div class="left item"><pre>${h.format_byte_size(c.file.size,binary=True)}</pre></div>
26 <div class="left item"><pre>${h.format_byte_size(c.file.size,binary=True)}</pre></div>
27 <div class="left item last"><pre>${c.file.mimetype}</pre></div>
27 <div class="left item last"><pre>${c.file.mimetype}</pre></div>
28 </div>
28 </div>
29 <div class="pull-right buttons">
29 <div class="pull-right buttons">
30 %if c.annotate:
30 %if c.annotate:
31 ${h.link_to(_('Show Source'), h.url('files_home', repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
31 ${h.link_to(_('Show Source'), h.url('files_home', repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
32 %else:
32 %else:
33 ${h.link_to(_('Show Annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
33 ${h.link_to(_('Show Annotation'),h.url('files_annotate_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
34 %endif
34 %endif
35 ${h.link_to(_('Show as Raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
35 ${h.link_to(_('Show as Raw'),h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
36 ${h.link_to(_('Download as Raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
36 ${h.link_to(_('Download as Raw'),h.url('files_rawfile_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path),class_="btn btn-default btn-xs")}
37 %if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
37 %if h.HasRepoPermissionLevel('write')(c.repo_name):
38 %if c.on_branch_head and not c.file.is_binary:
38 %if c.on_branch_head and not c.file.is_binary:
39 ${h.link_to(_('Edit on Branch: %s') % c.changeset.branch, h.url('files_edit_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path, anchor='edit'),class_="btn btn-default btn-xs")}
39 ${h.link_to(_('Edit on Branch: %s') % c.changeset.branch, h.url('files_edit_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path, anchor='edit'),class_="btn btn-default btn-xs")}
40 ${h.link_to(_('Delete'), h.url('files_delete_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path, anchor='edit'),class_="btn btn-danger btn-xs")}
40 ${h.link_to(_('Delete'), h.url('files_delete_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path, anchor='edit'),class_="btn btn-danger btn-xs")}
41 %elif c.on_branch_head and c.file.is_binary:
41 %elif c.on_branch_head and c.file.is_binary:
42 ${h.link_to(_('Edit'), '#', class_="btn btn-default btn-xs disabled", title=_('Editing binary files not allowed'),**{'data-toggle':'tooltip'})}
42 ${h.link_to(_('Edit'), '#', class_="btn btn-default btn-xs disabled", title=_('Editing binary files not allowed'),**{'data-toggle':'tooltip'})}
43 ${h.link_to(_('Delete'), h.url('files_delete_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path, anchor='edit'),class_="btn btn-danger btn-xs")}
43 ${h.link_to(_('Delete'), h.url('files_delete_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path, anchor='edit'),class_="btn btn-danger btn-xs")}
44 %else:
44 %else:
45 ${h.link_to(_('Edit'), '#', class_="btn btn-default btn-xs disabled", title=_('Editing files allowed only when on branch head revision'),**{'data-toggle':'tooltip'})}
45 ${h.link_to(_('Edit'), '#', class_="btn btn-default btn-xs disabled", title=_('Editing files allowed only when on branch head revision'),**{'data-toggle':'tooltip'})}
46 ${h.link_to(_('Delete'), '#', class_="btn btn-danger btn-xs disabled", title=_('Deleting files allowed only when on branch head revision'),**{'data-toggle':'tooltip'})}
46 ${h.link_to(_('Delete'), '#', class_="btn btn-danger btn-xs disabled", title=_('Deleting files allowed only when on branch head revision'),**{'data-toggle':'tooltip'})}
47 %endif
47 %endif
48 %endif
48 %endif
49 </div>
49 </div>
50 </div>
50 </div>
51 <div class="author">
51 <div class="author">
52 ${h.gravatar_div(h.email_or_none(c.changeset.author), size=16)}
52 ${h.gravatar_div(h.email_or_none(c.changeset.author), size=16)}
53 <div title="${c.changeset.author}" class="user">${h.person(c.changeset.author)}</div>
53 <div title="${c.changeset.author}" class="user">${h.person(c.changeset.author)}</div>
54 </div>
54 </div>
55 <div class="commit">${h.urlify_text(c.changeset.message,c.repo_name)}</div>
55 <div class="commit">${h.urlify_text(c.changeset.message,c.repo_name)}</div>
56 </div>
56 </div>
57 <ul class="list-group">
57 <ul class="list-group">
58 <li class="list-group-item">
58 <li class="list-group-item">
59 %if c.file.is_browser_compatible_image():
59 %if c.file.is_browser_compatible_image():
60 <img src="${h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path)}" class="img-preview"/>
60 <img src="${h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path)}" class="img-preview"/>
61 %elif c.file.is_binary:
61 %elif c.file.is_binary:
62 <div>
62 <div>
63 ${_('Binary file (%s)') % c.file.mimetype}
63 ${_('Binary file (%s)') % c.file.mimetype}
64 </div>
64 </div>
65 %else:
65 %else:
66 %if c.file.size < c.cut_off_limit or c.fulldiff:
66 %if c.file.size < c.cut_off_limit or c.fulldiff:
67 %if c.annotate:
67 %if c.annotate:
68 ${h.pygmentize_annotation(c.repo_name,c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
68 ${h.pygmentize_annotation(c.repo_name,c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
69 %else:
69 %else:
70 ${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
70 ${h.pygmentize(c.file,linenos=True,anchorlinenos=True,lineanchors='L',cssclass="code-highlight")}
71 %endif
71 %endif
72 %else:
72 %else:
73 <h4>
73 <h4>
74 ${_('File is too big to display.')}
74 ${_('File is too big to display.')}
75 %if c.annotate:
75 %if c.annotate:
76 ${h.link_to(_('Show full annotation anyway.'), h.url.current(fulldiff=1, **request.GET.mixed()))}
76 ${h.link_to(_('Show full annotation anyway.'), h.url.current(fulldiff=1, **request.GET.mixed()))}
77 %else:
77 %else:
78 ${h.link_to(_('Show as raw.'), h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))}
78 ${h.link_to(_('Show as raw.'), h.url('files_raw_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path))}
79 %endif
79 %endif
80 </h4>
80 </h4>
81 %endif
81 %endif
82 %endif
82 %endif
83 </li>
83 </li>
84 </ul>
84 </ul>
85 </div>
85 </div>
86
86
87 <script>
87 <script>
88 $(document).ready(function(){
88 $(document).ready(function(){
89 // fake html5 history state
89 // fake html5 history state
90 var _State = {
90 var _State = {
91 url: "${h.url.current()}",
91 url: "${h.url.current()}",
92 data: {
92 data: {
93 node_list_url: node_list_url.replace('__REV__',"${c.changeset.raw_id}").replace('__FPATH__', "${h.safe_unicode(c.file.path)}"),
93 node_list_url: node_list_url.replace('__REV__',"${c.changeset.raw_id}").replace('__FPATH__', "${h.safe_unicode(c.file.path)}"),
94 url_base: url_base.replace('__REV__',"${c.changeset.raw_id}"),
94 url_base: url_base.replace('__REV__',"${c.changeset.raw_id}"),
95 rev:"${c.changeset.raw_id}",
95 rev:"${c.changeset.raw_id}",
96 f_path: "${h.safe_unicode(c.file.path)}"
96 f_path: "${h.safe_unicode(c.file.path)}"
97 }
97 }
98 }
98 }
99 callbacks(_State); // defined in files.html, main callbacks. Triggered in pjax calls
99 callbacks(_State); // defined in files.html, main callbacks. Triggered in pjax calls
100 });
100 });
101 </script>
101 </script>
@@ -1,26 +1,26 b''
1 %if c.file:
1 %if c.file:
2 <h3 class="files_location">
2 <h3 class="files_location">
3 ${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.changeset.raw_id,c.file.path)}
3 ${_('Location')}: ${h.files_breadcrumbs(c.repo_name,c.changeset.raw_id,c.file.path)}
4 %if c.annotate:
4 %if c.annotate:
5 - ${_('annotation')}
5 - ${_('annotation')}
6 %endif
6 %endif
7 %if c.file.is_dir():
7 %if c.file.is_dir():
8 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
8 % if h.HasRepoPermissionLevel('write')(c.repo_name):
9 / <span title="${_('Add New File')}">
9 / <span title="${_('Add New File')}">
10 <a href="${h.url('files_add_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path, anchor='edit')}">
10 <a href="${h.url('files_add_home',repo_name=c.repo_name,revision=c.changeset.raw_id,f_path=c.f_path, anchor='edit')}">
11 <i class="icon-plus-circled" style="color:#5bb75b; font-size: 16px"></i></a>
11 <i class="icon-plus-circled" style="color:#5bb75b; font-size: 16px"></i></a>
12 </span>
12 </span>
13 % endif
13 % endif
14 %endif
14 %endif
15 </h3>
15 </h3>
16 %if c.file.is_dir():
16 %if c.file.is_dir():
17 <%include file='files_browser.html'/>
17 <%include file='files_browser.html'/>
18 %else:
18 %else:
19 <%include file='files_source.html'/>
19 <%include file='files_source.html'/>
20 %endif
20 %endif
21 %else:
21 %else:
22 <h2>
22 <h2>
23 <a href="#" onClick="javascript:parent.history.back();" target="main">${_('Go Back')}</a>
23 <a href="#" onClick="javascript:parent.history.back();" target="main">${_('Go Back')}</a>
24 ${_('No files at given path')}: "${c.f_path or "/"}"
24 ${_('No files at given path')}: "${c.f_path or "/"}"
25 </h2>
25 </h2>
26 %endif
26 %endif
@@ -1,410 +1,410 b''
1 <%inherit file="/base/base.html"/>
1 <%inherit file="/base/base.html"/>
2
2
3 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
3 <%namespace name="comment" file="/changeset/changeset_file_comment.html"/>
4
4
5 <%block name="title">
5 <%block name="title">
6 ${_('%s Pull Request %s') % (c.repo_name, c.pull_request.nice_id())}
6 ${_('%s Pull Request %s') % (c.repo_name, c.pull_request.nice_id())}
7 </%block>
7 </%block>
8
8
9 <%def name="breadcrumbs_links()">
9 <%def name="breadcrumbs_links()">
10 ${_('Pull request %s from %s#%s') % (c.pull_request.nice_id(), c.pull_request.org_repo.repo_name, c.cs_branch_name)}
10 ${_('Pull request %s from %s#%s') % (c.pull_request.nice_id(), c.pull_request.org_repo.repo_name, c.cs_branch_name)}
11 </%def>
11 </%def>
12
12
13 <%block name="header_menu">
13 <%block name="header_menu">
14 ${self.menu('repositories')}
14 ${self.menu('repositories')}
15 </%block>
15 </%block>
16
16
17 <%def name="main()">
17 <%def name="main()">
18 <% editable = not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or c.pull_request.owner_id == request.authuser.user_id) %>
18 <% editable = not c.pull_request.is_closed() and (h.HasPermissionAny('hg.admin')() or h.HasRepoPermissionLevel('admin')(c.repo_name) or c.pull_request.owner_id == request.authuser.user_id) %>
19 ${self.repo_context_bar('showpullrequest')}
19 ${self.repo_context_bar('showpullrequest')}
20 <div class="panel panel-primary">
20 <div class="panel panel-primary">
21 <div class="panel-heading clearfix">
21 <div class="panel-heading clearfix">
22 ${self.breadcrumbs()}
22 ${self.breadcrumbs()}
23 </div>
23 </div>
24
24
25 ${h.form(url('pullrequest_post', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), method='post', id='pull_request_form',class_='panel-body')}
25 ${h.form(url('pullrequest_post', repo_name=c.repo_name, pull_request_id=c.pull_request.pull_request_id), method='post', id='pull_request_form',class_='panel-body')}
26 <div class="form pr-box pull-left">
26 <div class="form pr-box pull-left">
27 <div class="pr-details-title ${'closed' if c.pull_request.is_closed() else ''}">
27 <div class="pr-details-title ${'closed' if c.pull_request.is_closed() else ''}">
28 <h3>
28 <h3>
29 ${_('Title')}: ${c.pull_request.title}
29 ${_('Title')}: ${c.pull_request.title}
30 %if c.pull_request.is_closed():
30 %if c.pull_request.is_closed():
31 (${_('Closed')})
31 (${_('Closed')})
32 %endif
32 %endif
33 </h3>
33 </h3>
34 </div>
34 </div>
35 <div id="pr-summary" class="form-horizontal">
35 <div id="pr-summary" class="form-horizontal">
36
36
37 <div class="pr-not-edit form-group" style="min-height:47px">
37 <div class="pr-not-edit form-group" style="min-height:47px">
38 <label>${_('Description')}:</label>
38 <label>${_('Description')}:</label>
39 %if editable:
39 %if editable:
40 <div style="margin: 20px 0; position: absolute">
40 <div style="margin: 20px 0; position: absolute">
41 <a class="btn btn-default btn-xs" onclick="$('.pr-do-edit').show();$('.pr-not-edit').hide()">${_("Edit")}</a>
41 <a class="btn btn-default btn-xs" onclick="$('.pr-do-edit').show();$('.pr-not-edit').hide()">${_("Edit")}</a>
42 </div>
42 </div>
43 %endif
43 %endif
44 <div>
44 <div>
45 <div class="form-control formatted-fixed">${h.urlify_text(c.pull_request.description, c.pull_request.org_repo.repo_name)}</div>
45 <div class="form-control formatted-fixed">${h.urlify_text(c.pull_request.description, c.pull_request.org_repo.repo_name)}</div>
46 </div>
46 </div>
47 </div>
47 </div>
48
48
49 %if editable:
49 %if editable:
50 <div class="pr-do-edit form-group" style="display:none">
50 <div class="pr-do-edit form-group" style="display:none">
51 <label for="pullrequest_title">${_('Title')}:</label>
51 <label for="pullrequest_title">${_('Title')}:</label>
52 <div>
52 <div>
53 ${h.text('pullrequest_title',class_='form-control',value=c.pull_request.title,placeholder=_('Summarize the changes'))}
53 ${h.text('pullrequest_title',class_='form-control',value=c.pull_request.title,placeholder=_('Summarize the changes'))}
54 </div>
54 </div>
55 </div>
55 </div>
56
56
57 <div class="pr-do-edit form-group" style="display:none">
57 <div class="pr-do-edit form-group" style="display:none">
58 <label for="pullrequest_desc">${_('Description')}:</label>
58 <label for="pullrequest_desc">${_('Description')}:</label>
59 <div>
59 <div>
60 ${h.textarea('pullrequest_desc',content=c.pull_request.description,placeholder=_('Write a short description on this pull request'),class_='form-control')}
60 ${h.textarea('pullrequest_desc',content=c.pull_request.description,placeholder=_('Write a short description on this pull request'),class_='form-control')}
61 </div>
61 </div>
62 </div>
62 </div>
63 %endif
63 %endif
64
64
65 <div class="form-group">
65 <div class="form-group">
66 <label>${_('Reviewer voting result')}:</label>
66 <label>${_('Reviewer voting result')}:</label>
67 <div>
67 <div>
68 <div class="changeset-status-container">
68 <div class="changeset-status-container">
69 %if c.current_voting_result:
69 %if c.current_voting_result:
70 <span class="changeset-status-ico" style="padding:0px 4px 0px 0px">
70 <span class="changeset-status-ico" style="padding:0px 4px 0px 0px">
71 <i class="icon-circle changeset-status-${c.current_voting_result}" title="${_('Pull request status calculated from votes')}"></i></span>
71 <i class="icon-circle changeset-status-${c.current_voting_result}" title="${_('Pull request status calculated from votes')}"></i></span>
72 <span class="changeset-status-lbl" data-toggle="tooltip" title="${_('Pull request status calculated from votes')}">
72 <span class="changeset-status-lbl" data-toggle="tooltip" title="${_('Pull request status calculated from votes')}">
73 %if c.pull_request.is_closed():
73 %if c.pull_request.is_closed():
74 ${_('Closed')},
74 ${_('Closed')},
75 %endif
75 %endif
76 ${h.changeset_status_lbl(c.current_voting_result)}
76 ${h.changeset_status_lbl(c.current_voting_result)}
77 </span>
77 </span>
78 %endif
78 %endif
79 </div>
79 </div>
80 </div>
80 </div>
81 </div>
81 </div>
82 <div class="form-group">
82 <div class="form-group">
83 <label>${_('Still not reviewed by')}:</label>
83 <label>${_('Still not reviewed by')}:</label>
84 <div>
84 <div>
85 % if len(c.pull_request_pending_reviewers) > 0:
85 % if len(c.pull_request_pending_reviewers) > 0:
86 <div data-toggle="tooltip" title="${', '.join([x.username for x in c.pull_request_pending_reviewers])}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
86 <div data-toggle="tooltip" title="${', '.join([x.username for x in c.pull_request_pending_reviewers])}">${ungettext('%d reviewer', '%d reviewers',len(c.pull_request_pending_reviewers)) % len(c.pull_request_pending_reviewers)}</div>
87 % elif len(c.pull_request_reviewers) > 0:
87 % elif len(c.pull_request_reviewers) > 0:
88 <div>${_('Pull request was reviewed by all reviewers')}</div>
88 <div>${_('Pull request was reviewed by all reviewers')}</div>
89 %else:
89 %else:
90 <div>${_('There are no reviewers')}</div>
90 <div>${_('There are no reviewers')}</div>
91 %endif
91 %endif
92 </div>
92 </div>
93 </div>
93 </div>
94 <div class="form-group">
94 <div class="form-group">
95 <label>${_('Origin')}:</label>
95 <label>${_('Origin')}:</label>
96 <div>
96 <div>
97 <div>
97 <div>
98 ${h.link_to_ref(c.pull_request.org_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev)}
98 ${h.link_to_ref(c.pull_request.org_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev)}
99 %if c.cs_ref_type != 'branch':
99 %if c.cs_ref_type != 'branch':
100 ${_('on')} ${h.link_to_ref(c.pull_request.org_repo.repo_name, 'branch', c.cs_branch_name)}
100 ${_('on')} ${h.link_to_ref(c.pull_request.org_repo.repo_name, 'branch', c.cs_branch_name)}
101 %endif
101 %endif
102 </div>
102 </div>
103 </div>
103 </div>
104 </div>
104 </div>
105 <div class="form-group">
105 <div class="form-group">
106 <label>${_('Target')}:</label>
106 <label>${_('Target')}:</label>
107 <div>
107 <div>
108 %if c.is_range:
108 %if c.is_range:
109 ${_("This is just a range of changesets and doesn't have a target or a real merge ancestor.")}
109 ${_("This is just a range of changesets and doesn't have a target or a real merge ancestor.")}
110 %else:
110 %else:
111 ${h.link_to_ref(c.pull_request.other_repo.repo_name, c.a_ref_type, c.a_ref_name)}
111 ${h.link_to_ref(c.pull_request.other_repo.repo_name, c.a_ref_type, c.a_ref_name)}
112 ## we don't know other rev - c.a_rev is ancestor and not necessarily on other_name_branch branch
112 ## we don't know other rev - c.a_rev is ancestor and not necessarily on other_name_branch branch
113 %endif
113 %endif
114 </div>
114 </div>
115 </div>
115 </div>
116 <div class="form-group">
116 <div class="form-group">
117 <label>${_('Pull changes')}:</label>
117 <label>${_('Pull changes')}:</label>
118 <div>
118 <div>
119 %if c.cs_ranges:
119 %if c.cs_ranges:
120 <div>
120 <div>
121 ## TODO: use cs_ranges[-1] or org_ref_parts[1] in both cases?
121 ## TODO: use cs_ranges[-1] or org_ref_parts[1] in both cases?
122 %if h.is_hg(c.pull_request.org_repo):
122 %if h.is_hg(c.pull_request.org_repo):
123 <span style="font-family: monospace">hg pull ${c.pull_request.org_repo.clone_url()} -r ${h.short_id(c.cs_ranges[-1].raw_id)}</span>
123 <span style="font-family: monospace">hg pull ${c.pull_request.org_repo.clone_url()} -r ${h.short_id(c.cs_ranges[-1].raw_id)}</span>
124 %elif h.is_git(c.pull_request.org_repo):
124 %elif h.is_git(c.pull_request.org_repo):
125 <span style="font-family: monospace">git pull ${c.pull_request.org_repo.clone_url()} ${c.pull_request.org_ref_parts[1]}</span>
125 <span style="font-family: monospace">git pull ${c.pull_request.org_repo.clone_url()} ${c.pull_request.org_ref_parts[1]}</span>
126 %endif
126 %endif
127 </div>
127 </div>
128 %endif
128 %endif
129 </div>
129 </div>
130 </div>
130 </div>
131 <div class="form-group">
131 <div class="form-group">
132 <label>${_('Created on')}:</label>
132 <label>${_('Created on')}:</label>
133 <div>
133 <div>
134 <div>${h.fmt_date(c.pull_request.created_on)}</div>
134 <div>${h.fmt_date(c.pull_request.created_on)}</div>
135 </div>
135 </div>
136 </div>
136 </div>
137 <div class="form-group">
137 <div class="form-group">
138 <label>${_('Owner')}:</label>
138 <label>${_('Owner')}:</label>
139 <div class="pr-not-edit">
139 <div class="pr-not-edit">
140 ${h.gravatar_div(c.pull_request.owner.email, size=20)}
140 ${h.gravatar_div(c.pull_request.owner.email, size=20)}
141 <span>${c.pull_request.owner.full_name_and_username}</span><br/>
141 <span>${c.pull_request.owner.full_name_and_username}</span><br/>
142 <span><a href="mailto:${c.pull_request.owner.email}">${c.pull_request.owner.email}</a></span><br/>
142 <span><a href="mailto:${c.pull_request.owner.email}">${c.pull_request.owner.email}</a></span><br/>
143 </div>
143 </div>
144 <div class="pr-do-edit ac" style="display:none">
144 <div class="pr-do-edit ac" style="display:none">
145 ${h.text('owner', class_='form-control', value=c.pull_request.owner.username, placeholder=_('Username'))}
145 ${h.text('owner', class_='form-control', value=c.pull_request.owner.username, placeholder=_('Username'))}
146 <div id="owner_completion_container"></div>
146 <div id="owner_completion_container"></div>
147 </div>
147 </div>
148 </div>
148 </div>
149
149
150 <div class="form-group">
150 <div class="form-group">
151 <label>${_('Next iteration')}:</label>
151 <label>${_('Next iteration')}:</label>
152 <div>
152 <div>
153 <div class="msg-div">${c.update_msg}</div>
153 <div class="msg-div">${c.update_msg}</div>
154 %if c.avail_revs:
154 %if c.avail_revs:
155 <div id="updaterevs" class="clearfix" style="max-height:200px; overflow-y:auto; overflow-x:hidden; margin-bottom: 10px; padding: 1px 0">
155 <div id="updaterevs" class="clearfix" style="max-height:200px; overflow-y:auto; overflow-x:hidden; margin-bottom: 10px; padding: 1px 0">
156 <div style="height:0;width:40px">
156 <div style="height:0;width:40px">
157 <canvas id="avail_graph_canvas" style="width:0"></canvas>
157 <canvas id="avail_graph_canvas" style="width:0"></canvas>
158 </div>
158 </div>
159 <table class="table" id="updaterevs-table" style="padding-left:50px">
159 <table class="table" id="updaterevs-table" style="padding-left:50px">
160 %for cnt, cs in enumerate(c.avail_cs):
160 %for cnt, cs in enumerate(c.avail_cs):
161 <tr id="chg_available_${cnt+1}" class="${'mergerow' if len(cs.parents) > 1 and not (editable and cs.revision in c.avail_revs) else ''}">
161 <tr id="chg_available_${cnt+1}" class="${'mergerow' if len(cs.parents) > 1 and not (editable and cs.revision in c.avail_revs) else ''}">
162 %if c.cs_ranges and cs.revision == c.cs_ranges[-1].revision:
162 %if c.cs_ranges and cs.revision == c.cs_ranges[-1].revision:
163 <td>
163 <td>
164 %if editable:
164 %if editable:
165 ${h.radio(name='updaterev', value='', checked=True)}
165 ${h.radio(name='updaterev', value='', checked=True)}
166 %endif
166 %endif
167 </td>
167 </td>
168 <td colspan="4">${_("Current revision - no change")}</td>
168 <td colspan="4">${_("Current revision - no change")}</td>
169 %else:
169 %else:
170 <td>
170 <td>
171 %if editable and cs.revision in c.avail_revs:
171 %if editable and cs.revision in c.avail_revs:
172 ${h.radio(name='updaterev', value=cs.raw_id)}
172 ${h.radio(name='updaterev', value=cs.raw_id)}
173 %endif
173 %endif
174 </td>
174 </td>
175 <td style="width: 120px"><span data-toggle="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
175 <td style="width: 120px"><span data-toggle="tooltip" title="${h.age(cs.date)}">${cs.date}</span></td>
176 <td>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id), class_='changeset_hash')}</td>
176 <td>${h.link_to(h.show_id(cs),h.url('changeset_home',repo_name=c.cs_repo.repo_name,revision=cs.raw_id), class_='changeset_hash')}</td>
177 <td>
177 <td>
178 <div class="pull-right" style="margin-top: -4px;">
178 <div class="pull-right" style="margin-top: -4px;">
179 %for tag in cs.tags:
179 %for tag in cs.tags:
180 <span class="tagtag" title="${_('Tag %s') % tag}">
180 <span class="tagtag" title="${_('Tag %s') % tag}">
181 ${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
181 ${h.link_to(tag,h.url('changeset_home',repo_name=c.repo_name,revision=cs.raw_id))}
182 </span>
182 </span>
183 %endfor
183 %endfor
184 </div>
184 </div>
185 <div class="message" style="white-space:normal; height:1.1em; max-width: 500px; padding:0">${h.urlify_text(cs.message, c.repo_name)}</div>
185 <div class="message" style="white-space:normal; height:1.1em; max-width: 500px; padding:0">${h.urlify_text(cs.message, c.repo_name)}</div>
186 </td>
186 </td>
187 %endif
187 %endif
188 </tr>
188 </tr>
189 %endfor
189 %endfor
190 </table>
190 </table>
191 </div>
191 </div>
192 <div class="msg-div">(${_("Pull request iterations do not change content once created. Select a revision and save to make a new iteration.")})</div>
192 <div class="msg-div">(${_("Pull request iterations do not change content once created. Select a revision and save to make a new iteration.")})</div>
193 %endif
193 %endif
194 <div class="msg-div">${c.update_msg_other}</div>
194 <div class="msg-div">${c.update_msg_other}</div>
195 </div>
195 </div>
196 </div>
196 </div>
197 %if editable:
197 %if editable:
198 <div class="form-group">
198 <div class="form-group">
199 <div class="buttons">
199 <div class="buttons">
200 ${h.submit('pr-form-save',_('Save Changes'),class_="btn btn-default btn-sm")}
200 ${h.submit('pr-form-save',_('Save Changes'),class_="btn btn-default btn-sm")}
201 ${h.submit('pr-form-clone',_('Create New Iteration with Changes'),class_="btn btn-default btn-sm",disabled='disabled')}
201 ${h.submit('pr-form-clone',_('Create New Iteration with Changes'),class_="btn btn-default btn-sm",disabled='disabled')}
202 ${h.reset('pr-form-reset',_('Cancel Changes'),class_="btn btn-default btn-sm")}
202 ${h.reset('pr-form-reset',_('Cancel Changes'),class_="btn btn-default btn-sm")}
203 </div>
203 </div>
204 </div>
204 </div>
205 %endif
205 %endif
206 </div>
206 </div>
207 </div>
207 </div>
208 ## REVIEWERS
208 ## REVIEWERS
209 <div class="pull-left">
209 <div class="pull-left">
210 <h4 class="pr-details-title">${_('Pull Request Reviewers')}</h4>
210 <h4 class="pr-details-title">${_('Pull Request Reviewers')}</h4>
211 <div id="reviewers" style="padding:0px 0px 5px 10px">
211 <div id="reviewers" style="padding:0px 0px 5px 10px">
212 ## members goes here !
212 ## members goes here !
213 <div>
213 <div>
214 %for member,status in c.pull_request_reviewers:
214 %for member,status in c.pull_request_reviewers:
215 <input type="hidden" value="${member.user_id}" name="org_review_members" />
215 <input type="hidden" value="${member.user_id}" name="org_review_members" />
216 %endfor
216 %endfor
217 <ul id="review_members" class="list-unstyled">
217 <ul id="review_members" class="list-unstyled">
218 %for member,status in c.pull_request_reviewers:
218 %for member,status in c.pull_request_reviewers:
219 ## WARNING: the HTML below is duplicate with
219 ## WARNING: the HTML below is duplicate with
220 ## kallithea/public/js/base.js
220 ## kallithea/public/js/base.js
221 ## If you change something here it should be reflected in the template too.
221 ## If you change something here it should be reflected in the template too.
222 <li id="reviewer_${member.user_id}">
222 <li id="reviewer_${member.user_id}">
223 <span class="reviewers_member">
223 <span class="reviewers_member">
224 <span class="reviewer_status" data-toggle="tooltip" title="${h.changeset_status_lbl(status)}">
224 <span class="reviewer_status" data-toggle="tooltip" title="${h.changeset_status_lbl(status)}">
225 <i class="icon-circle changeset-status-${status}"></i>
225 <i class="icon-circle changeset-status-${status}"></i>
226 </span>
226 </span>
227 ${h.gravatar(member.email, size=14)}
227 ${h.gravatar(member.email, size=14)}
228 <span>
228 <span>
229 ${member.full_name_and_username}
229 ${member.full_name_and_username}
230 %if c.pull_request.owner_id == member.user_id:
230 %if c.pull_request.owner_id == member.user_id:
231 (${_('Owner')})
231 (${_('Owner')})
232 %endif
232 %endif
233 </span>
233 </span>
234 <input type="hidden" value="${member.user_id}" name="review_members" />
234 <input type="hidden" value="${member.user_id}" name="review_members" />
235 %if editable:
235 %if editable:
236 <a href="#" class="reviewer_member_remove" onclick="removeReviewMember(${member.user_id})" title="${_('Remove reviewer')}">
236 <a href="#" class="reviewer_member_remove" onclick="removeReviewMember(${member.user_id})" title="${_('Remove reviewer')}">
237 <i class="icon-minus-circled"></i>
237 <i class="icon-minus-circled"></i>
238 </a>
238 </a>
239 %endif
239 %endif
240 </span>
240 </span>
241 </li>
241 </li>
242 %endfor
242 %endfor
243 </ul>
243 </ul>
244 </div>
244 </div>
245 %if editable:
245 %if editable:
246 <div class='ac'>
246 <div class='ac'>
247 <div class="reviewer_ac">
247 <div class="reviewer_ac">
248 ${h.text('user', class_='yui-ac-input form-control',placeholder=_('Type name of reviewer to add'))}
248 ${h.text('user', class_='yui-ac-input form-control',placeholder=_('Type name of reviewer to add'))}
249 <div id="reviewers_container"></div>
249 <div id="reviewers_container"></div>
250 </div>
250 </div>
251 </div>
251 </div>
252 %endif
252 %endif
253 </div>
253 </div>
254
254
255 %if not c.pull_request_reviewers:
255 %if not c.pull_request_reviewers:
256 <h4>${_('Potential Reviewers')}</h4>
256 <h4>${_('Potential Reviewers')}</h4>
257 <div style="margin: 10px 0 10px 10px; max-width: 250px">
257 <div style="margin: 10px 0 10px 10px; max-width: 250px">
258 <div>
258 <div>
259 ${_('Click to add the repository owner as reviewer:')}
259 ${_('Click to add the repository owner as reviewer:')}
260 </div>
260 </div>
261 <ul class="list-unstyled">
261 <ul class="list-unstyled">
262 %for u in [c.pull_request.other_repo.owner]:
262 %for u in [c.pull_request.other_repo.owner]:
263 <li>
263 <li>
264 <a class="missing_reviewer missing_reviewer_${u.user_id}"
264 <a class="missing_reviewer missing_reviewer_${u.user_id}"
265 href="#"
265 href="#"
266 data-user_id="${u.user_id}"
266 data-user_id="${u.user_id}"
267 data-fname="${u.name}"
267 data-fname="${u.name}"
268 data-lname="${u.lastname}"
268 data-lname="${u.lastname}"
269 data-nname="${u.username}"
269 data-nname="${u.username}"
270 data-gravatar_lnk="${h.gravatar_url(u.email, size=28, default='default')}"
270 data-gravatar_lnk="${h.gravatar_url(u.email, size=28, default='default')}"
271 data-gravatar_size="14"
271 data-gravatar_size="14"
272 title="Click to add reviewer to the list, then Save Changes.">${u.full_name}</a>
272 title="Click to add reviewer to the list, then Save Changes.">${u.full_name}</a>
273 </li>
273 </li>
274 %endfor
274 %endfor
275 </ul>
275 </ul>
276 </div>
276 </div>
277 %endif
277 %endif
278 </div>
278 </div>
279 <div style="clear:both">
279 <div style="clear:both">
280 </div>
280 </div>
281 ${h.end_form()}
281 ${h.end_form()}
282 </div>
282 </div>
283
283
284 <div class="panel panel-primary">
284 <div class="panel panel-primary">
285 <div class="panel-heading clearfix">
285 <div class="panel-heading clearfix">
286 <div class="breadcrumbs">${_('Pull Request Content')}</div>
286 <div class="breadcrumbs">${_('Pull Request Content')}</div>
287 </div>
287 </div>
288 <div class="panel-body">
288 <div class="panel-body">
289 <div>
289 <div>
290 <div id="changeset_compare_view_content">
290 <div id="changeset_compare_view_content">
291 <h5>
291 <h5>
292 ${comment.comment_count(c.inline_cnt, len(c.comments))}
292 ${comment.comment_count(c.inline_cnt, len(c.comments))}
293 </h5>
293 </h5>
294 ##CS
294 ##CS
295 <h5>
295 <h5>
296 ${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}
296 ${ungettext('Showing %s commit','Showing %s commits', len(c.cs_ranges)) % len(c.cs_ranges)}
297 </h5>
297 </h5>
298 <%include file="/compare/compare_cs.html" />
298 <%include file="/compare/compare_cs.html" />
299
299
300 <h5>
300 <h5>
301 ${_('Common ancestor')}:
301 ${_('Common ancestor')}:
302 ${h.link_to(h.short_id(c.a_rev),h.url('changeset_home',repo_name=c.a_repo.repo_name,revision=c.a_rev), class_="changeset_hash")}
302 ${h.link_to(h.short_id(c.a_rev),h.url('changeset_home',repo_name=c.a_repo.repo_name,revision=c.a_rev), class_="changeset_hash")}
303 </h5>
303 </h5>
304
304
305 ## FILES
305 ## FILES
306 <h5>
306 <h5>
307 % if c.limited_diff:
307 % if c.limited_diff:
308 ${ungettext('%s file changed', '%s files changed', len(c.file_diff_data)) % len(c.file_diff_data)}:
308 ${ungettext('%s file changed', '%s files changed', len(c.file_diff_data)) % len(c.file_diff_data)}:
309 % else:
309 % else:
310 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.file_diff_data)) % (len(c.file_diff_data),c.lines_added,c.lines_deleted)}:
310 ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.file_diff_data)) % (len(c.file_diff_data),c.lines_added,c.lines_deleted)}:
311 %endif
311 %endif
312 </h5>
312 </h5>
313 <div class="cs_files">
313 <div class="cs_files">
314 %if not c.file_diff_data:
314 %if not c.file_diff_data:
315 <span class="empty_data">${_('No files')}</span>
315 <span class="empty_data">${_('No files')}</span>
316 %endif
316 %endif
317 %for fid, url_fid, op, a_path, path, diff, stats in c.file_diff_data:
317 %for fid, url_fid, op, a_path, path, diff, stats in c.file_diff_data:
318 <div class="cs_${op} clearfix">
318 <div class="cs_${op} clearfix">
319 <div class="node pull-left">
319 <div class="node pull-left">
320 <i class="icon-diff-${op}"></i>
320 <i class="icon-diff-${op}"></i>
321 ${h.link_to(h.safe_unicode(path), '#%s' % fid)}
321 ${h.link_to(h.safe_unicode(path), '#%s' % fid)}
322 </div>
322 </div>
323 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
323 <div class="changes pull-right">${h.fancy_file_stats(stats)}</div>
324 </div>
324 </div>
325 %endfor
325 %endfor
326 %if c.limited_diff:
326 %if c.limited_diff:
327 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h5>
327 <h5>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h5>
328 %endif
328 %endif
329 </div>
329 </div>
330 </div>
330 </div>
331 </div>
331 </div>
332 </div>
332 </div>
333 <script>
333 <script>
334 var _USERS_AC_DATA = ${c.users_array|n};
334 var _USERS_AC_DATA = ${c.users_array|n};
335 var _GROUPS_AC_DATA = ${c.user_groups_array|n};
335 var _GROUPS_AC_DATA = ${c.user_groups_array|n};
336 // TODO: switch this to pyroutes
336 // TODO: switch this to pyroutes
337 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
337 AJAX_COMMENT_URL = "${url('pullrequest_comment',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id)}";
338 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
338 AJAX_COMMENT_DELETE_URL = "${url('pullrequest_comment_delete',repo_name=c.repo_name,comment_id='__COMMENT_ID__')}";
339
339
340 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
340 pyroutes.register('pullrequest_comment', "${url('pullrequest_comment',repo_name='%(repo_name)s',pull_request_id='%(pull_request_id)s')}", ['repo_name', 'pull_request_id']);
341 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
341 pyroutes.register('pullrequest_comment_delete', "${url('pullrequest_comment_delete',repo_name='%(repo_name)s',comment_id='%(comment_id)s')}", ['repo_name', 'comment_id']);
342
342
343 </script>
343 </script>
344
344
345 ## diff block
345 ## diff block
346 <div class="panel-body">
346 <div class="panel-body">
347 <div class="commentable-diff">
347 <div class="commentable-diff">
348 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
348 <%namespace name="diff_block" file="/changeset/diff_block.html"/>
349 ${diff_block.diff_block_js()}
349 ${diff_block.diff_block_js()}
350 ${diff_block.diff_block(c.a_repo.repo_name, c.a_ref_type, c.a_ref_name, c.a_rev,
350 ${diff_block.diff_block(c.a_repo.repo_name, c.a_ref_type, c.a_ref_name, c.a_rev,
351 c.cs_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev, c.file_diff_data)}
351 c.cs_repo.repo_name, c.cs_ref_type, c.cs_ref_name, c.cs_rev, c.file_diff_data)}
352 % if c.limited_diff:
352 % if c.limited_diff:
353 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h4>
353 <h4>${_('Changeset was too big and was cut off...')} <a href="${h.url.current(fulldiff=1, **request.GET.mixed())}">${_('Show full diff anyway')}</a></h4>
354 % endif
354 % endif
355 </div>
355 </div>
356
356
357 ## template for inline comment form
357 ## template for inline comment form
358 ${comment.comment_inline_form()}
358 ${comment.comment_inline_form()}
359
359
360 ## render comments and inlines
360 ## render comments and inlines
361 ${comment.generate_comments()}
361 ${comment.generate_comments()}
362
362
363 ## main comment form and it status
363 ## main comment form and it status
364 ${comment.comments(change_status=c.allowed_to_change_status)}
364 ${comment.comments(change_status=c.allowed_to_change_status)}
365
365
366 <script type="text/javascript">
366 <script type="text/javascript">
367 $(document).ready(function(){
367 $(document).ready(function(){
368 PullRequestAutoComplete($('#user'), $('#reviewers_container'), _USERS_AC_DATA);
368 PullRequestAutoComplete($('#user'), $('#reviewers_container'), _USERS_AC_DATA);
369 SimpleUserAutoComplete($('#owner'), $('#owner_completion_container'), _USERS_AC_DATA);
369 SimpleUserAutoComplete($('#owner'), $('#owner_completion_container'), _USERS_AC_DATA);
370
370
371 $('.code-difftable').on('click', '.add-bubble', function(e){
371 $('.code-difftable').on('click', '.add-bubble', function(e){
372 show_comment_form($(this));
372 show_comment_form($(this));
373 });
373 });
374
374
375 var avail_jsdata = ${c.avail_jsdata|n};
375 var avail_jsdata = ${c.avail_jsdata|n};
376 var avail_r = new BranchRenderer('avail_graph_canvas', 'updaterevs-table', 'chg_available_');
376 var avail_r = new BranchRenderer('avail_graph_canvas', 'updaterevs-table', 'chg_available_');
377 avail_r.render(avail_jsdata);
377 avail_r.render(avail_jsdata);
378
378
379 move_comments($(".comments .comments-list-chunk"));
379 move_comments($(".comments .comments-list-chunk"));
380
380
381 $('#updaterevs input').change(function(e){
381 $('#updaterevs input').change(function(e){
382 var update = !!e.target.value;
382 var update = !!e.target.value;
383 $('#pr-form-save').prop('disabled',update);
383 $('#pr-form-save').prop('disabled',update);
384 $('#pr-form-clone').prop('disabled',!update);
384 $('#pr-form-clone').prop('disabled',!update);
385 });
385 });
386 var $org_review_members = $('#review_members').clone();
386 var $org_review_members = $('#review_members').clone();
387 $('#pr-form-reset').click(function(e){
387 $('#pr-form-reset').click(function(e){
388 $('.pr-do-edit').hide();
388 $('.pr-do-edit').hide();
389 $('.pr-not-edit').show();
389 $('.pr-not-edit').show();
390 $('#pr-form-save').prop('disabled',false);
390 $('#pr-form-save').prop('disabled',false);
391 $('#pr-form-clone').prop('disabled',true);
391 $('#pr-form-clone').prop('disabled',true);
392 $('#review_members').html($org_review_members);
392 $('#review_members').html($org_review_members);
393 });
393 });
394
394
395 // hack: re-navigate to target after JS is done ... if a target is set and setting href thus won't reload
395 // hack: re-navigate to target after JS is done ... if a target is set and setting href thus won't reload
396 if (window.location.hash != "") {
396 if (window.location.hash != "") {
397 window.location.href = window.location.href;
397 window.location.href = window.location.href;
398 }
398 }
399
399
400 $('.missing_reviewer').click(function(){
400 $('.missing_reviewer').click(function(){
401 var $this = $(this);
401 var $this = $(this);
402 addReviewMember($this.data('user_id'), $this.data('fname'), $this.data('lname'), $this.data('nname'), $this.data('gravatar_lnk'), $this.data('gravatar_size'));
402 addReviewMember($this.data('user_id'), $this.data('fname'), $this.data('lname'), $this.data('nname'), $this.data('gravatar_lnk'), $this.data('gravatar_size'));
403 });
403 });
404 });
404 });
405 </script>
405 </script>
406 </div>
406 </div>
407
407
408 </div>
408 </div>
409
409
410 </%def>
410 </%def>
@@ -1,39 +1,39 b''
1 ##commit highlighting
1 ##commit highlighting
2
2
3 %for cnt,sr in enumerate(c.formated_results):
3 %for cnt,sr in enumerate(c.formated_results):
4 %if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(sr['repository'],'search results check'):
4 %if h.HasRepoPermissionLevel('read')(sr['repository'],'search results check'):
5 <div id="body${cnt}" class="codeblock">
5 <div id="body${cnt}" class="codeblock">
6 <div class="code-header">
6 <div class="code-header">
7 <div class="search-path">${h.link_to(h.literal('%s &raquo; %s' % (sr['repository'],sr['raw_id'])),
7 <div class="search-path">${h.link_to(h.literal('%s &raquo; %s' % (sr['repository'],sr['raw_id'])),
8 h.url('changeset_home',repo_name=sr['repository'],revision=sr['raw_id']))}
8 h.url('changeset_home',repo_name=sr['repository'],revision=sr['raw_id']))}
9 ${h.fmt_date(h.time_to_datetime(sr['date']))}
9 ${h.fmt_date(h.time_to_datetime(sr['date']))}
10 </div>
10 </div>
11 </div>
11 </div>
12 <div class="left">
12 <div class="left">
13 <div class="author">
13 <div class="author">
14 ${h.gravatar_div(h.email_or_none(sr['author']), size=20)}
14 ${h.gravatar_div(h.email_or_none(sr['author']), size=20)}
15 <span>${h.person(sr['author'])}</span><br/>
15 <span>${h.person(sr['author'])}</span><br/>
16 <span>${h.email_or_none(sr['author'])}</span><br/>
16 <span>${h.email_or_none(sr['author'])}</span><br/>
17 </div>
17 </div>
18 %if sr['message_hl']:
18 %if sr['message_hl']:
19 <div class="search-code-body">
19 <div class="search-code-body">
20 <pre>${h.literal(sr['message_hl'])}</pre>
20 <pre>${h.literal(sr['message_hl'])}</pre>
21 </div>
21 </div>
22 %else:
22 %else:
23 <div class="message">${h.urlify_text(sr['message'], sr['repository'])}</div>
23 <div class="message">${h.urlify_text(sr['message'], sr['repository'])}</div>
24 %endif
24 %endif
25 </div>
25 </div>
26 </div>
26 </div>
27 %else:
27 %else:
28 %if cnt == 0:
28 %if cnt == 0:
29 <div id="body${cnt}" class="codeblock">
29 <div id="body${cnt}" class="codeblock">
30 <div class="error">${_('Permission denied')}</div>
30 <div class="error">${_('Permission denied')}</div>
31 </div>
31 </div>
32 %endif
32 %endif
33 %endif
33 %endif
34 %endfor
34 %endfor
35 %if c.cur_query and c.formated_results:
35 %if c.cur_query and c.formated_results:
36 <ul class="pagination">
36 <ul class="pagination">
37 ${c.formated_results.pager()}
37 ${c.formated_results.pager()}
38 </ul>
38 </ul>
39 %endif
39 %endif
@@ -1,30 +1,30 b''
1 ##content highlighting
1 ##content highlighting
2
2
3 %for cnt,sr in enumerate(c.formated_results):
3 %for cnt,sr in enumerate(c.formated_results):
4 %if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(sr['repository'],'search results check'):
4 %if h.HasRepoPermissionLevel('read')(sr['repository'],'search results check'):
5 <div id="body${cnt}" class="codeblock">
5 <div id="body${cnt}" class="codeblock">
6 <div class="code-header">
6 <div class="code-header">
7 <div class="search-path">${h.link_to(h.literal('%s &raquo; %s' % (sr['repository'],sr['f_path'])),
7 <div class="search-path">${h.link_to(h.literal('%s &raquo; %s' % (sr['repository'],sr['f_path'])),
8 h.url('files_home',repo_name=sr['repository'],revision='tip',f_path=sr['f_path']))}
8 h.url('files_home',repo_name=sr['repository'],revision='tip',f_path=sr['f_path']))}
9 </div>
9 </div>
10 </div>
10 </div>
11 <div class="search-code-body">
11 <div class="search-code-body">
12 <pre>${h.literal(sr['content_short_hl'])}</pre>
12 <pre>${h.literal(sr['content_short_hl'])}</pre>
13 </div>
13 </div>
14 </div>
14 </div>
15 %else:
15 %else:
16 %if cnt == 0:
16 %if cnt == 0:
17 <div>
17 <div>
18 <div id="body${cnt}" class="codeblock">
18 <div id="body${cnt}" class="codeblock">
19 <div class="error">${_('Permission denied')}</div>
19 <div class="error">${_('Permission denied')}</div>
20 </div>
20 </div>
21 </div>
21 </div>
22 %endif
22 %endif
23
23
24 %endif
24 %endif
25 %endfor
25 %endfor
26 %if c.cur_query and c.formated_results:
26 %if c.cur_query and c.formated_results:
27 <ul class="pagination">
27 <ul class="pagination">
28 ${c.formated_results.pager()}
28 ${c.formated_results.pager()}
29 </ul>
29 </ul>
30 %endif
30 %endif
@@ -1,26 +1,26 b''
1 ##path search
1 ##path search
2
2
3 %for cnt,sr in enumerate(c.formated_results):
3 %for cnt,sr in enumerate(c.formated_results):
4 %if h.HasRepoPermissionAny('repository.write','repository.read','repository.admin')(sr['repository'],'search results check'):
4 %if h.HasRepoPermissionLevel('read')(sr['repository'],'search results check'):
5 <div class="panel panel-default">
5 <div class="panel panel-default">
6 <div class="panel-heading">
6 <div class="panel-heading">
7 ${h.link_to(h.literal('%s &raquo; %s' % (sr['repository'],sr['f_path'])),
7 ${h.link_to(h.literal('%s &raquo; %s' % (sr['repository'],sr['f_path'])),
8 h.url('files_home',repo_name=sr['repository'],revision='tip',f_path=sr['f_path']))}
8 h.url('files_home',repo_name=sr['repository'],revision='tip',f_path=sr['f_path']))}
9 </div>
9 </div>
10 </div>
10 </div>
11 %else:
11 %else:
12 %if cnt == 0:
12 %if cnt == 0:
13 <div class="error">
13 <div class="error">
14 <div class="link">
14 <div class="link">
15 ${_('Permission denied')}
15 ${_('Permission denied')}
16 </div>
16 </div>
17 </div>
17 </div>
18 %endif
18 %endif
19
19
20 %endif
20 %endif
21 %endfor
21 %endfor
22 %if c.cur_query and c.formated_results:
22 %if c.cur_query and c.formated_results:
23 <ul class="pagination">
23 <ul class="pagination">
24 ${c.formated_results.pager()}
24 ${c.formated_results.pager()}
25 </ul>
25 </ul>
26 %endif
26 %endif
General Comments 0
You need to be logged in to leave comments. Login now