##// END OF EJS Templates
release: Merge default into stable for release preparation
marcink -
r1849:5440352d merge stable
parent child Browse files
Show More

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

@@ -0,0 +1,97 b''
1 .. _integrations-ci:
2
3 CI Server integration
4 =====================
5
6
7 RhodeCode :ref:`integrations-webhook` integration is a powerfull tool to allow
8 interaction with systems like Jenkin, Bamboo, TeamCity, CircleCi or any other
9 CI server that allows triggering a build using HTTP call.
10
11 Below are few examples on how to use :ref:`integrations-webhook` to trigger
12 a CI build.
13
14
15 General Webhook
16 +++++++++++++++
17
18 :ref:`integrations-webhook` allows sending a JSON payload information to specified
19 url with GET or POST methods. There are several variables that could be used
20 in the URL greatly extending the flexibility of this type of integration.
21
22 Most of the modern CI systems such as Jenkins, TeamCity, Bamboo or CircleCi
23 allows triggering builds via GET or POST calls.
24
25 :ref:`integrations-webhook` can be either specified per each repository or
26 globally, if your CI maps directly to all your projects a global
27 :ref:`integrations-webhook` integration can be created and will trigger builds
28 for each change in projects. If only some projects allow triggering builds a
29 global integration will also work because mostly a CI system will ignore a
30 call for unspecified builds.
31
32
33 .. note::
34
35 A quick note on security. It's recommended to allow IP restrictions
36 to only allow RhodeCode server to trigger builds. If you need to
37 specify username and password this could be done by embedding it into a
38 trigger URL, e.g. `http://user:password@server.com/job/${project_id}
39
40
41 If users require to provide any custom parameters, they can be stored for each
42 project inside the :ref:`repo-xtra`. For example to migrate a current job that
43 has a numeric build id, storing this as `jenkins_build_id` key extra field
44 the url would look like that::
45
46 http://server/job/${extra:jenkins_build_id}/
47
48
49 .. note::
50
51 Please note that some variables will result in multiple calls.
52 e.g. for |HG| specifying `${branch}` will trigger as many builds as how
53 many branches the suer actually pushed. Same applies to `${commit_id}`
54 This will trigger many builds if many commits are pushed. This allows
55 triggering individual builds for each pushed commit.
56
57
58 Jenkins
59 +++++++
60
61 To use Jenkins CI with RhodeCode, a Jenkins Build with Parameters should be used.
62 Plugin details are available here: https://wiki.jenkins.io/display/JENKINS/Build+With+Parameters+Plugin
63
64 If the plugin is configured, RhodeCode can trigger builds automatically by
65 calling such example url provided in :ref:`integrations-webhook` integration::
66
67 http://server/job/${project_id}/build-branch-${branch}/buildWithParameters?token=TOKEN&PARAMETER=value&PARAMETER2=value2
68
69
70 Team City
71 +++++++++
72
73 To use TeamCity CI it's enough to call the API and provide a buildId.
74 Example url after configuring :ref:`repo-xtra` would look like that::
75
76 http://teacmtiyserver/viewType.html?buildTypeId=${extra:tc_build_id}
77
78
79 Each project can have many build configurations.
80 buildTypeId which is a unique ID for each build configuration (job).
81
82
83 CircleCi
84 ++++++++
85
86 To use CircleCi, a POST call needs to be triggered. Example build url would
87 look like this::
88
89 http://cicleCiServer/project/${repo_type}/${username}/${repo_id}/tree/${branch}
90
91
92 Circle Ci expects format of::
93
94 POST: /project/:vcs-type/:username/:project/tree/:branch
95
96
97 CircleCi API documentation can be found here: https://circleci.com/docs/api/v1-reference/
@@ -0,0 +1,103 b''
1 |RCE| 4.8.0 |RNS|
2 -----------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2017-06-30
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Code Review: added new reviewers logic. This features now is Common Criteria
14 compatible and allows to define Mandatory (non-removable) reviewers.
15 In addition new options were added to forbid adding new reviewers or forbid
16 author of commits or the pull request itself to be a reviewer of the code.
17 - Audit logs: introducing new audit logs tracking most important actions in
18 the system. Admins can track important events such as deletion of resources,
19 permissions changes, user groups changes. Each event tracks users with his
20 IP and user agent.
21 - Mercurial: enabled evolve extensions. Each repository can be now configured
22 to support evolve, commit phases, and evolve state are also shown in
23 commit and changelog views.
24 - VCS: expose newly pushed bookmarks or branches as quick links to open a
25 pull request on client output. Allows easier pull request creation via CLI.
26
27
28 General
29 ^^^^^^^
30
31 - Core: ported many views into pure pyramid code with python3.6 compatibility.
32 Now almost 80% of the code is ported, and future ready. It's our ongoing
33 effort to allow support for modern python version.
34 - Comments: show author tag in pull request comments to easily
35 discover the author of changes in discussions.
36 - Files: allow specifying custom filename for uploaded files via web interface.
37 - Pull requests: changed who is allowed to close a pull request. Now it's only
38 super-admin, owner or person who can merge.
39 Before it was every reviewer can close. Which really doesn't make sense.
40 - Users: show that user is disabled when editing his properties.
41 - Integrations: expose user_id, and username in Webhook integration
42 templates arguments.
43 - Integrations: exposed extra repo variables in template arguments of
44 Webhook integration.
45 - Login: add link when using external auth to make it easier to login
46 using oauth providers, such as Google or Github.
47 - Maintenance: added svn verify command to tasks to be able to verify the
48 filesystem and repo formats from web interface. Allows much easier tracking
49 of incompatible filesystem storage of subversion repositories.
50 - Events: expose permalink urls for pull requests, and repositories.
51 Permalink url should provide a non-changeable url that can be used in
52 external system.
53 - Svn: increase possibility to specify compatibility to pre 1.9 version.
54
55
56 Security
57 ^^^^^^^^
58
59 - security(high): fixed possibility to delete other users inline comments
60 for users who were repository admins.
61 - security(med): fixed XSS inside the tooltip for author string.
62 - security(med): fixed stored XSS in notifications inbox.
63 - security(med): use custom writer for RST rendering to prevent injection of javascript: tags.
64 - security(med): escape flash messaged VCS errors to prevent reflected XSS attacks.
65 - security(low): use 404 instead of 403 code on permission decorator to
66 prevent brute force resource discovery attacks.
67 - security(low): fixed self XSS inside autocomplete files view.
68 - security(low): fixed self Xss inside repo strip view.
69 - security(low): fixed self Xss inside the email add functionality.
70 - security(none): use new safe escaped user attributes across the application.
71 Will prevent all possible XSS attack vectors from user stored attributes.
72 This specially can come from external authentication systems which doesn't
73 validate the data.
74
75
76 Performance
77 ^^^^^^^^^^^
78
79
80
81
82 Fixes
83 ^^^^^
84
85 - Pull requests: make sure we process comments in the order of IDS when
86 linking them. In some edge cases it could lead to comments not displaying
87 correctly.
88 - Emails: fixed newlines in email templates that can break email sending code.
89 - Markdown: fixed hr and strong tags styling.
90 - Notifications: fixed problem with 500 errors on non-numeric entries in url.
91 - API: use simple schema validator to be consistent how we validate between
92 API and web views for create user and create user_group calls.
93 - Users: fixed problem with personal repo group wasn't shown for disabled users.
94 - Oauth: improve Google extraction of first/last name from returned data.
95
96
97 Upgrade notes
98 ^^^^^^^^^^^^^
99
100
101 - API: the `update_pull_request` method will no longer support a close action.
102 Users should use the existing `close_pull_request` method which allows
103 specifying a message and status while closing a pull request. No newline at end of file
@@ -0,0 +1,187 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22 import csv
23 import datetime
24
25 import pytest
26
27 from rhodecode.tests import *
28 from rhodecode.tests.fixture import FIXTURES
29 from rhodecode.model.db import UserLog
30 from rhodecode.model.meta import Session
31 from rhodecode.lib.utils2 import safe_unicode
32
33
34 def route_path(name, params=None, **kwargs):
35 import urllib
36 from rhodecode.apps._base import ADMIN_PREFIX
37
38 base_url = {
39 'admin_home': ADMIN_PREFIX,
40 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs',
41
42 }[name].format(**kwargs)
43
44 if params:
45 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
46 return base_url
47
48
49 class TestAdminController(TestController):
50
51 @pytest.fixture(scope='class', autouse=True)
52 def prepare(self, request, pylonsapp):
53 UserLog.query().delete()
54 Session().commit()
55
56 def strptime(val):
57 fmt = '%Y-%m-%d %H:%M:%S'
58 if '.' not in val:
59 return datetime.datetime.strptime(val, fmt)
60
61 nofrag, frag = val.split(".")
62 date = datetime.datetime.strptime(nofrag, fmt)
63
64 frag = frag[:6] # truncate to microseconds
65 frag += (6 - len(frag)) * '0' # add 0s
66 return date.replace(microsecond=int(frag))
67
68 with open(os.path.join(FIXTURES, 'journal_dump.csv')) as f:
69 for row in csv.DictReader(f):
70 ul = UserLog()
71 for k, v in row.iteritems():
72 v = safe_unicode(v)
73 if k == 'action_date':
74 v = strptime(v)
75 if k in ['user_id', 'repository_id']:
76 # nullable due to FK problems
77 v = None
78 setattr(ul, k, v)
79 Session().add(ul)
80 Session().commit()
81
82 @request.addfinalizer
83 def cleanup():
84 UserLog.query().delete()
85 Session().commit()
86
87 def test_index(self):
88 self.log_user()
89 response = self.app.get(route_path('admin_audit_logs'))
90 response.mustcontain('Admin audit logs')
91
92 def test_filter_all_entries(self):
93 self.log_user()
94 response = self.app.get(route_path('admin_audit_logs'))
95 all_count = UserLog.query().count()
96 response.mustcontain('%s entries' % all_count)
97
98 def test_filter_journal_filter_exact_match_on_repository(self):
99 self.log_user()
100 response = self.app.get(route_path('admin_audit_logs',
101 params=dict(filter='repository:rhodecode')))
102 response.mustcontain('3 entries')
103
104 def test_filter_journal_filter_exact_match_on_repository_CamelCase(self):
105 self.log_user()
106 response = self.app.get(route_path('admin_audit_logs',
107 params=dict(filter='repository:RhodeCode')))
108 response.mustcontain('3 entries')
109
110 def test_filter_journal_filter_wildcard_on_repository(self):
111 self.log_user()
112 response = self.app.get(route_path('admin_audit_logs',
113 params=dict(filter='repository:*test*')))
114 response.mustcontain('862 entries')
115
116 def test_filter_journal_filter_prefix_on_repository(self):
117 self.log_user()
118 response = self.app.get(route_path('admin_audit_logs',
119 params=dict(filter='repository:test*')))
120 response.mustcontain('257 entries')
121
122 def test_filter_journal_filter_prefix_on_repository_CamelCase(self):
123 self.log_user()
124 response = self.app.get(route_path('admin_audit_logs',
125 params=dict(filter='repository:Test*')))
126 response.mustcontain('257 entries')
127
128 def test_filter_journal_filter_prefix_on_repository_and_user(self):
129 self.log_user()
130 response = self.app.get(route_path('admin_audit_logs',
131 params=dict(filter='repository:test* AND username:demo')))
132 response.mustcontain('130 entries')
133
134 def test_filter_journal_filter_prefix_on_repository_or_target_repo(self):
135 self.log_user()
136 response = self.app.get(route_path('admin_audit_logs',
137 params=dict(filter='repository:test* OR repository:rhodecode')))
138 response.mustcontain('260 entries') # 257 + 3
139
140 def test_filter_journal_filter_exact_match_on_username(self):
141 self.log_user()
142 response = self.app.get(route_path('admin_audit_logs',
143 params=dict(filter='username:demo')))
144 response.mustcontain('1087 entries')
145
146 def test_filter_journal_filter_exact_match_on_username_camelCase(self):
147 self.log_user()
148 response = self.app.get(route_path('admin_audit_logs',
149 params=dict(filter='username:DemO')))
150 response.mustcontain('1087 entries')
151
152 def test_filter_journal_filter_wildcard_on_username(self):
153 self.log_user()
154 response = self.app.get(route_path('admin_audit_logs',
155 params=dict(filter='username:*test*')))
156 entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count()
157 response.mustcontain('{} entries'.format(entries_count))
158
159 def test_filter_journal_filter_prefix_on_username(self):
160 self.log_user()
161 response = self.app.get(route_path('admin_audit_logs',
162 params=dict(filter='username:demo*')))
163 response.mustcontain('1101 entries')
164
165 def test_filter_journal_filter_prefix_on_user_or_other_user(self):
166 self.log_user()
167 response = self.app.get(route_path('admin_audit_logs',
168 params=dict(filter='username:demo OR username:volcan')))
169 response.mustcontain('1095 entries') # 1087 + 8
170
171 def test_filter_journal_filter_wildcard_on_action(self):
172 self.log_user()
173 response = self.app.get(route_path('admin_audit_logs',
174 params=dict(filter='action:*pull_request*')))
175 response.mustcontain('187 entries')
176
177 def test_filter_journal_filter_on_date(self):
178 self.log_user()
179 response = self.app.get(route_path('admin_audit_logs',
180 params=dict(filter='date:20121010')))
181 response.mustcontain('47 entries')
182
183 def test_filter_journal_filter_on_date_2(self):
184 self.log_user()
185 response = self.app.get(route_path('admin_audit_logs',
186 params=dict(filter='date:20121020')))
187 response.mustcontain('17 entries')
@@ -0,0 +1,82 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.tests import TestController
24 from rhodecode.tests.fixture import Fixture
25
26 fixture = Fixture()
27
28
29 def route_path(name, params=None, **kwargs):
30 import urllib
31 from rhodecode.apps._base import ADMIN_PREFIX
32
33 base_url = {
34 'admin_home': ADMIN_PREFIX,
35 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}',
36 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}',
37 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}',
38 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}',
39
40 }[name].format(**kwargs)
41
42 if params:
43 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
44 return base_url
45
46
47 class TestAdminMainView(TestController):
48
49 def test_redirect_admin_home(self):
50 self.log_user()
51 response = self.app.get(route_path('admin_home'), status=302)
52 assert response.location.endswith('/audit_logs')
53
54 def test_redirect_pull_request_view(self, view):
55 self.log_user()
56 self.app.get(
57 route_path(view, pull_request_id='xxxx'),
58 status=404)
59
60 @pytest.mark.backends("git", "hg")
61 @pytest.mark.parametrize('view', [
62 'pull_requests_global',
63 'pull_requests_global_0',
64 'pull_requests_global_1',
65 ])
66 def test_redirect_pull_request_view(self, view, pr_util):
67 self.log_user()
68 pull_request = pr_util.create_pull_request()
69 pull_request_id = pull_request.pull_request_id
70
71 response = self.app.get(
72 route_path(view, pull_request_id=pull_request_id),
73 status=302)
74 assert response.location.endswith(
75 'pull-request/{}'.format(pull_request_id))
76
77 repo_name = pull_request.target_repo.repo_name
78 redirect_url = route_path(
79 'pullrequest_show', repo_name=repo_name,
80 pull_request_id=pull_request.pull_request_id)
81
82 assert redirect_url in response.location
@@ -0,0 +1,73 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.view import view_config
24 from sqlalchemy.orm import joinedload
25
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.model.db import UserLog
28 from rhodecode.lib.user_log_filter import user_log_filter
29 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
30 from rhodecode.lib.utils2 import safe_int
31 from rhodecode.lib.helpers import Page
32
33 log = logging.getLogger(__name__)
34
35
36 class AdminAuditLogsView(BaseAppView):
37 def load_default_context(self):
38 c = self._get_local_tmpl_context()
39 self._register_global_c(c)
40 return c
41
42 @LoginRequired()
43 @HasPermissionAllDecorator('hg.admin')
44 @view_config(
45 route_name='admin_audit_logs', request_method='GET',
46 renderer='rhodecode:templates/admin/admin_audit_logs.mako')
47 def admin_audit_logs(self):
48 c = self.load_default_context()
49
50 users_log = UserLog.query()\
51 .options(joinedload(UserLog.user))\
52 .options(joinedload(UserLog.repository))
53
54 # FILTERING
55 c.search_term = self.request.GET.get('filter')
56 try:
57 users_log = user_log_filter(users_log, c.search_term)
58 except Exception:
59 # we want this to crash for now
60 raise
61
62 users_log = users_log.order_by(UserLog.action_date.desc())
63
64 p = safe_int(self.request.GET.get('page', 1), 1)
65
66 def url_generator(**kw):
67 if c.search_term:
68 kw['filter'] = c.search_term
69 return self.request.current_route_path(_query=kw)
70
71 c.audit_logs = Page(users_log, page=p, items_per_page=10,
72 url=url_generator)
73 return self._get_template_context(c)
@@ -0,0 +1,63 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23
24 from pyramid.httpexceptions import HTTPFound
25 from pyramid.view import view_config
26
27 from rhodecode.apps._base import BaseAppView
28 from rhodecode.lib import helpers as h
29 from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator)
30 from rhodecode.model.db import PullRequest
31
32
33 log = logging.getLogger(__name__)
34
35
36 class AdminMainView(BaseAppView):
37
38 @LoginRequired()
39 @HasPermissionAllDecorator('hg.admin')
40 @view_config(
41 route_name='admin_home', request_method='GET')
42 def admin_main(self):
43 # redirect _admin to audit logs...
44 raise HTTPFound(h.route_path('admin_audit_logs'))
45
46 @LoginRequired()
47 @view_config(route_name='pull_requests_global_0', request_method='GET')
48 @view_config(route_name='pull_requests_global_1', request_method='GET')
49 @view_config(route_name='pull_requests_global', request_method='GET')
50 def pull_requests(self):
51 """
52 Global redirect for Pull Requests
53
54 :param pull_request_id: id of pull requests in the system
55 """
56
57 pull_request_id = self.request.matchdict.get('pull_request_id')
58 pull_request = PullRequest.get_or_404(pull_request_id, pyramid_exc=True)
59 repo_name = pull_request.target_repo.repo_name
60
61 raise HTTPFound(
62 h.route_path('pullrequest_show', repo_name=repo_name,
63 pull_request_id=pull_request_id))
@@ -0,0 +1,49 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.config import routing_links
21
22
23 def includeme(config):
24
25 config.add_route(
26 name='home',
27 pattern='/')
28
29 config.add_route(
30 name='user_autocomplete_data',
31 pattern='/_users')
32
33 config.add_route(
34 name='user_group_autocomplete_data',
35 pattern='/_user_groups')
36
37 config.add_route(
38 name='repo_list_data',
39 pattern='/_repos')
40
41 config.add_route(
42 name='goto_switcher_data',
43 pattern='/_goto_data')
44
45 # register our static links via redirection mechanism
46 routing_links.connect_redirection_links(config)
47
48 # Scan module for configuration decorators.
49 config.scan()
@@ -0,0 +1,40 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21
22 def assert_and_get_content(result):
23 repos = []
24 groups = []
25 commits = []
26 for data in result:
27 for data_item in data['children']:
28 assert data_item['id']
29 assert data_item['text']
30 assert data_item['url']
31 if data_item['type'] == 'repo':
32 repos.append(data_item)
33 elif data_item['type'] == 'group':
34 groups.append(data_item)
35 elif data_item['type'] == 'commit':
36 commits.append(data_item)
37 else:
38 raise Exception('invalid type %s' % data_item['type'])
39
40 return repos, groups, commits No newline at end of file
@@ -0,0 +1,151 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import json
22
23 import pytest
24
25 from . import assert_and_get_content
26 from rhodecode.tests import TestController, TEST_USER_ADMIN_LOGIN
27 from rhodecode.tests.fixture import Fixture
28
29 from rhodecode.lib.utils import map_groups
30 from rhodecode.model.repo import RepoModel
31 from rhodecode.model.repo_group import RepoGroupModel
32 from rhodecode.model.db import Session, Repository, RepoGroup
33
34 fixture = Fixture()
35
36
37 def route_path(name, params=None, **kwargs):
38 import urllib
39
40 base_url = {
41 'goto_switcher_data': '/_goto_data',
42 }[name].format(**kwargs)
43
44 if params:
45 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
46 return base_url
47
48
49 class TestGotoSwitcherData(TestController):
50
51 required_repos_with_groups = [
52 'abc',
53 'abc-fork',
54 'forks/abcd',
55 'abcd',
56 'abcde',
57 'a/abc',
58 'aa/abc',
59 'aaa/abc',
60 'aaaa/abc',
61 'repos_abc/aaa/abc',
62 'abc_repos/abc',
63 'abc_repos/abcd',
64 'xxx/xyz',
65 'forked-abc/a/abc'
66 ]
67
68 @pytest.fixture(autouse=True, scope='class')
69 def prepare(self, request, pylonsapp):
70 for repo_and_group in self.required_repos_with_groups:
71 # create structure of groups and return the last group
72
73 repo_group = map_groups(repo_and_group)
74
75 RepoModel()._create_repo(
76 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
77 repo_group=getattr(repo_group, 'group_id', None))
78
79 Session().commit()
80
81 request.addfinalizer(self.cleanup)
82
83 def cleanup(self):
84 # first delete all repos
85 for repo_and_groups in self.required_repos_with_groups:
86 repo = Repository.get_by_repo_name(repo_and_groups)
87 if repo:
88 RepoModel().delete(repo)
89 Session().commit()
90
91 # then delete all empty groups
92 for repo_and_groups in self.required_repos_with_groups:
93 if '/' in repo_and_groups:
94 r_group = repo_and_groups.rsplit('/', 1)[0]
95 repo_group = RepoGroup.get_by_group_name(r_group)
96 if not repo_group:
97 continue
98 parents = repo_group.parents
99 RepoGroupModel().delete(repo_group, force_delete=True)
100 Session().commit()
101
102 for el in reversed(parents):
103 RepoGroupModel().delete(el, force_delete=True)
104 Session().commit()
105
106 def test_returns_list_of_repos_and_groups(self, xhr_header):
107 self.log_user()
108
109 response = self.app.get(
110 route_path('goto_switcher_data'),
111 extra_environ=xhr_header, status=200)
112 result = json.loads(response.body)['results']
113
114 repos, groups, commits = assert_and_get_content(result)
115
116 assert len(repos) == len(Repository.get_all())
117 assert len(groups) == len(RepoGroup.get_all())
118 assert len(commits) == 0
119
120 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
121 self.log_user()
122
123 response = self.app.get(
124 route_path('goto_switcher_data'),
125 params={'query': 'abc'},
126 extra_environ=xhr_header, status=200)
127 result = json.loads(response.body)['results']
128
129 repos, groups, commits = assert_and_get_content(result)
130
131 assert len(repos) == 13
132 assert len(groups) == 5
133 assert len(commits) == 0
134
135 def test_returns_list_of_properly_sorted_and_filtered(self, xhr_header):
136 self.log_user()
137
138 response = self.app.get(
139 route_path('goto_switcher_data'),
140 params={'query': 'abc'},
141 extra_environ=xhr_header, status=200)
142 result = json.loads(response.body)['results']
143
144 repos, groups, commits = assert_and_get_content(result)
145
146 test_repos = [x['text'] for x in repos[:4]]
147 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
148
149 test_groups = [x['text'] for x in groups[:4]]
150 assert ['abc_repos', 'repos_abc',
151 'forked-abc', 'forked-abc/a'] == test_groups
@@ -0,0 +1,103 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import json
22
23 from . import assert_and_get_content
24 from rhodecode.tests import TestController
25 from rhodecode.tests.fixture import Fixture
26 from rhodecode.model.db import Repository
27
28 fixture = Fixture()
29
30
31 def route_path(name, params=None, **kwargs):
32 import urllib
33
34 base_url = {
35 'repo_list_data': '/_repos',
36 }[name].format(**kwargs)
37
38 if params:
39 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
40 return base_url
41
42
43 class TestRepoListData(TestController):
44
45 def test_returns_list_of_repos_and_groups(self, xhr_header):
46 self.log_user()
47
48 response = self.app.get(
49 route_path('repo_list_data'),
50 extra_environ=xhr_header, status=200)
51 result = json.loads(response.body)['results']
52
53 repos, groups, commits = assert_and_get_content(result)
54
55 assert len(repos) == len(Repository.get_all())
56 assert len(groups) == 0
57 assert len(commits) == 0
58
59 def test_returns_list_of_repos_and_groups_filtered(self, xhr_header):
60 self.log_user()
61
62 response = self.app.get(
63 route_path('repo_list_data'),
64 params={'query': 'vcs_test_git'},
65 extra_environ=xhr_header, status=200)
66 result = json.loads(response.body)['results']
67
68 repos, groups, commits = assert_and_get_content(result)
69
70 assert len(repos) == len(Repository.query().filter(
71 Repository.repo_name.ilike('%vcs_test_git%')).all())
72 assert len(groups) == 0
73 assert len(commits) == 0
74
75 def test_returns_list_of_repos_and_groups_filtered_with_type(self, xhr_header):
76 self.log_user()
77
78 response = self.app.get(
79 route_path('repo_list_data'),
80 params={'query': 'vcs_test_git', 'repo_type': 'git'},
81 extra_environ=xhr_header, status=200)
82 result = json.loads(response.body)['results']
83
84 repos, groups, commits = assert_and_get_content(result)
85
86 assert len(repos) == len(Repository.query().filter(
87 Repository.repo_name.ilike('%vcs_test_git%')).all())
88 assert len(groups) == 0
89 assert len(commits) == 0
90
91 def test_returns_list_of_repos_non_ascii_query(self, xhr_header):
92 self.log_user()
93 response = self.app.get(
94 route_path('repo_list_data'),
95 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'},
96 extra_environ=xhr_header, status=200)
97 result = json.loads(response.body)['results']
98
99 repos, groups, commits = assert_and_get_content(result)
100
101 assert len(repos) == 0
102 assert len(groups) == 0
103 assert len(commits) == 0
@@ -0,0 +1,112 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import json
22 import pytest
23
24 from rhodecode.tests import TestController
25 from rhodecode.tests.fixture import Fixture
26
27
28 fixture = Fixture()
29
30
31 def route_path(name, params=None, **kwargs):
32 import urllib
33
34 base_url = {
35 'user_autocomplete_data': '/_users',
36 'user_group_autocomplete_data': '/_user_groups'
37 }[name].format(**kwargs)
38
39 if params:
40 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
41 return base_url
42
43
44 class TestUserAutocompleteData(TestController):
45
46 def test_returns_list_of_users(self, user_util, xhr_header):
47 self.log_user()
48 user = user_util.create_user(active=True)
49 user_name = user.username
50 response = self.app.get(
51 route_path('user_autocomplete_data'),
52 extra_environ=xhr_header, status=200)
53 result = json.loads(response.body)
54 values = [suggestion['value'] for suggestion in result['suggestions']]
55 assert user_name in values
56
57 def test_returns_inactive_users_when_active_flag_sent(
58 self, user_util, xhr_header):
59 self.log_user()
60 user = user_util.create_user(active=False)
61 user_name = user.username
62
63 response = self.app.get(
64 route_path('user_autocomplete_data',
65 params=dict(user_groups='true', active='0')),
66 extra_environ=xhr_header, status=200)
67 result = json.loads(response.body)
68 values = [suggestion['value'] for suggestion in result['suggestions']]
69 assert user_name in values
70
71 response = self.app.get(
72 route_path('user_autocomplete_data',
73 params=dict(user_groups='true', active='1')),
74 extra_environ=xhr_header, status=200)
75 result = json.loads(response.body)
76 values = [suggestion['value'] for suggestion in result['suggestions']]
77 assert user_name not in values
78
79 def test_returns_groups_when_user_groups_flag_sent(
80 self, user_util, xhr_header):
81 self.log_user()
82 group = user_util.create_user_group(user_groups_active=True)
83 group_name = group.users_group_name
84 response = self.app.get(
85 route_path('user_autocomplete_data',
86 params=dict(user_groups='true')),
87 extra_environ=xhr_header, status=200)
88 result = json.loads(response.body)
89 values = [suggestion['value'] for suggestion in result['suggestions']]
90 assert group_name in values
91
92 @pytest.mark.parametrize('query, count', [
93 ('hello1', 0),
94 ('dev', 2),
95 ])
96 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header,
97 query, count):
98 self.log_user()
99
100 user_util._test_name = 'dev-test'
101 user_util.create_user()
102
103 user_util._test_name = 'dev-group-test'
104 user_util.create_user_group()
105
106 response = self.app.get(
107 route_path('user_autocomplete_data',
108 params=dict(user_groups='true', query=query)),
109 extra_environ=xhr_header, status=200)
110
111 result = json.loads(response.body)
112 assert len(result['suggestions']) == count
@@ -0,0 +1,117 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 # -*- coding: utf-8 -*-
21
22 # Copyright (C) 2016-2017 RhodeCode GmbH
23 #
24 # This program is free software: you can redistribute it and/or modify
25 # it under the terms of the GNU Affero General Public License, version 3
26 # (only), as published by the Free Software Foundation.
27 #
28 # This program is distributed in the hope that it will be useful,
29 # but WITHOUT ANY WARRANTY; without even the implied warranty of
30 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
31 # GNU General Public License for more details.
32 #
33 # You should have received a copy of the GNU Affero General Public License
34 # along with this program. If not, see <http://www.gnu.org/licenses/>.
35 #
36 # This program is dual-licensed. If you wish to learn more about the
37 # RhodeCode Enterprise Edition, including its added features, Support services,
38 # and proprietary license terms, please see https://rhodecode.com/licenses/
39
40 import json
41
42 import pytest
43
44 from rhodecode.tests import TestController
45 from rhodecode.tests.fixture import Fixture
46
47
48 fixture = Fixture()
49
50
51 def route_path(name, params=None, **kwargs):
52 import urllib
53
54 base_url = {
55 'user_autocomplete_data': '/_users',
56 'user_group_autocomplete_data': '/_user_groups'
57 }[name].format(**kwargs)
58
59 if params:
60 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
61 return base_url
62
63
64 class TestUserGroupAutocompleteData(TestController):
65
66 def test_returns_list_of_user_groups(self, user_util, xhr_header):
67 self.log_user()
68 user_group = user_util.create_user_group(active=True)
69 user_group_name = user_group.users_group_name
70 response = self.app.get(
71 route_path('user_group_autocomplete_data'),
72 extra_environ=xhr_header, status=200)
73 result = json.loads(response.body)
74 values = [suggestion['value'] for suggestion in result['suggestions']]
75 assert user_group_name in values
76
77 def test_returns_inactive_user_groups_when_active_flag_sent(
78 self, user_util, xhr_header):
79 self.log_user()
80 user_group = user_util.create_user_group(active=False)
81 user_group_name = user_group.users_group_name
82
83 response = self.app.get(
84 route_path('user_group_autocomplete_data',
85 params=dict(active='0')),
86 extra_environ=xhr_header, status=200)
87 result = json.loads(response.body)
88 values = [suggestion['value'] for suggestion in result['suggestions']]
89 assert user_group_name in values
90
91 response = self.app.get(
92 route_path('user_group_autocomplete_data',
93 params=dict(active='1')),
94 extra_environ=xhr_header, status=200)
95 result = json.loads(response.body)
96 values = [suggestion['value'] for suggestion in result['suggestions']]
97 assert user_group_name not in values
98
99 @pytest.mark.parametrize('query, count', [
100 ('hello1', 0),
101 ('dev', 1),
102 ])
103 def test_result_is_limited_when_query_is_sent(self, user_util, xhr_header, query, count):
104 self.log_user()
105
106 user_util._test_name = 'dev-test'
107 user_util.create_user_group()
108
109 response = self.app.get(
110 route_path('user_group_autocomplete_data',
111 params=dict(user_groups='true',
112 query=query)),
113 extra_environ=xhr_header, status=200)
114
115 result = json.loads(response.body)
116
117 assert len(result['suggestions']) == count
@@ -0,0 +1,304 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import re
22 import logging
23
24 from pyramid.view import view_config
25
26 from rhodecode.apps._base import BaseAppView
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous, \
29 HasRepoGroupPermissionAnyDecorator
30 from rhodecode.lib.index import searcher_from_config
31 from rhodecode.lib.utils2 import safe_unicode, str2bool
32 from rhodecode.lib.ext_json import json
33 from rhodecode.model.db import func, Repository, RepoGroup
34 from rhodecode.model.repo import RepoModel
35 from rhodecode.model.repo_group import RepoGroupModel
36 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
37 from rhodecode.model.user import UserModel
38 from rhodecode.model.user_group import UserGroupModel
39
40 log = logging.getLogger(__name__)
41
42
43 class HomeView(BaseAppView):
44
45 def load_default_context(self):
46 c = self._get_local_tmpl_context()
47 c.user = c.auth_user.get_instance()
48 self._register_global_c(c)
49 return c
50
51 @LoginRequired()
52 @view_config(
53 route_name='user_autocomplete_data', request_method='GET',
54 renderer='json_ext', xhr=True)
55 def user_autocomplete_data(self):
56 query = self.request.GET.get('query')
57 active = str2bool(self.request.GET.get('active') or True)
58 include_groups = str2bool(self.request.GET.get('user_groups'))
59 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
60 skip_default_user = str2bool(self.request.GET.get('skip_default_user'))
61
62 log.debug('generating user list, query:%s, active:%s, with_groups:%s',
63 query, active, include_groups)
64
65 _users = UserModel().get_users(
66 name_contains=query, only_active=active)
67
68 def maybe_skip_default_user(usr):
69 if skip_default_user and usr['username'] == UserModel.cls.DEFAULT_USER:
70 return False
71 return True
72 _users = filter(maybe_skip_default_user, _users)
73
74 if include_groups:
75 # extend with user groups
76 _user_groups = UserGroupModel().get_user_groups(
77 name_contains=query, only_active=active,
78 expand_groups=expand_groups)
79 _users = _users + _user_groups
80
81 return {'suggestions': _users}
82
83 @LoginRequired()
84 @NotAnonymous()
85 @view_config(
86 route_name='user_group_autocomplete_data', request_method='GET',
87 renderer='json_ext', xhr=True)
88 def user_group_autocomplete_data(self):
89 query = self.request.GET.get('query')
90 active = str2bool(self.request.GET.get('active') or True)
91 expand_groups = str2bool(self.request.GET.get('user_groups_expand'))
92
93 log.debug('generating user group list, query:%s, active:%s',
94 query, active)
95
96 _user_groups = UserGroupModel().get_user_groups(
97 name_contains=query, only_active=active,
98 expand_groups=expand_groups)
99 _user_groups = _user_groups
100
101 return {'suggestions': _user_groups}
102
103 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
104 query = Repository.query()\
105 .order_by(func.length(Repository.repo_name))\
106 .order_by(Repository.repo_name)
107
108 if repo_type:
109 query = query.filter(Repository.repo_type == repo_type)
110
111 if name_contains:
112 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
113 query = query.filter(
114 Repository.repo_name.ilike(ilike_expression))
115 query = query.limit(limit)
116
117 all_repos = query.all()
118 # permission checks are inside this function
119 repo_iter = ScmModel().get_repos(all_repos)
120 return [
121 {
122 'id': obj['name'],
123 'text': obj['name'],
124 'type': 'repo',
125 'obj': obj['dbrepo'],
126 'url': h.route_path('repo_summary', repo_name=obj['name'])
127 }
128 for obj in repo_iter]
129
130 def _get_repo_group_list(self, name_contains=None, limit=20):
131 query = RepoGroup.query()\
132 .order_by(func.length(RepoGroup.group_name))\
133 .order_by(RepoGroup.group_name)
134
135 if name_contains:
136 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
137 query = query.filter(
138 RepoGroup.group_name.ilike(ilike_expression))
139 query = query.limit(limit)
140
141 all_groups = query.all()
142 repo_groups_iter = ScmModel().get_repo_groups(all_groups)
143 return [
144 {
145 'id': obj.group_name,
146 'text': obj.group_name,
147 'type': 'group',
148 'obj': {},
149 'url': h.route_path('repo_group_home', repo_group_name=obj.group_name)
150 }
151 for obj in repo_groups_iter]
152
153 def _get_hash_commit_list(self, auth_user, hash_starts_with=None):
154 if not hash_starts_with or len(hash_starts_with) < 3:
155 return []
156
157 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
158
159 if len(commit_hashes) != 1:
160 return []
161
162 commit_hash_prefix = commit_hashes[0]
163
164 searcher = searcher_from_config(self.request.registry.settings)
165 result = searcher.search(
166 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user,
167 raise_on_exc=False)
168
169 return [
170 {
171 'id': entry['commit_id'],
172 'text': entry['commit_id'],
173 'type': 'commit',
174 'obj': {'repo': entry['repository']},
175 'url': h.url('changeset_home',
176 repo_name=entry['repository'],
177 revision=entry['commit_id'])
178 }
179 for entry in result['results']]
180
181 @LoginRequired()
182 @view_config(
183 route_name='repo_list_data', request_method='GET',
184 renderer='json_ext', xhr=True)
185 def repo_list_data(self):
186 _ = self.request.translate
187
188 query = self.request.GET.get('query')
189 repo_type = self.request.GET.get('repo_type')
190 log.debug('generating repo list, query:%s, repo_type:%s',
191 query, repo_type)
192
193 res = []
194 repos = self._get_repo_list(query, repo_type=repo_type)
195 if repos:
196 res.append({
197 'text': _('Repositories'),
198 'children': repos
199 })
200
201 data = {
202 'more': False,
203 'results': res
204 }
205 return data
206
207 @LoginRequired()
208 @view_config(
209 route_name='goto_switcher_data', request_method='GET',
210 renderer='json_ext', xhr=True)
211 def goto_switcher_data(self):
212 c = self.load_default_context()
213
214 _ = self.request.translate
215
216 query = self.request.GET.get('query')
217 log.debug('generating goto switcher list, query %s', query)
218
219 res = []
220 repo_groups = self._get_repo_group_list(query)
221 if repo_groups:
222 res.append({
223 'text': _('Groups'),
224 'children': repo_groups
225 })
226
227 repos = self._get_repo_list(query)
228 if repos:
229 res.append({
230 'text': _('Repositories'),
231 'children': repos
232 })
233
234 commits = self._get_hash_commit_list(c.auth_user, query)
235 if commits:
236 unique_repos = {}
237 for commit in commits:
238 unique_repos.setdefault(commit['obj']['repo'], []
239 ).append(commit)
240
241 for repo in unique_repos:
242 res.append({
243 'text': _('Commits in %(repo)s') % {'repo': repo},
244 'children': unique_repos[repo]
245 })
246
247 data = {
248 'more': False,
249 'results': res
250 }
251 return data
252
253 def _get_groups_and_repos(self, repo_group_id=None):
254 # repo groups groups
255 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
256 _perms = ['group.read', 'group.write', 'group.admin']
257 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
258 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
259 repo_group_list=repo_group_list_acl, admin=False)
260
261 # repositories
262 repo_list = Repository.get_all_repos(group_id=repo_group_id)
263 _perms = ['repository.read', 'repository.write', 'repository.admin']
264 repo_list_acl = RepoList(repo_list, perm_set=_perms)
265 repo_data = RepoModel().get_repos_as_dict(
266 repo_list=repo_list_acl, admin=False)
267
268 return repo_data, repo_group_data
269
270 @LoginRequired()
271 @view_config(
272 route_name='home', request_method='GET',
273 renderer='rhodecode:templates/index.mako')
274 def main_page(self):
275 c = self.load_default_context()
276 c.repo_group = None
277
278 repo_data, repo_group_data = self._get_groups_and_repos()
279 # json used to render the grids
280 c.repos_data = json.dumps(repo_data)
281 c.repo_groups_data = json.dumps(repo_group_data)
282
283 return self._get_template_context(c)
284
285 @LoginRequired()
286 @HasRepoGroupPermissionAnyDecorator(
287 'group.read', 'group.write', 'group.admin')
288 @view_config(
289 route_name='repo_group_home', request_method='GET',
290 renderer='rhodecode:templates/index_repo_group.mako')
291 @view_config(
292 route_name='repo_group_home_slash', request_method='GET',
293 renderer='rhodecode:templates/index_repo_group.mako')
294 def repo_group_main_page(self):
295 c = self.load_default_context()
296 c.repo_group = self.request.db_repo_group
297 repo_data, repo_group_data = self._get_groups_and_repos(
298 c.repo_group.group_id)
299
300 # json used to render the grids
301 c.repos_data = json.dumps(repo_data)
302 c.repo_groups_data = json.dumps(repo_group_data)
303
304 return self._get_template_context(c)
@@ -0,0 +1,93 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.model.db import User, UserEmailMap
25 from rhodecode.tests import (
26 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
27 assert_session_flash)
28 from rhodecode.tests.fixture import Fixture
29
30 fixture = Fixture()
31
32
33 def route_path(name, **kwargs):
34 return {
35 'my_account_emails':
36 ADMIN_PREFIX + '/my_account/emails',
37 'my_account_emails_add':
38 ADMIN_PREFIX + '/my_account/emails/new',
39 'my_account_emails_delete':
40 ADMIN_PREFIX + '/my_account/emails/delete',
41 }[name].format(**kwargs)
42
43
44 class TestMyAccountEmails(TestController):
45 def test_my_account_my_emails(self):
46 self.log_user()
47 response = self.app.get(route_path('my_account_emails'))
48 response.mustcontain('No additional emails specified')
49
50 def test_my_account_my_emails_add_existing_email(self):
51 self.log_user()
52 response = self.app.get(route_path('my_account_emails'))
53 response.mustcontain('No additional emails specified')
54 response = self.app.post(route_path('my_account_emails_add'),
55 {'new_email': TEST_USER_REGULAR_EMAIL,
56 'csrf_token': self.csrf_token})
57 assert_session_flash(response, 'This e-mail address is already taken')
58
59 def test_my_account_my_emails_add_mising_email_in_form(self):
60 self.log_user()
61 response = self.app.get(route_path('my_account_emails'))
62 response.mustcontain('No additional emails specified')
63 response = self.app.post(route_path('my_account_emails_add'),
64 {'csrf_token': self.csrf_token})
65 assert_session_flash(response, 'Please enter an email address')
66
67 def test_my_account_my_emails_add_remove(self):
68 self.log_user()
69 response = self.app.get(route_path('my_account_emails'))
70 response.mustcontain('No additional emails specified')
71
72 response = self.app.post(route_path('my_account_emails_add'),
73 {'new_email': 'foo@barz.com',
74 'csrf_token': self.csrf_token})
75
76 response = self.app.get(route_path('my_account_emails'))
77
78 email_id = UserEmailMap.query().filter(
79 UserEmailMap.user == User.get_by_username(
80 TEST_USER_ADMIN_LOGIN)).filter(
81 UserEmailMap.email == 'foo@barz.com').one().email_id
82
83 response.mustcontain('foo@barz.com')
84 response.mustcontain('<input id="del_email_id" name="del_email_id" '
85 'type="hidden" value="%s" />' % email_id)
86
87 response = self.app.post(
88 route_path('my_account_emails_delete'), {
89 'del_email_id': email_id,
90 'csrf_token': self.csrf_token})
91 assert_session_flash(response, 'Email successfully deleted')
92 response = self.app.get(route_path('my_account_emails'))
93 response.mustcontain('No additional emails specified')
@@ -0,0 +1,76 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.apps._base import ADMIN_PREFIX
24 from rhodecode.model.db import User, UserEmailMap, Repository, UserFollowing
25 from rhodecode.tests import (
26 TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL,
27 assert_session_flash)
28 from rhodecode.tests.fixture import Fixture
29
30 fixture = Fixture()
31
32
33 def route_path(name, **kwargs):
34 return {
35 'my_account_repos':
36 ADMIN_PREFIX + '/my_account/repos',
37 'my_account_watched':
38 ADMIN_PREFIX + '/my_account/watched',
39 'my_account_perms':
40 ADMIN_PREFIX + '/my_account/perms',
41 'my_account_notifications':
42 ADMIN_PREFIX + '/my_account/notifications',
43 }[name].format(**kwargs)
44
45
46 class TestMyAccountSimpleViews(TestController):
47
48 def test_my_account_my_repos(self, autologin_user):
49 response = self.app.get(route_path('my_account_repos'))
50 repos = Repository.query().filter(
51 Repository.user == User.get_by_username(
52 TEST_USER_ADMIN_LOGIN)).all()
53 for repo in repos:
54 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
55
56 def test_my_account_my_watched(self, autologin_user):
57 response = self.app.get(route_path('my_account_watched'))
58
59 repos = UserFollowing.query().filter(
60 UserFollowing.user == User.get_by_username(
61 TEST_USER_ADMIN_LOGIN)).all()
62 for repo in repos:
63 response.mustcontain(
64 '"name_raw": "%s"' % repo.follows_repository.repo_name)
65
66 def test_my_account_perms(self, autologin_user):
67 response = self.app.get(route_path('my_account_perms'))
68 assert_response = response.assert_response()
69 assert assert_response.get_elements('.perm_tag.none')
70 assert assert_response.get_elements('.perm_tag.read')
71 assert assert_response.get_elements('.perm_tag.write')
72 assert assert_response.get_elements('.perm_tag.admin')
73
74 def test_my_account_notifications(self, autologin_user):
75 response = self.app.get(route_path('my_account_notifications'))
76 response.mustcontain('Test flash message')
@@ -0,0 +1,35 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 from rhodecode.config.routing import ADMIN_PREFIX
22
23
24 def admin_routes(config):
25 config.add_route(
26 name='ops_ping',
27 pattern='/ping')
28
29
30 def includeme(config):
31
32 config.include(admin_routes, route_prefix=ADMIN_PREFIX + '/ops')
33
34 # Scan module for configuration decorators.
35 config.scan()
@@ -0,0 +1,54 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.view import view_config
24
25 from rhodecode.apps._base import BaseAppView
26
27
28 log = logging.getLogger(__name__)
29
30
31 class OpsView(BaseAppView):
32
33 def load_default_context(self):
34 c = self._get_local_tmpl_context()
35 c.user = c.auth_user.get_instance()
36 self._register_global_c(c)
37 return c
38
39 @view_config(
40 route_name='ops_ping', request_method='GET',
41 renderer='json_ext')
42 def ops_ping(self):
43 data = {
44 'instance': self.request.registry.settings.get('instance_id'),
45 }
46 if getattr(self.request, 'user'):
47 data.update({
48 'caller_ip': self.request.user.ip_addr,
49 'caller_name': self.request.user.username,
50 })
51 return {'ok': data}
52
53
54
@@ -0,0 +1,33 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import add_route_with_slash
21
22
23 def includeme(config):
24
25 # Summary
26 add_route_with_slash(
27 config,
28 name='repo_group_home',
29 pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True)
30
31 # Scan module for configuration decorators.
32 config.scan()
33
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,83 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22 from rhodecode.model.db import Repository
23
24
25 def route_path(name, params=None, **kwargs):
26 import urllib
27
28 base_url = {
29 'pullrequest_show_all': '/{repo_name}/pull-request',
30 'pullrequest_show_all_data': '/{repo_name}/pull-request-data',
31 }[name].format(**kwargs)
32
33 if params:
34 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
35 return base_url
36
37
38 @pytest.mark.backends("git", "hg")
39 @pytest.mark.usefixtures('autologin_user', 'app')
40 class TestPullRequestList(object):
41
42 @pytest.mark.parametrize('params, expected_title', [
43 ({'source': 0, 'closed': 1}, 'Closed Pull Requests'),
44 ({'source': 0, 'my': 1}, 'opened by me'),
45 ({'source': 0, 'awaiting_review': 1}, 'awaiting review'),
46 ({'source': 0, 'awaiting_my_review': 1}, 'awaiting my review'),
47 ({'source': 1}, 'Pull Requests from'),
48 ])
49 def test_showing_list_page(self, backend, pr_util, params, expected_title):
50 pull_request = pr_util.create_pull_request()
51
52 response = self.app.get(
53 route_path('pullrequest_show_all',
54 repo_name=pull_request.target_repo.repo_name,
55 params=params))
56
57 assert_response = response.assert_response()
58 assert_response.element_equals_to('.panel-title', expected_title)
59 element = assert_response.get_element('.panel-title')
60 element_text = assert_response._element_to_string(element)
61
62 def test_showing_list_page_data(self, backend, pr_util, xhr_header):
63 pull_request = pr_util.create_pull_request()
64 response = self.app.get(
65 route_path('pullrequest_show_all_data',
66 repo_name=pull_request.target_repo.repo_name),
67 extra_environ=xhr_header)
68
69 assert response.json['recordsTotal'] == 1
70 assert response.json['data'][0]['description'] == 'Description'
71
72 def test_description_is_escaped_on_index_page(self, backend, pr_util, xhr_header):
73 xss_description = "<script>alert('Hi!')</script>"
74 pull_request = pr_util.create_pull_request(description=xss_description)
75
76 response = self.app.get(
77 route_path('pullrequest_show_all_data',
78 repo_name=pull_request.target_repo.repo_name),
79 extra_environ=xhr_header)
80
81 assert response.json['recordsTotal'] == 1
82 assert response.json['data'][0]['description'] == \
83 "&lt;script&gt;alert(&#39;Hi!&#39;)&lt;/script&gt;"
@@ -0,0 +1,233 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import mock
22 import pytest
23
24 from rhodecode.lib.utils2 import str2bool
25 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
26 from rhodecode.model.db import Repository, UserRepoToPerm, Permission, User
27 from rhodecode.model.meta import Session
28 from rhodecode.tests import (
29 url, HG_REPO, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN,
30 assert_session_flash)
31 from rhodecode.tests.fixture import Fixture
32
33 fixture = Fixture()
34
35
36 def route_path(name, params=None, **kwargs):
37 import urllib
38
39 base_url = {
40 'edit_repo': '/{repo_name}/settings',
41 'edit_repo_advanced': '/{repo_name}/settings/advanced',
42 'edit_repo_caches': '/{repo_name}/settings/caches',
43 'edit_repo_perms': '/{repo_name}/settings/permissions',
44 }[name].format(**kwargs)
45
46 if params:
47 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
48 return base_url
49
50
51 def _get_permission_for_user(user, repo):
52 perm = UserRepoToPerm.query()\
53 .filter(UserRepoToPerm.repository ==
54 Repository.get_by_repo_name(repo))\
55 .filter(UserRepoToPerm.user == User.get_by_username(user))\
56 .all()
57 return perm
58
59
60 @pytest.mark.usefixtures('autologin_user', 'app')
61 class TestAdminRepoSettings(object):
62 @pytest.mark.parametrize('urlname', [
63 'edit_repo',
64 'edit_repo_caches',
65 'edit_repo_perms',
66 'edit_repo_advanced',
67 ])
68 def test_show_page(self, urlname, app, backend):
69 app.get(route_path(urlname, repo_name=backend.repo_name), status=200)
70
71 def test_edit_accessible_when_missing_requirements(
72 self, backend_hg, autologin_user):
73 scm_patcher = mock.patch.object(
74 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
75 with scm_patcher:
76 self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name))
77
78 @pytest.mark.parametrize('urlname', [
79 'repo_vcs_settings',
80 'repo_settings_issuetracker',
81 'edit_repo_fields',
82 'edit_repo_remote',
83 'edit_repo_statistics',
84 ])
85 def test_show_page_pylons(self, urlname, app):
86 app.get(url(urlname, repo_name=HG_REPO))
87
88 @pytest.mark.parametrize('update_settings', [
89 {'repo_description': 'alter-desc'},
90 {'repo_owner': TEST_USER_REGULAR_LOGIN},
91 {'repo_private': 'true'},
92 {'repo_enable_locking': 'true'},
93 {'repo_enable_downloads': 'true'},
94 ])
95 def test_update_repo_settings(self, update_settings, csrf_token, backend, user_util):
96 repo = user_util.create_repo(repo_type=backend.alias)
97 repo_name = repo.repo_name
98
99 params = fixture._get_repo_create_params(
100 csrf_token=csrf_token,
101 repo_name=repo_name,
102 repo_type=backend.alias,
103 repo_owner=TEST_USER_ADMIN_LOGIN,
104 repo_description='DESC',
105
106 repo_private='false',
107 repo_enable_locking='false',
108 repo_enable_downloads='false')
109 params.update(update_settings)
110 self.app.post(
111 route_path('edit_repo', repo_name=repo_name),
112 params=params, status=302)
113
114 repo = Repository.get_by_repo_name(repo_name)
115 assert repo.user.username == \
116 update_settings.get('repo_owner', repo.user.username)
117
118 assert repo.description == \
119 update_settings.get('repo_description', repo.description)
120
121 assert repo.private == \
122 str2bool(update_settings.get(
123 'repo_private', repo.private))
124
125 assert repo.enable_locking == \
126 str2bool(update_settings.get(
127 'repo_enable_locking', repo.enable_locking))
128
129 assert repo.enable_downloads == \
130 str2bool(update_settings.get(
131 'repo_enable_downloads', repo.enable_downloads))
132
133 def test_update_repo_name_via_settings(self, csrf_token, user_util, backend):
134 repo = user_util.create_repo(repo_type=backend.alias)
135 repo_name = repo.repo_name
136
137 repo_group = user_util.create_repo_group()
138 repo_group_name = repo_group.group_name
139 new_name = repo_group_name + '_' + repo_name
140
141 params = fixture._get_repo_create_params(
142 csrf_token=csrf_token,
143 repo_name=new_name,
144 repo_type=backend.alias,
145 repo_owner=TEST_USER_ADMIN_LOGIN,
146 repo_description='DESC',
147 repo_private='false',
148 repo_enable_locking='false',
149 repo_enable_downloads='false')
150 self.app.post(
151 route_path('edit_repo', repo_name=repo_name),
152 params=params, status=302)
153 repo = Repository.get_by_repo_name(new_name)
154 assert repo.repo_name == new_name
155
156 def test_update_repo_group_via_settings(self, csrf_token, user_util, backend):
157 repo = user_util.create_repo(repo_type=backend.alias)
158 repo_name = repo.repo_name
159
160 repo_group = user_util.create_repo_group()
161 repo_group_name = repo_group.group_name
162 repo_group_id = repo_group.group_id
163
164 new_name = repo_group_name + '/' + repo_name
165 params = fixture._get_repo_create_params(
166 csrf_token=csrf_token,
167 repo_name=repo_name,
168 repo_type=backend.alias,
169 repo_owner=TEST_USER_ADMIN_LOGIN,
170 repo_description='DESC',
171 repo_group=repo_group_id,
172 repo_private='false',
173 repo_enable_locking='false',
174 repo_enable_downloads='false')
175 self.app.post(
176 route_path('edit_repo', repo_name=repo_name),
177 params=params, status=302)
178 repo = Repository.get_by_repo_name(new_name)
179 assert repo.repo_name == new_name
180
181 def test_set_private_flag_sets_default_user_permissions_to_none(
182 self, autologin_user, backend, csrf_token):
183
184 # initially repository perm should be read
185 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
186 assert len(perm) == 1
187 assert perm[0].permission.permission_name == 'repository.read'
188 assert not backend.repo.private
189
190 response = self.app.post(
191 route_path('edit_repo', repo_name=backend.repo_name),
192 params=fixture._get_repo_create_params(
193 repo_private='true',
194 repo_name=backend.repo_name,
195 repo_type=backend.alias,
196 repo_owner=TEST_USER_ADMIN_LOGIN,
197 csrf_token=csrf_token), status=302)
198
199 assert_session_flash(
200 response,
201 msg='Repository %s updated successfully' % (backend.repo_name))
202
203 repo = Repository.get_by_repo_name(backend.repo_name)
204 assert repo.private is True
205
206 # now the repo default permission should be None
207 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
208 assert len(perm) == 1
209 assert perm[0].permission.permission_name == 'repository.none'
210
211 response = self.app.post(
212 route_path('edit_repo', repo_name=backend.repo_name),
213 params=fixture._get_repo_create_params(
214 repo_private='false',
215 repo_name=backend.repo_name,
216 repo_type=backend.alias,
217 repo_owner=TEST_USER_ADMIN_LOGIN,
218 csrf_token=csrf_token), status=302)
219
220 assert_session_flash(
221 response,
222 msg='Repository %s updated successfully' % (backend.repo_name))
223 assert backend.repo.private is False
224
225 # we turn off private now the repo default permission should stay None
226 perm = _get_permission_for_user(user='default', repo=backend.repo_name)
227 assert len(perm) == 1
228 assert perm[0].permission.permission_name == 'repository.none'
229
230 # update this permission back
231 perm[0].permission = Permission.get_by_key('repository.read')
232 Session().add(perm[0])
233 Session().commit()
@@ -0,0 +1,150 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import pytest
22
23 from rhodecode.lib.utils2 import safe_unicode, safe_str
24 from rhodecode.model.db import Repository
25 from rhodecode.model.repo import RepoModel
26 from rhodecode.tests import (
27 HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator)
28 from rhodecode.tests.fixture import Fixture
29 from rhodecode.tests.utils import repo_on_filesystem
30
31 fixture = Fixture()
32
33
34 def route_path(name, params=None, **kwargs):
35 import urllib
36
37 base_url = {
38 'repo_summary_explicit': '/{repo_name}/summary',
39 'repo_summary': '/{repo_name}',
40 'edit_repo_advanced': '/{repo_name}/settings/advanced',
41 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete',
42 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork',
43 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking',
44 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal',
45
46 }[name].format(**kwargs)
47
48 if params:
49 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
50 return base_url
51
52
53 @pytest.mark.usefixtures('autologin_user', 'app')
54 class TestAdminRepoSettingsAdvanced(object):
55
56 def test_set_repo_fork_has_no_self_id(self, autologin_user, backend):
57 repo = backend.repo
58 response = self.app.get(
59 route_path('edit_repo_advanced', repo_name=backend.repo_name))
60 opt = """<option value="%s">vcs_test_git</option>""" % repo.repo_id
61 response.mustcontain(no=[opt])
62
63 def test_set_fork_of_target_repo(
64 self, autologin_user, backend, csrf_token):
65 target_repo = 'target_%s' % backend.alias
66 fixture.create_repo(target_repo, repo_type=backend.alias)
67 repo2 = Repository.get_by_repo_name(target_repo)
68 response = self.app.post(
69 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
70 params={'id_fork_of': repo2.repo_id,
71 'csrf_token': csrf_token})
72 repo = Repository.get_by_repo_name(backend.repo_name)
73 repo2 = Repository.get_by_repo_name(target_repo)
74 assert_session_flash(
75 response,
76 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name))
77
78 assert repo.fork == repo2
79 response = response.follow()
80 # check if given repo is selected
81
82 opt = 'This repository is a fork of <a href="%s">%s</a>' % (
83 route_path('repo_summary', repo_name=repo2.repo_name),
84 repo2.repo_name)
85
86 response.mustcontain(opt)
87
88 fixture.destroy_repo(target_repo, forks='detach')
89
90 @pytest.mark.backends("hg", "git")
91 def test_set_fork_of_other_type_repo(
92 self, autologin_user, backend, csrf_token):
93 TARGET_REPO_MAP = {
94 'git': {
95 'type': 'hg',
96 'repo_name': HG_REPO},
97 'hg': {
98 'type': 'git',
99 'repo_name': GIT_REPO},
100 }
101 target_repo = TARGET_REPO_MAP[backend.alias]
102
103 repo2 = Repository.get_by_repo_name(target_repo['repo_name'])
104 response = self.app.post(
105 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
106 params={'id_fork_of': repo2.repo_id,
107 'csrf_token': csrf_token})
108 assert_session_flash(
109 response,
110 'Cannot set repository as fork of repository with other type')
111
112 def test_set_fork_of_none(self, autologin_user, backend, csrf_token):
113 # mark it as None
114 response = self.app.post(
115 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
116 params={'id_fork_of': None, '_method': 'put',
117 'csrf_token': csrf_token})
118 assert_session_flash(
119 response,
120 'Marked repo %s as fork of %s'
121 % (backend.repo_name, "Nothing"))
122 assert backend.repo.fork is None
123
124 def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token):
125 repo = Repository.get_by_repo_name(backend.repo_name)
126 response = self.app.post(
127 route_path('edit_repo_advanced_fork', repo_name=backend.repo_name),
128 params={'id_fork_of': repo.repo_id, 'csrf_token': csrf_token})
129 assert_session_flash(
130 response, 'An error occurred during this operation')
131
132 @pytest.mark.parametrize(
133 "suffix",
134 ['', u'ąęł' , '123'],
135 ids=no_newline_id_generator)
136 def test_advanced_delete(self, autologin_user, backend, suffix, csrf_token):
137 repo = backend.create_repo(name_suffix=suffix)
138 repo_name = repo.repo_name
139 repo_name_str = safe_str(repo.repo_name)
140
141 response = self.app.post(
142 route_path('edit_repo_advanced_delete', repo_name=repo_name_str),
143 params={'csrf_token': csrf_token})
144 assert_session_flash(response,
145 u'Deleted repository `{}`'.format(repo_name))
146 response.follow()
147
148 # check if repo was deleted from db
149 assert RepoModel().get_by_repo_name(repo_name) is None
150 assert not repo_on_filesystem(repo_name_str)
@@ -0,0 +1,117 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import mock
22 import pytest
23
24 import rhodecode
25 from rhodecode.model.settings import SettingsModel
26 from rhodecode.tests import url
27 from rhodecode.tests.utils import AssertResponse
28
29
30 def route_path(name, params=None, **kwargs):
31 import urllib
32
33 base_url = {
34 'edit_repo': '/{repo_name}/settings',
35 }[name].format(**kwargs)
36
37 if params:
38 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
39 return base_url
40
41
42 @pytest.mark.usefixtures('autologin_user', 'app')
43 class TestAdminRepoVcsSettings(object):
44
45 @pytest.mark.parametrize('setting_name, setting_backends', [
46 ('hg_use_rebase_for_merging', ['hg']),
47 ])
48 def test_labs_settings_visible_if_enabled(
49 self, setting_name, setting_backends, backend):
50 if backend.alias not in setting_backends:
51 pytest.skip('Setting not available for backend {}'.format(backend))
52
53 vcs_settings_url = url(
54 'repo_vcs_settings', repo_name=backend.repo.repo_name)
55
56 with mock.patch.dict(
57 rhodecode.CONFIG, {'labs_settings_active': 'true'}):
58 response = self.app.get(vcs_settings_url)
59
60 assertr = AssertResponse(response)
61 assertr.one_element_exists('#rhodecode_{}'.format(setting_name))
62
63 @pytest.mark.parametrize('setting_name, setting_backends', [
64 ('hg_use_rebase_for_merging', ['hg']),
65 ])
66 def test_labs_settings_not_visible_if_disabled(
67 self, setting_name, setting_backends, backend):
68 if backend.alias not in setting_backends:
69 pytest.skip('Setting not available for backend {}'.format(backend))
70
71 vcs_settings_url = url(
72 'repo_vcs_settings', repo_name=backend.repo.repo_name)
73
74 with mock.patch.dict(
75 rhodecode.CONFIG, {'labs_settings_active': 'false'}):
76 response = self.app.get(vcs_settings_url)
77
78 assertr = AssertResponse(response)
79 assertr.no_element_exists('#rhodecode_{}'.format(setting_name))
80
81 @pytest.mark.parametrize('setting_name, setting_backends', [
82 ('hg_use_rebase_for_merging', ['hg']),
83 ])
84 def test_update_boolean_settings(
85 self, csrf_token, setting_name, setting_backends, backend):
86 if backend.alias not in setting_backends:
87 pytest.skip('Setting not available for backend {}'.format(backend))
88
89 repo = backend.create_repo()
90
91 settings_model = SettingsModel(repo=repo)
92 vcs_settings_url = url(
93 'repo_vcs_settings', repo_name=repo.repo_name)
94
95 self.app.post(
96 vcs_settings_url,
97 params={
98 'inherit_global_settings': False,
99 'new_svn_branch': 'dummy-value-for-testing',
100 'new_svn_tag': 'dummy-value-for-testing',
101 'rhodecode_{}'.format(setting_name): 'true',
102 'csrf_token': csrf_token,
103 })
104 setting = settings_model.get_setting_by_name(setting_name)
105 assert setting.app_settings_value
106
107 self.app.post(
108 vcs_settings_url,
109 params={
110 'inherit_global_settings': False,
111 'new_svn_branch': 'dummy-value-for-testing',
112 'new_svn_tag': 'dummy-value-for-testing',
113 'rhodecode_{}'.format(setting_name): 'false',
114 'csrf_token': csrf_token,
115 })
116 setting = settings_model.get_setting_by_name(setting_name)
117 assert not setting.app_settings_value
@@ -0,0 +1,76 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 from rhodecode.lib import helpers as h
22 from rhodecode.lib.utils2 import safe_int
23
24
25 def reviewer_as_json(user, reasons=None, mandatory=False):
26 """
27 Returns json struct of a reviewer for frontend
28
29 :param user: the reviewer
30 :param reasons: list of strings of why they are reviewers
31 :param mandatory: bool, to set user as mandatory
32 """
33
34 return {
35 'user_id': user.user_id,
36 'reasons': reasons or [],
37 'mandatory': mandatory,
38 'username': user.username,
39 'first_name': user.first_name,
40 'last_name': user.last_name,
41 'gravatar_link': h.gravatar_url(user.email, 14),
42 }
43
44
45 def get_default_reviewers_data(
46 current_user, source_repo, source_commit, target_repo, target_commit):
47
48 """ Return json for default reviewers of a repository """
49
50 reasons = ['Default reviewer', 'Repository owner']
51 default = reviewer_as_json(
52 user=current_user, reasons=reasons, mandatory=False)
53
54 return {
55 'api_ver': 'v1', # define version for later possible schema upgrade
56 'reviewers': [default],
57 'rules': {},
58 'rules_data': {},
59 }
60
61
62 def validate_default_reviewers(review_members, reviewer_rules):
63 """
64 Function to validate submitted reviewers against the saved rules
65
66 """
67 reviewers = []
68 reviewer_by_id = {}
69 for r in review_members:
70 reviewer_user_id = safe_int(r['user_id'])
71 entry = (reviewer_user_id, r['reasons'], r['mandatory'])
72
73 reviewer_by_id[reviewer_user_id] = entry
74 reviewers.append(entry)
75
76 return reviewers
@@ -0,0 +1,50 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 import logging
21
22 from pyramid.httpexceptions import HTTPNotFound
23 from pyramid.view import view_config
24
25 from rhodecode.apps._base import BaseReferencesView
26 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
27 from rhodecode.lib import helpers as h
28
29 log = logging.getLogger(__name__)
30
31
32 class RepoBookmarksView(BaseReferencesView):
33
34 @LoginRequired()
35 @HasRepoPermissionAnyDecorator(
36 'repository.read', 'repository.write', 'repository.admin')
37 @view_config(
38 route_name='bookmarks_home', request_method='GET',
39 renderer='rhodecode:templates/bookmarks/bookmarks.mako')
40 def bookmarks(self):
41 c = self.load_default_context()
42
43 if not h.is_hg(self.db_repo):
44 raise HTTPNotFound()
45
46 ref_items = self.rhodecode_vcs_repo.bookmarks.items()
47 self.load_refs_context(
48 ref_items=ref_items, partials_template='bookmarks/bookmarks_data.mako')
49
50 return self._get_template_context(c)
@@ -0,0 +1,51 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22 from pyramid.view import view_config
23
24 from rhodecode.apps._base import BaseReferencesView
25 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
26
27
28 log = logging.getLogger(__name__)
29
30
31 class RepoBranchesView(BaseReferencesView):
32
33 @LoginRequired()
34 @HasRepoPermissionAnyDecorator(
35 'repository.read', 'repository.write', 'repository.admin')
36 @view_config(
37 route_name='branches_home', request_method='GET',
38 renderer='rhodecode:templates/branches/branches.mako')
39 def branches(self):
40 c = self.load_default_context()
41 c.closed_branches = self.rhodecode_vcs_repo.branches_closed
42 # NOTE(marcink):
43 # we need this trick because of PartialRenderer still uses the
44 # global 'c', we might not need this after full pylons migration
45 self._register_global_c(c)
46
47 ref_items = self.rhodecode_vcs_repo.branches_all.items()
48 self.load_refs_context(
49 ref_items=ref_items, partials_template='branches/branches_data.mako')
50
51 return self._get_template_context(c)
@@ -0,0 +1,78 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
25
26 from rhodecode.apps._base import RepoAppView
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator, \
28 CSRFRequired
29 from rhodecode.lib import helpers as h
30 from rhodecode.model.meta import Session
31 from rhodecode.model.scm import ScmModel
32
33 log = logging.getLogger(__name__)
34
35
36 class RepoCachesView(RepoAppView):
37 def load_default_context(self):
38 c = self._get_local_tmpl_context()
39
40 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
41 c.repo_info = self.db_repo
42
43 self._register_global_c(c)
44 return c
45
46 @LoginRequired()
47 @HasRepoPermissionAnyDecorator('repository.admin')
48 @view_config(
49 route_name='edit_repo_caches', request_method='GET',
50 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
51 def repo_caches(self):
52 c = self.load_default_context()
53 c.active = 'caches'
54
55 return self._get_template_context(c)
56
57 @LoginRequired()
58 @HasRepoPermissionAnyDecorator('repository.admin')
59 @CSRFRequired()
60 @view_config(
61 route_name='edit_repo_caches', request_method='POST')
62 def repo_caches_purge(self):
63 _ = self.request.translate
64 c = self.load_default_context()
65 c.active = 'caches'
66
67 try:
68 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
69 Session().commit()
70 h.flash(_('Cache invalidation successful'),
71 category='success')
72 except Exception:
73 log.exception("Exception during cache invalidation")
74 h.flash(_('An error occurred during cache invalidation'),
75 category='error')
76
77 raise HTTPFound(h.route_path(
78 'edit_repo_caches', repo_name=self.db_repo_name)) No newline at end of file
@@ -0,0 +1,98 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 import deform
24 from pyramid.httpexceptions import HTTPFound
25 from pyramid.view import view_config
26
27 from rhodecode.apps._base import RepoAppView
28 from rhodecode.forms import RcForm
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator,
33 HasRepoPermissionAllDecorator, CSRFRequired)
34 from rhodecode.model.db import RepositoryField, RepoGroup
35 from rhodecode.model.forms import RepoPermsForm
36 from rhodecode.model.meta import Session
37 from rhodecode.model.repo import RepoModel
38 from rhodecode.model.scm import RepoGroupList, ScmModel
39 from rhodecode.model.validation_schema.schemas import repo_schema
40
41 log = logging.getLogger(__name__)
42
43
44 class RepoSettingsPermissionsView(RepoAppView):
45
46 def load_default_context(self):
47 c = self._get_local_tmpl_context()
48
49 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
50 c.repo_info = self.db_repo
51
52 self._register_global_c(c)
53 return c
54
55 @LoginRequired()
56 @HasRepoPermissionAnyDecorator('repository.admin')
57 @view_config(
58 route_name='edit_repo_perms', request_method='GET',
59 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
60 def edit_permissions(self):
61 c = self.load_default_context()
62 c.active = 'permissions'
63 return self._get_template_context(c)
64
65 @LoginRequired()
66 @HasRepoPermissionAllDecorator('repository.admin')
67 @CSRFRequired()
68 @view_config(
69 route_name='edit_repo_perms', request_method='POST',
70 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
71 def edit_permissions_update(self):
72 _ = self.request.translate
73 c = self.load_default_context()
74 c.active = 'permissions'
75 data = self.request.POST
76 # store private flag outside of HTML to verify if we can modify
77 # default user permissions, prevents submition of FAKE post data
78 # into the form for private repos
79 data['repo_private'] = self.db_repo.private
80 form = RepoPermsForm()().to_python(data)
81 changes = RepoModel().update_permissions(
82 self.db_repo_name, form['perm_additions'], form['perm_updates'],
83 form['perm_deletions'])
84
85 action_data = {
86 'added': changes['added'],
87 'updated': changes['updated'],
88 'deleted': changes['deleted'],
89 }
90 audit_logger.store_web(
91 'repo.edit.permissions', action_data=action_data,
92 user=self._rhodecode_user, repo=self.db_repo)
93
94 Session().commit()
95 h.flash(_('Repository permissions updated'), category='success')
96
97 raise HTTPFound(
98 self.request.route_path('edit_repo_perms', repo_name=self.db_repo_name))
This diff has been collapsed as it changes many lines, (584 lines changed) Show them Hide them
@@ -0,0 +1,584 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 import collections
24 from pyramid.httpexceptions import HTTPFound, HTTPNotFound
25 from pyramid.view import view_config
26
27 from rhodecode.apps._base import RepoAppView, DataGridAppView
28 from rhodecode.lib import helpers as h, diffs, codeblocks
29 from rhodecode.lib.auth import (
30 LoginRequired, HasRepoPermissionAnyDecorator)
31 from rhodecode.lib.utils import PartialRenderer
32 from rhodecode.lib.utils2 import str2bool, safe_int, safe_str
33 from rhodecode.lib.vcs.backends.base import EmptyCommit
34 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError, \
35 RepositoryRequirementError, NodeDoesNotExistError
36 from rhodecode.model.comment import CommentsModel
37 from rhodecode.model.db import PullRequest, PullRequestVersion, \
38 ChangesetComment, ChangesetStatus
39 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
40
41 log = logging.getLogger(__name__)
42
43
44 class RepoPullRequestsView(RepoAppView, DataGridAppView):
45
46 def load_default_context(self):
47 c = self._get_local_tmpl_context(include_app_defaults=True)
48 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
49 c.repo_info = self.db_repo
50 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
51 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
52 self._register_global_c(c)
53 return c
54
55 def _get_pull_requests_list(
56 self, repo_name, source, filter_type, opened_by, statuses):
57
58 draw, start, limit = self._extract_chunk(self.request)
59 search_q, order_by, order_dir = self._extract_ordering(self.request)
60 _render = PartialRenderer('data_table/_dt_elements.mako')
61
62 # pagination
63
64 if filter_type == 'awaiting_review':
65 pull_requests = PullRequestModel().get_awaiting_review(
66 repo_name, source=source, opened_by=opened_by,
67 statuses=statuses, offset=start, length=limit,
68 order_by=order_by, order_dir=order_dir)
69 pull_requests_total_count = PullRequestModel().count_awaiting_review(
70 repo_name, source=source, statuses=statuses,
71 opened_by=opened_by)
72 elif filter_type == 'awaiting_my_review':
73 pull_requests = PullRequestModel().get_awaiting_my_review(
74 repo_name, source=source, opened_by=opened_by,
75 user_id=self._rhodecode_user.user_id, statuses=statuses,
76 offset=start, length=limit, order_by=order_by,
77 order_dir=order_dir)
78 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
79 repo_name, source=source, user_id=self._rhodecode_user.user_id,
80 statuses=statuses, opened_by=opened_by)
81 else:
82 pull_requests = PullRequestModel().get_all(
83 repo_name, source=source, opened_by=opened_by,
84 statuses=statuses, offset=start, length=limit,
85 order_by=order_by, order_dir=order_dir)
86 pull_requests_total_count = PullRequestModel().count_all(
87 repo_name, source=source, statuses=statuses,
88 opened_by=opened_by)
89
90 data = []
91 comments_model = CommentsModel()
92 for pr in pull_requests:
93 comments = comments_model.get_all_comments(
94 self.db_repo.repo_id, pull_request=pr)
95
96 data.append({
97 'name': _render('pullrequest_name',
98 pr.pull_request_id, pr.target_repo.repo_name),
99 'name_raw': pr.pull_request_id,
100 'status': _render('pullrequest_status',
101 pr.calculated_review_status()),
102 'title': _render(
103 'pullrequest_title', pr.title, pr.description),
104 'description': h.escape(pr.description),
105 'updated_on': _render('pullrequest_updated_on',
106 h.datetime_to_time(pr.updated_on)),
107 'updated_on_raw': h.datetime_to_time(pr.updated_on),
108 'created_on': _render('pullrequest_updated_on',
109 h.datetime_to_time(pr.created_on)),
110 'created_on_raw': h.datetime_to_time(pr.created_on),
111 'author': _render('pullrequest_author',
112 pr.author.full_contact, ),
113 'author_raw': pr.author.full_name,
114 'comments': _render('pullrequest_comments', len(comments)),
115 'comments_raw': len(comments),
116 'closed': pr.is_closed(),
117 })
118
119 data = ({
120 'draw': draw,
121 'data': data,
122 'recordsTotal': pull_requests_total_count,
123 'recordsFiltered': pull_requests_total_count,
124 })
125 return data
126
127 @LoginRequired()
128 @HasRepoPermissionAnyDecorator(
129 'repository.read', 'repository.write', 'repository.admin')
130 @view_config(
131 route_name='pullrequest_show_all', request_method='GET',
132 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
133 def pull_request_list(self):
134 c = self.load_default_context()
135
136 req_get = self.request.GET
137 c.source = str2bool(req_get.get('source'))
138 c.closed = str2bool(req_get.get('closed'))
139 c.my = str2bool(req_get.get('my'))
140 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
141 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
142
143 c.active = 'open'
144 if c.my:
145 c.active = 'my'
146 if c.closed:
147 c.active = 'closed'
148 if c.awaiting_review and not c.source:
149 c.active = 'awaiting'
150 if c.source and not c.awaiting_review:
151 c.active = 'source'
152 if c.awaiting_my_review:
153 c.active = 'awaiting_my'
154
155 return self._get_template_context(c)
156
157 @LoginRequired()
158 @HasRepoPermissionAnyDecorator(
159 'repository.read', 'repository.write', 'repository.admin')
160 @view_config(
161 route_name='pullrequest_show_all_data', request_method='GET',
162 renderer='json_ext', xhr=True)
163 def pull_request_list_data(self):
164
165 # additional filters
166 req_get = self.request.GET
167 source = str2bool(req_get.get('source'))
168 closed = str2bool(req_get.get('closed'))
169 my = str2bool(req_get.get('my'))
170 awaiting_review = str2bool(req_get.get('awaiting_review'))
171 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
172
173 filter_type = 'awaiting_review' if awaiting_review \
174 else 'awaiting_my_review' if awaiting_my_review \
175 else None
176
177 opened_by = None
178 if my:
179 opened_by = [self._rhodecode_user.user_id]
180
181 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
182 if closed:
183 statuses = [PullRequest.STATUS_CLOSED]
184
185 data = self._get_pull_requests_list(
186 repo_name=self.db_repo_name, source=source,
187 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
188
189 return data
190
191 def _get_pr_version(self, pull_request_id, version=None):
192 pull_request_id = safe_int(pull_request_id)
193 at_version = None
194
195 if version and version == 'latest':
196 pull_request_ver = PullRequest.get(pull_request_id)
197 pull_request_obj = pull_request_ver
198 _org_pull_request_obj = pull_request_obj
199 at_version = 'latest'
200 elif version:
201 pull_request_ver = PullRequestVersion.get_or_404(version)
202 pull_request_obj = pull_request_ver
203 _org_pull_request_obj = pull_request_ver.pull_request
204 at_version = pull_request_ver.pull_request_version_id
205 else:
206 _org_pull_request_obj = pull_request_obj = PullRequest.get_or_404(
207 pull_request_id)
208
209 pull_request_display_obj = PullRequest.get_pr_display_object(
210 pull_request_obj, _org_pull_request_obj)
211
212 return _org_pull_request_obj, pull_request_obj, \
213 pull_request_display_obj, at_version
214
215 def _get_diffset(self, source_repo_name, source_repo,
216 source_ref_id, target_ref_id,
217 target_commit, source_commit, diff_limit, fulldiff,
218 file_limit, display_inline_comments):
219
220 vcs_diff = PullRequestModel().get_diff(
221 source_repo, source_ref_id, target_ref_id)
222
223 diff_processor = diffs.DiffProcessor(
224 vcs_diff, format='newdiff', diff_limit=diff_limit,
225 file_limit=file_limit, show_full_diff=fulldiff)
226
227 _parsed = diff_processor.prepare()
228
229 def _node_getter(commit):
230 def get_node(fname):
231 try:
232 return commit.get_node(fname)
233 except NodeDoesNotExistError:
234 return None
235
236 return get_node
237
238 diffset = codeblocks.DiffSet(
239 repo_name=self.db_repo_name,
240 source_repo_name=source_repo_name,
241 source_node_getter=_node_getter(target_commit),
242 target_node_getter=_node_getter(source_commit),
243 comments=display_inline_comments
244 )
245 diffset = diffset.render_patchset(
246 _parsed, target_commit.raw_id, source_commit.raw_id)
247
248 return diffset
249
250 @LoginRequired()
251 @HasRepoPermissionAnyDecorator(
252 'repository.read', 'repository.write', 'repository.admin')
253 # @view_config(
254 # route_name='pullrequest_show', request_method='GET',
255 # renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
256 def pull_request_show(self):
257 pull_request_id = safe_int(
258 self.request.matchdict.get('pull_request_id'))
259 c = self.load_default_context()
260
261 version = self.request.GET.get('version')
262 from_version = self.request.GET.get('from_version') or version
263 merge_checks = self.request.GET.get('merge_checks')
264 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
265
266 (pull_request_latest,
267 pull_request_at_ver,
268 pull_request_display_obj,
269 at_version) = self._get_pr_version(
270 pull_request_id, version=version)
271 pr_closed = pull_request_latest.is_closed()
272
273 if pr_closed and (version or from_version):
274 # not allow to browse versions
275 raise HTTPFound(h.route_path(
276 'pullrequest_show', repo_name=self.db_repo_name,
277 pull_request_id=pull_request_id))
278
279 versions = pull_request_display_obj.versions()
280
281 c.at_version = at_version
282 c.at_version_num = (at_version
283 if at_version and at_version != 'latest'
284 else None)
285 c.at_version_pos = ChangesetComment.get_index_from_version(
286 c.at_version_num, versions)
287
288 (prev_pull_request_latest,
289 prev_pull_request_at_ver,
290 prev_pull_request_display_obj,
291 prev_at_version) = self._get_pr_version(
292 pull_request_id, version=from_version)
293
294 c.from_version = prev_at_version
295 c.from_version_num = (prev_at_version
296 if prev_at_version and prev_at_version != 'latest'
297 else None)
298 c.from_version_pos = ChangesetComment.get_index_from_version(
299 c.from_version_num, versions)
300
301 # define if we're in COMPARE mode or VIEW at version mode
302 compare = at_version != prev_at_version
303
304 # pull_requests repo_name we opened it against
305 # ie. target_repo must match
306 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
307 raise HTTPNotFound()
308
309 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
310 pull_request_at_ver)
311
312 c.pull_request = pull_request_display_obj
313 c.pull_request_latest = pull_request_latest
314
315 if compare or (at_version and not at_version == 'latest'):
316 c.allowed_to_change_status = False
317 c.allowed_to_update = False
318 c.allowed_to_merge = False
319 c.allowed_to_delete = False
320 c.allowed_to_comment = False
321 c.allowed_to_close = False
322 else:
323 can_change_status = PullRequestModel().check_user_change_status(
324 pull_request_at_ver, self._rhodecode_user)
325 c.allowed_to_change_status = can_change_status and not pr_closed
326
327 c.allowed_to_update = PullRequestModel().check_user_update(
328 pull_request_latest, self._rhodecode_user) and not pr_closed
329 c.allowed_to_merge = PullRequestModel().check_user_merge(
330 pull_request_latest, self._rhodecode_user) and not pr_closed
331 c.allowed_to_delete = PullRequestModel().check_user_delete(
332 pull_request_latest, self._rhodecode_user) and not pr_closed
333 c.allowed_to_comment = not pr_closed
334 c.allowed_to_close = c.allowed_to_merge and not pr_closed
335
336 c.forbid_adding_reviewers = False
337 c.forbid_author_to_review = False
338 c.forbid_commit_author_to_review = False
339
340 if pull_request_latest.reviewer_data and \
341 'rules' in pull_request_latest.reviewer_data:
342 rules = pull_request_latest.reviewer_data['rules'] or {}
343 try:
344 c.forbid_adding_reviewers = rules.get(
345 'forbid_adding_reviewers')
346 c.forbid_author_to_review = rules.get(
347 'forbid_author_to_review')
348 c.forbid_commit_author_to_review = rules.get(
349 'forbid_commit_author_to_review')
350 except Exception:
351 pass
352
353 # check merge capabilities
354 _merge_check = MergeCheck.validate(
355 pull_request_latest, user=self._rhodecode_user)
356 c.pr_merge_errors = _merge_check.error_details
357 c.pr_merge_possible = not _merge_check.failed
358 c.pr_merge_message = _merge_check.merge_msg
359
360 c.pull_request_review_status = _merge_check.review_status
361 if merge_checks:
362 self.request.override_renderer = \
363 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
364 return self._get_template_context(c)
365
366 comments_model = CommentsModel()
367
368 # reviewers and statuses
369 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
370 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
371
372 # GENERAL COMMENTS with versions #
373 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
374 q = q.order_by(ChangesetComment.comment_id.asc())
375 general_comments = q
376
377 # pick comments we want to render at current version
378 c.comment_versions = comments_model.aggregate_comments(
379 general_comments, versions, c.at_version_num)
380 c.comments = c.comment_versions[c.at_version_num]['until']
381
382 # INLINE COMMENTS with versions #
383 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
384 q = q.order_by(ChangesetComment.comment_id.asc())
385 inline_comments = q
386
387 c.inline_versions = comments_model.aggregate_comments(
388 inline_comments, versions, c.at_version_num, inline=True)
389
390 # inject latest version
391 latest_ver = PullRequest.get_pr_display_object(
392 pull_request_latest, pull_request_latest)
393
394 c.versions = versions + [latest_ver]
395
396 # if we use version, then do not show later comments
397 # than current version
398 display_inline_comments = collections.defaultdict(
399 lambda: collections.defaultdict(list))
400 for co in inline_comments:
401 if c.at_version_num:
402 # pick comments that are at least UPTO given version, so we
403 # don't render comments for higher version
404 should_render = co.pull_request_version_id and \
405 co.pull_request_version_id <= c.at_version_num
406 else:
407 # showing all, for 'latest'
408 should_render = True
409
410 if should_render:
411 display_inline_comments[co.f_path][co.line_no].append(co)
412
413 # load diff data into template context, if we use compare mode then
414 # diff is calculated based on changes between versions of PR
415
416 source_repo = pull_request_at_ver.source_repo
417 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
418
419 target_repo = pull_request_at_ver.target_repo
420 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
421
422 if compare:
423 # in compare switch the diff base to latest commit from prev version
424 target_ref_id = prev_pull_request_display_obj.revisions[0]
425
426 # despite opening commits for bookmarks/branches/tags, we always
427 # convert this to rev to prevent changes after bookmark or branch change
428 c.source_ref_type = 'rev'
429 c.source_ref = source_ref_id
430
431 c.target_ref_type = 'rev'
432 c.target_ref = target_ref_id
433
434 c.source_repo = source_repo
435 c.target_repo = target_repo
436
437 c.commit_ranges = []
438 source_commit = EmptyCommit()
439 target_commit = EmptyCommit()
440 c.missing_requirements = False
441
442 source_scm = source_repo.scm_instance()
443 target_scm = target_repo.scm_instance()
444
445 # try first shadow repo, fallback to regular repo
446 try:
447 commits_source_repo = pull_request_latest.get_shadow_repo()
448 except Exception:
449 log.debug('Failed to get shadow repo', exc_info=True)
450 commits_source_repo = source_scm
451
452 c.commits_source_repo = commits_source_repo
453 commit_cache = {}
454 try:
455 pre_load = ["author", "branch", "date", "message"]
456 show_revs = pull_request_at_ver.revisions
457 for rev in show_revs:
458 comm = commits_source_repo.get_commit(
459 commit_id=rev, pre_load=pre_load)
460 c.commit_ranges.append(comm)
461 commit_cache[comm.raw_id] = comm
462
463 # Order here matters, we first need to get target, and then
464 # the source
465 target_commit = commits_source_repo.get_commit(
466 commit_id=safe_str(target_ref_id))
467
468 source_commit = commits_source_repo.get_commit(
469 commit_id=safe_str(source_ref_id))
470
471 except CommitDoesNotExistError:
472 log.warning(
473 'Failed to get commit from `{}` repo'.format(
474 commits_source_repo), exc_info=True)
475 except RepositoryRequirementError:
476 log.warning(
477 'Failed to get all required data from repo', exc_info=True)
478 c.missing_requirements = True
479
480 c.ancestor = None # set it to None, to hide it from PR view
481
482 try:
483 ancestor_id = source_scm.get_common_ancestor(
484 source_commit.raw_id, target_commit.raw_id, target_scm)
485 c.ancestor_commit = source_scm.get_commit(ancestor_id)
486 except Exception:
487 c.ancestor_commit = None
488
489 c.statuses = source_repo.statuses(
490 [x.raw_id for x in c.commit_ranges])
491
492 # auto collapse if we have more than limit
493 collapse_limit = diffs.DiffProcessor._collapse_commits_over
494 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
495 c.compare_mode = compare
496
497 # diff_limit is the old behavior, will cut off the whole diff
498 # if the limit is applied otherwise will just hide the
499 # big files from the front-end
500 diff_limit = c.visual.cut_off_limit_diff
501 file_limit = c.visual.cut_off_limit_file
502
503 c.missing_commits = False
504 if (c.missing_requirements
505 or isinstance(source_commit, EmptyCommit)
506 or source_commit == target_commit):
507
508 c.missing_commits = True
509 else:
510
511 c.diffset = self._get_diffset(
512 c.source_repo.repo_name, commits_source_repo,
513 source_ref_id, target_ref_id,
514 target_commit, source_commit,
515 diff_limit, c.fulldiff, file_limit, display_inline_comments)
516
517 c.limited_diff = c.diffset.limited_diff
518
519 # calculate removed files that are bound to comments
520 comment_deleted_files = [
521 fname for fname in display_inline_comments
522 if fname not in c.diffset.file_stats]
523
524 c.deleted_files_comments = collections.defaultdict(dict)
525 for fname, per_line_comments in display_inline_comments.items():
526 if fname in comment_deleted_files:
527 c.deleted_files_comments[fname]['stats'] = 0
528 c.deleted_files_comments[fname]['comments'] = list()
529 for lno, comments in per_line_comments.items():
530 c.deleted_files_comments[fname]['comments'].extend(
531 comments)
532
533 # this is a hack to properly display links, when creating PR, the
534 # compare view and others uses different notation, and
535 # compare_commits.mako renders links based on the target_repo.
536 # We need to swap that here to generate it properly on the html side
537 c.target_repo = c.source_repo
538
539 c.commit_statuses = ChangesetStatus.STATUSES
540
541 c.show_version_changes = not pr_closed
542 if c.show_version_changes:
543 cur_obj = pull_request_at_ver
544 prev_obj = prev_pull_request_at_ver
545
546 old_commit_ids = prev_obj.revisions
547 new_commit_ids = cur_obj.revisions
548 commit_changes = PullRequestModel()._calculate_commit_id_changes(
549 old_commit_ids, new_commit_ids)
550 c.commit_changes_summary = commit_changes
551
552 # calculate the diff for commits between versions
553 c.commit_changes = []
554 mark = lambda cs, fw: list(
555 h.itertools.izip_longest([], cs, fillvalue=fw))
556 for c_type, raw_id in mark(commit_changes.added, 'a') \
557 + mark(commit_changes.removed, 'r') \
558 + mark(commit_changes.common, 'c'):
559
560 if raw_id in commit_cache:
561 commit = commit_cache[raw_id]
562 else:
563 try:
564 commit = commits_source_repo.get_commit(raw_id)
565 except CommitDoesNotExistError:
566 # in case we fail extracting still use "dummy" commit
567 # for display in commit diff
568 commit = h.AttributeDict(
569 {'raw_id': raw_id,
570 'message': 'EMPTY or MISSING COMMIT'})
571 c.commit_changes.append([c_type, commit])
572
573 # current user review statuses for each version
574 c.review_versions = {}
575 if self._rhodecode_user.user_id in allowed_reviewers:
576 for co in general_comments:
577 if co.author.user_id == self._rhodecode_user.user_id:
578 # each comment has a status change
579 status = co.status_change
580 if status:
581 _ver_pr = status[0].comment.pull_request_version_id
582 c.review_versions[_ver_pr] = status[0]
583
584 return self._get_template_context(c)
@@ -0,0 +1,64 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.view import view_config
24
25 from rhodecode.apps._base import RepoAppView
26 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28
29 log = logging.getLogger(__name__)
30
31
32 class RepoReviewRulesView(RepoAppView):
33 def load_default_context(self):
34 c = self._get_local_tmpl_context()
35
36 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
37 c.repo_info = self.db_repo
38
39 self._register_global_c(c)
40 return c
41
42 @LoginRequired()
43 @HasRepoPermissionAnyDecorator('repository.admin')
44 @view_config(
45 route_name='repo_reviewers', request_method='GET',
46 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
47 def repo_review_rules(self):
48 c = self.load_default_context()
49 c.active = 'reviewers'
50
51 return self._get_template_context(c)
52
53 @LoginRequired()
54 @HasRepoPermissionAnyDecorator(
55 'repository.read', 'repository.write', 'repository.admin')
56 @view_config(
57 route_name='repo_default_reviewers_data', request_method='GET',
58 renderer='json_ext')
59 def repo_default_reviewers_data(self):
60 review_data = get_default_reviewers_data(
61 self.db_repo.user, None, None, None, None)
62 return review_data
63
64
@@ -0,0 +1,179 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 import deform
24 from pyramid.httpexceptions import HTTPFound
25 from pyramid.view import view_config
26
27 from rhodecode.apps._base import RepoAppView
28 from rhodecode.forms import RcForm
29 from rhodecode.lib import helpers as h
30 from rhodecode.lib import audit_logger
31 from rhodecode.lib.auth import (
32 LoginRequired, HasRepoPermissionAnyDecorator,
33 HasRepoPermissionAllDecorator, CSRFRequired)
34 from rhodecode.model.db import RepositoryField, RepoGroup
35 from rhodecode.model.meta import Session
36 from rhodecode.model.repo import RepoModel
37 from rhodecode.model.scm import RepoGroupList, ScmModel
38 from rhodecode.model.validation_schema.schemas import repo_schema
39
40 log = logging.getLogger(__name__)
41
42
43 class RepoSettingsView(RepoAppView):
44
45 def load_default_context(self):
46 c = self._get_local_tmpl_context()
47
48 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
49 c.repo_info = self.db_repo
50
51 acl_groups = RepoGroupList(
52 RepoGroup.query().all(),
53 perm_set=['group.write', 'group.admin'])
54 c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
55 c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
56
57 # in case someone no longer have a group.write access to a repository
58 # pre fill the list with this entry, we don't care if this is the same
59 # but it will allow saving repo data properly.
60 repo_group = self.db_repo.group
61 if repo_group and repo_group.group_id not in c.repo_groups_choices:
62 c.repo_groups_choices.append(repo_group.group_id)
63 c.repo_groups.append(RepoGroup._generate_choice(repo_group))
64
65 if c.repository_requirements_missing or self.rhodecode_vcs_repo is None:
66 # we might be in missing requirement state, so we load things
67 # without touching scm_instance()
68 c.landing_revs_choices, c.landing_revs = \
69 ScmModel().get_repo_landing_revs()
70 else:
71 c.landing_revs_choices, c.landing_revs = \
72 ScmModel().get_repo_landing_revs(self.db_repo)
73
74 c.personal_repo_group = c.auth_user.personal_repo_group
75 c.repo_fields = RepositoryField.query()\
76 .filter(RepositoryField.repository == self.db_repo).all()
77
78 self._register_global_c(c)
79 return c
80
81 def _get_schema(self, c, old_values=None):
82 return repo_schema.RepoSettingsSchema().bind(
83 repo_type=self.db_repo.repo_type,
84 repo_type_options=[self.db_repo.repo_type],
85 repo_ref_options=c.landing_revs_choices,
86 repo_ref_items=c.landing_revs,
87 repo_repo_group_options=c.repo_groups_choices,
88 repo_repo_group_items=c.repo_groups,
89 # user caller
90 user=self._rhodecode_user,
91 old_values=old_values
92 )
93
94 @LoginRequired()
95 @HasRepoPermissionAnyDecorator('repository.admin')
96 @view_config(
97 route_name='edit_repo', request_method='GET',
98 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
99 def edit_settings(self):
100 c = self.load_default_context()
101 c.active = 'settings'
102
103 defaults = RepoModel()._get_defaults(self.db_repo_name)
104 defaults['repo_owner'] = defaults['user']
105 defaults['repo_landing_commit_ref'] = defaults['repo_landing_rev']
106
107 schema = self._get_schema(c)
108 c.form = RcForm(schema, appstruct=defaults)
109 return self._get_template_context(c)
110
111 @LoginRequired()
112 @HasRepoPermissionAllDecorator('repository.admin')
113 @CSRFRequired()
114 @view_config(
115 route_name='edit_repo', request_method='POST',
116 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
117 def edit_settings_update(self):
118 _ = self.request.translate
119 c = self.load_default_context()
120 c.active = 'settings'
121 old_repo_name = self.db_repo_name
122
123 old_values = self.db_repo.get_api_data()
124 schema = self._get_schema(c, old_values=old_values)
125
126 c.form = RcForm(schema)
127 pstruct = self.request.POST.items()
128 pstruct.append(('repo_type', self.db_repo.repo_type))
129 try:
130 schema_data = c.form.validate(pstruct)
131 except deform.ValidationFailure as err_form:
132 return self._get_template_context(c)
133
134 # data is now VALID, proceed with updates
135 # save validated data back into the updates dict
136 validated_updates = dict(
137 repo_name=schema_data['repo_group']['repo_name_without_group'],
138 repo_group=schema_data['repo_group']['repo_group_id'],
139
140 user=schema_data['repo_owner'],
141 repo_description=schema_data['repo_description'],
142 repo_private=schema_data['repo_private'],
143 clone_uri=schema_data['repo_clone_uri'],
144 repo_landing_rev=schema_data['repo_landing_commit_ref'],
145 repo_enable_statistics=schema_data['repo_enable_statistics'],
146 repo_enable_locking=schema_data['repo_enable_locking'],
147 repo_enable_downloads=schema_data['repo_enable_downloads'],
148 )
149 # detect if CLONE URI changed, if we get OLD means we keep old values
150 if schema_data['repo_clone_uri_change'] == 'OLD':
151 validated_updates['clone_uri'] = self.db_repo.clone_uri
152
153 # use the new full name for redirect
154 new_repo_name = schema_data['repo_group']['repo_name_with_group']
155
156 # save extra fields into our validated data
157 for key, value in pstruct:
158 if key.startswith(RepositoryField.PREFIX):
159 validated_updates[key] = value
160
161 try:
162 RepoModel().update(self.db_repo, **validated_updates)
163 ScmModel().mark_for_invalidation(new_repo_name)
164
165 audit_logger.store_web(
166 'repo.edit', action_data={'old_data': old_values},
167 user=self._rhodecode_user, repo=self.db_repo)
168
169 Session().commit()
170
171 h.flash(_('Repository {} updated successfully').format(
172 old_repo_name), category='success')
173 except Exception:
174 log.exception("Exception during update of repository")
175 h.flash(_('Error occurred during update of repository {}').format(
176 old_repo_name), category='error')
177
178 raise HTTPFound(
179 self.request.route_path('edit_repo', repo_name=new_repo_name))
@@ -0,0 +1,226 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22
23 from pyramid.view import view_config
24 from pyramid.httpexceptions import HTTPFound
25
26 from rhodecode.apps._base import RepoAppView
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib import audit_logger
29 from rhodecode.lib.auth import (
30 LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired)
31 from rhodecode.lib.exceptions import AttachedForksError
32 from rhodecode.lib.utils2 import safe_int
33 from rhodecode.lib.vcs import RepositoryError
34 from rhodecode.model.db import Session, UserFollowing, User, Repository
35 from rhodecode.model.repo import RepoModel
36 from rhodecode.model.scm import ScmModel
37
38 log = logging.getLogger(__name__)
39
40
41 class RepoSettingsView(RepoAppView):
42
43 def load_default_context(self):
44 c = self._get_local_tmpl_context()
45
46 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
47 c.repo_info = self.db_repo
48
49 self._register_global_c(c)
50 return c
51
52 @LoginRequired()
53 @HasRepoPermissionAnyDecorator('repository.admin')
54 @view_config(
55 route_name='edit_repo_advanced', request_method='GET',
56 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
57 def edit_advanced(self):
58 c = self.load_default_context()
59 c.active = 'advanced'
60
61 c.default_user_id = User.get_default_user().user_id
62 c.in_public_journal = UserFollowing.query() \
63 .filter(UserFollowing.user_id == c.default_user_id) \
64 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
65
66 c.has_origin_repo_read_perm = False
67 if self.db_repo.fork:
68 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
69 'repository.write', 'repository.read', 'repository.admin')(
70 self.db_repo.fork.repo_name, 'repo set as fork page')
71
72 return self._get_template_context(c)
73
74 @LoginRequired()
75 @HasRepoPermissionAnyDecorator('repository.admin')
76 @CSRFRequired()
77 @view_config(
78 route_name='edit_repo_advanced_delete', request_method='POST',
79 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
80 def edit_advanced_delete(self):
81 """
82 Deletes the repository, or shows warnings if deletion is not possible
83 because of attached forks or other errors.
84 """
85 _ = self.request.translate
86 handle_forks = self.request.POST.get('forks', None)
87
88 try:
89 _forks = self.db_repo.forks.count()
90 if _forks and handle_forks:
91 if handle_forks == 'detach_forks':
92 handle_forks = 'detach'
93 h.flash(_('Detached %s forks') % _forks, category='success')
94 elif handle_forks == 'delete_forks':
95 handle_forks = 'delete'
96 h.flash(_('Deleted %s forks') % _forks, category='success')
97
98 old_data = self.db_repo.get_api_data()
99 RepoModel().delete(self.db_repo, forks=handle_forks)
100
101 repo = audit_logger.RepoWrap(repo_id=None,
102 repo_name=self.db_repo.repo_name)
103 audit_logger.store_web(
104 'repo.delete', action_data={'old_data': old_data},
105 user=self._rhodecode_user, repo=repo)
106
107 ScmModel().mark_for_invalidation(self.db_repo_name, delete=True)
108 h.flash(
109 _('Deleted repository `%s`') % self.db_repo_name,
110 category='success')
111 Session().commit()
112 except AttachedForksError:
113 repo_advanced_url = h.route_path(
114 'edit_repo_advanced', repo_name=self.db_repo_name,
115 _anchor='advanced-delete')
116 delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url)
117 h.flash(_('Cannot delete `{repo}` it still contains attached forks. '
118 'Try using {delete_or_detach} option.')
119 .format(repo=self.db_repo_name, delete_or_detach=delete_anchor),
120 category='warning')
121
122 # redirect to advanced for forks handle action ?
123 raise HTTPFound(repo_advanced_url)
124
125 except Exception:
126 log.exception("Exception during deletion of repository")
127 h.flash(_('An error occurred during deletion of `%s`')
128 % self.db_repo_name, category='error')
129 # redirect to advanced for more deletion options
130 raise HTTPFound(
131 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name),
132 _anchor='advanced-delete')
133
134 raise HTTPFound(h.route_path('home'))
135
136 @LoginRequired()
137 @HasRepoPermissionAnyDecorator('repository.admin')
138 @CSRFRequired()
139 @view_config(
140 route_name='edit_repo_advanced_journal', request_method='POST',
141 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
142 def edit_advanced_journal(self):
143 """
144 Set's this repository to be visible in public journal,
145 in other words making default user to follow this repo
146 """
147 _ = self.request.translate
148
149 try:
150 user_id = User.get_default_user().user_id
151 ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id)
152 h.flash(_('Updated repository visibility in public journal'),
153 category='success')
154 Session().commit()
155 except Exception:
156 h.flash(_('An error occurred during setting this '
157 'repository in public journal'),
158 category='error')
159
160 raise HTTPFound(
161 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
162
163 @LoginRequired()
164 @HasRepoPermissionAnyDecorator('repository.admin')
165 @CSRFRequired()
166 @view_config(
167 route_name='edit_repo_advanced_fork', request_method='POST',
168 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
169 def edit_advanced_fork(self):
170 """
171 Mark given repository as a fork of another
172 """
173 _ = self.request.translate
174
175 new_fork_id = self.request.POST.get('id_fork_of')
176 try:
177
178 if new_fork_id and not new_fork_id.isdigit():
179 log.error('Given fork id %s is not an INT', new_fork_id)
180
181 fork_id = safe_int(new_fork_id)
182 repo = ScmModel().mark_as_fork(
183 self.db_repo_name, fork_id, self._rhodecode_user.user_id)
184 fork = repo.fork.repo_name if repo.fork else _('Nothing')
185 Session().commit()
186 h.flash(_('Marked repo %s as fork of %s') % (self.db_repo_name, fork),
187 category='success')
188 except RepositoryError as e:
189 log.exception("Repository Error occurred")
190 h.flash(str(e), category='error')
191 except Exception as e:
192 log.exception("Exception while editing fork")
193 h.flash(_('An error occurred during this operation'),
194 category='error')
195
196 raise HTTPFound(
197 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
198
199 @LoginRequired()
200 @HasRepoPermissionAnyDecorator('repository.admin')
201 @CSRFRequired()
202 @view_config(
203 route_name='edit_repo_advanced_locking', request_method='POST',
204 renderer='rhodecode:templates/admin/repos/repo_edit.mako')
205 def edit_advanced_locking(self):
206 """
207 Toggle locking of repository
208 """
209 _ = self.request.translate
210 set_lock = self.request.POST.get('set_lock')
211 set_unlock = self.request.POST.get('set_unlock')
212
213 try:
214 if set_lock:
215 Repository.lock(self.db_repo, self._rhodecode_user.user_id,
216 lock_reason=Repository.LOCK_WEB)
217 h.flash(_('Locked repository'), category='success')
218 elif set_unlock:
219 Repository.unlock(self.db_repo)
220 h.flash(_('Unlocked repository'), category='success')
221 except Exception as e:
222 log.exception("Exception during unlocking")
223 h.flash(_('An error occurred during unlocking'), category='error')
224
225 raise HTTPFound(
226 h.route_path('edit_repo_advanced', repo_name=self.db_repo_name))
@@ -0,0 +1,368 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22 import string
23
24 from pyramid.view import view_config
25
26 from beaker.cache import cache_region
27
28
29 from rhodecode.controllers import utils
30
31 from rhodecode.apps._base import RepoAppView
32 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
33 from rhodecode.lib import caches, helpers as h
34 from rhodecode.lib.helpers import RepoPage
35 from rhodecode.lib.utils2 import safe_str, safe_int
36 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
37 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.vcs.backends.base import EmptyCommit
40 from rhodecode.lib.vcs.exceptions import CommitError, EmptyRepositoryError
41 from rhodecode.model.db import Statistics, CacheKey, User
42 from rhodecode.model.meta import Session
43 from rhodecode.model.repo import ReadmeFinder
44 from rhodecode.model.scm import ScmModel
45
46 log = logging.getLogger(__name__)
47
48
49 class RepoSummaryView(RepoAppView):
50
51 def load_default_context(self):
52 c = self._get_local_tmpl_context(include_app_defaults=True)
53
54 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
55 c.repo_info = self.db_repo
56 c.rhodecode_repo = None
57 if not c.repository_requirements_missing:
58 c.rhodecode_repo = self.rhodecode_vcs_repo
59
60 self._register_global_c(c)
61 return c
62
63 def _get_readme_data(self, db_repo, default_renderer):
64 repo_name = db_repo.repo_name
65 log.debug('Looking for README file')
66
67 @cache_region('long_term')
68 def _generate_readme(cache_key):
69 readme_data = None
70 readme_node = None
71 readme_filename = None
72 commit = self._get_landing_commit_or_none(db_repo)
73 if commit:
74 log.debug("Searching for a README file.")
75 readme_node = ReadmeFinder(default_renderer).search(commit)
76 if readme_node:
77 relative_url = h.url('files_raw_home',
78 repo_name=repo_name,
79 revision=commit.raw_id,
80 f_path=readme_node.path)
81 readme_data = self._render_readme_or_none(
82 commit, readme_node, relative_url)
83 readme_filename = readme_node.path
84 return readme_data, readme_filename
85
86 invalidator_context = CacheKey.repo_context_cache(
87 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
88
89 with invalidator_context as context:
90 context.invalidate()
91 computed = context.compute()
92
93 return computed
94
95 def _get_landing_commit_or_none(self, db_repo):
96 log.debug("Getting the landing commit.")
97 try:
98 commit = db_repo.get_landing_commit()
99 if not isinstance(commit, EmptyCommit):
100 return commit
101 else:
102 log.debug("Repository is empty, no README to render.")
103 except CommitError:
104 log.exception(
105 "Problem getting commit when trying to render the README.")
106
107 def _render_readme_or_none(self, commit, readme_node, relative_url):
108 log.debug(
109 'Found README file `%s` rendering...', readme_node.path)
110 renderer = MarkupRenderer()
111 try:
112 html_source = renderer.render(
113 readme_node.content, filename=readme_node.path)
114 if relative_url:
115 return relative_links(html_source, relative_url)
116 return html_source
117 except Exception:
118 log.exception(
119 "Exception while trying to render the README")
120
121 def _load_commits_context(self, c):
122 p = safe_int(self.request.GET.get('page'), 1)
123 size = safe_int(self.request.GET.get('size'), 10)
124
125 def url_generator(**kw):
126 query_params = {
127 'size': size
128 }
129 query_params.update(kw)
130 return h.route_path(
131 'repo_summary_commits',
132 repo_name=c.rhodecode_db_repo.repo_name, _query=query_params)
133
134 pre_load = ['author', 'branch', 'date', 'message']
135 try:
136 collection = self.rhodecode_vcs_repo.get_commits(pre_load=pre_load)
137 except EmptyRepositoryError:
138 collection = self.rhodecode_vcs_repo
139
140 c.repo_commits = RepoPage(
141 collection, page=p, items_per_page=size, url=url_generator)
142 page_ids = [x.raw_id for x in c.repo_commits]
143 c.comments = self.db_repo.get_comments(page_ids)
144 c.statuses = self.db_repo.statuses(page_ids)
145
146 @LoginRequired()
147 @HasRepoPermissionAnyDecorator(
148 'repository.read', 'repository.write', 'repository.admin')
149 @view_config(
150 route_name='repo_summary_commits', request_method='GET',
151 renderer='rhodecode:templates/summary/summary_commits.mako')
152 def summary_commits(self):
153 c = self.load_default_context()
154 self._load_commits_context(c)
155 return self._get_template_context(c)
156
157 @LoginRequired()
158 @HasRepoPermissionAnyDecorator(
159 'repository.read', 'repository.write', 'repository.admin')
160 @view_config(
161 route_name='repo_summary', request_method='GET',
162 renderer='rhodecode:templates/summary/summary.mako')
163 @view_config(
164 route_name='repo_summary_slash', request_method='GET',
165 renderer='rhodecode:templates/summary/summary.mako')
166 def summary(self):
167 c = self.load_default_context()
168
169 # Prepare the clone URL
170 username = ''
171 if self._rhodecode_user.username != User.DEFAULT_USER:
172 username = safe_str(self._rhodecode_user.username)
173
174 _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
175 if '{repo}' in _def_clone_uri:
176 _def_clone_uri_by_id = _def_clone_uri.replace(
177 '{repo}', '_{repoid}')
178 elif '{repoid}' in _def_clone_uri:
179 _def_clone_uri_by_id = _def_clone_uri.replace(
180 '_{repoid}', '{repo}')
181
182 c.clone_repo_url = self.db_repo.clone_url(
183 user=username, uri_tmpl=_def_clone_uri)
184 c.clone_repo_url_id = self.db_repo.clone_url(
185 user=username, uri_tmpl=_def_clone_uri_by_id)
186
187 # If enabled, get statistics data
188
189 c.show_stats = bool(self.db_repo.enable_statistics)
190
191 stats = Session().query(Statistics) \
192 .filter(Statistics.repository == self.db_repo) \
193 .scalar()
194
195 c.stats_percentage = 0
196
197 if stats and stats.languages:
198 c.no_data = False is self.db_repo.enable_statistics
199 lang_stats_d = json.loads(stats.languages)
200
201 # Sort first by decreasing count and second by the file extension,
202 # so we have a consistent output.
203 lang_stats_items = sorted(lang_stats_d.iteritems(),
204 key=lambda k: (-k[1], k[0]))[:10]
205 lang_stats = [(x, {"count": y,
206 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
207 for x, y in lang_stats_items]
208
209 c.trending_languages = json.dumps(lang_stats)
210 else:
211 c.no_data = True
212 c.trending_languages = json.dumps({})
213
214 scm_model = ScmModel()
215 c.enable_downloads = self.db_repo.enable_downloads
216 c.repository_followers = scm_model.get_followers(self.db_repo)
217 c.repository_forks = scm_model.get_forks(self.db_repo)
218 c.repository_is_user_following = scm_model.is_following_repo(
219 self.db_repo_name, self._rhodecode_user.user_id)
220
221 # first interaction with the VCS instance after here...
222 if c.repository_requirements_missing:
223 self.request.override_renderer = \
224 'rhodecode:templates/summary/missing_requirements.mako'
225 return self._get_template_context(c)
226
227 c.readme_data, c.readme_file = \
228 self._get_readme_data(self.db_repo, c.visual.default_renderer)
229
230 # loads the summary commits template context
231 self._load_commits_context(c)
232
233 return self._get_template_context(c)
234
235 def get_request_commit_id(self):
236 return self.request.matchdict['commit_id']
237
238 @LoginRequired()
239 @HasRepoPermissionAnyDecorator(
240 'repository.read', 'repository.write', 'repository.admin')
241 @view_config(
242 route_name='repo_stats', request_method='GET',
243 renderer='json_ext')
244 def repo_stats(self):
245 commit_id = self.get_request_commit_id()
246
247 _namespace = caches.get_repo_namespace_key(
248 caches.SUMMARY_STATS, self.db_repo_name)
249 show_stats = bool(self.db_repo.enable_statistics)
250 cache_manager = caches.get_cache_manager(
251 'repo_cache_long', _namespace)
252 _cache_key = caches.compute_key_from_params(
253 self.db_repo_name, commit_id, show_stats)
254
255 def compute_stats():
256 code_stats = {}
257 size = 0
258 try:
259 scm_instance = self.db_repo.scm_instance()
260 commit = scm_instance.get_commit(commit_id)
261
262 for node in commit.get_filenodes_generator():
263 size += node.size
264 if not show_stats:
265 continue
266 ext = string.lower(node.extension)
267 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
268 if ext_info:
269 if ext in code_stats:
270 code_stats[ext]['count'] += 1
271 else:
272 code_stats[ext] = {"count": 1, "desc": ext_info}
273 except EmptyRepositoryError:
274 pass
275 return {'size': h.format_byte_size_binary(size),
276 'code_stats': code_stats}
277
278 stats = cache_manager.get(_cache_key, createfunc=compute_stats)
279 return stats
280
281 @LoginRequired()
282 @HasRepoPermissionAnyDecorator(
283 'repository.read', 'repository.write', 'repository.admin')
284 @view_config(
285 route_name='repo_refs_data', request_method='GET',
286 renderer='json_ext')
287 def repo_refs_data(self):
288 _ = self.request.translate
289 self.load_default_context()
290
291 repo = self.rhodecode_vcs_repo
292 refs_to_create = [
293 (_("Branch"), repo.branches, 'branch'),
294 (_("Tag"), repo.tags, 'tag'),
295 (_("Bookmark"), repo.bookmarks, 'book'),
296 ]
297 res = self._create_reference_data(
298 repo, self.db_repo_name, refs_to_create)
299 data = {
300 'more': False,
301 'results': res
302 }
303 return data
304
305 @LoginRequired()
306 @HasRepoPermissionAnyDecorator(
307 'repository.read', 'repository.write', 'repository.admin')
308 @view_config(
309 route_name='repo_refs_changelog_data', request_method='GET',
310 renderer='json_ext')
311 def repo_refs_changelog_data(self):
312 _ = self.request.translate
313 self.load_default_context()
314
315 repo = self.rhodecode_vcs_repo
316
317 refs_to_create = [
318 (_("Branches"), repo.branches, 'branch'),
319 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
320 # TODO: enable when vcs can handle bookmarks filters
321 # (_("Bookmarks"), repo.bookmarks, "book"),
322 ]
323 res = self._create_reference_data(
324 repo, self.db_repo_name, refs_to_create)
325 data = {
326 'more': False,
327 'results': res
328 }
329 return data
330
331 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
332 format_ref_id = utils.get_format_ref_id(repo)
333
334 result = []
335 for title, refs, ref_type in refs_to_create:
336 if refs:
337 result.append({
338 'text': title,
339 'children': self._create_reference_items(
340 repo, full_repo_name, refs, ref_type,
341 format_ref_id),
342 })
343 return result
344
345 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
346 format_ref_id):
347 result = []
348 is_svn = h.is_svn(repo)
349 for ref_name, raw_id in refs.iteritems():
350 files_url = self._create_files_url(
351 repo, full_repo_name, ref_name, raw_id, is_svn)
352 result.append({
353 'text': ref_name,
354 'id': format_ref_id(ref_name, raw_id),
355 'raw_id': raw_id,
356 'type': ref_type,
357 'files_url': files_url,
358 })
359 return result
360
361 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id, is_svn):
362 use_commit_id = '/' in ref_name or is_svn
363 return h.url(
364 'files_home',
365 repo_name=full_repo_name,
366 f_path=ref_name if is_svn else '',
367 revision=raw_id if use_commit_id else ref_name,
368 at=ref_name)
@@ -0,0 +1,45 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22 from pyramid.view import view_config
23
24 from rhodecode.apps._base import BaseReferencesView
25 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
26
27 log = logging.getLogger(__name__)
28
29
30 class RepoTagsView(BaseReferencesView):
31
32 @LoginRequired()
33 @HasRepoPermissionAnyDecorator(
34 'repository.read', 'repository.write', 'repository.admin')
35 @view_config(
36 route_name='tags_home', request_method='GET',
37 renderer='rhodecode:templates/tags/tags.mako')
38 def tags(self):
39 c = self.load_default_context()
40
41 ref_items = self.rhodecode_vcs_repo.tags.items()
42 self.load_refs_context(
43 ref_items=ref_items, partials_template='tags/tags_data.mako')
44
45 return self._get_template_context(c)
@@ -0,0 +1,44 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import ADMIN_PREFIX
21
22
23 def includeme(config):
24
25 config.add_route(
26 name='search',
27 pattern=ADMIN_PREFIX + '/search')
28
29 config.add_route(
30 name='search_repo',
31 pattern='/{repo_name:.*?[^/]}/search', repo_route=True)
32
33 # Scan module for configuration decorators.
34 config.scan()
35
36
37 # # FULL TEXT SEARCH
38 # rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
39 # controller='search')
40 # rmap.connect('search_repo_home', '/{repo_name}/search',
41 # controller='search',
42 # action='index',
43 # conditions={'function': check_repo},
44 # requirements=URL_NAME_REQUIREMENTS) No newline at end of file
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
@@ -0,0 +1,202 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22
23 import mock
24 import pytest
25 from whoosh import query
26
27 from rhodecode.tests import (
28 TestController, SkipTest, HG_REPO,
29 TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
30 from rhodecode.tests.utils import AssertResponse
31
32
33 def route_path(name, **kwargs):
34 from rhodecode.apps._base import ADMIN_PREFIX
35 return {
36 'search':
37 ADMIN_PREFIX + '/search',
38 'search_repo':
39 '/{repo_name}/search',
40
41 }[name].format(**kwargs)
42
43
44 class TestSearchController(TestController):
45
46 def test_index(self):
47 self.log_user()
48 response = self.app.get(route_path('search'))
49 assert_response = AssertResponse(response)
50 assert_response.one_element_exists('input#q')
51
52 def test_search_files_empty_search(self):
53 if os.path.isdir(self.index_location):
54 raise SkipTest('skipped due to existing index')
55 else:
56 self.log_user()
57 response = self.app.get(route_path('search'),
58 {'q': HG_REPO})
59 response.mustcontain('There is no index to search in. '
60 'Please run whoosh indexer')
61
62 def test_search_validation(self):
63 self.log_user()
64 response = self.app.get(route_path('search'),
65 {'q': query, 'type': 'content', 'page_limit': 1000})
66
67 response.mustcontain(
68 'page_limit - 1000 is greater than maximum value 500')
69
70 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
71 ('todo', 23, [
72 'vcs/backends/hg/inmemory.py',
73 'vcs/tests/test_git.py']),
74 ('extension:rst installation', 6, [
75 'docs/index.rst',
76 'docs/installation.rst']),
77 ('def repo', 87, [
78 'vcs/tests/test_git.py',
79 'vcs/tests/test_changesets.py']),
80 ('repository:%s def test' % HG_REPO, 18, [
81 'vcs/tests/test_git.py',
82 'vcs/tests/test_changesets.py']),
83 ('"def main"', 9, [
84 'vcs/__init__.py',
85 'vcs/tests/__init__.py',
86 'vcs/utils/progressbar.py']),
87 ('owner:test_admin', 358, [
88 'vcs/tests/base.py',
89 'MANIFEST.in',
90 'vcs/utils/termcolors.py',
91 'docs/theme/ADC/static/documentation.png']),
92 ('owner:test_admin def main', 72, [
93 'vcs/__init__.py',
94 'vcs/tests/test_utils_filesize.py',
95 'vcs/tests/test_cli.py']),
96 ('owner:michał test', 0, []),
97 ])
98 def test_search_files(self, query, expected_hits, expected_paths):
99 self.log_user()
100 response = self.app.get(route_path('search'),
101 {'q': query, 'type': 'content', 'page_limit': 500})
102
103 response.mustcontain('%s results' % expected_hits)
104 for path in expected_paths:
105 response.mustcontain(path)
106
107 @pytest.mark.parametrize("query, expected_hits, expected_commits", [
108 ('bother to ask where to fetch repo during tests', 3, [
109 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1'),
110 ('git', 'c6eb379775c578a95dad8ddab53f963b80894850'),
111 ('svn', '98')]),
112 ('michał', 0, []),
113 ('changed:tests/utils.py', 36, [
114 ('hg', 'a00c1b6f5d7a6ae678fd553a8b81d92367f7ecf1')]),
115 ('changed:vcs/utils/archivers.py', 11, [
116 ('hg', '25213a5fbb048dff8ba65d21e466a835536e5b70'),
117 ('hg', '47aedd538bf616eedcb0e7d630ea476df0e159c7'),
118 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
119 ('hg', '04ad456aefd6461aea24f90b63954b6b1ce07b3e'),
120 ('git', 'c994f0de03b2a0aa848a04fc2c0d7e737dba31fc'),
121 ('git', 'd1f898326327e20524fe22417c22d71064fe54a1'),
122 ('git', 'fe568b4081755c12abf6ba673ba777fc02a415f3'),
123 ('git', 'bafe786f0d8c2ff7da5c1dcfcfa577de0b5e92f1')]),
124 ('added:README.rst', 3, [
125 ('hg', '3803844fdbd3b711175fc3da9bdacfcd6d29a6fb'),
126 ('git', 'ff7ca51e58c505fec0dd2491de52c622bb7a806b'),
127 ('svn', '8')]),
128 ('changed:lazy.py', 15, [
129 ('hg', 'eaa291c5e6ae6126a203059de9854ccf7b5baa12'),
130 ('git', '17438a11f72b93f56d0e08e7d1fa79a378578a82'),
131 ('svn', '82'),
132 ('svn', '262'),
133 ('hg', 'f5d23247fad4856a1dabd5838afade1e0eed24fb'),
134 ('git', '33fa3223355104431402a888fa77a4e9956feb3e')
135 ]),
136 ('author:marcin@python-blog.com '
137 'commit_id:b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
138 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
139 ('b986218ba1c9b0d6a259fac9b050b1724ed8e545', 1, [
140 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
141 ('b986218b', 1, [
142 ('hg', 'b986218ba1c9b0d6a259fac9b050b1724ed8e545')]),
143 ])
144 def test_search_commit_messages(
145 self, query, expected_hits, expected_commits, enabled_backends):
146 self.log_user()
147 response = self.app.get(route_path('search'),
148 {'q': query, 'type': 'commit', 'page_limit': 500})
149
150 response.mustcontain('%s results' % expected_hits)
151 for backend, commit_id in expected_commits:
152 if backend in enabled_backends:
153 response.mustcontain(commit_id)
154
155 @pytest.mark.parametrize("query, expected_hits, expected_paths", [
156 ('readme.rst', 3, []),
157 ('test*', 75, []),
158 ('*model*', 1, []),
159 ('extension:rst', 48, []),
160 ('extension:rst api', 24, []),
161 ])
162 def test_search_file_paths(self, query, expected_hits, expected_paths):
163 self.log_user()
164 response = self.app.get(route_path('search'),
165 {'q': query, 'type': 'path', 'page_limit': 500})
166
167 response.mustcontain('%s results' % expected_hits)
168 for path in expected_paths:
169 response.mustcontain(path)
170
171 def test_search_commit_message_specific_repo(self, backend):
172 self.log_user()
173 response = self.app.get(
174 route_path('search_repo',repo_name=backend.repo_name),
175 {'q': 'bother to ask where to fetch repo during tests',
176 'type': 'commit'})
177
178 response.mustcontain('1 results')
179
180 def test_filters_are_not_applied_for_admin_user(self):
181 self.log_user()
182 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
183 self.app.get(route_path('search'),
184 {'q': 'test query', 'type': 'commit'})
185 assert search_mock.call_count == 1
186 _, kwargs = search_mock.call_args
187 assert kwargs['filter'] is None
188
189 def test_filters_are_applied_for_normal_user(self, enabled_backends):
190 self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
191 with mock.patch('whoosh.searching.Searcher.search') as search_mock:
192 self.app.get(route_path('search'),
193 {'q': 'test query', 'type': 'commit'})
194 assert search_mock.call_count == 1
195 _, kwargs = search_mock.call_args
196 assert isinstance(kwargs['filter'], query.Or)
197 expected_repositories = [
198 'vcs_test_{}'.format(b) for b in enabled_backends]
199 queried_repositories = [
200 name for type_, name in kwargs['filter'].all_terms()]
201 for repository in expected_repositories:
202 assert repository in queried_repositories
@@ -0,0 +1,133 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import logging
22 import urllib
23 from pyramid.view import view_config
24 from webhelpers.util import update_params
25
26 from rhodecode.apps._base import BaseAppView, RepoAppView
27 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator)
28 from rhodecode.lib.helpers import Page
29 from rhodecode.lib.utils2 import safe_str, safe_int
30 from rhodecode.lib.index import searcher_from_config
31 from rhodecode.model import validation_schema
32 from rhodecode.model.validation_schema.schemas import search_schema
33
34 log = logging.getLogger(__name__)
35
36
37 def search(request, tmpl_context, repo_name):
38 searcher = searcher_from_config(request.registry.settings)
39 formatted_results = []
40 execution_time = ''
41
42 schema = search_schema.SearchParamsSchema()
43
44 search_params = {}
45 errors = []
46 try:
47 search_params = schema.deserialize(
48 dict(search_query=request.GET.get('q'),
49 search_type=request.GET.get('type'),
50 search_sort=request.GET.get('sort'),
51 page_limit=request.GET.get('page_limit'),
52 requested_page=request.GET.get('page'))
53 )
54 except validation_schema.Invalid as e:
55 errors = e.children
56
57 def url_generator(**kw):
58 q = urllib.quote(safe_str(search_query))
59 return update_params(
60 "?q=%s&type=%s" % (q, safe_str(search_type)), **kw)
61
62 c = tmpl_context
63 search_query = search_params.get('search_query')
64 search_type = search_params.get('search_type')
65 search_sort = search_params.get('search_sort')
66 if search_params.get('search_query'):
67 page_limit = search_params['page_limit']
68 requested_page = search_params['requested_page']
69
70 try:
71 search_result = searcher.search(
72 search_query, search_type, c.auth_user, repo_name,
73 requested_page, page_limit, search_sort)
74
75 formatted_results = Page(
76 search_result['results'], page=requested_page,
77 item_count=search_result['count'],
78 items_per_page=page_limit, url=url_generator)
79 finally:
80 searcher.cleanup()
81
82 if not search_result['error']:
83 execution_time = '%s results (%.3f seconds)' % (
84 search_result['count'],
85 search_result['runtime'])
86 elif not errors:
87 node = schema['search_query']
88 errors = [
89 validation_schema.Invalid(node, search_result['error'])]
90
91 c.perm_user = c.auth_user
92 c.repo_name = repo_name
93 c.sort = search_sort
94 c.url_generator = url_generator
95 c.errors = errors
96 c.formatted_results = formatted_results
97 c.runtime = execution_time
98 c.cur_query = search_query
99 c.search_type = search_type
100 c.searcher = searcher
101
102
103 class SearchView(BaseAppView):
104 def load_default_context(self):
105 c = self._get_local_tmpl_context()
106 self._register_global_c(c)
107 return c
108
109 @LoginRequired()
110 @view_config(
111 route_name='search', request_method='GET',
112 renderer='rhodecode:templates/search/search.mako')
113 def search(self):
114 c = self.load_default_context()
115 search(self.request, c, repo_name=None)
116 return self._get_template_context(c)
117
118
119 class SearchRepoView(RepoAppView):
120 def load_default_context(self):
121 c = self._get_local_tmpl_context()
122 self._register_global_c(c)
123 return c
124
125 @LoginRequired()
126 @HasRepoPermissionAnyDecorator('repository.admin')
127 @view_config(
128 route_name='search_repo', request_method='GET',
129 renderer='rhodecode:templates/search/search.mako')
130 def search_repo(self):
131 c = self.load_default_context()
132 search(self.request, c, repo_name=self.db_repo_name)
133 return self._get_template_context(c)
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644, binary diff hidden
NO CONTENT: new file 100644, binary diff hidden
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: new file 100644
NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,5 +1,5 b''
1 [bumpversion]
1 [bumpversion]
2 current_version = 4.7.2
2 current_version = 4.8.0
3 message = release: Bump version {current_version} to {new_version}
3 message = release: Bump version {current_version} to {new_version}
4
4
5 [bumpversion:file:rhodecode/VERSION]
5 [bumpversion:file:rhodecode/VERSION]
@@ -5,25 +5,20 b' done = false'
5 done = true
5 done = true
6
6
7 [task:rc_tools_pinned]
7 [task:rc_tools_pinned]
8 done = true
9
8
10 [task:fixes_on_stable]
9 [task:fixes_on_stable]
11 done = true
12
10
13 [task:pip2nix_generated]
11 [task:pip2nix_generated]
14 done = true
15
12
16 [task:changelog_updated]
13 [task:changelog_updated]
17 done = true
18
14
19 [task:generate_api_docs]
15 [task:generate_api_docs]
20 done = true
16
17 [task:updated_translation]
21
18
22 [release]
19 [release]
23 state = prepared
20 state = in_progress
24 version = 4.7.2
21 version = 4.8.0
25
26 [task:updated_translation]
27
22
28 [task:generate_js_routes]
23 [task:generate_js_routes]
29
24
@@ -33,6 +33,7 b' include rhodecode/public/502.html'
33 # images, css
33 # images, css
34 include rhodecode/public/css/*.css
34 include rhodecode/public/css/*.css
35 include rhodecode/public/images/*.*
35 include rhodecode/public/images/*.*
36 include rhodecode/public/images/ee_features/*.*
36
37
37 # sound files
38 # sound files
38 include rhodecode/public/sounds/*.mp3
39 include rhodecode/public/sounds/*.mp3
@@ -174,32 +174,33 b' let'
174 '';
174 '';
175
175
176 postInstall = ''
176 postInstall = ''
177 echo "Writing meta information for rccontrol to nix-support/rccontrol"
178 mkdir -p $out/nix-support/rccontrol
179 cp -v rhodecode/VERSION $out/nix-support/rccontrol/version
180 echo "DONE: Meta information for rccontrol written"
181
177 # python based programs need to be wrapped
182 # python based programs need to be wrapped
183 ln -s ${self.pyramid}/bin/* $out/bin/
184 ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
178 ln -s ${self.supervisor}/bin/supervisor* $out/bin/
185 ln -s ${self.supervisor}/bin/supervisor* $out/bin/
179 ln -s ${self.gunicorn}/bin/gunicorn $out/bin/
180 ln -s ${self.PasteScript}/bin/paster $out/bin/
186 ln -s ${self.PasteScript}/bin/paster $out/bin/
181 ln -s ${self.channelstream}/bin/channelstream $out/bin/
187 ln -s ${self.channelstream}/bin/channelstream $out/bin/
182 ln -s ${self.pyramid}/bin/* $out/bin/ #*/
183
188
184 # rhodecode-tools
189 # rhodecode-tools
185 # TODO: johbo: re-think this. Do the tools import anything from enterprise?
186 ln -s ${self.rhodecode-tools}/bin/rhodecode-* $out/bin/
190 ln -s ${self.rhodecode-tools}/bin/rhodecode-* $out/bin/
187
191
188 # note that condition should be restricted when adding further tools
192 # note that condition should be restricted when adding further tools
189 for file in $out/bin/*; do #*/
193 for file in $out/bin/*;
194 do
190 wrapProgram $file \
195 wrapProgram $file \
196 --prefix PATH : $PATH \
191 --prefix PYTHONPATH : $PYTHONPATH \
197 --prefix PYTHONPATH : $PYTHONPATH \
192 --prefix PATH : $PATH \
193 --set PYTHONHASHSEED random
198 --set PYTHONHASHSEED random
194 done
199 done
195
200
196 mkdir $out/etc
201 mkdir $out/etc
197 cp configs/production.ini $out/etc
202 cp configs/production.ini $out/etc
198
203
199 echo "Writing meta information for rccontrol to nix-support/rccontrol"
200 mkdir -p $out/nix-support/rccontrol
201 cp -v rhodecode/VERSION $out/nix-support/rccontrol/version
202 echo "DONE: Meta information for rccontrol written"
203
204
204 # TODO: johbo: Make part of ac-tests
205 # TODO: johbo: Make part of ac-tests
205 if [ ! -f rhodecode/public/js/scripts.js ]; then
206 if [ ! -f rhodecode/public/js/scripts.js ]; then
@@ -62,6 +62,9 b' Below config if for an Apache Reverse Pr'
62 ProxyPass / http://127.0.0.1:10002/ timeout=7200 Keepalive=On
62 ProxyPass / http://127.0.0.1:10002/ timeout=7200 Keepalive=On
63 ProxyPassReverse / http://127.0.0.1:10002/
63 ProxyPassReverse / http://127.0.0.1:10002/
64
64
65 # Increase headers for large Mercurial headers
66 LimitRequestLine 16380
67
65 # strict http prevents from https -> http downgrade
68 # strict http prevents from https -> http downgrade
66 Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"
69 Header always set Strict-Transport-Security "max-age=63072000; includeSubdomains; preload"
67
70
@@ -5,7 +5,10 b' Use the following example to configure N'
5
5
6
6
7 .. code-block:: nginx
7 .. code-block:: nginx
8 ## rate limiter for certain pages to prevent brute force attacks
9 limit_req_zone $binary_remote_addr zone=dl_limit:10m rate=1r/s;
8
10
11 ## custom log format
9 log_format log_custom '$remote_addr - $remote_user [$time_local] '
12 log_format log_custom '$remote_addr - $remote_user [$time_local] '
10 '"$request" $status $body_bytes_sent '
13 '"$request" $status $body_bytes_sent '
11 '"$http_referer" "$http_user_agent" '
14 '"$http_referer" "$http_user_agent" '
@@ -109,6 +112,12 b' Use the following example to configure N'
109 proxy_set_header Connection "upgrade";
112 proxy_set_header Connection "upgrade";
110 }
113 }
111
114
115 location /_admin/login {
116 ## rate limit this endpoint
117 limit_req zone=dl_limit burst=10 nodelay;
118 try_files $uri @rhode;
119 }
120
112 location / {
121 location / {
113 try_files $uri @rhode;
122 try_files $uri @rhode;
114 }
123 }
@@ -3,8 +3,12 b''
3 Repository Extra Fields
3 Repository Extra Fields
4 =======================
4 =======================
5
5
6 Extra fields attached to a |repo| allow you to configure additional actions for
6 Extra fields attached to a |repo| allow you to configure additional fields for
7 |RCX|. To install and read more about |RCX|, see the :ref:`install-rcx` section.
7 each repository. This allows storing custom data per-repository.
8
9 It can be used in :ref:`integrations-webhook` or in |RCX|.
10 To install and read more about |RCX|, see the :ref:`install-rcx` section.
11
8
12
9 Enabling Extra Fields
13 Enabling Extra Fields
10 ---------------------
14 ---------------------
@@ -27,9 +31,14 b' 2. On the |repo| settings page, select t'
27
31
28 .. image:: ../images/extra-repo-fields.png
32 .. image:: ../images/extra-repo-fields.png
29
33
34 The most important is the `New field key` variable which under the value will
35 be stored. It needs to be unique for each repository. The label and description
36 will be generated in repository settings where users can actually save some
37 values inside generated extra fields.
30
38
31 Example Usage
39
32 -------------
40 Example Usage in extensions
41 ---------------------------
33
42
34 To use the extra fields in an extension, see the example below. For more
43 To use the extra fields in an extension, see the example below. For more
35 information and examples, see the :ref:`extensions-hooks-ref` section.
44 information and examples, see the :ref:`extensions-hooks-ref` section.
@@ -7,7 +7,7 b' The VCS Server handles |RCM| backend fun'
7 a VCS Server to run with a |RCM| instance. If you do not, you will be missing
7 a VCS Server to run with a |RCM| instance. If you do not, you will be missing
8 the connection between |RCM| and its |repos|. This will cause error messages
8 the connection between |RCM| and its |repos|. This will cause error messages
9 on the web interface. You can run your setup in the following configurations,
9 on the web interface. You can run your setup in the following configurations,
10 currently the best performance is one VCS Server per |RCM| instance:
10 currently the best performance is one of following:
11
11
12 * One VCS Server per |RCM| instance.
12 * One VCS Server per |RCM| instance.
13 * One VCS Server handling multiple instances.
13 * One VCS Server handling multiple instances.
@@ -59,7 +59,8 b' instance in the'
59 \vcs.backends <available-vcs-systems>
59 \vcs.backends <available-vcs-systems>
60 Set a comma-separated list of the |repo| options available from the
60 Set a comma-separated list of the |repo| options available from the
61 web interface. The default is ``hg, git, svn``,
61 web interface. The default is ``hg, git, svn``,
62 which is all |repo| types available.
62 which is all |repo| types available. The order of backends is also the
63 order backend will try to detect requests type.
63
64
64 \vcs.connection_timeout <seconds>
65 \vcs.connection_timeout <seconds>
65 Set the length of time in seconds that the VCS Server waits for
66 Set the length of time in seconds that the VCS Server waits for
@@ -159,9 +160,10 b' for full details see the :ref:`RhodeCode'
159
160
160 - NAME: vcsserver-1
161 - NAME: vcsserver-1
161 - STATUS: RUNNING
162 - STATUS: RUNNING
162 - TYPE: VCSServer
163 logs:/home/ubuntu/.rccontrol/vcsserver-1/vcsserver.log
163 - VERSION: 1.0.0
164 - VERSION: 4.7.2 VCSServer
164 - URL: http://127.0.0.1:10001
165 - URL: http://127.0.0.1:10008
166 - CONFIG: /home/ubuntu/.rccontrol/vcsserver-1/vcsserver.ini
165
167
166 $ rccontrol restart vcsserver-1
168 $ rccontrol restart vcsserver-1
167 Instance "vcsserver-1" successfully stopped.
169 Instance "vcsserver-1" successfully stopped.
@@ -181,7 +183,9 b' For a more detailed explanation of the l'
181 .. rst-class:: dl-horizontal
183 .. rst-class:: dl-horizontal
182
184
183 \host <ip-address>
185 \host <ip-address>
184 Set the host on which the VCS Server will run.
186 Set the host on which the VCS Server will run. VCSServer is not
187 protected by any authentication, so we *highly* recommend running it
188 under localhost ip that is `127.0.0.1`
185
189
186 \port <int>
190 \port <int>
187 Set the port number on which the VCS Server will be available.
191 Set the port number on which the VCS Server will be available.
@@ -189,13 +193,22 b' For a more detailed explanation of the l'
189 \locale <locale_utf>
193 \locale <locale_utf>
190 Set the locale the VCS Server expects.
194 Set the locale the VCS Server expects.
191
195
192 \threadpool_size <int>
196 \workers <int>
193 Set the size of the threadpool used to communicate
197 Set the number of process workers.Recommended
194 with the WSGI workers. This should be at least 6 times the number of
198 value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
195 WSGI worker processes.
196
199
197 \timeout <seconds>
200 \max_requests <int>
198 Set the timeout for RPC communication in seconds.
201 The maximum number of requests a worker will process before restarting.
202 Any value greater than zero will limit the number of requests a work
203 will process before automatically restarting. This is a simple method
204 to help limit the damage of memory leaks.
205
206 \max_requests_jitter <int>
207 The maximum jitter to add to the max_requests setting.
208 The jitter causes the restart per worker to be randomized by
209 randint(0, max_requests_jitter). This is intended to stagger worker
210 restarts to avoid all workers restarting at the same time.
211
199
212
200 .. note::
213 .. note::
201
214
@@ -204,27 +217,54 b' For a more detailed explanation of the l'
204 .. code-block:: ini
217 .. code-block:: ini
205
218
206 ################################################################################
219 ################################################################################
207 # RhodeCode VCSServer - configuration #
220 # RhodeCode VCSServer with HTTP Backend - configuration #
208 # #
221 # #
209 ################################################################################
222 ################################################################################
210
223
211 [DEFAULT]
224
225 [server:main]
226 ## COMMON ##
212 host = 127.0.0.1
227 host = 127.0.0.1
213 port = 9900
228 port = 10002
229
230 ##########################
231 ## GUNICORN WSGI SERVER ##
232 ##########################
233 ## run with gunicorn --log-config vcsserver.ini --paste vcsserver.ini
234 use = egg:gunicorn#main
235 ## Sets the number of process workers. Recommended
236 ## value is (2 * NUMBER_OF_CPUS + 1), eg 2CPU = 5 workers
237 workers = 3
238 ## process name
239 proc_name = rhodecode_vcsserver
240 ## type of worker class, one of sync, gevent
241 ## recommended for bigger setup is using of of other than sync one
242 worker_class = sync
243 ## The maximum number of simultaneous clients. Valid only for Gevent
244 #worker_connections = 10
245 ## max number of requests that worker will handle before being gracefully
246 ## restarted, could prevent memory leaks
247 max_requests = 1000
248 max_requests_jitter = 30
249 ## amount of time a worker can spend with handling a request before it
250 ## gets killed and restarted. Set to 6hrs
251 timeout = 21600
252
253 [app:main]
254 use = egg:rhodecode-vcsserver
255
256 pyramid.default_locale_name = en
257 pyramid.includes =
258
259 ## default locale used by VCS systems
214 locale = en_US.UTF-8
260 locale = en_US.UTF-8
215 # number of worker threads, this should be set based on a formula threadpool=N*6
216 # where N is number of RhodeCode Enterprise workers, eg. running 2 instances
217 # 8 gunicorn workers each would be 2 * 8 * 6 = 96, threadpool_size = 96
218 threadpool_size = 16
219 timeout = 0
220
261
221 # cache regions, please don't change
262 # cache regions, please don't change
222 beaker.cache.regions = repo_object
263 beaker.cache.regions = repo_object
223 beaker.cache.repo_object.type = memorylru
264 beaker.cache.repo_object.type = memorylru
224 beaker.cache.repo_object.max_items = 1000
265 beaker.cache.repo_object.max_items = 100
225
226 # cache auto-expires after N seconds
266 # cache auto-expires after N seconds
227 beaker.cache.repo_object.expire = 10
267 beaker.cache.repo_object.expire = 300
228 beaker.cache.repo_object.enabled = true
268 beaker.cache.repo_object.enabled = true
229
269
230
270
@@ -270,20 +310,6 b' For a more detailed explanation of the l'
270 level = DEBUG
310 level = DEBUG
271 formatter = generic
311 formatter = generic
272
312
273 [handler_file]
274 class = FileHandler
275 args = ('vcsserver.log', 'a',)
276 level = DEBUG
277 formatter = generic
278
279 [handler_file_rotating]
280 class = logging.handlers.TimedRotatingFileHandler
281 # 'D', 5 - rotate every 5days
282 # you can set 'h', 'midnight'
283 args = ('vcsserver.log', 'D', 5, 10,)
284 level = DEBUG
285 formatter = generic
286
287 ################
313 ################
288 ## FORMATTERS ##
314 ## FORMATTERS ##
289 ################
315 ################
@@ -6,7 +6,7 b' pull_request methods'
6 close_pull_request
6 close_pull_request
7 ------------------
7 ------------------
8
8
9 .. py:function:: close_pull_request(apiuser, repoid, pullrequestid, userid=<Optional:<OptionalAttr:apiuser>>)
9 .. py:function:: close_pull_request(apiuser, repoid, pullrequestid, userid=<Optional:<OptionalAttr:apiuser>>, message=<Optional:''>)
10
10
11 Close the pull request specified by `pullrequestid`.
11 Close the pull request specified by `pullrequestid`.
12
12
@@ -19,6 +19,9 b' close_pull_request'
19 :type pullrequestid: int
19 :type pullrequestid: int
20 :param userid: Close the pull request as this user.
20 :param userid: Close the pull request as this user.
21 :type userid: Optional(str or int)
21 :type userid: Optional(str or int)
22 :param message: Optional message to close the Pull Request with. If not
23 specified it will be generated automatically.
24 :type message: Optional(str)
22
25
23 Example output:
26 Example output:
24
27
@@ -27,6 +30,7 b' close_pull_request'
27 "id": <id_given_in_input>,
30 "id": <id_given_in_input>,
28 "result": {
31 "result": {
29 "pull_request_id": "<int>",
32 "pull_request_id": "<int>",
33 "close_status": "<str:status_lbl>,
30 "closed": "<bool>"
34 "closed": "<bool>"
31 },
35 },
32 "error": null
36 "error": null
@@ -105,10 +109,12 b' create_pull_request'
105 :param description: Set the pull request description.
109 :param description: Set the pull request description.
106 :type description: Optional(str)
110 :type description: Optional(str)
107 :param reviewers: Set the new pull request reviewers list.
111 :param reviewers: Set the new pull request reviewers list.
112 Reviewer defined by review rules will be added automatically to the
113 defined list.
108 :type reviewers: Optional(list)
114 :type reviewers: Optional(list)
109 Accepts username strings or objects of the format:
115 Accepts username strings or objects of the format:
110
116
111 {'username': 'nick', 'reasons': ['original author']}
117 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
112
118
113
119
114 get_pull_request
120 get_pull_request
@@ -320,7 +326,7 b' merge_pull_request'
320 update_pull_request
326 update_pull_request
321 -------------------
327 -------------------
322
328
323 .. py:function:: update_pull_request(apiuser, repoid, pullrequestid, title=<Optional:''>, description=<Optional:''>, reviewers=<Optional:None>, update_commits=<Optional:None>, close_pull_request=<Optional:None>)
329 .. py:function:: update_pull_request(apiuser, repoid, pullrequestid, title=<Optional:''>, description=<Optional:''>, reviewers=<Optional:None>, update_commits=<Optional:None>)
324
330
325 Updates a pull request.
331 Updates a pull request.
326
332
@@ -336,10 +342,12 b' update_pull_request'
336 :type description: Optional(str)
342 :type description: Optional(str)
337 :param reviewers: Update pull request reviewers list with new value.
343 :param reviewers: Update pull request reviewers list with new value.
338 :type reviewers: Optional(list)
344 :type reviewers: Optional(list)
345 Accepts username strings or objects of the format:
346
347 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
348
339 :param update_commits: Trigger update of commits for this pull request
349 :param update_commits: Trigger update of commits for this pull request
340 :type: update_commits: Optional(bool)
350 :type: update_commits: Optional(bool)
341 :param close_pull_request: Close this pull request with rejected state
342 :type: close_pull_request: Optional(bool)
343
351
344 Example output:
352 Example output:
345
353
@@ -527,6 +527,7 b' get_repo_settings'
527 "id": 237,
527 "id": 237,
528 "result": {
528 "result": {
529 "extensions_largefiles": true,
529 "extensions_largefiles": true,
530 "extensions_evolve": true,
530 "hooks_changegroup_push_logger": true,
531 "hooks_changegroup_push_logger": true,
531 "hooks_changegroup_repo_size": false,
532 "hooks_changegroup_repo_size": false,
532 "hooks_outgoing_pull_logger": true,
533 "hooks_outgoing_pull_logger": true,
@@ -762,6 +763,49 b' lock'
762 }
763 }
763
764
764
765
766 maintenance
767 -----------
768
769 .. py:function:: maintenance(apiuser, repoid)
770
771 Triggers a maintenance on the given repository.
772
773 This command can only be run using an |authtoken| with admin
774 rights to the specified repository. For more information,
775 see :ref:`config-token-ref`.
776
777 This command takes the following options:
778
779 :param apiuser: This is filled automatically from the |authtoken|.
780 :type apiuser: AuthUser
781 :param repoid: The repository name or repository ID.
782 :type repoid: str or int
783
784 Example output:
785
786 .. code-block:: bash
787
788 id : <id_given_in_input>
789 result : {
790 "msg": "executed maintenance command",
791 "executed_actions": [
792 <action_message>, <action_message2>...
793 ],
794 "repository": "<repository name>"
795 }
796 error : null
797
798 Example error output:
799
800 .. code-block:: bash
801
802 id : <id_given_in_input>
803 result : null
804 error : {
805 "Unable to execute maintenance on `<reponame>`"
806 }
807
808
765 pull
809 pull
766 ----
810 ----
767
811
@@ -66,7 +66,7 b' RhodeCode VCSServer repositories into th'
66 RhodeCode currently is using Mercurial Version Control System, please make sure
66 RhodeCode currently is using Mercurial Version Control System, please make sure
67 you have it installed before continuing.
67 you have it installed before continuing.
68
68
69 To obtain the required sources, use the following commands:
69 To obtain the required sources, use the following commands::
70
70
71 mkdir rhodecode-develop && cd rhodecode-develop
71 mkdir rhodecode-develop && cd rhodecode-develop
72 hg clone https://code.rhodecode.com/rhodecode-enterprise-ce
72 hg clone https://code.rhodecode.com/rhodecode-enterprise-ce
@@ -80,9 +80,9 b' To obtain the required sources, use the '
80 Install some required libraries
80 Install some required libraries
81 -------------------------------
81 -------------------------------
82
82
83 There are some required drivers that we need to install to test RhodeCode
83 There are some required drivers and dev libraries that we need to install to
84 under different types of databases. For example in Ubuntu we need to install
84 test RhodeCode under different types of databases. For example in Ubuntu we
85 the following.
85 need to install the following.
86
86
87 required libraries::
87 required libraries::
88
88
@@ -20,7 +20,7 b' and commit files and |repos| while manag'
20 * Migration from existing databases.
20 * Migration from existing databases.
21 * |RCM| SDK.
21 * |RCM| SDK.
22 * Built-in analytics
22 * Built-in analytics
23 * Built in integrations including: Slack, Jenkins, Webhooks, Jira, Redmine, Hipchat
23 * Built in integrations including: Slack, Webhooks (used for Jenkins/TeamCity and other CIs), Jira, Redmine, Hipchat
24 * Pluggable authentication system.
24 * Pluggable authentication system.
25 * Support for AD, |LDAP|, Crowd, CAS, PAM.
25 * Support for AD, |LDAP|, Crowd, CAS, PAM.
26 * Support for external authentication via Oauth Google, Github, Bitbucket, Twitter.
26 * Support for external authentication via Oauth Google, Github, Bitbucket, Twitter.
@@ -17,7 +17,8 b' Type/Name |RC| Edi'
17 :ref:`integrations-slack` |RCCEshort| https://slack.com/
17 :ref:`integrations-slack` |RCCEshort| https://slack.com/
18 :ref:`integrations-hipchat` |RCCEshort| https://www.hipchat.com/
18 :ref:`integrations-hipchat` |RCCEshort| https://www.hipchat.com/
19 :ref:`integrations-webhook` |RCCEshort| POST events as `json` to a custom url
19 :ref:`integrations-webhook` |RCCEshort| POST events as `json` to a custom url
20 :ref:`integrations-email` |RCEEshort| Send repo push commits by email
20 :ref:`integrations-ci` |RCCEshort| Trigger Builds for Common CI Systems
21 :ref:`integrations-email` |RCCEshort| Send repo push commits by email
21 :ref:`integrations-redmine` |RCEEshort| Close/Resolve/Reference redmine issues
22 :ref:`integrations-redmine` |RCEEshort| Close/Resolve/Reference redmine issues
22 :ref:`integrations-jira` |RCEEshort| Close/Resolve/Reference JIRA issues
23 :ref:`integrations-jira` |RCEEshort| Close/Resolve/Reference JIRA issues
23 ============================ ============ =====================================
24 ============================ ============ =====================================
@@ -3,9 +3,9 b''
3 Webhook integration
3 Webhook integration
4 ===================
4 ===================
5
5
6 The Webhook integration allows you to POST events such as repository pushes
6 The :ref:`creating-integrations` integration allows you to POST events such as
7 or pull requests to a custom http endpoint as a json dict with details of the
7 repository pushes or pull requests to a custom http endpoint as a JSON dict
8 event.
8 with details of the event.
9
9
10 Starting from 4.5.0 release, webhook integration allows to use variables
10 Starting from 4.5.0 release, webhook integration allows to use variables
11 inside the URL. For example in URL `https://server-example.com/${repo_name}`
11 inside the URL. For example in URL `https://server-example.com/${repo_name}`
@@ -14,8 +14,10 b' triggered from. Some of the variables li'
14 `${branch}` will result in webhook be called multiple times when multiple
14 `${branch}` will result in webhook be called multiple times when multiple
15 branches are pushed.
15 branches are pushed.
16
16
17 Some of the variables like `${pull_request_id}` will be replaced only in
17 Starting from 4.8.0 also repository extra fields can be used. A format to use
18 the pull request related events.
18 them is `${extra:field_key}`. It's usefull to use them to specify custom
19 repo only parameters. Some of the variables like `${pull_request_id}`
20 will be replaced only in the pull request related events.
19
21
20 To create a webhook integration, select "webhook" in the integration settings
22 To create a webhook integration, select "webhook" in the integration settings
21 and use the URL and key from your any previous custom webhook created. See
23 and use the URL and key from your any previous custom webhook created. See
@@ -9,6 +9,7 b' Release Notes'
9 .. toctree::
9 .. toctree::
10 :maxdepth: 1
10 :maxdepth: 1
11
11
12 release-notes-4.8.0.rst
12 release-notes-4.7.2.rst
13 release-notes-4.7.2.rst
13 release-notes-4.7.1.rst
14 release-notes-4.7.1.rst
14 release-notes-4.7.0.rst
15 release-notes-4.7.0.rst
@@ -3,8 +3,13 b''
3 Getting Started with VCS
3 Getting Started with VCS
4 ------------------------
4 ------------------------
5
5
6 When using |RCM|, you will be working with |git| or |hg| |repos| from the
6 When using |RCM|, you will be working with |git|, |svn| or |hg| |repos| from the
7 command line.
7 command line or using a GUI client such as Tortoise, Tower or SourceTree.
8
9 |RCM| uses a standard |git|, |svn| and |hg| protocols. So all tools that
10 can interact with there protocols are supported, including Eclipse or PyCharm
11 plugins.
12
8
13
9 If you have never used either before, the following information should
14 If you have never used either before, the following information should
10 help you set up your local machine so that you can sync changes with the
15 help you set up your local machine so that you can sync changes with the
@@ -7,7 +7,7 b' buildEnv { name = "bower-env"; ignoreCol'
7 (fetchbower "paper-tooltip" "PolymerElements/paper-tooltip#1.1.3" "PolymerElements/paper-tooltip#^1.1.2" "0vmrm1n8k9sk9nvqy03q177axy22pia6i3j1gxbk72j3pqiqvg6k")
7 (fetchbower "paper-tooltip" "PolymerElements/paper-tooltip#1.1.3" "PolymerElements/paper-tooltip#^1.1.2" "0vmrm1n8k9sk9nvqy03q177axy22pia6i3j1gxbk72j3pqiqvg6k")
8 (fetchbower "paper-toast" "PolymerElements/paper-toast#1.3.0" "PolymerElements/paper-toast#^1.3.0" "0x9rqxsks5455s8pk4aikpp99ijdn6kxr9gvhwh99nbcqdzcxq1m")
8 (fetchbower "paper-toast" "PolymerElements/paper-toast#1.3.0" "PolymerElements/paper-toast#^1.3.0" "0x9rqxsks5455s8pk4aikpp99ijdn6kxr9gvhwh99nbcqdzcxq1m")
9 (fetchbower "paper-toggle-button" "PolymerElements/paper-toggle-button#1.2.0" "PolymerElements/paper-toggle-button#^1.2.0" "0mphcng3ngspbpg4jjn0mb91nvr4xc1phq3qswib15h6sfww1b2w")
9 (fetchbower "paper-toggle-button" "PolymerElements/paper-toggle-button#1.2.0" "PolymerElements/paper-toggle-button#^1.2.0" "0mphcng3ngspbpg4jjn0mb91nvr4xc1phq3qswib15h6sfww1b2w")
10 (fetchbower "iron-ajax" "PolymerElements/iron-ajax#1.4.3" "PolymerElements/iron-ajax#^1.4.3" "0m3dx27arwmlcp00b7n516sc5a51f40p9vapr1nvd57l3i3z0pzm")
10 (fetchbower "iron-ajax" "PolymerElements/iron-ajax#1.4.3" "PolymerElements/iron-ajax#^1.4.3" "1b1z3112ggjdflgrwbpmnbsh3kgcm4hn255wshvrlzds4w069gja")
11 (fetchbower "iron-autogrow-textarea" "PolymerElements/iron-autogrow-textarea#1.0.13" "PolymerElements/iron-autogrow-textarea#^1.0.13" "0zwhpl97vii1s8k0lgain8i9dnw29b0mxc5ixdscx9las13n2lqq")
11 (fetchbower "iron-autogrow-textarea" "PolymerElements/iron-autogrow-textarea#1.0.13" "PolymerElements/iron-autogrow-textarea#^1.0.13" "0zwhpl97vii1s8k0lgain8i9dnw29b0mxc5ixdscx9las13n2lqq")
12 (fetchbower "iron-a11y-keys" "PolymerElements/iron-a11y-keys#1.0.6" "PolymerElements/iron-a11y-keys#^1.0.6" "1xz3mgghfcxixq28sdb654iaxj4nyi1bzcwf77ydkms6fviqs9mv")
12 (fetchbower "iron-a11y-keys" "PolymerElements/iron-a11y-keys#1.0.6" "PolymerElements/iron-a11y-keys#^1.0.6" "1xz3mgghfcxixq28sdb654iaxj4nyi1bzcwf77ydkms6fviqs9mv")
13 (fetchbower "iron-flex-layout" "PolymerElements/iron-flex-layout#1.3.1" "PolymerElements/iron-flex-layout#^1.0.0" "0nswv3ih3bhflgcd2wjfmddqswzgqxb2xbq65jk9w3rkj26hplbl")
13 (fetchbower "iron-flex-layout" "PolymerElements/iron-flex-layout#1.3.1" "PolymerElements/iron-flex-layout#^1.0.0" "0nswv3ih3bhflgcd2wjfmddqswzgqxb2xbq65jk9w3rkj26hplbl")
@@ -601,13 +601,13 b''
601 };
601 };
602 };
602 };
603 deform = super.buildPythonPackage {
603 deform = super.buildPythonPackage {
604 name = "deform-2.0a2";
604 name = "deform-2.0.4";
605 buildInputs = with self; [];
605 buildInputs = with self; [];
606 doCheck = false;
606 doCheck = false;
607 propagatedBuildInputs = with self; [Chameleon colander peppercorn translationstring zope.deprecation];
607 propagatedBuildInputs = with self; [Chameleon colander iso8601 peppercorn translationstring zope.deprecation];
608 src = fetchurl {
608 src = fetchurl {
609 url = "https://pypi.python.org/packages/8d/b3/aab57e81da974a806dc9c5fa024a6404720f890a6dcf2e80885e3cb4609a/deform-2.0a2.tar.gz";
609 url = "https://pypi.python.org/packages/66/3b/eefcb07abcab7a97f6665aa2d0cf1af741d9d6e78a2e4657fd2b89f89880/deform-2.0.4.tar.gz";
610 md5 = "7a90d41f7fbc18002ce74f39bd90a5e4";
610 md5 = "34756e42cf50dd4b4430809116c4ec0a";
611 };
611 };
612 meta = {
612 meta = {
613 license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
613 license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
@@ -887,13 +887,13 b''
887 };
887 };
888 };
888 };
889 ipython-genutils = super.buildPythonPackage {
889 ipython-genutils = super.buildPythonPackage {
890 name = "ipython-genutils-0.1.0";
890 name = "ipython-genutils-0.2.0";
891 buildInputs = with self; [];
891 buildInputs = with self; [];
892 doCheck = false;
892 doCheck = false;
893 propagatedBuildInputs = with self; [];
893 propagatedBuildInputs = with self; [];
894 src = fetchurl {
894 src = fetchurl {
895 url = "https://pypi.python.org/packages/71/b7/a64c71578521606edbbce15151358598f3dfb72a3431763edc2baf19e71f/ipython_genutils-0.1.0.tar.gz";
895 url = "https://pypi.python.org/packages/e8/69/fbeffffc05236398ebfcfb512b6d2511c622871dca1746361006da310399/ipython_genutils-0.2.0.tar.gz";
896 md5 = "9a8afbe0978adbcbfcb3b35b2d015a56";
896 md5 = "5a4f9781f78466da0ea1a648f3e1f79f";
897 };
897 };
898 meta = {
898 meta = {
899 license = [ pkgs.lib.licenses.bsdOriginal ];
899 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -1004,13 +1004,13 b''
1004 };
1004 };
1005 };
1005 };
1006 mistune = super.buildPythonPackage {
1006 mistune = super.buildPythonPackage {
1007 name = "mistune-0.7.3";
1007 name = "mistune-0.7.4";
1008 buildInputs = with self; [];
1008 buildInputs = with self; [];
1009 doCheck = false;
1009 doCheck = false;
1010 propagatedBuildInputs = with self; [];
1010 propagatedBuildInputs = with self; [];
1011 src = fetchurl {
1011 src = fetchurl {
1012 url = "https://pypi.python.org/packages/88/1e/be99791262b3a794332fda598a07c2749a433b9378586361ba9d8e824607/mistune-0.7.3.tar.gz";
1012 url = "https://pypi.python.org/packages/25/a4/12a584c0c59c9fed529f8b3c47ca8217c0cf8bcc5e1089d3256410cfbdbc/mistune-0.7.4.tar.gz";
1013 md5 = "4eba50bd121b83716fa4be6a4049004b";
1013 md5 = "92d01cb717e9e74429e9bde9d29ac43b";
1014 };
1014 };
1015 meta = {
1015 meta = {
1016 license = [ pkgs.lib.licenses.bsdOriginal ];
1016 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -1082,13 +1082,13 b''
1082 };
1082 };
1083 };
1083 };
1084 objgraph = super.buildPythonPackage {
1084 objgraph = super.buildPythonPackage {
1085 name = "objgraph-2.0.0";
1085 name = "objgraph-3.1.0";
1086 buildInputs = with self; [];
1086 buildInputs = with self; [];
1087 doCheck = false;
1087 doCheck = false;
1088 propagatedBuildInputs = with self; [];
1088 propagatedBuildInputs = with self; [];
1089 src = fetchurl {
1089 src = fetchurl {
1090 url = "https://pypi.python.org/packages/d7/33/ace750b59247496ed769b170586c5def7202683f3d98e737b75b767ff29e/objgraph-2.0.0.tar.gz";
1090 url = "https://pypi.python.org/packages/f4/b3/082e54e62094cb2ec84f8d5a49e0142cef99016491cecba83309cff920ae/objgraph-3.1.0.tar.gz";
1091 md5 = "25b0d5e5adc74aa63ead15699614159c";
1091 md5 = "eddbd96039796bfbd13eee403701e64a";
1092 };
1092 };
1093 meta = {
1093 meta = {
1094 license = [ pkgs.lib.licenses.mit ];
1094 license = [ pkgs.lib.licenses.mit ];
@@ -1186,13 +1186,13 b''
1186 };
1186 };
1187 };
1187 };
1188 prompt-toolkit = super.buildPythonPackage {
1188 prompt-toolkit = super.buildPythonPackage {
1189 name = "prompt-toolkit-1.0.13";
1189 name = "prompt-toolkit-1.0.14";
1190 buildInputs = with self; [];
1190 buildInputs = with self; [];
1191 doCheck = false;
1191 doCheck = false;
1192 propagatedBuildInputs = with self; [six wcwidth];
1192 propagatedBuildInputs = with self; [six wcwidth];
1193 src = fetchurl {
1193 src = fetchurl {
1194 url = "https://pypi.python.org/packages/23/be/4876b52d5cc159cbd4b0ff6e7aa419a26470849a43a8f647857a4a24467b/prompt_toolkit-1.0.13.tar.gz";
1194 url = "https://pypi.python.org/packages/55/56/8c39509b614bda53e638b7500f12577d663ac1b868aef53426fc6a26c3f5/prompt_toolkit-1.0.14.tar.gz";
1195 md5 = "427b496d2c147bd3819bc3a7f6e0d493";
1195 md5 = "f24061ae133ed32c6b764e92bd48c496";
1196 };
1196 };
1197 meta = {
1197 meta = {
1198 license = [ pkgs.lib.licenses.bsdOriginal ];
1198 license = [ pkgs.lib.licenses.bsdOriginal ];
@@ -1641,7 +1641,7 b''
1641 };
1641 };
1642 };
1642 };
1643 rhodecode-enterprise-ce = super.buildPythonPackage {
1643 rhodecode-enterprise-ce = super.buildPythonPackage {
1644 name = "rhodecode-enterprise-ce-4.7.2";
1644 name = "rhodecode-enterprise-ce-4.8.0";
1645 buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj];
1645 buildInputs = with self; [pytest py pytest-cov pytest-sugar pytest-runner pytest-catchlog pytest-profiling gprof2dot pytest-timeout mock WebTest cov-core coverage configobj];
1646 doCheck = true;
1646 doCheck = true;
1647 propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt];
1647 propagatedBuildInputs = with self; [Babel Beaker FormEncode Mako Markdown MarkupSafe MySQL-python Paste PasteDeploy PasteScript Pygments pygments-markdown-lexer Pylons Routes SQLAlchemy Tempita URLObject WebError WebHelpers WebHelpers2 WebOb WebTest Whoosh alembic amqplib anyjson appenlight-client authomatic backport-ipaddress cssselect celery channelstream colander decorator deform docutils gevent gunicorn infrae.cache ipython iso8601 kombu lxml msgpack-python nbconvert packaging psycopg2 py-gfm pycrypto pycurl pyparsing pyramid pyramid-debugtoolbar pyramid-mako pyramid-beaker pysqlite python-dateutil python-ldap python-memcached python-pam recaptcha-client repoze.lru requests simplejson subprocess32 waitress zope.cachedescriptors dogpile.cache dogpile.core psutil py-bcrypt];
@@ -16,7 +16,7 b' colander==1.2'
16 configobj==5.0.6
16 configobj==5.0.6
17 cssselect==1.0.1
17 cssselect==1.0.1
18 decorator==4.0.11
18 decorator==4.0.11
19 deform==2.0a2
19 deform==2.0.4
20 docutils==0.12
20 docutils==0.12
21 dogpile.cache==0.6.1
21 dogpile.cache==0.6.1
22 dogpile.core==0.4.1
22 dogpile.core==0.4.1
@@ -38,7 +38,7 b' meld3==1.0.2'
38 msgpack-python==0.4.8
38 msgpack-python==0.4.8
39 MySQL-python==1.2.5
39 MySQL-python==1.2.5
40 nose==1.3.6
40 nose==1.3.6
41 objgraph==2.0.0
41 objgraph==3.1.0
42 packaging==15.2
42 packaging==15.2
43 paramiko==1.15.1
43 paramiko==1.15.1
44 Paste==2.0.3
44 Paste==2.0.3
@@ -1,1 +1,1 b''
1 4.7.2 No newline at end of file
1 4.8.0 No newline at end of file
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}'
51 EXTENSIONS = {}
51 EXTENSIONS = {}
52
52
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 71 # defines current db version for migrations
54 __dbversion__ = 78 # defines current db version for migrations
55 __platform__ = platform.system()
55 __platform__ = platform.system()
56 __license__ = 'AGPLv3, and Commercial License'
56 __license__ = 'AGPLv3, and Commercial License'
57 __author__ = 'RhodeCode GmbH'
57 __author__ = 'RhodeCode GmbH'
@@ -35,8 +35,9 b' from pyramid.httpexceptions import HTTPN'
35
35
36 from rhodecode.api.exc import (
36 from rhodecode.api.exc import (
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
37 JSONRPCBaseError, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
38 from rhodecode.apps._base import TemplateArgs
38 from rhodecode.lib.auth import AuthUser
39 from rhodecode.lib.auth import AuthUser
39 from rhodecode.lib.base import get_ip_addr
40 from rhodecode.lib.base import get_ip_addr, attach_context_attributes
40 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.ext_json import json
41 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.lib.utils2 import safe_str
42 from rhodecode.lib.plugins.utils import get_plugin_settings
43 from rhodecode.lib.plugins.utils import get_plugin_settings
@@ -278,6 +279,11 b' def request_view(request):'
278 'request': request,
279 'request': request,
279 'apiuser': auth_u
280 'apiuser': auth_u
280 })
281 })
282
283 # register some common functions for usage
284 attach_context_attributes(TemplateArgs(), request, request.rpc_user.user_id,
285 attach_to_request=True)
286
281 try:
287 try:
282 ret_value = func(**call_params)
288 ret_value = func(**call_params)
283 return jsonrpc_response(request, ret_value)
289 return jsonrpc_response(request, ret_value)
@@ -43,16 +43,16 b' class TestClosePullRequest(object):'
43 response = api_call(self.app, params)
43 response = api_call(self.app, params)
44 expected = {
44 expected = {
45 'pull_request_id': pull_request_id,
45 'pull_request_id': pull_request_id,
46 'close_status': 'Rejected',
46 'closed': True,
47 'closed': True,
47 }
48 }
48 assert_ok(id_, expected, response.body)
49 assert_ok(id_, expected, response.body)
49 action = 'user_closed_pull_request:%d' % pull_request_id
50 journal = UserLog.query()\
50 journal = UserLog.query()\
51 .filter(UserLog.user_id == author)\
51 .filter(UserLog.user_id == author) \
52 .order_by('user_log_id') \
52 .filter(UserLog.repository_id == repo)\
53 .filter(UserLog.repository_id == repo)\
53 .filter(UserLog.action == action)\
54 .all()
54 .all()
55 assert len(journal) == 1
55 assert journal[-1].action == 'repo.pull_request.close'
56
56
57 @pytest.mark.backends("git", "hg")
57 @pytest.mark.backends("git", "hg")
58 def test_api_close_pull_request_already_closed_error(self, pr_util):
58 def test_api_close_pull_request_already_closed_error(self, pr_util):
@@ -62,13 +62,12 b' class TestCommentPullRequest(object):'
62 }
62 }
63 assert_ok(id_, expected, response.body)
63 assert_ok(id_, expected, response.body)
64
64
65 action = 'user_commented_pull_request:%d' % pull_request_id
66 journal = UserLog.query()\
65 journal = UserLog.query()\
67 .filter(UserLog.user_id == author)\
66 .filter(UserLog.user_id == author)\
68 .filter(UserLog.repository_id == repo)\
67 .filter(UserLog.repository_id == repo) \
69 .filter(UserLog.action == action)\
68 .order_by('user_log_id') \
70 .all()
69 .all()
71 assert len(journal) == 2
70 assert journal[-1].action == 'repo.pull_request.comment.create'
72
71
73 @pytest.mark.backends("git", "hg")
72 @pytest.mark.backends("git", "hg")
74 def test_api_comment_pull_request_change_status(
73 def test_api_comment_pull_request_change_status(
@@ -77,7 +77,7 b' class TestCreatePullRequestApi(object):'
77 assert pull_request.source_repo.repo_name == data['source_repo']
77 assert pull_request.source_repo.repo_name == data['source_repo']
78 assert pull_request.target_repo.repo_name == data['target_repo']
78 assert pull_request.target_repo.repo_name == data['target_repo']
79 assert pull_request.revisions == [self.commit_ids['change']]
79 assert pull_request.revisions == [self.commit_ids['change']]
80 assert pull_request.reviewers == []
80 assert len(pull_request.reviewers) == 1
81
81
82 @pytest.mark.backends("git", "hg")
82 @pytest.mark.backends("git", "hg")
83 def test_create_with_empty_description(self, backend):
83 def test_create_with_empty_description(self, backend):
@@ -98,7 +98,12 b' class TestCreatePullRequestApi(object):'
98 def test_create_with_reviewers_specified_by_names(
98 def test_create_with_reviewers_specified_by_names(
99 self, backend, no_notifications):
99 self, backend, no_notifications):
100 data = self._prepare_data(backend)
100 data = self._prepare_data(backend)
101 reviewers = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
101 reviewers = [
102 {'username': TEST_USER_REGULAR_LOGIN,
103 'reasons': ['added manually']},
104 {'username': TEST_USER_ADMIN_LOGIN,
105 'reasons': ['added manually']},
106 ]
102 data['reviewers'] = reviewers
107 data['reviewers'] = reviewers
103 id_, params = build_data(
108 id_, params = build_data(
104 self.apikey_regular, 'create_pull_request', **data)
109 self.apikey_regular, 'create_pull_request', **data)
@@ -110,16 +115,26 b' class TestCreatePullRequestApi(object):'
110 assert result['result']['msg'] == expected_message
115 assert result['result']['msg'] == expected_message
111 pull_request_id = result['result']['pull_request_id']
116 pull_request_id = result['result']['pull_request_id']
112 pull_request = PullRequestModel().get(pull_request_id)
117 pull_request = PullRequestModel().get(pull_request_id)
113 actual_reviewers = [r.user.username for r in pull_request.reviewers]
118 actual_reviewers = [
119 {'username': r.user.username,
120 'reasons': ['added manually'],
121 } for r in pull_request.reviewers
122 ]
114 assert sorted(actual_reviewers) == sorted(reviewers)
123 assert sorted(actual_reviewers) == sorted(reviewers)
115
124
116 @pytest.mark.backends("git", "hg")
125 @pytest.mark.backends("git", "hg")
117 def test_create_with_reviewers_specified_by_ids(
126 def test_create_with_reviewers_specified_by_ids(
118 self, backend, no_notifications):
127 self, backend, no_notifications):
119 data = self._prepare_data(backend)
128 data = self._prepare_data(backend)
120 reviewer_names = [TEST_USER_REGULAR_LOGIN, TEST_USER_ADMIN_LOGIN]
121 reviewers = [
129 reviewers = [
122 UserModel().get_by_username(n).user_id for n in reviewer_names]
130 {'username': UserModel().get_by_username(
131 TEST_USER_REGULAR_LOGIN).user_id,
132 'reasons': ['added manually']},
133 {'username': UserModel().get_by_username(
134 TEST_USER_ADMIN_LOGIN).user_id,
135 'reasons': ['added manually']},
136 ]
137
123 data['reviewers'] = reviewers
138 data['reviewers'] = reviewers
124 id_, params = build_data(
139 id_, params = build_data(
125 self.apikey_regular, 'create_pull_request', **data)
140 self.apikey_regular, 'create_pull_request', **data)
@@ -131,14 +146,17 b' class TestCreatePullRequestApi(object):'
131 assert result['result']['msg'] == expected_message
146 assert result['result']['msg'] == expected_message
132 pull_request_id = result['result']['pull_request_id']
147 pull_request_id = result['result']['pull_request_id']
133 pull_request = PullRequestModel().get(pull_request_id)
148 pull_request = PullRequestModel().get(pull_request_id)
134 actual_reviewers = [r.user.username for r in pull_request.reviewers]
149 actual_reviewers = [
135 assert sorted(actual_reviewers) == sorted(reviewer_names)
150 {'username': r.user.user_id,
151 'reasons': ['added manually'],
152 } for r in pull_request.reviewers
153 ]
154 assert sorted(actual_reviewers) == sorted(reviewers)
136
155
137 @pytest.mark.backends("git", "hg")
156 @pytest.mark.backends("git", "hg")
138 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
157 def test_create_fails_when_the_reviewer_is_not_found(self, backend):
139 data = self._prepare_data(backend)
158 data = self._prepare_data(backend)
140 reviewers = ['somebody']
159 data['reviewers'] = [{'username': 'somebody'}]
141 data['reviewers'] = reviewers
142 id_, params = build_data(
160 id_, params = build_data(
143 self.apikey_regular, 'create_pull_request', **data)
161 self.apikey_regular, 'create_pull_request', **data)
144 response = api_call(self.app, params)
162 response = api_call(self.app, params)
@@ -153,7 +171,7 b' class TestCreatePullRequestApi(object):'
153 id_, params = build_data(
171 id_, params = build_data(
154 self.apikey_regular, 'create_pull_request', **data)
172 self.apikey_regular, 'create_pull_request', **data)
155 response = api_call(self.app, params)
173 response = api_call(self.app, params)
156 expected_message = 'reviewers should be specified as a list'
174 expected_message = {u'': '"test_regular,test_admin" is not iterable'}
157 assert_error(id_, expected_message, given=response.body)
175 assert_error(id_, expected_message, given=response.body)
158
176
159 @pytest.mark.backends("git", "hg")
177 @pytest.mark.backends("git", "hg")
@@ -59,6 +59,21 b' class TestCreateUser(object):'
59 expected = "email `%s` already exist" % (TEST_USER_REGULAR_EMAIL,)
59 expected = "email `%s` already exist" % (TEST_USER_REGULAR_EMAIL,)
60 assert_error(id_, expected, given=response.body)
60 assert_error(id_, expected, given=response.body)
61
61
62 def test_api_create_user_with_wrong_username(self):
63 bad_username = '<> HELLO WORLD <>'
64 id_, params = build_data(
65 self.apikey, 'create_user',
66 username=bad_username,
67 email='new@email.com',
68 password='trololo')
69 response = api_call(self.app, params)
70
71 expected = {'username':
72 "Username may only contain alphanumeric characters "
73 "underscores, periods or dashes and must begin with "
74 "alphanumeric character or underscore"}
75 assert_error(id_, expected, given=response.body)
76
62 def test_api_create_user(self):
77 def test_api_create_user(self):
63 username = 'test_new_api_user'
78 username = 'test_new_api_user'
64 email = username + "@foo.com"
79 email = username + "@foo.com"
@@ -175,7 +190,6 b' class TestCreateUser(object):'
175 fixture.destroy_repo_group(username)
190 fixture.destroy_repo_group(username)
176 fixture.destroy_user(usr.user_id)
191 fixture.destroy_user(usr.user_id)
177
192
178
179 @mock.patch.object(UserModel, 'create_or_update', crash)
193 @mock.patch.object(UserModel, 'create_or_update', crash)
180 def test_api_create_user_when_exception_happened(self):
194 def test_api_create_user_when_exception_happened(self):
181
195
@@ -112,3 +112,16 b' class TestCreateUserGroup(object):'
112
112
113 expected = 'failed to create group `%s`' % (group_name,)
113 expected = 'failed to create group `%s`' % (group_name,)
114 assert_error(id_, expected, given=response.body)
114 assert_error(id_, expected, given=response.body)
115
116 def test_api_create_user_group_with_wrong_name(self, user_util):
117
118 group_name = 'wrong NAME <>'
119 id_, params = build_data(
120 self.apikey, 'create_user_group', group_name=group_name)
121 response = api_call(self.app, params)
122
123 expected = {"user_group_name":
124 "Allowed in name are letters, numbers, and `-`, `_`, "
125 "`.` Name must start with a letter or number. "
126 "Got `{}`".format(group_name)}
127 assert_error(id_, expected, given=response.body)
@@ -30,43 +30,45 b' from rhodecode.api.tests.utils import ('
30 class TestApiDeleteRepo(object):
30 class TestApiDeleteRepo(object):
31 def test_api_delete_repo(self, backend):
31 def test_api_delete_repo(self, backend):
32 repo = backend.create_repo()
32 repo = backend.create_repo()
33
33 repo_name = repo.repo_name
34 id_, params = build_data(
34 id_, params = build_data(
35 self.apikey, 'delete_repo', repoid=repo.repo_name, )
35 self.apikey, 'delete_repo', repoid=repo.repo_name, )
36 response = api_call(self.app, params)
36 response = api_call(self.app, params)
37
37
38 expected = {
38 expected = {
39 'msg': 'Deleted repository `%s`' % (repo.repo_name,),
39 'msg': 'Deleted repository `%s`' % (repo_name,),
40 'success': True
40 'success': True
41 }
41 }
42 assert_ok(id_, expected, given=response.body)
42 assert_ok(id_, expected, given=response.body)
43
43
44 def test_api_delete_repo_by_non_admin(self, backend, user_regular):
44 def test_api_delete_repo_by_non_admin(self, backend, user_regular):
45 repo = backend.create_repo(cur_user=user_regular.username)
45 repo = backend.create_repo(cur_user=user_regular.username)
46 repo_name = repo.repo_name
46 id_, params = build_data(
47 id_, params = build_data(
47 user_regular.api_key, 'delete_repo', repoid=repo.repo_name, )
48 user_regular.api_key, 'delete_repo', repoid=repo.repo_name, )
48 response = api_call(self.app, params)
49 response = api_call(self.app, params)
49
50
50 expected = {
51 expected = {
51 'msg': 'Deleted repository `%s`' % (repo.repo_name,),
52 'msg': 'Deleted repository `%s`' % (repo_name,),
52 'success': True
53 'success': True
53 }
54 }
54 assert_ok(id_, expected, given=response.body)
55 assert_ok(id_, expected, given=response.body)
55
56
56 def test_api_delete_repo_by_non_admin_no_permission(self, backend):
57 def test_api_delete_repo_by_non_admin_no_permission(self, backend):
57 repo = backend.create_repo()
58 repo = backend.create_repo()
59 repo_name = repo.repo_name
58 id_, params = build_data(
60 id_, params = build_data(
59 self.apikey_regular, 'delete_repo', repoid=repo.repo_name, )
61 self.apikey_regular, 'delete_repo', repoid=repo.repo_name, )
60 response = api_call(self.app, params)
62 response = api_call(self.app, params)
61 expected = 'repository `%s` does not exist' % (repo.repo_name)
63 expected = 'repository `%s` does not exist' % (repo_name)
62 assert_error(id_, expected, given=response.body)
64 assert_error(id_, expected, given=response.body)
63
65
64 def test_api_delete_repo_exception_occurred(self, backend):
66 def test_api_delete_repo_exception_occurred(self, backend):
65 repo = backend.create_repo()
67 repo = backend.create_repo()
68 repo_name = repo.repo_name
66 id_, params = build_data(
69 id_, params = build_data(
67 self.apikey, 'delete_repo', repoid=repo.repo_name, )
70 self.apikey, 'delete_repo', repoid=repo.repo_name, )
68 with mock.patch.object(RepoModel, 'delete', crash):
71 with mock.patch.object(RepoModel, 'delete', crash):
69 response = api_call(self.app, params)
72 response = api_call(self.app, params)
70 expected = 'failed to delete repository `%s`' % (
73 expected = 'failed to delete repository `%s`' % (repo_name,)
71 repo.repo_name,)
72 assert_error(id_, expected, given=response.body)
74 assert_error(id_, expected, given=response.body)
@@ -28,7 +28,7 b' from rhodecode.api.tests.utils import ('
28
28
29 @pytest.mark.usefixtures("testuser_api", "app")
29 @pytest.mark.usefixtures("testuser_api", "app")
30 class TestApiGetGist(object):
30 class TestApiGetGist(object):
31 def test_api_get_gist(self, gist_util):
31 def test_api_get_gist(self, gist_util, http_host_stub):
32 gist = gist_util.create_gist()
32 gist = gist_util.create_gist()
33 gist_id = gist.gist_access_id
33 gist_id = gist.gist_access_id
34 gist_created_on = gist.created_on
34 gist_created_on = gist.created_on
@@ -45,14 +45,14 b' class TestApiGetGist(object):'
45 'expires': -1.0,
45 'expires': -1.0,
46 'gist_id': int(gist_id),
46 'gist_id': int(gist_id),
47 'type': 'public',
47 'type': 'public',
48 'url': 'http://test.example.com:80/_admin/gists/%s' % (gist_id,),
48 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,),
49 'acl_level': Gist.ACL_LEVEL_PUBLIC,
49 'acl_level': Gist.ACL_LEVEL_PUBLIC,
50 'content': None,
50 'content': None,
51 }
51 }
52
52
53 assert_ok(id_, expected, given=response.body)
53 assert_ok(id_, expected, given=response.body)
54
54
55 def test_api_get_gist_with_content(self, gist_util):
55 def test_api_get_gist_with_content(self, gist_util, http_host_stub):
56 mapping = {
56 mapping = {
57 u'filename1.txt': {'content': u'hello world'},
57 u'filename1.txt': {'content': u'hello world'},
58 u'filename1ą.txt': {'content': u'hello worldę'}
58 u'filename1ą.txt': {'content': u'hello worldę'}
@@ -73,7 +73,7 b' class TestApiGetGist(object):'
73 'expires': -1.0,
73 'expires': -1.0,
74 'gist_id': int(gist_id),
74 'gist_id': int(gist_id),
75 'type': 'public',
75 'type': 'public',
76 'url': 'http://test.example.com:80/_admin/gists/%s' % (gist_id,),
76 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,),
77 'acl_level': Gist.ACL_LEVEL_PUBLIC,
77 'acl_level': Gist.ACL_LEVEL_PUBLIC,
78 'content': {
78 'content': {
79 u'filename1.txt': u'hello world',
79 u'filename1.txt': u'hello world',
@@ -19,13 +19,13 b''
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21
21
22 import mock
23 import pytest
22 import pytest
24 import urlobject
23 import urlobject
25 from pylons import url
24 from pylons import url
26
25
27 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
28 build_data, api_call, assert_error, assert_ok)
27 build_data, api_call, assert_error, assert_ok)
28 from rhodecode.lib.utils2 import safe_unicode
29
29
30 pytestmark = pytest.mark.backends("git", "hg")
30 pytestmark = pytest.mark.backends("git", "hg")
31
31
@@ -33,7 +33,7 b' pytestmark = pytest.mark.backends("git",'
33 @pytest.mark.usefixtures("testuser_api", "app")
33 @pytest.mark.usefixtures("testuser_api", "app")
34 class TestGetPullRequest(object):
34 class TestGetPullRequest(object):
35
35
36 def test_api_get_pull_request(self, pr_util):
36 def test_api_get_pull_request(self, pr_util, http_host_only_stub):
37 from rhodecode.model.pull_request import PullRequestModel
37 from rhodecode.model.pull_request import PullRequestModel
38 pull_request = pr_util.create_pull_request(mergeable=True)
38 pull_request = pr_util.create_pull_request(mergeable=True)
39 id_, params = build_data(
39 id_, params = build_data(
@@ -50,16 +50,16 b' class TestGetPullRequest(object):'
50 'pullrequest_show',
50 'pullrequest_show',
51 repo_name=pull_request.target_repo.repo_name,
51 repo_name=pull_request.target_repo.repo_name,
52 pull_request_id=pull_request.pull_request_id, qualified=True))
52 pull_request_id=pull_request.pull_request_id, qualified=True))
53 pr_url = unicode(
53
54 url_obj.with_netloc('test.example.com:80'))
54 pr_url = safe_unicode(
55 source_url = unicode(
55 url_obj.with_netloc(http_host_only_stub))
56 pull_request.source_repo.clone_url()
56 source_url = safe_unicode(
57 .with_netloc('test.example.com:80'))
57 pull_request.source_repo.clone_url().with_netloc(http_host_only_stub))
58 target_url = unicode(
58 target_url = safe_unicode(
59 pull_request.target_repo.clone_url()
59 pull_request.target_repo.clone_url().with_netloc(http_host_only_stub))
60 .with_netloc('test.example.com:80'))
60 shadow_url = safe_unicode(
61 shadow_url = unicode(
62 PullRequestModel().get_shadow_clone_url(pull_request))
61 PullRequestModel().get_shadow_clone_url(pull_request))
62
63 expected = {
63 expected = {
64 'pull_request_id': pull_request.pull_request_id,
64 'pull_request_id': pull_request.pull_request_id,
65 'url': pr_url,
65 'url': pr_url,
@@ -109,7 +109,8 b' class TestGetPullRequest(object):'
109 'reasons': reasons,
109 'reasons': reasons,
110 'review_status': st[0][1].status if st else 'not_reviewed',
110 'review_status': st[0][1].status if st else 'not_reviewed',
111 }
111 }
112 for reviewer, reasons, st in pull_request.reviewers_statuses()
112 for reviewer, reasons, mandatory, st in
113 pull_request.reviewers_statuses()
113 ]
114 ]
114 }
115 }
115 assert_ok(id_, expected, response.body)
116 assert_ok(id_, expected, response.body)
@@ -95,13 +95,13 b' class TestMergePullRequest(object):'
95
95
96 assert_ok(id_, expected, response.body)
96 assert_ok(id_, expected, response.body)
97
97
98 action = 'user_merged_pull_request:%d' % (pull_request_id, )
99 journal = UserLog.query()\
98 journal = UserLog.query()\
100 .filter(UserLog.user_id == author)\
99 .filter(UserLog.user_id == author)\
101 .filter(UserLog.repository_id == repo)\
100 .filter(UserLog.repository_id == repo) \
102 .filter(UserLog.action == action)\
101 .order_by('user_log_id') \
103 .all()
102 .all()
104 assert len(journal) == 1
103 assert journal[-2].action == 'repo.pull_request.merge'
104 assert journal[-1].action == 'repo.pull_request.close'
105
105
106 id_, params = build_data(
106 id_, params = build_data(
107 self.apikey, 'merge_pull_request',
107 self.apikey, 'merge_pull_request',
@@ -33,7 +33,7 b' class TestUpdatePullRequest(object):'
33
33
34 @pytest.mark.backends("git", "hg")
34 @pytest.mark.backends("git", "hg")
35 def test_api_update_pull_request_title_or_description(
35 def test_api_update_pull_request_title_or_description(
36 self, pr_util, silence_action_logger, no_notifications):
36 self, pr_util, no_notifications):
37 pull_request = pr_util.create_pull_request()
37 pull_request = pr_util.create_pull_request()
38
38
39 id_, params = build_data(
39 id_, params = build_data(
@@ -61,7 +61,7 b' class TestUpdatePullRequest(object):'
61
61
62 @pytest.mark.backends("git", "hg")
62 @pytest.mark.backends("git", "hg")
63 def test_api_try_update_closed_pull_request(
63 def test_api_try_update_closed_pull_request(
64 self, pr_util, silence_action_logger, no_notifications):
64 self, pr_util, no_notifications):
65 pull_request = pr_util.create_pull_request()
65 pull_request = pr_util.create_pull_request()
66 PullRequestModel().close_pull_request(
66 PullRequestModel().close_pull_request(
67 pull_request, TEST_USER_ADMIN_LOGIN)
67 pull_request, TEST_USER_ADMIN_LOGIN)
@@ -78,8 +78,7 b' class TestUpdatePullRequest(object):'
78 assert_error(id_, expected, response.body)
78 assert_error(id_, expected, response.body)
79
79
80 @pytest.mark.backends("git", "hg")
80 @pytest.mark.backends("git", "hg")
81 def test_api_update_update_commits(
81 def test_api_update_update_commits(self, pr_util, no_notifications):
82 self, pr_util, silence_action_logger, no_notifications):
83 commits = [
82 commits = [
84 {'message': 'a'},
83 {'message': 'a'},
85 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
84 {'message': 'b', 'added': [FileNode('file_b', 'test_content\n')]},
@@ -119,20 +118,28 b' class TestUpdatePullRequest(object):'
119
118
120 @pytest.mark.backends("git", "hg")
119 @pytest.mark.backends("git", "hg")
121 def test_api_update_change_reviewers(
120 def test_api_update_change_reviewers(
122 self, pr_util, silence_action_logger, no_notifications):
121 self, user_util, pr_util, no_notifications):
122 a = user_util.create_user()
123 b = user_util.create_user()
124 c = user_util.create_user()
125 new_reviewers = [
126 {'username': b.username,'reasons': ['updated via API'],
127 'mandatory':False},
128 {'username': c.username, 'reasons': ['updated via API'],
129 'mandatory':False},
130 ]
123
131
124 users = [x.username for x in User.get_all()]
132 added = [b.username, c.username]
125 new = [users.pop(0)]
133 removed = [a.username]
126 removed = sorted(new)
127 added = sorted(users)
128
134
129 pull_request = pr_util.create_pull_request(reviewers=new)
135 pull_request = pr_util.create_pull_request(
136 reviewers=[(a.username, ['added via API'], False)])
130
137
131 id_, params = build_data(
138 id_, params = build_data(
132 self.apikey, 'update_pull_request',
139 self.apikey, 'update_pull_request',
133 repoid=pull_request.target_repo.repo_name,
140 repoid=pull_request.target_repo.repo_name,
134 pullrequestid=pull_request.pull_request_id,
141 pullrequestid=pull_request.pull_request_id,
135 reviewers=added)
142 reviewers=new_reviewers)
136 response = api_call(self.app, params)
143 response = api_call(self.app, params)
137 expected = {
144 expected = {
138 "msg": "Updated pull request `{}`".format(
145 "msg": "Updated pull request `{}`".format(
@@ -152,7 +159,7 b' class TestUpdatePullRequest(object):'
152 self.apikey, 'update_pull_request',
159 self.apikey, 'update_pull_request',
153 repoid=pull_request.target_repo.repo_name,
160 repoid=pull_request.target_repo.repo_name,
154 pullrequestid=pull_request.pull_request_id,
161 pullrequestid=pull_request.pull_request_id,
155 reviewers=['bad_name'])
162 reviewers=[{'username': 'bad_name'}])
156 response = api_call(self.app, params)
163 response = api_call(self.app, params)
157
164
158 expected = 'user `bad_name` does not exist'
165 expected = 'user `bad_name` does not exist'
@@ -165,7 +172,7 b' class TestUpdatePullRequest(object):'
165 self.apikey, 'update_pull_request',
172 self.apikey, 'update_pull_request',
166 repoid='fake',
173 repoid='fake',
167 pullrequestid='fake',
174 pullrequestid='fake',
168 reviewers=['bad_name'])
175 reviewers=[{'username': 'bad_name'}])
169 response = api_call(self.app, params)
176 response = api_call(self.app, params)
170
177
171 expected = 'repository `fake` does not exist'
178 expected = 'repository `fake` does not exist'
@@ -181,7 +188,7 b' class TestUpdatePullRequest(object):'
181 self.apikey, 'update_pull_request',
188 self.apikey, 'update_pull_request',
182 repoid=pull_request.target_repo.repo_name,
189 repoid=pull_request.target_repo.repo_name,
183 pullrequestid=999999,
190 pullrequestid=999999,
184 reviewers=['bad_name'])
191 reviewers=[{'username': 'bad_name'}])
185 response = api_call(self.app, params)
192 response = api_call(self.app, params)
186
193
187 expected = 'pull request `999999` does not exist'
194 expected = 'pull request `999999` does not exist'
@@ -26,7 +26,7 b' from rhodecode.tests import TEST_USER_AD'
26 from rhodecode.api.tests.utils import (
26 from rhodecode.api.tests.utils import (
27 build_data, api_call, assert_error, assert_ok, crash, jsonify)
27 build_data, api_call, assert_error, assert_ok, crash, jsonify)
28 from rhodecode.tests.fixture import Fixture
28 from rhodecode.tests.fixture import Fixture
29
29 from rhodecode.tests.plugin import http_host_stub, http_host_only_stub
30
30
31 fixture = Fixture()
31 fixture = Fixture()
32
32
@@ -71,14 +71,15 b' class TestApiUpdateRepo(object):'
71 ({'repo_name': 'new_repo_name'},
71 ({'repo_name': 'new_repo_name'},
72 {
72 {
73 'repo_name': 'new_repo_name',
73 'repo_name': 'new_repo_name',
74 'url': 'http://test.example.com:80/new_repo_name'
74 'url': 'http://{}/new_repo_name'.format(http_host_only_stub())
75 }),
75 }),
76
76
77 ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
77 ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
78 '_group': 'test_group_for_update'},
78 '_group': 'test_group_for_update'},
79 {
79 {
80 'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
80 'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME),
81 'url': 'http://test.example.com:80/test_group_for_update/{}'.format(UPDATE_REPO_NAME)
81 'url': 'http://{}/test_group_for_update/{}'.format(
82 http_host_only_stub(), UPDATE_REPO_NAME)
82 }),
83 }),
83 ])
84 ])
84 def test_api_update_repo(self, updates, expected, backend):
85 def test_api_update_repo(self, updates, expected, backend):
@@ -115,7 +116,8 b' class TestApiUpdateRepo(object):'
115 master_repo = backend.create_repo()
116 master_repo = backend.create_repo()
116 repo = backend.create_repo()
117 repo = backend.create_repo()
117 updates = {
118 updates = {
118 'fork_of': master_repo.repo_name
119 'fork_of': master_repo.repo_name,
120 'fork_of_id': master_repo.repo_id
119 }
121 }
120 expected_api_data = repo.get_api_data(include_secrets=True)
122 expected_api_data = repo.get_api_data(include_secrets=True)
121 expected_api_data.update(updates)
123 expected_api_data.update(updates)
@@ -130,6 +132,7 b' class TestApiUpdateRepo(object):'
130 assert_ok(id_, expected, given=response.body)
132 assert_ok(id_, expected, given=response.body)
131 result = response.json['result']['repository']
133 result = response.json['result']['repository']
132 assert result['fork_of'] == master_repo.repo_name
134 assert result['fork_of'] == master_repo.repo_name
135 assert result['fork_of_id'] == master_repo.repo_id
133
136
134 def test_api_update_repo_fork_of_not_found(self, backend):
137 def test_api_update_repo_fork_of_not_found(self, backend):
135 master_repo_name = 'fake-parent-repo'
138 master_repo_name = 'fake-parent-repo'
@@ -21,7 +21,8 b''
21
21
22 import logging
22 import logging
23
23
24 from rhodecode.api import jsonrpc_method, JSONRPCError
24 from rhodecode import events
25 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
25 from rhodecode.api.utils import (
26 from rhodecode.api.utils import (
26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
@@ -34,6 +35,9 b' from rhodecode.model.comment import Comm'
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment
35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 from rhodecode.model.settings import SettingsModel
37 from rhodecode.model.settings import SettingsModel
38 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
37
41
38 log = logging.getLogger(__name__)
42 log = logging.getLogger(__name__)
39
43
@@ -224,8 +228,9 b' def get_pull_requests(request, apiuser, '
224
228
225
229
226 @jsonrpc_method()
230 @jsonrpc_method()
227 def merge_pull_request(request, apiuser, repoid, pullrequestid,
231 def merge_pull_request(
228 userid=Optional(OAttr('apiuser'))):
232 request, apiuser, repoid, pullrequestid,
233 userid=Optional(OAttr('apiuser'))):
229 """
234 """
230 Merge the pull request specified by `pullrequestid` into its target
235 Merge the pull request specified by `pullrequestid` into its target
231 repository.
236 repository.
@@ -273,7 +278,12 b' def merge_pull_request(request, apiuser,'
273 merge_possible = not check.failed
278 merge_possible = not check.failed
274
279
275 if not merge_possible:
280 if not merge_possible:
276 reasons = ','.join([msg for _e, msg in check.errors])
281 error_messages = []
282 for err_type, error_msg in check.errors:
283 error_msg = request.translate(error_msg)
284 error_messages.append(error_msg)
285
286 reasons = ','.join(error_messages)
277 raise JSONRPCError(
287 raise JSONRPCError(
278 'merge not possible for following reasons: {}'.format(reasons))
288 'merge not possible for following reasons: {}'.format(reasons))
279
289
@@ -300,63 +310,6 b' def merge_pull_request(request, apiuser,'
300
310
301
311
302 @jsonrpc_method()
312 @jsonrpc_method()
303 def close_pull_request(request, apiuser, repoid, pullrequestid,
304 userid=Optional(OAttr('apiuser'))):
305 """
306 Close the pull request specified by `pullrequestid`.
307
308 :param apiuser: This is filled automatically from the |authtoken|.
309 :type apiuser: AuthUser
310 :param repoid: Repository name or repository ID to which the pull
311 request belongs.
312 :type repoid: str or int
313 :param pullrequestid: ID of the pull request to be closed.
314 :type pullrequestid: int
315 :param userid: Close the pull request as this user.
316 :type userid: Optional(str or int)
317
318 Example output:
319
320 .. code-block:: bash
321
322 "id": <id_given_in_input>,
323 "result": {
324 "pull_request_id": "<int>",
325 "closed": "<bool>"
326 },
327 "error": null
328
329 """
330 repo = get_repo_or_error(repoid)
331 if not isinstance(userid, Optional):
332 if (has_superadmin_permission(apiuser) or
333 HasRepoPermissionAnyApi('repository.admin')(
334 user=apiuser, repo_name=repo.repo_name)):
335 apiuser = get_user_or_error(userid)
336 else:
337 raise JSONRPCError('userid is not the same as your user')
338
339 pull_request = get_pull_request_or_error(pullrequestid)
340 if not PullRequestModel().check_user_update(
341 pull_request, apiuser, api=True):
342 raise JSONRPCError(
343 'pull request `%s` close failed, no permission to close.' % (
344 pullrequestid,))
345 if pull_request.is_closed():
346 raise JSONRPCError(
347 'pull request `%s` is already closed' % (pullrequestid,))
348
349 PullRequestModel().close_pull_request(
350 pull_request.pull_request_id, apiuser)
351 Session().commit()
352 data = {
353 'pull_request_id': pull_request.pull_request_id,
354 'closed': True,
355 }
356 return data
357
358
359 @jsonrpc_method()
360 def comment_pull_request(
313 def comment_pull_request(
361 request, apiuser, repoid, pullrequestid, message=Optional(None),
314 request, apiuser, repoid, pullrequestid, message=Optional(None),
362 commit_id=Optional(None), status=Optional(None),
315 commit_id=Optional(None), status=Optional(None),
@@ -529,24 +482,26 b' def create_pull_request('
529 :param description: Set the pull request description.
482 :param description: Set the pull request description.
530 :type description: Optional(str)
483 :type description: Optional(str)
531 :param reviewers: Set the new pull request reviewers list.
484 :param reviewers: Set the new pull request reviewers list.
485 Reviewer defined by review rules will be added automatically to the
486 defined list.
532 :type reviewers: Optional(list)
487 :type reviewers: Optional(list)
533 Accepts username strings or objects of the format:
488 Accepts username strings or objects of the format:
534
489
535 {'username': 'nick', 'reasons': ['original author']}
490 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
536 """
491 """
537
492
538 source = get_repo_or_error(source_repo)
493 source_db_repo = get_repo_or_error(source_repo)
539 target = get_repo_or_error(target_repo)
494 target_db_repo = get_repo_or_error(target_repo)
540 if not has_superadmin_permission(apiuser):
495 if not has_superadmin_permission(apiuser):
541 _perms = ('repository.admin', 'repository.write', 'repository.read',)
496 _perms = ('repository.admin', 'repository.write', 'repository.read',)
542 validate_repo_permissions(apiuser, source_repo, source, _perms)
497 validate_repo_permissions(apiuser, source_repo, source_db_repo, _perms)
543
498
544 full_source_ref = resolve_ref_or_error(source_ref, source)
499 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
545 full_target_ref = resolve_ref_or_error(target_ref, target)
500 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
546 source_commit = get_commit_or_error(full_source_ref, source)
501 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
547 target_commit = get_commit_or_error(full_target_ref, target)
502 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
548 source_scm = source.scm_instance()
503 source_scm = source_db_repo.scm_instance()
549 target_scm = target.scm_instance()
504 target_scm = target_db_repo.scm_instance()
550
505
551 commit_ranges = target_scm.compare(
506 commit_ranges = target_scm.compare(
552 target_commit.raw_id, source_commit.raw_id, source_scm,
507 target_commit.raw_id, source_commit.raw_id, source_scm,
@@ -562,20 +517,36 b' def create_pull_request('
562 raise JSONRPCError('no common ancestor found')
517 raise JSONRPCError('no common ancestor found')
563
518
564 reviewer_objects = Optional.extract(reviewers) or []
519 reviewer_objects = Optional.extract(reviewers) or []
565 if not isinstance(reviewer_objects, list):
520
566 raise JSONRPCError('reviewers should be specified as a list')
521 if reviewer_objects:
522 schema = ReviewerListSchema()
523 try:
524 reviewer_objects = schema.deserialize(reviewer_objects)
525 except Invalid as err:
526 raise JSONRPCValidationError(colander_exc=err)
527
528 # validate users
529 for reviewer_object in reviewer_objects:
530 user = get_user_or_error(reviewer_object['username'])
531 reviewer_object['user_id'] = user.user_id
567
532
568 reviewers_reasons = []
533 get_default_reviewers_data, get_validated_reviewers = \
569 for reviewer_object in reviewer_objects:
534 PullRequestModel().get_reviewer_functions()
570 reviewer_reasons = []
535
571 if isinstance(reviewer_object, (basestring, int)):
536 reviewer_rules = get_default_reviewers_data(
572 reviewer_username = reviewer_object
537 apiuser.get_instance(), source_db_repo,
573 else:
538 source_commit, target_db_repo, target_commit)
574 reviewer_username = reviewer_object['username']
575 reviewer_reasons = reviewer_object.get('reasons', [])
576
539
577 user = get_user_or_error(reviewer_username)
540 # specified rules are later re-validated, thus we can assume users will
578 reviewers_reasons.append((user.user_id, reviewer_reasons))
541 # eventually provide those that meet the reviewer criteria.
542 if not reviewer_objects:
543 reviewer_objects = reviewer_rules['reviewers']
544
545 try:
546 reviewers = get_validated_reviewers(
547 reviewer_objects, reviewer_rules)
548 except ValueError as e:
549 raise JSONRPCError('Reviewers Validation: {}'.format(e))
579
550
580 pull_request_model = PullRequestModel()
551 pull_request_model = PullRequestModel()
581 pull_request = pull_request_model.create(
552 pull_request = pull_request_model.create(
@@ -586,7 +557,7 b' def create_pull_request('
586 target_ref=full_target_ref,
557 target_ref=full_target_ref,
587 revisions=reversed(
558 revisions=reversed(
588 [commit.raw_id for commit in reversed(commit_ranges)]),
559 [commit.raw_id for commit in reversed(commit_ranges)]),
589 reviewers=reviewers_reasons,
560 reviewers=reviewers,
590 title=title,
561 title=title,
591 description=Optional.extract(description)
562 description=Optional.extract(description)
592 )
563 )
@@ -603,7 +574,7 b' def create_pull_request('
603 def update_pull_request(
574 def update_pull_request(
604 request, apiuser, repoid, pullrequestid, title=Optional(''),
575 request, apiuser, repoid, pullrequestid, title=Optional(''),
605 description=Optional(''), reviewers=Optional(None),
576 description=Optional(''), reviewers=Optional(None),
606 update_commits=Optional(None), close_pull_request=Optional(None)):
577 update_commits=Optional(None)):
607 """
578 """
608 Updates a pull request.
579 Updates a pull request.
609
580
@@ -619,10 +590,12 b' def update_pull_request('
619 :type description: Optional(str)
590 :type description: Optional(str)
620 :param reviewers: Update pull request reviewers list with new value.
591 :param reviewers: Update pull request reviewers list with new value.
621 :type reviewers: Optional(list)
592 :type reviewers: Optional(list)
593 Accepts username strings or objects of the format:
594
595 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
596
622 :param update_commits: Trigger update of commits for this pull request
597 :param update_commits: Trigger update of commits for this pull request
623 :type: update_commits: Optional(bool)
598 :type: update_commits: Optional(bool)
624 :param close_pull_request: Close this pull request with rejected state
625 :type: close_pull_request: Optional(bool)
626
599
627 Example output:
600 Example output:
628
601
@@ -665,29 +638,38 b' def update_pull_request('
665 pullrequestid,))
638 pullrequestid,))
666
639
667 reviewer_objects = Optional.extract(reviewers) or []
640 reviewer_objects = Optional.extract(reviewers) or []
668 if not isinstance(reviewer_objects, list):
641
669 raise JSONRPCError('reviewers should be specified as a list')
642 if reviewer_objects:
643 schema = ReviewerListSchema()
644 try:
645 reviewer_objects = schema.deserialize(reviewer_objects)
646 except Invalid as err:
647 raise JSONRPCValidationError(colander_exc=err)
648
649 # validate users
650 for reviewer_object in reviewer_objects:
651 user = get_user_or_error(reviewer_object['username'])
652 reviewer_object['user_id'] = user.user_id
670
653
671 reviewers_reasons = []
654 get_default_reviewers_data, get_validated_reviewers = \
672 reviewer_ids = set()
655 PullRequestModel().get_reviewer_functions()
673 for reviewer_object in reviewer_objects:
674 reviewer_reasons = []
675 if isinstance(reviewer_object, (int, basestring)):
676 reviewer_username = reviewer_object
677 else:
678 reviewer_username = reviewer_object['username']
679 reviewer_reasons = reviewer_object.get('reasons', [])
680
656
681 user = get_user_or_error(reviewer_username)
657 # re-use stored rules
682 reviewer_ids.add(user.user_id)
658 reviewer_rules = pull_request.reviewer_data
683 reviewers_reasons.append((user.user_id, reviewer_reasons))
659 try:
660 reviewers = get_validated_reviewers(
661 reviewer_objects, reviewer_rules)
662 except ValueError as e:
663 raise JSONRPCError('Reviewers Validation: {}'.format(e))
664 else:
665 reviewers = []
684
666
685 title = Optional.extract(title)
667 title = Optional.extract(title)
686 description = Optional.extract(description)
668 description = Optional.extract(description)
687 if title or description:
669 if title or description:
688 PullRequestModel().edit(
670 PullRequestModel().edit(
689 pull_request, title or pull_request.title,
671 pull_request, title or pull_request.title,
690 description or pull_request.description)
672 description or pull_request.description, apiuser)
691 Session().commit()
673 Session().commit()
692
674
693 commit_changes = {"added": [], "common": [], "removed": []}
675 commit_changes = {"added": [], "common": [], "removed": []}
@@ -699,9 +681,9 b' def update_pull_request('
699 Session().commit()
681 Session().commit()
700
682
701 reviewers_changes = {"added": [], "removed": []}
683 reviewers_changes = {"added": [], "removed": []}
702 if reviewer_ids:
684 if reviewers:
703 added_reviewers, removed_reviewers = \
685 added_reviewers, removed_reviewers = \
704 PullRequestModel().update_reviewers(pull_request, reviewers_reasons)
686 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
705
687
706 reviewers_changes['added'] = sorted(
688 reviewers_changes['added'] = sorted(
707 [get_user_or_error(n).username for n in added_reviewers])
689 [get_user_or_error(n).username for n in added_reviewers])
@@ -709,11 +691,6 b' def update_pull_request('
709 [get_user_or_error(n).username for n in removed_reviewers])
691 [get_user_or_error(n).username for n in removed_reviewers])
710 Session().commit()
692 Session().commit()
711
693
712 if str2bool(Optional.extract(close_pull_request)):
713 PullRequestModel().close_pull_request_with_comment(
714 pull_request, apiuser, repo)
715 Session().commit()
716
717 data = {
694 data = {
718 'msg': 'Updated pull request `{}`'.format(
695 'msg': 'Updated pull request `{}`'.format(
719 pull_request.pull_request_id),
696 pull_request.pull_request_id),
@@ -723,3 +700,80 b' def update_pull_request('
723 }
700 }
724
701
725 return data
702 return data
703
704
705 @jsonrpc_method()
706 def close_pull_request(
707 request, apiuser, repoid, pullrequestid,
708 userid=Optional(OAttr('apiuser')), message=Optional('')):
709 """
710 Close the pull request specified by `pullrequestid`.
711
712 :param apiuser: This is filled automatically from the |authtoken|.
713 :type apiuser: AuthUser
714 :param repoid: Repository name or repository ID to which the pull
715 request belongs.
716 :type repoid: str or int
717 :param pullrequestid: ID of the pull request to be closed.
718 :type pullrequestid: int
719 :param userid: Close the pull request as this user.
720 :type userid: Optional(str or int)
721 :param message: Optional message to close the Pull Request with. If not
722 specified it will be generated automatically.
723 :type message: Optional(str)
724
725 Example output:
726
727 .. code-block:: bash
728
729 "id": <id_given_in_input>,
730 "result": {
731 "pull_request_id": "<int>",
732 "close_status": "<str:status_lbl>,
733 "closed": "<bool>"
734 },
735 "error": null
736
737 """
738 _ = request.translate
739
740 repo = get_repo_or_error(repoid)
741 if not isinstance(userid, Optional):
742 if (has_superadmin_permission(apiuser) or
743 HasRepoPermissionAnyApi('repository.admin')(
744 user=apiuser, repo_name=repo.repo_name)):
745 apiuser = get_user_or_error(userid)
746 else:
747 raise JSONRPCError('userid is not the same as your user')
748
749 pull_request = get_pull_request_or_error(pullrequestid)
750
751 if pull_request.is_closed():
752 raise JSONRPCError(
753 'pull request `%s` is already closed' % (pullrequestid,))
754
755 # only owner or admin or person with write permissions
756 allowed_to_close = PullRequestModel().check_user_update(
757 pull_request, apiuser, api=True)
758
759 if not allowed_to_close:
760 raise JSONRPCError(
761 'pull request `%s` close failed, no permission to close.' % (
762 pullrequestid,))
763
764 # message we're using to close the PR, else it's automatically generated
765 message = Optional.extract(message)
766
767 # finally close the PR, with proper message comment
768 comment, status = PullRequestModel().close_pull_request_with_comment(
769 pull_request, apiuser, repo, message=message)
770 status_lbl = ChangesetStatus.get_status_lbl(status)
771
772 Session().commit()
773
774 data = {
775 'pull_request_id': pull_request.pull_request_id,
776 'close_status': status_lbl,
777 'closed': True,
778 }
779 return data
@@ -29,6 +29,8 b' from rhodecode.api.utils import ('
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 validate_set_owner_permissions)
31 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger
33 from rhodecode.lib import repo_maintenance
32 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
33 from rhodecode.lib.utils2 import str2bool, time_to_datetime
35 from rhodecode.lib.utils2 import str2bool, time_to_datetime
34 from rhodecode.lib.ext_json import json
36 from rhodecode.lib.ext_json import json
@@ -915,12 +917,13 b' def update_repo('
915
917
916 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
918 ref_choices, _labels = ScmModel().get_repo_landing_revs(repo=repo)
917
919
920 old_values = repo.get_api_data()
918 schema = repo_schema.RepoSchema().bind(
921 schema = repo_schema.RepoSchema().bind(
919 repo_type_options=rhodecode.BACKENDS.keys(),
922 repo_type_options=rhodecode.BACKENDS.keys(),
920 repo_ref_options=ref_choices,
923 repo_ref_options=ref_choices,
921 # user caller
924 # user caller
922 user=apiuser,
925 user=apiuser,
923 old_values=repo.get_api_data())
926 old_values=old_values)
924 try:
927 try:
925 schema_data = schema.deserialize(dict(
928 schema_data = schema.deserialize(dict(
926 # we save old value, users cannot change type
929 # we save old value, users cannot change type
@@ -965,6 +968,9 b' def update_repo('
965
968
966 try:
969 try:
967 RepoModel().update(repo, **validated_updates)
970 RepoModel().update(repo, **validated_updates)
971 audit_logger.store_api(
972 'repo.edit', action_data={'old_data': old_values},
973 user=apiuser, repo=repo)
968 Session().commit()
974 Session().commit()
969 return {
975 return {
970 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
976 'msg': 'updated repo ID:%s %s' % (repo.repo_id, repo.repo_name),
@@ -1153,6 +1159,7 b' def delete_repo(request, apiuser, repoid'
1153 """
1159 """
1154
1160
1155 repo = get_repo_or_error(repoid)
1161 repo = get_repo_or_error(repoid)
1162 repo_name = repo.repo_name
1156 if not has_superadmin_permission(apiuser):
1163 if not has_superadmin_permission(apiuser):
1157 _perms = ('repository.admin',)
1164 _perms = ('repository.admin',)
1158 validate_repo_permissions(apiuser, repoid, repo, _perms)
1165 validate_repo_permissions(apiuser, repoid, repo, _perms)
@@ -1170,18 +1177,26 b' def delete_repo(request, apiuser, repoid'
1170 'Cannot delete `%s` it still contains attached forks' %
1177 'Cannot delete `%s` it still contains attached forks' %
1171 (repo.repo_name,)
1178 (repo.repo_name,)
1172 )
1179 )
1180 old_data = repo.get_api_data()
1181 RepoModel().delete(repo, forks=forks)
1173
1182
1174 RepoModel().delete(repo, forks=forks)
1183 repo = audit_logger.RepoWrap(repo_id=None,
1184 repo_name=repo.repo_name)
1185
1186 audit_logger.store_api(
1187 'repo.delete', action_data={'old_data': old_data},
1188 user=apiuser, repo=repo)
1189
1190 ScmModel().mark_for_invalidation(repo_name, delete=True)
1175 Session().commit()
1191 Session().commit()
1176 return {
1192 return {
1177 'msg': 'Deleted repository `%s`%s' % (
1193 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg),
1178 repo.repo_name, _forks_msg),
1179 'success': True
1194 'success': True
1180 }
1195 }
1181 except Exception:
1196 except Exception:
1182 log.exception("Exception occurred while trying to delete repo")
1197 log.exception("Exception occurred while trying to delete repo")
1183 raise JSONRPCError(
1198 raise JSONRPCError(
1184 'failed to delete repository `%s`' % (repo.repo_name,)
1199 'failed to delete repository `%s`' % (repo_name,)
1185 )
1200 )
1186
1201
1187
1202
@@ -1460,7 +1475,7 b' def comment_commit('
1460 rc_config = SettingsModel().get_all_settings()
1475 rc_config = SettingsModel().get_all_settings()
1461 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1476 renderer = rc_config.get('rhodecode_markup_renderer', 'rst')
1462 status_change_label = ChangesetStatus.get_status_lbl(status)
1477 status_change_label = ChangesetStatus.get_status_lbl(status)
1463 comm = CommentsModel().create(
1478 comment = CommentsModel().create(
1464 message, repo, user, commit_id=commit_id,
1479 message, repo, user, commit_id=commit_id,
1465 status_change=status_change_label,
1480 status_change=status_change_label,
1466 status_change_type=status,
1481 status_change_type=status,
@@ -1472,7 +1487,7 b' def comment_commit('
1472 # also do a status change
1487 # also do a status change
1473 try:
1488 try:
1474 ChangesetStatusModel().set_status(
1489 ChangesetStatusModel().set_status(
1475 repo, status, user, comm, revision=commit_id,
1490 repo, status, user, comment, revision=commit_id,
1476 dont_allow_on_closed_pull_request=True
1491 dont_allow_on_closed_pull_request=True
1477 )
1492 )
1478 except StatusChangeOnClosedPullRequestError:
1493 except StatusChangeOnClosedPullRequestError:
@@ -1486,7 +1501,7 b' def comment_commit('
1486 return {
1501 return {
1487 'msg': (
1502 'msg': (
1488 'Commented on commit `%s` for repository `%s`' % (
1503 'Commented on commit `%s` for repository `%s`' % (
1489 comm.revision, repo.repo_name)),
1504 comment.revision, repo.repo_name)),
1490 'status_change': status,
1505 'status_change': status,
1491 'success': True,
1506 'success': True,
1492 }
1507 }
@@ -1867,6 +1882,11 b' def strip(request, apiuser, repoid, revi'
1867
1882
1868 try:
1883 try:
1869 ScmModel().strip(repo, revision, branch)
1884 ScmModel().strip(repo, revision, branch)
1885 audit_logger.store_api(
1886 'repo.commit.strip', action_data={'commit_id': revision},
1887 repo=repo,
1888 user=apiuser, commit=True)
1889
1870 return {
1890 return {
1871 'msg': 'Stripped commit %s from repo `%s`' % (
1891 'msg': 'Stripped commit %s from repo `%s`' % (
1872 revision, repo.repo_name),
1892 revision, repo.repo_name),
@@ -1902,6 +1922,7 b' def get_repo_settings(request, apiuser, '
1902 "id": 237,
1922 "id": 237,
1903 "result": {
1923 "result": {
1904 "extensions_largefiles": true,
1924 "extensions_largefiles": true,
1925 "extensions_evolve": true,
1905 "hooks_changegroup_push_logger": true,
1926 "hooks_changegroup_push_logger": true,
1906 "hooks_changegroup_repo_size": false,
1927 "hooks_changegroup_repo_size": false,
1907 "hooks_outgoing_pull_logger": true,
1928 "hooks_outgoing_pull_logger": true,
@@ -1985,3 +2006,65 b' def set_repo_settings(request, apiuser, '
1985
2006
1986 # Indicate success.
2007 # Indicate success.
1987 return True
2008 return True
2009
2010
2011 @jsonrpc_method()
2012 def maintenance(request, apiuser, repoid):
2013 """
2014 Triggers a maintenance on the given repository.
2015
2016 This command can only be run using an |authtoken| with admin
2017 rights to the specified repository. For more information,
2018 see :ref:`config-token-ref`.
2019
2020 This command takes the following options:
2021
2022 :param apiuser: This is filled automatically from the |authtoken|.
2023 :type apiuser: AuthUser
2024 :param repoid: The repository name or repository ID.
2025 :type repoid: str or int
2026
2027 Example output:
2028
2029 .. code-block:: bash
2030
2031 id : <id_given_in_input>
2032 result : {
2033 "msg": "executed maintenance command",
2034 "executed_actions": [
2035 <action_message>, <action_message2>...
2036 ],
2037 "repository": "<repository name>"
2038 }
2039 error : null
2040
2041 Example error output:
2042
2043 .. code-block:: bash
2044
2045 id : <id_given_in_input>
2046 result : null
2047 error : {
2048 "Unable to execute maintenance on `<reponame>`"
2049 }
2050
2051 """
2052
2053 repo = get_repo_or_error(repoid)
2054 if not has_superadmin_permission(apiuser):
2055 _perms = ('repository.admin',)
2056 validate_repo_permissions(apiuser, repoid, repo, _perms)
2057
2058 try:
2059 maintenance = repo_maintenance.RepoMaintenance()
2060 executed_actions = maintenance.execute(repo)
2061
2062 return {
2063 'msg': 'executed maintenance command',
2064 'executed_actions': executed_actions,
2065 'repository': repo.repo_name
2066 }
2067 except Exception:
2068 log.exception("Exception occurred while trying to run maintenance")
2069 raise JSONRPCError(
2070 'Unable to execute maintenance on `%s`' % repo.repo_name)
@@ -27,6 +27,7 b' from rhodecode.api.utils import ('
27 has_superadmin_permission, Optional, OAttr, get_user_or_error,
27 has_superadmin_permission, Optional, OAttr, get_user_or_error,
28 get_repo_group_or_error, get_perm_or_error, get_user_group_or_error,
28 get_repo_group_or_error, get_perm_or_error, get_user_group_or_error,
29 get_origin, validate_repo_group_permissions, validate_set_owner_permissions)
29 get_origin, validate_repo_group_permissions, validate_set_owner_permissions)
30 from rhodecode.lib import audit_logger
30 from rhodecode.lib.auth import (
31 from rhodecode.lib.auth import (
31 HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAnyApi)
32 HasRepoGroupPermissionAnyApi, HasUserGroupPermissionAnyApi)
32 from rhodecode.model.db import Session
33 from rhodecode.model.db import Session
@@ -222,6 +223,13 b' def create_repo_group('
222 group_name=validated_group_name,
223 group_name=validated_group_name,
223 group_description=schema_data['repo_group_name'],
224 group_description=schema_data['repo_group_name'],
224 copy_permissions=schema_data['repo_group_copy_permissions'])
225 copy_permissions=schema_data['repo_group_copy_permissions'])
226 Session().flush()
227
228 repo_group_data = repo_group.get_api_data()
229 audit_logger.store_api(
230 'repo_group.create', action_data={'data': repo_group_data},
231 user=apiuser)
232
225 Session().commit()
233 Session().commit()
226 return {
234 return {
227 'msg': 'Created new repo group `%s`' % validated_group_name,
235 'msg': 'Created new repo group `%s`' % validated_group_name,
@@ -310,8 +318,13 b' def update_repo_group('
310 enable_locking=schema_data['repo_group_enable_locking'],
318 enable_locking=schema_data['repo_group_enable_locking'],
311 )
319 )
312
320
321 old_data = repo_group.get_api_data()
313 try:
322 try:
314 RepoGroupModel().update(repo_group, validated_updates)
323 RepoGroupModel().update(repo_group, validated_updates)
324 audit_logger.store_api(
325 'repo_group.edit', action_data={'old_data': old_data},
326 user=apiuser)
327
315 Session().commit()
328 Session().commit()
316 return {
329 return {
317 'msg': 'updated repository group ID:%s %s' % (
330 'msg': 'updated repository group ID:%s %s' % (
@@ -365,8 +378,12 b' def delete_repo_group(request, apiuser, '
365 validate_repo_group_permissions(
378 validate_repo_group_permissions(
366 apiuser, repogroupid, repo_group, ('group.admin',))
379 apiuser, repogroupid, repo_group, ('group.admin',))
367
380
381 old_data = repo_group.get_api_data()
368 try:
382 try:
369 RepoGroupModel().delete(repo_group)
383 RepoGroupModel().delete(repo_group)
384 audit_logger.store_api(
385 'repo_group.delete', action_data={'old_data': old_data},
386 user=apiuser)
370 Session().commit()
387 Session().commit()
371 return {
388 return {
372 'msg': 'deleted repo group ID:%s %s' %
389 'msg': 'deleted repo group ID:%s %s' %
@@ -20,14 +20,18 b''
20
20
21 import logging
21 import logging
22
22
23 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
23 from rhodecode.api import (
24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
24 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
25 Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update)
26 Optional, OAttr, has_superadmin_permission, get_user_or_error, store_update)
27 from rhodecode.lib import audit_logger
26 from rhodecode.lib.auth import AuthUser, PasswordGenerator
28 from rhodecode.lib.auth import AuthUser, PasswordGenerator
27 from rhodecode.lib.exceptions import DefaultUserException
29 from rhodecode.lib.exceptions import DefaultUserException
28 from rhodecode.lib.utils2 import safe_int, str2bool
30 from rhodecode.lib.utils2 import safe_int, str2bool
29 from rhodecode.model.db import Session, User, Repository
31 from rhodecode.model.db import Session, User, Repository
30 from rhodecode.model.user import UserModel
32 from rhodecode.model.user import UserModel
33 from rhodecode.model import validation_schema
34 from rhodecode.model.validation_schema.schemas import user_schema
31
35
32 log = logging.getLogger(__name__)
36 log = logging.getLogger(__name__)
33
37
@@ -237,20 +241,54 b' def create_user(request, apiuser, userna'
237 if isinstance(create_repo_group, basestring):
241 if isinstance(create_repo_group, basestring):
238 create_repo_group = str2bool(create_repo_group)
242 create_repo_group = str2bool(create_repo_group)
239
243
244 username = Optional.extract(username)
245 password = Optional.extract(password)
246 email = Optional.extract(email)
247 first_name = Optional.extract(firstname)
248 last_name = Optional.extract(lastname)
249 active = Optional.extract(active)
250 admin = Optional.extract(admin)
251 extern_type = Optional.extract(extern_type)
252 extern_name = Optional.extract(extern_name)
253
254 schema = user_schema.UserSchema().bind(
255 # user caller
256 user=apiuser)
257 try:
258 schema_data = schema.deserialize(dict(
259 username=username,
260 email=email,
261 password=password,
262 first_name=first_name,
263 last_name=last_name,
264 active=active,
265 admin=admin,
266 extern_type=extern_type,
267 extern_name=extern_name,
268 ))
269 except validation_schema.Invalid as err:
270 raise JSONRPCValidationError(colander_exc=err)
271
240 try:
272 try:
241 user = UserModel().create_or_update(
273 user = UserModel().create_or_update(
242 username=Optional.extract(username),
274 username=schema_data['username'],
243 password=Optional.extract(password),
275 password=schema_data['password'],
244 email=Optional.extract(email),
276 email=schema_data['email'],
245 firstname=Optional.extract(firstname),
277 firstname=schema_data['first_name'],
246 lastname=Optional.extract(lastname),
278 lastname=schema_data['last_name'],
247 active=Optional.extract(active),
279 active=schema_data['active'],
248 admin=Optional.extract(admin),
280 admin=schema_data['admin'],
249 extern_type=Optional.extract(extern_type),
281 extern_type=schema_data['extern_type'],
250 extern_name=Optional.extract(extern_name),
282 extern_name=schema_data['extern_name'],
251 force_password_change=Optional.extract(force_password_change),
283 force_password_change=Optional.extract(force_password_change),
252 create_repo_group=create_repo_group
284 create_repo_group=create_repo_group
253 )
285 )
286 Session().flush()
287 creation_data = user.get_api_data()
288 audit_logger.store_api(
289 'user.create', action_data={'data': creation_data},
290 user=apiuser)
291
254 Session().commit()
292 Session().commit()
255 return {
293 return {
256 'msg': 'created new user `%s`' % username,
294 'msg': 'created new user `%s`' % username,
@@ -326,7 +364,7 b' def update_user(request, apiuser, userid'
326 raise JSONRPCForbidden()
364 raise JSONRPCForbidden()
327
365
328 user = get_user_or_error(userid)
366 user = get_user_or_error(userid)
329
367 old_data = user.get_api_data()
330 # only non optional arguments will be stored in updates
368 # only non optional arguments will be stored in updates
331 updates = {}
369 updates = {}
332
370
@@ -343,6 +381,9 b' def update_user(request, apiuser, userid'
343 store_update(updates, extern_type, 'extern_type')
381 store_update(updates, extern_type, 'extern_type')
344
382
345 user = UserModel().update_user(user, **updates)
383 user = UserModel().update_user(user, **updates)
384 audit_logger.store_api(
385 'user.edit', action_data={'old_data': old_data},
386 user=apiuser)
346 Session().commit()
387 Session().commit()
347 return {
388 return {
348 'msg': 'updated user ID:%s %s' % (user.user_id, user.username),
389 'msg': 'updated user ID:%s %s' % (user.user_id, user.username),
@@ -405,9 +446,13 b' def delete_user(request, apiuser, userid'
405 raise JSONRPCForbidden()
446 raise JSONRPCForbidden()
406
447
407 user = get_user_or_error(userid)
448 user = get_user_or_error(userid)
408
449 old_data = user.get_api_data()
409 try:
450 try:
410 UserModel().delete(userid)
451 UserModel().delete(userid)
452 audit_logger.store_api(
453 'user.delete', action_data={'old_data': old_data},
454 user=apiuser)
455
411 Session().commit()
456 Session().commit()
412 return {
457 return {
413 'msg': 'deleted user ID:%s %s' % (user.user_id, user.username),
458 'msg': 'deleted user ID:%s %s' % (user.user_id, user.username),
@@ -20,15 +20,19 b''
20
20
21 import logging
21 import logging
22
22
23 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCForbidden
23 from rhodecode.api import (
24 jsonrpc_method, JSONRPCError, JSONRPCForbidden, JSONRPCValidationError)
24 from rhodecode.api.utils import (
25 from rhodecode.api.utils import (
25 Optional, OAttr, store_update, has_superadmin_permission, get_origin,
26 Optional, OAttr, store_update, has_superadmin_permission, get_origin,
26 get_user_or_error, get_user_group_or_error, get_perm_or_error)
27 get_user_or_error, get_user_group_or_error, get_perm_or_error)
28 from rhodecode.lib import audit_logger
27 from rhodecode.lib.auth import HasUserGroupPermissionAnyApi, HasPermissionAnyApi
29 from rhodecode.lib.auth import HasUserGroupPermissionAnyApi, HasPermissionAnyApi
28 from rhodecode.lib.exceptions import UserGroupAssignedException
30 from rhodecode.lib.exceptions import UserGroupAssignedException
29 from rhodecode.model.db import Session
31 from rhodecode.model.db import Session
30 from rhodecode.model.scm import UserGroupList
32 from rhodecode.model.scm import UserGroupList
31 from rhodecode.model.user_group import UserGroupModel
33 from rhodecode.model.user_group import UserGroupModel
34 from rhodecode.model import validation_schema
35 from rhodecode.model.validation_schema.schemas import user_group_schema
32
36
33 log = logging.getLogger(__name__)
37 log = logging.getLogger(__name__)
34
38
@@ -210,20 +214,41 b' def create_user_group('
210 if UserGroupModel().get_by_name(group_name):
214 if UserGroupModel().get_by_name(group_name):
211 raise JSONRPCError("user group `%s` already exist" % (group_name,))
215 raise JSONRPCError("user group `%s` already exist" % (group_name,))
212
216
213 try:
217 if isinstance(owner, Optional):
214 if isinstance(owner, Optional):
218 owner = apiuser.user_id
215 owner = apiuser.user_id
219
220 owner = get_user_or_error(owner)
221 active = Optional.extract(active)
222 description = Optional.extract(description)
216
223
217 owner = get_user_or_error(owner)
224 schema = user_group_schema.UserGroupSchema().bind(
218 active = Optional.extract(active)
225 # user caller
219 description = Optional.extract(description)
226 user=apiuser)
220 ug = UserGroupModel().create(
227 try:
221 name=group_name, description=description, owner=owner,
228 schema_data = schema.deserialize(dict(
222 active=active)
229 user_group_name=group_name,
230 user_group_description=description,
231 user_group_owner=owner.username,
232 user_group_active=active,
233 ))
234 except validation_schema.Invalid as err:
235 raise JSONRPCValidationError(colander_exc=err)
236
237 try:
238 user_group = UserGroupModel().create(
239 name=schema_data['user_group_name'],
240 description=schema_data['user_group_description'],
241 owner=owner,
242 active=schema_data['user_group_active'])
243 Session().flush()
244 creation_data = user_group.get_api_data()
245 audit_logger.store_api(
246 'user_group.create', action_data={'data': creation_data},
247 user=apiuser)
223 Session().commit()
248 Session().commit()
224 return {
249 return {
225 'msg': 'created new user group `%s`' % group_name,
250 'msg': 'created new user group `%s`' % group_name,
226 'user_group': ug.get_api_data()
251 'user_group': creation_data
227 }
252 }
228 except Exception:
253 except Exception:
229 log.exception("Error occurred during creation of user group")
254 log.exception("Error occurred during creation of user group")
@@ -291,6 +316,7 b' def update_user_group(request, apiuser, '
291 if not isinstance(owner, Optional):
316 if not isinstance(owner, Optional):
292 owner = get_user_or_error(owner)
317 owner = get_user_or_error(owner)
293
318
319 old_data = user_group.get_api_data()
294 updates = {}
320 updates = {}
295 store_update(updates, group_name, 'users_group_name')
321 store_update(updates, group_name, 'users_group_name')
296 store_update(updates, description, 'user_group_description')
322 store_update(updates, description, 'user_group_description')
@@ -298,6 +324,9 b' def update_user_group(request, apiuser, '
298 store_update(updates, active, 'users_group_active')
324 store_update(updates, active, 'users_group_active')
299 try:
325 try:
300 UserGroupModel().update(user_group, updates)
326 UserGroupModel().update(user_group, updates)
327 audit_logger.store_api(
328 'user_group.edit', action_data={'old_data': old_data},
329 user=apiuser)
301 Session().commit()
330 Session().commit()
302 return {
331 return {
303 'msg': 'updated user group ID:%s %s' % (
332 'msg': 'updated user group ID:%s %s' % (
@@ -359,8 +388,12 b' def delete_user_group(request, apiuser, '
359 raise JSONRPCError(
388 raise JSONRPCError(
360 'user group `%s` does not exist' % (usergroupid,))
389 'user group `%s` does not exist' % (usergroupid,))
361
390
391 old_data = user_group.get_api_data()
362 try:
392 try:
363 UserGroupModel().delete(user_group)
393 UserGroupModel().delete(user_group)
394 audit_logger.store_api(
395 'user_group.delete', action_data={'old_data': old_data},
396 user=apiuser)
364 Session().commit()
397 Session().commit()
365 return {
398 return {
366 'msg': 'deleted user group ID:%s %s' % (
399 'msg': 'deleted user group ID:%s %s' % (
@@ -438,6 +471,12 b' def add_user_to_user_group(request, apiu'
438 user.username, user_group.users_group_name
471 user.username, user_group.users_group_name
439 )
472 )
440 msg = msg if success else 'User is already in that group'
473 msg = msg if success else 'User is already in that group'
474 if success:
475 user_data = user.get_api_data()
476 audit_logger.store_api(
477 'user_group.edit.member.add', action_data={'user': user_data},
478 user=apiuser)
479
441 Session().commit()
480 Session().commit()
442
481
443 return {
482 return {
@@ -501,6 +540,12 b' def remove_user_from_user_group(request,'
501 user.username, user_group.users_group_name
540 user.username, user_group.users_group_name
502 )
541 )
503 msg = msg if success else "User wasn't in group"
542 msg = msg if success else "User wasn't in group"
543 if success:
544 user_data = user.get_api_data()
545 audit_logger.store_api(
546 'user_group.edit.member.delete', action_data={'user': user_data},
547 user=apiuser)
548
504 Session().commit()
549 Session().commit()
505 return {'success': success, 'msg': msg}
550 return {'success': success, 'msg': msg}
506 except Exception:
551 except Exception:
@@ -0,0 +1,19 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2017 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
@@ -24,8 +24,12 b' from pylons import tmpl_context as c'
24 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
25
25
26 from rhodecode.lib import helpers as h
26 from rhodecode.lib import helpers as h
27 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int
27 from rhodecode.lib.utils import PartialRenderer
28 from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time
29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 from rhodecode.lib.ext_json import json
28 from rhodecode.model import repo
31 from rhodecode.model import repo
32 from rhodecode.model import repo_group
29 from rhodecode.model.db import User
33 from rhodecode.model.db import User
30 from rhodecode.model.scm import ScmModel
34 from rhodecode.model.scm import ScmModel
31
35
@@ -36,6 +40,30 b" ADMIN_PREFIX = '/_admin'"
36 STATIC_FILE_PREFIX = '/_static'
40 STATIC_FILE_PREFIX = '/_static'
37
41
38
42
43 def add_route_with_slash(config,name, pattern, **kw):
44 config.add_route(name, pattern, **kw)
45 if not pattern.endswith('/'):
46 config.add_route(name + '_slash', pattern + '/', **kw)
47
48
49 def get_format_ref_id(repo):
50 """Returns a `repo` specific reference formatter function"""
51 if h.is_svn(repo):
52 return _format_ref_id_svn
53 else:
54 return _format_ref_id
55
56
57 def _format_ref_id(name, raw_id):
58 """Default formatting of a given reference `name`"""
59 return name
60
61
62 def _format_ref_id_svn(name, raw_id):
63 """Special way of formatting a reference for Subversion including path"""
64 return '%s@%s' % (name, raw_id)
65
66
39 class TemplateArgs(StrictAttributeDict):
67 class TemplateArgs(StrictAttributeDict):
40 pass
68 pass
41
69
@@ -77,9 +105,14 b' class BaseAppView(object):'
77 raise HTTPFound(
105 raise HTTPFound(
78 self.request.route_path('my_account_password'))
106 self.request.route_path('my_account_password'))
79
107
80 def _get_local_tmpl_context(self):
108 def _get_local_tmpl_context(self, include_app_defaults=False):
81 c = TemplateArgs()
109 c = TemplateArgs()
82 c.auth_user = self.request.user
110 c.auth_user = self.request.user
111 if include_app_defaults:
112 # NOTE(marcink): after full pyramid migration include_app_defaults
113 # should be turned on by default
114 from rhodecode.lib.base import attach_context_attributes
115 attach_context_attributes(c, self.request, self.request.user.user_id)
83 return c
116 return c
84
117
85 def _register_global_c(self, tmpl_args):
118 def _register_global_c(self, tmpl_args):
@@ -121,15 +154,106 b' class RepoAppView(BaseAppView):'
121 self.db_repo_name = self.db_repo.repo_name
154 self.db_repo_name = self.db_repo.repo_name
122 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
155 self.db_repo_pull_requests = ScmModel().get_pull_requests(self.db_repo)
123
156
124 def _get_local_tmpl_context(self):
157 def _handle_missing_requirements(self, error):
125 c = super(RepoAppView, self)._get_local_tmpl_context()
158 log.error(
159 'Requirements are missing for repository %s: %s',
160 self.db_repo_name, error.message)
161
162 def _get_local_tmpl_context(self, include_app_defaults=False):
163 c = super(RepoAppView, self)._get_local_tmpl_context(
164 include_app_defaults=include_app_defaults)
165
126 # register common vars for this type of view
166 # register common vars for this type of view
127 c.rhodecode_db_repo = self.db_repo
167 c.rhodecode_db_repo = self.db_repo
128 c.repo_name = self.db_repo_name
168 c.repo_name = self.db_repo_name
129 c.repository_pull_requests = self.db_repo_pull_requests
169 c.repository_pull_requests = self.db_repo_pull_requests
170
171 c.repository_requirements_missing = False
172 try:
173 self.rhodecode_vcs_repo = self.db_repo.scm_instance()
174 except RepositoryRequirementError as e:
175 c.repository_requirements_missing = True
176 self._handle_missing_requirements(e)
177
130 return c
178 return c
131
179
132
180
181 class DataGridAppView(object):
182 """
183 Common class to have re-usable grid rendering components
184 """
185
186 def _extract_ordering(self, request, column_map=None):
187 column_map = column_map or {}
188 column_index = safe_int(request.GET.get('order[0][column]'))
189 order_dir = request.GET.get(
190 'order[0][dir]', 'desc')
191 order_by = request.GET.get(
192 'columns[%s][data][sort]' % column_index, 'name_raw')
193
194 # translate datatable to DB columns
195 order_by = column_map.get(order_by) or order_by
196
197 search_q = request.GET.get('search[value]')
198 return search_q, order_by, order_dir
199
200 def _extract_chunk(self, request):
201 start = safe_int(request.GET.get('start'), 0)
202 length = safe_int(request.GET.get('length'), 25)
203 draw = safe_int(request.GET.get('draw'))
204 return draw, start, length
205
206
207 class BaseReferencesView(RepoAppView):
208 """
209 Base for reference view for branches, tags and bookmarks.
210 """
211 def load_default_context(self):
212 c = self._get_local_tmpl_context()
213
214 # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead
215 c.repo_info = self.db_repo
216
217 self._register_global_c(c)
218 return c
219
220 def load_refs_context(self, ref_items, partials_template):
221 _render = PartialRenderer(partials_template)
222 _data = []
223 pre_load = ["author", "date", "message"]
224
225 is_svn = h.is_svn(self.rhodecode_vcs_repo)
226 format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
227
228 for ref_name, commit_id in ref_items:
229 commit = self.rhodecode_vcs_repo.get_commit(
230 commit_id=commit_id, pre_load=pre_load)
231
232 # TODO: johbo: Unify generation of reference links
233 use_commit_id = '/' in ref_name or is_svn
234 files_url = h.url(
235 'files_home',
236 repo_name=c.repo_name,
237 f_path=ref_name if is_svn else '',
238 revision=commit_id if use_commit_id else ref_name,
239 at=ref_name)
240
241 _data.append({
242 "name": _render('name', ref_name, files_url),
243 "name_raw": ref_name,
244 "date": _render('date', commit.date),
245 "date_raw": datetime_to_time(commit.date),
246 "author": _render('author', commit.author),
247 "commit": _render(
248 'commit', commit.message, commit.raw_id, commit.idx),
249 "commit_raw": commit.idx,
250 "compare": _render(
251 'compare', format_ref_id(ref_name, commit.raw_id)),
252 })
253 c.has_references = bool(_data)
254 c.data = json.dumps(_data)
255
256
133 class RepoRoutePredicate(object):
257 class RepoRoutePredicate(object):
134 def __init__(self, val, config):
258 def __init__(self, val, config):
135 self.val = val
259 self.val = val
@@ -140,11 +264,15 b' class RepoRoutePredicate(object):'
140 phash = text
264 phash = text
141
265
142 def __call__(self, info, request):
266 def __call__(self, info, request):
267
268 if hasattr(request, 'vcs_call'):
269 # skip vcs calls
270 return
271
143 repo_name = info['match']['repo_name']
272 repo_name = info['match']['repo_name']
144 repo_model = repo.RepoModel()
273 repo_model = repo.RepoModel()
145 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
274 by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
146 # if we match quickly from database, short circuit the operation,
275
147 # and validate repo based on the type.
148 if by_name_match:
276 if by_name_match:
149 # register this as request object we can re-use later
277 # register this as request object we can re-use later
150 request.db_repo = by_name_match
278 request.db_repo = by_name_match
@@ -158,6 +286,72 b' class RepoRoutePredicate(object):'
158 return False
286 return False
159
287
160
288
289 class RepoTypeRoutePredicate(object):
290 def __init__(self, val, config):
291 self.val = val or ['hg', 'git', 'svn']
292
293 def text(self):
294 return 'repo_accepted_type = %s' % self.val
295
296 phash = text
297
298 def __call__(self, info, request):
299 if hasattr(request, 'vcs_call'):
300 # skip vcs calls
301 return
302
303 rhodecode_db_repo = request.db_repo
304
305 log.debug(
306 '%s checking repo type for %s in %s',
307 self.__class__.__name__, rhodecode_db_repo.repo_type, self.val)
308
309 if rhodecode_db_repo.repo_type in self.val:
310 return True
311 else:
312 log.warning('Current view is not supported for repo type:%s',
313 rhodecode_db_repo.repo_type)
314 #
315 # h.flash(h.literal(
316 # _('Action not supported for %s.' % rhodecode_repo.alias)),
317 # category='warning')
318 # return redirect(
319 # route_path('repo_summary', repo_name=cls.rhodecode_db_repo.repo_name))
320
321 return False
322
323
324 class RepoGroupRoutePredicate(object):
325 def __init__(self, val, config):
326 self.val = val
327
328 def text(self):
329 return 'repo_group_route = %s' % self.val
330
331 phash = text
332
333 def __call__(self, info, request):
334 if hasattr(request, 'vcs_call'):
335 # skip vcs calls
336 return
337
338 repo_group_name = info['match']['repo_group_name']
339 repo_group_model = repo_group.RepoGroupModel()
340 by_name_match = repo_group_model.get_by_group_name(
341 repo_group_name, cache=True)
342
343 if by_name_match:
344 # register this as request object we can re-use later
345 request.db_repo_group = by_name_match
346 return True
347
348 return False
349
350
161 def includeme(config):
351 def includeme(config):
162 config.add_route_predicate(
352 config.add_route_predicate(
163 'repo_route', RepoRoutePredicate)
353 'repo_route', RepoRoutePredicate)
354 config.add_route_predicate(
355 'repo_accepted_types', RepoTypeRoutePredicate)
356 config.add_route_predicate(
357 'repo_group_route', RepoGroupRoutePredicate)
@@ -30,6 +30,20 b' def admin_routes(config):'
30 """
30 """
31
31
32 config.add_route(
32 config.add_route(
33 name='admin_audit_logs',
34 pattern='/audit_logs')
35
36 config.add_route(
37 name='pull_requests_global_0', # backward compat
38 pattern='/pull_requests/{pull_request_id:[0-9]+}')
39 config.add_route(
40 name='pull_requests_global_1', # backward compat
41 pattern='/pull-requests/{pull_request_id:[0-9]+}')
42 config.add_route(
43 name='pull_requests_global',
44 pattern='/pull-request/{pull_request_id:[0-9]+}')
45
46 config.add_route(
33 name='admin_settings_open_source',
47 name='admin_settings_open_source',
34 pattern='/settings/open_source')
48 pattern='/settings/open_source')
35 config.add_route(
49 config.add_route(
@@ -50,6 +64,11 b' def admin_routes(config):'
50 name='admin_settings_sessions_cleanup',
64 name='admin_settings_sessions_cleanup',
51 pattern='/settings/sessions/cleanup')
65 pattern='/settings/sessions/cleanup')
52
66
67 # global permissions
68 config.add_route(
69 name='admin_permissions_ips',
70 pattern='/permissions/ips')
71
53 # users admin
72 # users admin
54 config.add_route(
73 config.add_route(
55 name='users',
74 name='users',
@@ -70,6 +89,28 b' def admin_routes(config):'
70 name='edit_user_auth_tokens_delete',
89 name='edit_user_auth_tokens_delete',
71 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
90 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
72
91
92 # user emails
93 config.add_route(
94 name='edit_user_emails',
95 pattern='/users/{user_id:\d+}/edit/emails')
96 config.add_route(
97 name='edit_user_emails_add',
98 pattern='/users/{user_id:\d+}/edit/emails/new')
99 config.add_route(
100 name='edit_user_emails_delete',
101 pattern='/users/{user_id:\d+}/edit/emails/delete')
102
103 # user IPs
104 config.add_route(
105 name='edit_user_ips',
106 pattern='/users/{user_id:\d+}/edit/ips')
107 config.add_route(
108 name='edit_user_ips_add',
109 pattern='/users/{user_id:\d+}/edit/ips/new')
110 config.add_route(
111 name='edit_user_ips_delete',
112 pattern='/users/{user_id:\d+}/edit/ips/delete')
113
73 # user groups management
114 # user groups management
74 config.add_route(
115 config.add_route(
75 name='edit_user_groups_management',
116 name='edit_user_groups_management',
@@ -93,6 +134,8 b' def includeme(config):'
93 navigation_registry = NavigationRegistry(labs_active=labs_active)
134 navigation_registry = NavigationRegistry(labs_active=labs_active)
94 config.registry.registerUtility(navigation_registry)
135 config.registry.registerUtility(navigation_registry)
95
136
137 # main admin routes
138 config.add_route(name='admin_home', pattern=ADMIN_PREFIX)
96 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
139 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
97
140
98 # Scan module for configuration decorators.
141 # Scan module for configuration decorators.
@@ -20,7 +20,9 b''
20
20
21 import pytest
21 import pytest
22
22
23 from rhodecode.model.db import User, UserApiKeys
23 from rhodecode.model.db import User, UserApiKeys, UserEmailMap
24 from rhodecode.model.meta import Session
25 from rhodecode.model.user import UserModel
24
26
25 from rhodecode.tests import (
27 from rhodecode.tests import (
26 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
28 TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
@@ -44,6 +46,20 b' def route_path(name, params=None, **kwar'
44 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
46 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/new',
45 'edit_user_auth_tokens_delete':
47 'edit_user_auth_tokens_delete':
46 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
48 ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens/delete',
49
50 'edit_user_emails':
51 ADMIN_PREFIX + '/users/{user_id}/edit/emails',
52 'edit_user_emails_add':
53 ADMIN_PREFIX + '/users/{user_id}/edit/emails/new',
54 'edit_user_emails_delete':
55 ADMIN_PREFIX + '/users/{user_id}/edit/emails/delete',
56
57 'edit_user_ips':
58 ADMIN_PREFIX + '/users/{user_id}/edit/ips',
59 'edit_user_ips_add':
60 ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
61 'edit_user_ips_delete':
62 ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
47 }[name].format(**kwargs)
63 }[name].format(**kwargs)
48
64
49 if params:
65 if params:
@@ -135,8 +151,131 b' class TestAdminUsersView(TestController)'
135
151
136 response = self.app.post(
152 response = self.app.post(
137 route_path('edit_user_auth_tokens_delete', user_id=user_id),
153 route_path('edit_user_auth_tokens_delete', user_id=user_id),
138 {'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token})
154 {'del_auth_token': keys[0].user_api_key_id,
155 'csrf_token': self.csrf_token})
139
156
140 assert_session_flash(response, 'Auth token successfully deleted')
157 assert_session_flash(response, 'Auth token successfully deleted')
141 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
158 keys = UserApiKeys.query().filter(UserApiKeys.user_id == user_id).all()
142 assert 2 == len(keys)
159 assert 2 == len(keys)
160
161 def test_ips(self):
162 self.log_user()
163 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
164 response = self.app.get(route_path('edit_user_ips', user_id=user.user_id))
165 response.mustcontain('All IP addresses are allowed')
166
167 @pytest.mark.parametrize("test_name, ip, ip_range, failure", [
168 ('127/24', '127.0.0.1/24', '127.0.0.0 - 127.0.0.255', False),
169 ('10/32', '10.0.0.10/32', '10.0.0.10 - 10.0.0.10', False),
170 ('0/16', '0.0.0.0/16', '0.0.0.0 - 0.0.255.255', False),
171 ('0/8', '0.0.0.0/8', '0.0.0.0 - 0.255.255.255', False),
172 ('127_bad_mask', '127.0.0.1/99', '127.0.0.1 - 127.0.0.1', True),
173 ('127_bad_ip', 'foobar', 'foobar', True),
174 ])
175 def test_ips_add(self, user_util, test_name, ip, ip_range, failure):
176 self.log_user()
177 user = user_util.create_user(username=test_name)
178 user_id = user.user_id
179
180 response = self.app.post(
181 route_path('edit_user_ips_add', user_id=user_id),
182 params={'new_ip': ip, 'csrf_token': self.csrf_token})
183
184 if failure:
185 assert_session_flash(
186 response, 'Please enter a valid IPv4 or IpV6 address')
187 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
188
189 response.mustcontain(no=[ip])
190 response.mustcontain(no=[ip_range])
191
192 else:
193 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
194 response.mustcontain(ip)
195 response.mustcontain(ip_range)
196
197 def test_ips_delete(self, user_util):
198 self.log_user()
199 user = user_util.create_user()
200 user_id = user.user_id
201 ip = '127.0.0.1/32'
202 ip_range = '127.0.0.1 - 127.0.0.1'
203 new_ip = UserModel().add_extra_ip(user_id, ip)
204 Session().commit()
205 new_ip_id = new_ip.ip_id
206
207 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
208 response.mustcontain(ip)
209 response.mustcontain(ip_range)
210
211 self.app.post(
212 route_path('edit_user_ips_delete', user_id=user_id),
213 params={'del_ip_id': new_ip_id, 'csrf_token': self.csrf_token})
214
215 response = self.app.get(route_path('edit_user_ips', user_id=user_id))
216 response.mustcontain('All IP addresses are allowed')
217 response.mustcontain(no=[ip])
218 response.mustcontain(no=[ip_range])
219
220 def test_emails(self):
221 self.log_user()
222 user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
223 response = self.app.get(route_path('edit_user_emails', user_id=user.user_id))
224 response.mustcontain('No additional emails specified')
225
226 def test_emails_add(self, user_util):
227 self.log_user()
228 user = user_util.create_user()
229 user_id = user.user_id
230
231 self.app.post(
232 route_path('edit_user_emails_add', user_id=user_id),
233 params={'new_email': 'example@rhodecode.com',
234 'csrf_token': self.csrf_token})
235
236 response = self.app.get(route_path('edit_user_emails', user_id=user_id))
237 response.mustcontain('example@rhodecode.com')
238
239 def test_emails_add_existing_email(self, user_util, user_regular):
240 existing_email = user_regular.email
241
242 self.log_user()
243 user = user_util.create_user()
244 user_id = user.user_id
245
246 response = self.app.post(
247 route_path('edit_user_emails_add', user_id=user_id),
248 params={'new_email': existing_email,
249 'csrf_token': self.csrf_token})
250 assert_session_flash(
251 response, 'This e-mail address is already taken')
252
253 response = self.app.get(route_path('edit_user_emails', user_id=user_id))
254 response.mustcontain(no=[existing_email])
255
256 def test_emails_delete(self, user_util):
257 self.log_user()
258 user = user_util.create_user()
259 user_id = user.user_id
260
261 self.app.post(
262 route_path('edit_user_emails_add', user_id=user_id),
263 params={'new_email': 'example@rhodecode.com',
264 'csrf_token': self.csrf_token})
265
266 response = self.app.get(route_path('edit_user_emails', user_id=user_id))
267 response.mustcontain('example@rhodecode.com')
268
269 user_email = UserEmailMap.query()\
270 .filter(UserEmailMap.email == 'example@rhodecode.com') \
271 .filter(UserEmailMap.user_id == user_id)\
272 .one()
273
274 del_email_id = user_email.email_id
275 self.app.post(
276 route_path('edit_user_emails_delete', user_id=user_id),
277 params={'del_email_id': del_email_id,
278 'csrf_token': self.csrf_token})
279
280 response = self.app.get(route_path('edit_user_emails', user_id=user_id))
281 response.mustcontain(no=['example@rhodecode.com']) No newline at end of file
@@ -21,7 +21,7 b''
21 import collections
21 import collections
22 import logging
22 import logging
23
23
24 from pylons import tmpl_context as c
24
25 from pyramid.view import view_config
25 from pyramid.view import view_config
26
26
27 from rhodecode.apps._base import BaseAppView
27 from rhodecode.apps._base import BaseAppView
@@ -34,15 +34,21 b' log = logging.getLogger(__name__)'
34
34
35 class OpenSourceLicensesAdminSettingsView(BaseAppView):
35 class OpenSourceLicensesAdminSettingsView(BaseAppView):
36
36
37 def load_default_context(self):
38 c = self._get_local_tmpl_context()
39 self._register_global_c(c)
40 return c
41
37 @LoginRequired()
42 @LoginRequired()
38 @HasPermissionAllDecorator('hg.admin')
43 @HasPermissionAllDecorator('hg.admin')
39 @view_config(
44 @view_config(
40 route_name='admin_settings_open_source', request_method='GET',
45 route_name='admin_settings_open_source', request_method='GET',
41 renderer='rhodecode:templates/admin/settings/settings.mako')
46 renderer='rhodecode:templates/admin/settings/settings.mako')
42 def open_source_licenses(self):
47 def open_source_licenses(self):
48 c = self.load_default_context()
43 c.active = 'open_source'
49 c.active = 'open_source'
44 c.navlist = navigation_list(self.request)
50 c.navlist = navigation_list(self.request)
45 c.opensource_licenses = collections.OrderedDict(
51 c.opensource_licenses = collections.OrderedDict(
46 sorted(read_opensource_licenses().items(), key=lambda t: t[0]))
52 sorted(read_opensource_licenses().items(), key=lambda t: t[0]))
47
53
48 return {}
54 return self._get_template_context(c)
@@ -20,7 +20,6 b''
20
20
21 import logging
21 import logging
22
22
23 from pylons import tmpl_context as c
24 from pyramid.view import view_config
23 from pyramid.view import view_config
25 from pyramid.httpexceptions import HTTPFound
24 from pyramid.httpexceptions import HTTPFound
26
25
@@ -37,6 +36,11 b' log = logging.getLogger(__name__)'
37
36
38
37
39 class AdminSessionSettingsView(BaseAppView):
38 class AdminSessionSettingsView(BaseAppView):
39 def load_default_context(self):
40 c = self._get_local_tmpl_context()
41
42 self._register_global_c(c)
43 return c
40
44
41 @LoginRequired()
45 @LoginRequired()
42 @HasPermissionAllDecorator('hg.admin')
46 @HasPermissionAllDecorator('hg.admin')
@@ -44,6 +48,8 b' class AdminSessionSettingsView(BaseAppVi'
44 route_name='admin_settings_sessions', request_method='GET',
48 route_name='admin_settings_sessions', request_method='GET',
45 renderer='rhodecode:templates/admin/settings/settings.mako')
49 renderer='rhodecode:templates/admin/settings/settings.mako')
46 def settings_sessions(self):
50 def settings_sessions(self):
51 c = self.load_default_context()
52
47 c.active = 'sessions'
53 c.active = 'sessions'
48 c.navlist = navigation_list(self.request)
54 c.navlist = navigation_list(self.request)
49
55
@@ -59,11 +65,11 b' class AdminSessionSettingsView(BaseAppVi'
59 c.session_expired_count = c.session_model.get_expired_count(
65 c.session_expired_count = c.session_model.get_expired_count(
60 older_than_seconds)
66 older_than_seconds)
61
67
62 return {}
68 return self._get_template_context(c)
63
69
64 @LoginRequired()
70 @LoginRequired()
71 @HasPermissionAllDecorator('hg.admin')
65 @CSRFRequired()
72 @CSRFRequired()
66 @HasPermissionAllDecorator('hg.admin')
67 @view_config(
73 @view_config(
68 route_name='admin_settings_sessions_cleanup', request_method='POST')
74 route_name='admin_settings_sessions_cleanup', request_method='POST')
69 def settings_sessions_cleanup(self):
75 def settings_sessions_cleanup(self):
@@ -33,8 +33,8 b' log = logging.getLogger(__name__)'
33 class SvnConfigAdminSettingsView(BaseAppView):
33 class SvnConfigAdminSettingsView(BaseAppView):
34
34
35 @LoginRequired()
35 @LoginRequired()
36 @HasPermissionAllDecorator('hg.admin')
36 @CSRFRequired()
37 @CSRFRequired()
37 @HasPermissionAllDecorator('hg.admin')
38 @view_config(
38 @view_config(
39 route_name='admin_settings_vcs_svn_generate_cfg',
39 route_name='admin_settings_vcs_svn_generate_cfg',
40 request_method='POST', renderer='json')
40 request_method='POST', renderer='json')
@@ -22,7 +22,6 b' import logging'
22 import urllib2
22 import urllib2
23 import packaging.version
23 import packaging.version
24
24
25 from pylons import tmpl_context as c
26 from pyramid.view import view_config
25 from pyramid.view import view_config
27
26
28 import rhodecode
27 import rhodecode
@@ -39,6 +38,10 b' log = logging.getLogger(__name__)'
39
38
40
39
41 class AdminSystemInfoSettingsView(BaseAppView):
40 class AdminSystemInfoSettingsView(BaseAppView):
41 def load_default_context(self):
42 c = self._get_local_tmpl_context()
43 self._register_global_c(c)
44 return c
42
45
43 @staticmethod
46 @staticmethod
44 def get_update_data(update_url):
47 def get_update_data(update_url):
@@ -64,6 +67,7 b' class AdminSystemInfoSettingsView(BaseAp'
64 renderer='rhodecode:templates/admin/settings/settings.mako')
67 renderer='rhodecode:templates/admin/settings/settings.mako')
65 def settings_system_info(self):
68 def settings_system_info(self):
66 _ = self.request.translate
69 _ = self.request.translate
70 c = self.load_default_context()
67
71
68 c.active = 'system'
72 c.active = 'system'
69 c.navlist = navigation_list(self.request)
73 c.navlist = navigation_list(self.request)
@@ -106,6 +110,7 b' class AdminSystemInfoSettingsView(BaseAp'
106 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
110 (_('RhodeCode Server IP'), val('server')['server_ip'], state('server')),
107 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
111 (_('RhodeCode Server ID'), val('server')['server_id'], state('server')),
108 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
112 (_('RhodeCode Configuration'), val('rhodecode_config')['path'], state('rhodecode_config')),
113 (_('RhodeCode Certificate'), val('rhodecode_config')['cert_path'], state('rhodecode_config')),
109 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
114 (_('Workers'), val('rhodecode_config')['config']['server:main'].get('workers', '?'), state('rhodecode_config')),
110 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
115 (_('Worker Type'), val('rhodecode_config')['config']['server:main'].get('worker_class', 'sync'), state('rhodecode_config')),
111 ('', '', ''), # spacer
116 ('', '', ''), # spacer
@@ -163,7 +168,7 b' class AdminSystemInfoSettingsView(BaseAp'
163 else:
168 else:
164 self.request.session.flash(
169 self.request.session.flash(
165 'You are not allowed to do this', queue='warning')
170 'You are not allowed to do this', queue='warning')
166 return {}
171 return self._get_template_context(c)
167
172
168 @LoginRequired()
173 @LoginRequired()
169 @HasPermissionAllDecorator('hg.admin')
174 @HasPermissionAllDecorator('hg.admin')
@@ -172,6 +177,7 b' class AdminSystemInfoSettingsView(BaseAp'
172 renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
177 renderer='rhodecode:templates/admin/settings/settings_system_update.mako')
173 def settings_system_info_check_update(self):
178 def settings_system_info_check_update(self):
174 _ = self.request.translate
179 _ = self.request.translate
180 c = self.load_default_context()
175
181
176 update_url = self.get_update_url()
182 update_url = self.get_update_url()
177
183
@@ -200,4 +206,4 b' class AdminSystemInfoSettingsView(BaseAp'
200 c.should_upgrade = True
206 c.should_upgrade = True
201 c.important_notices = latest['general']
207 c.important_notices = latest['general']
202
208
203 return {}
209 return self._get_template_context(c)
@@ -20,15 +20,16 b''
20
20
21 import logging
21 import logging
22 import datetime
22 import datetime
23 import formencode
23
24
24 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound
25 from pyramid.view import view_config
26 from pyramid.view import view_config
26 from sqlalchemy.sql.functions import coalesce
27 from sqlalchemy.sql.functions import coalesce
27
28
28 from rhodecode.lib.helpers import Page
29 from rhodecode.apps._base import BaseAppView, DataGridAppView
29 from rhodecode_tools.lib.ext_json import json
30
30
31 from rhodecode.apps._base import BaseAppView
31 from rhodecode.lib import audit_logger
32 from rhodecode.lib.ext_json import json
32 from rhodecode.lib.auth import (
33 from rhodecode.lib.auth import (
33 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
34 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
@@ -37,13 +38,13 b' from rhodecode.lib.utils2 import safe_in'
37 from rhodecode.model.auth_token import AuthTokenModel
38 from rhodecode.model.auth_token import AuthTokenModel
38 from rhodecode.model.user import UserModel
39 from rhodecode.model.user import UserModel
39 from rhodecode.model.user_group import UserGroupModel
40 from rhodecode.model.user_group import UserGroupModel
40 from rhodecode.model.db import User, or_
41 from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
41 from rhodecode.model.meta import Session
42 from rhodecode.model.meta import Session
42
43
43 log = logging.getLogger(__name__)
44 log = logging.getLogger(__name__)
44
45
45
46
46 class AdminUsersView(BaseAppView):
47 class AdminUsersView(BaseAppView, DataGridAppView):
47 ALLOW_SCOPED_TOKENS = False
48 ALLOW_SCOPED_TOKENS = False
48 """
49 """
49 This view has alternative version inside EE, if modified please take a look
50 This view has alternative version inside EE, if modified please take a look
@@ -64,28 +65,6 b' class AdminUsersView(BaseAppView):'
64 # is a pyramid view
65 # is a pyramid view
65 raise HTTPFound('/')
66 raise HTTPFound('/')
66
67
67 def _extract_ordering(self, request):
68 column_index = safe_int(request.GET.get('order[0][column]'))
69 order_dir = request.GET.get(
70 'order[0][dir]', 'desc')
71 order_by = request.GET.get(
72 'columns[%s][data][sort]' % column_index, 'name_raw')
73
74 # translate datatable to DB columns
75 order_by = {
76 'first_name': 'name',
77 'last_name': 'lastname',
78 }.get(order_by) or order_by
79
80 search_q = request.GET.get('search[value]')
81 return search_q, order_by, order_dir
82
83 def _extract_chunk(self, request):
84 start = safe_int(request.GET.get('start'), 0)
85 length = safe_int(request.GET.get('length'), 25)
86 draw = safe_int(request.GET.get('draw'))
87 return draw, start, length
88
89 @HasPermissionAllDecorator('hg.admin')
68 @HasPermissionAllDecorator('hg.admin')
90 @view_config(
69 @view_config(
91 route_name='users', request_method='GET',
70 route_name='users', request_method='GET',
@@ -97,8 +76,8 b' class AdminUsersView(BaseAppView):'
97 @HasPermissionAllDecorator('hg.admin')
76 @HasPermissionAllDecorator('hg.admin')
98 @view_config(
77 @view_config(
99 # renderer defined below
78 # renderer defined below
100 route_name='users_data', request_method='GET', renderer='json',
79 route_name='users_data', request_method='GET',
101 xhr=True)
80 renderer='json_ext', xhr=True)
102 def users_list_data(self):
81 def users_list_data(self):
103 draw, start, limit = self._extract_chunk(self.request)
82 draw, start, limit = self._extract_chunk(self.request)
104 search_q, order_by, order_dir = self._extract_ordering(self.request)
83 search_q, order_by, order_dir = self._extract_ordering(self.request)
@@ -149,8 +128,8 b' class AdminUsersView(BaseAppView):'
149 users_data.append({
128 users_data.append({
150 "username": h.gravatar_with_user(user.username),
129 "username": h.gravatar_with_user(user.username),
151 "email": user.email,
130 "email": user.email,
152 "first_name": h.escape(user.name),
131 "first_name": user.first_name,
153 "last_name": h.escape(user.lastname),
132 "last_name": user.last_name,
154 "last_login": h.format_date(user.last_login),
133 "last_login": h.format_date(user.last_login),
155 "last_activity": h.format_date(user.last_activity),
134 "last_activity": h.format_date(user.last_activity),
156 "active": h.bool2icon(user.active),
135 "active": h.bool2icon(user.active),
@@ -216,15 +195,23 b' class AdminUsersView(BaseAppView):'
216
195
217 user_id = self.request.matchdict.get('user_id')
196 user_id = self.request.matchdict.get('user_id')
218 c.user = User.get_or_404(user_id, pyramid_exc=True)
197 c.user = User.get_or_404(user_id, pyramid_exc=True)
198
219 self._redirect_for_default_user(c.user.username)
199 self._redirect_for_default_user(c.user.username)
220
200
201 user_data = c.user.get_api_data()
221 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
202 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
222 description = self.request.POST.get('description')
203 description = self.request.POST.get('description')
223 role = self.request.POST.get('role')
204 role = self.request.POST.get('role')
224
205
225 token = AuthTokenModel().create(
206 token = AuthTokenModel().create(
226 c.user.user_id, description, lifetime, role)
207 c.user.user_id, description, lifetime, role)
208 token_data = token.get_api_data()
209
227 self.maybe_attach_token_scope(token)
210 self.maybe_attach_token_scope(token)
211 audit_logger.store_web(
212 'user.edit.token.add', action_data={
213 'data': {'token': token_data, 'user': user_data}},
214 user=self._rhodecode_user, )
228 Session().commit()
215 Session().commit()
229
216
230 h.flash(_("Auth token successfully created"), category='success')
217 h.flash(_("Auth token successfully created"), category='success')
@@ -242,11 +229,19 b' class AdminUsersView(BaseAppView):'
242 user_id = self.request.matchdict.get('user_id')
229 user_id = self.request.matchdict.get('user_id')
243 c.user = User.get_or_404(user_id, pyramid_exc=True)
230 c.user = User.get_or_404(user_id, pyramid_exc=True)
244 self._redirect_for_default_user(c.user.username)
231 self._redirect_for_default_user(c.user.username)
232 user_data = c.user.get_api_data()
245
233
246 del_auth_token = self.request.POST.get('del_auth_token')
234 del_auth_token = self.request.POST.get('del_auth_token')
247
235
248 if del_auth_token:
236 if del_auth_token:
237 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
238 token_data = token.get_api_data()
239
249 AuthTokenModel().delete(del_auth_token, c.user.user_id)
240 AuthTokenModel().delete(del_auth_token, c.user.user_id)
241 audit_logger.store_web(
242 'user.edit.token.delete', action_data={
243 'data': {'token': token_data, 'user': user_data}},
244 user=self._rhodecode_user,)
250 Session().commit()
245 Session().commit()
251 h.flash(_("Auth token successfully deleted"), category='success')
246 h.flash(_("Auth token successfully deleted"), category='success')
252
247
@@ -255,6 +250,186 b' class AdminUsersView(BaseAppView):'
255 @LoginRequired()
250 @LoginRequired()
256 @HasPermissionAllDecorator('hg.admin')
251 @HasPermissionAllDecorator('hg.admin')
257 @view_config(
252 @view_config(
253 route_name='edit_user_emails', request_method='GET',
254 renderer='rhodecode:templates/admin/users/user_edit.mako')
255 def emails(self):
256 _ = self.request.translate
257 c = self.load_default_context()
258
259 user_id = self.request.matchdict.get('user_id')
260 c.user = User.get_or_404(user_id, pyramid_exc=True)
261 self._redirect_for_default_user(c.user.username)
262
263 c.active = 'emails'
264 c.user_email_map = UserEmailMap.query() \
265 .filter(UserEmailMap.user == c.user).all()
266
267 return self._get_template_context(c)
268
269 @LoginRequired()
270 @HasPermissionAllDecorator('hg.admin')
271 @CSRFRequired()
272 @view_config(
273 route_name='edit_user_emails_add', request_method='POST')
274 def emails_add(self):
275 _ = self.request.translate
276 c = self.load_default_context()
277
278 user_id = self.request.matchdict.get('user_id')
279 c.user = User.get_or_404(user_id, pyramid_exc=True)
280 self._redirect_for_default_user(c.user.username)
281
282 email = self.request.POST.get('new_email')
283 user_data = c.user.get_api_data()
284 try:
285 UserModel().add_extra_email(c.user.user_id, email)
286 audit_logger.store_web(
287 'user.edit.email.add', action_data={'email': email, 'user': user_data},
288 user=self._rhodecode_user)
289 Session().commit()
290 h.flash(_("Added new email address `%s` for user account") % email,
291 category='success')
292 except formencode.Invalid as error:
293 h.flash(h.escape(error.error_dict['email']), category='error')
294 except Exception:
295 log.exception("Exception during email saving")
296 h.flash(_('An error occurred during email saving'),
297 category='error')
298 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
299
300 @LoginRequired()
301 @HasPermissionAllDecorator('hg.admin')
302 @CSRFRequired()
303 @view_config(
304 route_name='edit_user_emails_delete', request_method='POST')
305 def emails_delete(self):
306 _ = self.request.translate
307 c = self.load_default_context()
308
309 user_id = self.request.matchdict.get('user_id')
310 c.user = User.get_or_404(user_id, pyramid_exc=True)
311 self._redirect_for_default_user(c.user.username)
312
313 email_id = self.request.POST.get('del_email_id')
314 user_model = UserModel()
315
316 email = UserEmailMap.query().get(email_id).email
317 user_data = c.user.get_api_data()
318 user_model.delete_extra_email(c.user.user_id, email_id)
319 audit_logger.store_web(
320 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
321 user=self._rhodecode_user)
322 Session().commit()
323 h.flash(_("Removed email address from user account"),
324 category='success')
325 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
326
327 @LoginRequired()
328 @HasPermissionAllDecorator('hg.admin')
329 @view_config(
330 route_name='edit_user_ips', request_method='GET',
331 renderer='rhodecode:templates/admin/users/user_edit.mako')
332 def ips(self):
333 _ = self.request.translate
334 c = self.load_default_context()
335
336 user_id = self.request.matchdict.get('user_id')
337 c.user = User.get_or_404(user_id, pyramid_exc=True)
338 self._redirect_for_default_user(c.user.username)
339
340 c.active = 'ips'
341 c.user_ip_map = UserIpMap.query() \
342 .filter(UserIpMap.user == c.user).all()
343
344 c.inherit_default_ips = c.user.inherit_default_permissions
345 c.default_user_ip_map = UserIpMap.query() \
346 .filter(UserIpMap.user == User.get_default_user()).all()
347
348 return self._get_template_context(c)
349
350 @LoginRequired()
351 @HasPermissionAllDecorator('hg.admin')
352 @CSRFRequired()
353 @view_config(
354 route_name='edit_user_ips_add', request_method='POST')
355 def ips_add(self):
356 _ = self.request.translate
357 c = self.load_default_context()
358
359 user_id = self.request.matchdict.get('user_id')
360 c.user = User.get_or_404(user_id, pyramid_exc=True)
361 # NOTE(marcink): this view is allowed for default users, as we can
362 # edit their IP white list
363
364 user_model = UserModel()
365 desc = self.request.POST.get('description')
366 try:
367 ip_list = user_model.parse_ip_range(
368 self.request.POST.get('new_ip'))
369 except Exception as e:
370 ip_list = []
371 log.exception("Exception during ip saving")
372 h.flash(_('An error occurred during ip saving:%s' % (e,)),
373 category='error')
374 added = []
375 user_data = c.user.get_api_data()
376 for ip in ip_list:
377 try:
378 user_model.add_extra_ip(c.user.user_id, ip, desc)
379 audit_logger.store_web(
380 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
381 user=self._rhodecode_user)
382 Session().commit()
383 added.append(ip)
384 except formencode.Invalid as error:
385 msg = error.error_dict['ip']
386 h.flash(msg, category='error')
387 except Exception:
388 log.exception("Exception during ip saving")
389 h.flash(_('An error occurred during ip saving'),
390 category='error')
391 if added:
392 h.flash(
393 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
394 category='success')
395 if 'default_user' in self.request.POST:
396 # case for editing global IP list we do it for 'DEFAULT' user
397 raise HTTPFound(h.route_path('admin_permissions_ips'))
398 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
399
400 @LoginRequired()
401 @HasPermissionAllDecorator('hg.admin')
402 @CSRFRequired()
403 @view_config(
404 route_name='edit_user_ips_delete', request_method='POST')
405 def ips_delete(self):
406 _ = self.request.translate
407 c = self.load_default_context()
408
409 user_id = self.request.matchdict.get('user_id')
410 c.user = User.get_or_404(user_id, pyramid_exc=True)
411 # NOTE(marcink): this view is allowed for default users, as we can
412 # edit their IP white list
413
414 ip_id = self.request.POST.get('del_ip_id')
415 user_model = UserModel()
416 user_data = c.user.get_api_data()
417 ip = UserIpMap.query().get(ip_id).ip_addr
418 user_model.delete_extra_ip(c.user.user_id, ip_id)
419 audit_logger.store_web(
420 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
421 user=self._rhodecode_user)
422 Session().commit()
423 h.flash(_("Removed ip address from user whitelist"), category='success')
424
425 if 'default_user' in self.request.POST:
426 # case for editing global IP list we do it for 'DEFAULT' user
427 raise HTTPFound(h.route_path('admin_permissions_ips'))
428 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
429
430 @LoginRequired()
431 @HasPermissionAllDecorator('hg.admin')
432 @view_config(
258 route_name='edit_user_groups_management', request_method='GET',
433 route_name='edit_user_groups_management', request_method='GET',
259 renderer='rhodecode:templates/admin/users/user_edit.mako')
434 renderer='rhodecode:templates/admin/users/user_edit.mako')
260 def groups_management(self):
435 def groups_management(self):
@@ -264,7 +439,8 b' class AdminUsersView(BaseAppView):'
264 c.user = User.get_or_404(user_id, pyramid_exc=True)
439 c.user = User.get_or_404(user_id, pyramid_exc=True)
265 c.data = c.user.group_member
440 c.data = c.user.group_member
266 self._redirect_for_default_user(c.user.username)
441 self._redirect_for_default_user(c.user.username)
267 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group) for group in c.user.group_member]
442 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
443 for group in c.user.group_member]
268 c.groups = json.dumps(groups)
444 c.groups = json.dumps(groups)
269 c.active = 'groups'
445 c.active = 'groups'
270
446
@@ -272,6 +448,7 b' class AdminUsersView(BaseAppView):'
272
448
273 @LoginRequired()
449 @LoginRequired()
274 @HasPermissionAllDecorator('hg.admin')
450 @HasPermissionAllDecorator('hg.admin')
451 @CSRFRequired()
275 @view_config(
452 @view_config(
276 route_name='edit_user_groups_management_updates', request_method='POST')
453 route_name='edit_user_groups_management_updates', request_method='POST')
277 def groups_management_updates(self):
454 def groups_management_updates(self):
@@ -314,15 +491,15 b' class AdminUsersView(BaseAppView):'
314 p = safe_int(self.request.GET.get('page', 1), 1)
491 p = safe_int(self.request.GET.get('page', 1), 1)
315
492
316 filter_term = self.request.GET.get('filter')
493 filter_term = self.request.GET.get('filter')
317 c.user_log = UserModel().get_user_log(c.user, filter_term)
494 user_log = UserModel().get_user_log(c.user, filter_term)
318
495
319 def url_generator(**kw):
496 def url_generator(**kw):
320 if filter_term:
497 if filter_term:
321 kw['filter'] = filter_term
498 kw['filter'] = filter_term
322 return self.request.current_route_path(_query=kw)
499 return self.request.current_route_path(_query=kw)
323
500
324 c.user_log = Page(c.user_log, page=p, items_per_page=10,
501 c.audit_logs = h.Page(
325 url=url_generator)
502 user_log, page=p, items_per_page=10, url=url_generator)
326 c.filter_term = filter_term
503 c.filter_term = filter_term
327 return self._get_template_context(c)
504 return self._get_template_context(c)
328
505
@@ -30,8 +30,6 b' Channel Stream controller for rhodecode'
30 import logging
30 import logging
31 import uuid
31 import uuid
32
32
33 from pylons import tmpl_context as c
34 from pyramid.settings import asbool
35 from pyramid.view import view_config
33 from pyramid.view import view_config
36 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPBadGateway
34 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPBadGateway
37
35
@@ -46,7 +44,6 b' from rhodecode.lib.channelstream import '
46 update_history_from_logs,
44 update_history_from_logs,
47 STATE_PUBLIC_KEYS)
45 STATE_PUBLIC_KEYS)
48 from rhodecode.lib.auth import NotAnonymous
46 from rhodecode.lib.auth import NotAnonymous
49 from rhodecode.lib.utils2 import str2bool
50
47
51 log = logging.getLogger(__name__)
48 log = logging.getLogger(__name__)
52
49
@@ -82,7 +79,7 b' class ChannelstreamView(object):'
82 log.error('Incorrect permissions for requested channels')
79 log.error('Incorrect permissions for requested channels')
83 raise HTTPForbidden()
80 raise HTTPForbidden()
84
81
85 user = c.rhodecode_user
82 user = self._rhodecode_user
86 if user.user_id:
83 if user.user_id:
87 user_data = get_user_data(user.user_id)
84 user_data = get_user_data(user.user_id)
88 else:
85 else:
@@ -95,7 +92,7 b' class ChannelstreamView(object):'
95 'display_name': None,
92 'display_name': None,
96 'display_link': None,
93 'display_link': None,
97 }
94 }
98 user_data['permissions'] = c.rhodecode_user.permissions
95 user_data['permissions'] = self._rhodecode_user.permissions
99 payload = {
96 payload = {
100 'username': user.username,
97 'username': user.username,
101 'user_state': user_data,
98 'user_state': user_data,
@@ -18,31 +18,34 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import json
22
21
23 from mock import patch
24 import pytest
22 import pytest
25 from pylons import tmpl_context as c
26
23
27 import rhodecode
24 import rhodecode
28 from rhodecode.lib.utils import map_groups
25 from rhodecode.model.db import Repository
29 from rhodecode.model.db import Repository, User, RepoGroup
30 from rhodecode.model.meta import Session
26 from rhodecode.model.meta import Session
31 from rhodecode.model.repo import RepoModel
27 from rhodecode.model.repo import RepoModel
32 from rhodecode.model.repo_group import RepoGroupModel
28 from rhodecode.model.repo_group import RepoGroupModel
33 from rhodecode.model.settings import SettingsModel
29 from rhodecode.model.settings import SettingsModel
34 from rhodecode.tests import TestController, url, TEST_USER_ADMIN_LOGIN
30 from rhodecode.tests import TestController
35 from rhodecode.tests.fixture import Fixture
31 from rhodecode.tests.fixture import Fixture
32 from rhodecode.lib import helpers as h
33
34 fixture = Fixture()
36
35
37
36
38 fixture = Fixture()
37 def route_path(name, **kwargs):
38 return {
39 'home': '/',
40 'repo_group_home': '/{repo_group_name}'
41 }[name].format(**kwargs)
39
42
40
43
41 class TestHomeController(TestController):
44 class TestHomeController(TestController):
42
45
43 def test_index(self):
46 def test_index(self):
44 self.log_user()
47 self.log_user()
45 response = self.app.get(url(controller='home', action='index'))
48 response = self.app.get(route_path('home'))
46 # if global permission is set
49 # if global permission is set
47 response.mustcontain('Add Repository')
50 response.mustcontain('Add Repository')
48
51
@@ -51,8 +54,10 b' class TestHomeController(TestController)'
51 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
54 response.mustcontain('"name_raw": "%s"' % repo.repo_name)
52
55
53 def test_index_contains_statics_with_ver(self):
56 def test_index_contains_statics_with_ver(self):
57 from pylons import tmpl_context as c
58
54 self.log_user()
59 self.log_user()
55 response = self.app.get(url(controller='home', action='index'))
60 response = self.app.get(route_path('home'))
56
61
57 rhodecode_version_hash = c.rhodecode_version_hash
62 rhodecode_version_hash = c.rhodecode_version_hash
58 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
63 response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
@@ -60,7 +65,7 b' class TestHomeController(TestController)'
60
65
61 def test_index_contains_backend_specific_details(self, backend):
66 def test_index_contains_backend_specific_details(self, backend):
62 self.log_user()
67 self.log_user()
63 response = self.app.get(url(controller='home', action='index'))
68 response = self.app.get(route_path('home'))
64 tip = backend.repo.get_commit().raw_id
69 tip = backend.repo.get_commit().raw_id
65
70
66 # html in javascript variable:
71 # html in javascript variable:
@@ -72,17 +77,16 b' class TestHomeController(TestController)'
72
77
73 def test_index_with_anonymous_access_disabled(self):
78 def test_index_with_anonymous_access_disabled(self):
74 with fixture.anon_access(False):
79 with fixture.anon_access(False):
75 response = self.app.get(url(controller='home', action='index'),
80 response = self.app.get(route_path('home'), status=302)
76 status=302)
77 assert 'login' in response.location
81 assert 'login' in response.location
78
82
79 def test_index_page_on_groups(self, autologin_user, repo_group):
83 def test_index_page_on_groups(self, autologin_user, repo_group):
80 response = self.app.get(url('repo_group_home', group_name='gr1'))
84 response = self.app.get(route_path('repo_group_home', repo_group_name='gr1'))
81 response.mustcontain("gr1/repo_in_group")
85 response.mustcontain("gr1/repo_in_group")
82
86
83 def test_index_page_on_group_with_trailing_slash(
87 def test_index_page_on_group_with_trailing_slash(
84 self, autologin_user, repo_group):
88 self, autologin_user, repo_group):
85 response = self.app.get(url('repo_group_home', group_name='gr1') + '/')
89 response = self.app.get(route_path('repo_group_home', repo_group_name='gr1') + '/')
86 response.mustcontain("gr1/repo_in_group")
90 response.mustcontain("gr1/repo_in_group")
87
91
88 @pytest.fixture(scope='class')
92 @pytest.fixture(scope='class')
@@ -96,21 +100,19 b' class TestHomeController(TestController)'
96 RepoGroupModel().delete(repo_group='gr1', force_delete=True)
100 RepoGroupModel().delete(repo_group='gr1', force_delete=True)
97 Session().commit()
101 Session().commit()
98
102
99 def test_index_with_name_with_tags(self, autologin_user):
103 def test_index_with_name_with_tags(self, user_util, autologin_user):
100 user = User.get_by_username('test_admin')
104 user = user_util.create_user()
105 username = user.username
101 user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">'
106 user.name = '<img src="/image1" onload="alert(\'Hello, World!\');">'
102 user.lastname = (
107 user.lastname = '#"><img src=x onerror=prompt(document.cookie);>'
103 '<img src="/image2" onload="alert(\'Hello, World!\');">')
108
104 Session().add(user)
109 Session().add(user)
105 Session().commit()
110 Session().commit()
111 user_util.create_repo(owner=username)
106
112
107 response = self.app.get(url(controller='home', action='index'))
113 response = self.app.get(route_path('home'))
108 response.mustcontain(
114 response.mustcontain(h.html_escape(user.first_name))
109 '&lt;img src=&#34;/image1&#34; onload=&#34;'
115 response.mustcontain(h.html_escape(user.last_name))
110 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
111 response.mustcontain(
112 '&lt;img src=&#34;/image2&#34; onload=&#34;'
113 'alert(&#39;Hello, World!&#39;);&#34;&gt;')
114
116
115 @pytest.mark.parametrize("name, state", [
117 @pytest.mark.parametrize("name, state", [
116 ('Disabled', False),
118 ('Disabled', False),
@@ -125,266 +127,8 b' class TestHomeController(TestController)'
125 Session().commit()
127 Session().commit()
126 SettingsModel().invalidate_settings_cache()
128 SettingsModel().invalidate_settings_cache()
127
129
128 response = self.app.get(url(controller='home', action='index'))
130 response = self.app.get(route_path('home'))
129 if state is True:
131 if state is True:
130 response.mustcontain(version_string)
132 response.mustcontain(version_string)
131 if state is False:
133 if state is False:
132 response.mustcontain(no=[version_string])
134 response.mustcontain(no=[version_string])
133
134
135 class TestUserAutocompleteData(TestController):
136 def test_returns_list_of_users(self, user_util):
137 self.log_user()
138 user = user_util.create_user(is_active=True)
139 user_name = user.username
140 response = self.app.get(
141 url(controller='home', action='user_autocomplete_data'),
142 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
143 result = json.loads(response.body)
144 values = [suggestion['value'] for suggestion in result['suggestions']]
145 assert user_name in values
146
147 def test_returns_inactive_users_when_active_flag_sent(self, user_util):
148 self.log_user()
149 user = user_util.create_user(is_active=False)
150 user_name = user.username
151 response = self.app.get(
152 url(controller='home', action='user_autocomplete_data',
153 user_groups='true', active='0'),
154 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
155 result = json.loads(response.body)
156 values = [suggestion['value'] for suggestion in result['suggestions']]
157 assert user_name in values
158
159 def test_returns_groups_when_user_groups_sent(self, user_util):
160 self.log_user()
161 group = user_util.create_user_group(user_groups_active=True)
162 group_name = group.users_group_name
163 response = self.app.get(
164 url(controller='home', action='user_autocomplete_data',
165 user_groups='true'),
166 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
167 result = json.loads(response.body)
168 values = [suggestion['value'] for suggestion in result['suggestions']]
169 assert group_name in values
170
171 def test_result_is_limited_when_query_is_sent(self):
172 self.log_user()
173 fake_result = [
174 {
175 'first_name': 'John',
176 'value_display': 'hello{} (John Smith)'.format(i),
177 'icon_link': '/images/user14.png',
178 'value': 'hello{}'.format(i),
179 'last_name': 'Smith',
180 'username': 'hello{}'.format(i),
181 'id': i,
182 'value_type': u'user'
183 }
184 for i in range(10)
185 ]
186 users_patcher = patch.object(
187 RepoModel, 'get_users', return_value=fake_result)
188 groups_patcher = patch.object(
189 RepoModel, 'get_user_groups', return_value=fake_result)
190
191 query = 'hello'
192 with users_patcher as users_mock, groups_patcher as groups_mock:
193 response = self.app.get(
194 url(controller='home', action='user_autocomplete_data',
195 user_groups='true', query=query),
196 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
197
198 result = json.loads(response.body)
199 users_mock.assert_called_once_with(
200 name_contains=query, only_active=True)
201 groups_mock.assert_called_once_with(
202 name_contains=query, only_active=True)
203 assert len(result['suggestions']) == 20
204
205
206 def assert_and_get_content(result):
207 repos = []
208 groups = []
209 commits = []
210 for data in result:
211 for data_item in data['children']:
212 assert data_item['id']
213 assert data_item['text']
214 assert data_item['url']
215 if data_item['type'] == 'repo':
216 repos.append(data_item)
217 elif data_item['type'] == 'group':
218 groups.append(data_item)
219 elif data_item['type'] == 'commit':
220 commits.append(data_item)
221 else:
222 raise Exception('invalid type %s' % data_item['type'])
223
224 return repos, groups, commits
225
226
227 class TestGotoSwitcherData(TestController):
228 required_repos_with_groups = [
229 'abc',
230 'abc-fork',
231 'forks/abcd',
232 'abcd',
233 'abcde',
234 'a/abc',
235 'aa/abc',
236 'aaa/abc',
237 'aaaa/abc',
238 'repos_abc/aaa/abc',
239 'abc_repos/abc',
240 'abc_repos/abcd',
241 'xxx/xyz',
242 'forked-abc/a/abc'
243 ]
244
245 @pytest.fixture(autouse=True, scope='class')
246 def prepare(self, request, pylonsapp):
247 for repo_and_group in self.required_repos_with_groups:
248 # create structure of groups and return the last group
249
250 repo_group = map_groups(repo_and_group)
251
252 RepoModel()._create_repo(
253 repo_and_group, 'hg', 'test-ac', TEST_USER_ADMIN_LOGIN,
254 repo_group=getattr(repo_group, 'group_id', None))
255
256 Session().commit()
257
258 request.addfinalizer(self.cleanup)
259
260 def cleanup(self):
261 # first delete all repos
262 for repo_and_groups in self.required_repos_with_groups:
263 repo = Repository.get_by_repo_name(repo_and_groups)
264 if repo:
265 RepoModel().delete(repo)
266 Session().commit()
267
268 # then delete all empty groups
269 for repo_and_groups in self.required_repos_with_groups:
270 if '/' in repo_and_groups:
271 r_group = repo_and_groups.rsplit('/', 1)[0]
272 repo_group = RepoGroup.get_by_group_name(r_group)
273 if not repo_group:
274 continue
275 parents = repo_group.parents
276 RepoGroupModel().delete(repo_group, force_delete=True)
277 Session().commit()
278
279 for el in reversed(parents):
280 RepoGroupModel().delete(el, force_delete=True)
281 Session().commit()
282
283 def test_returns_list_of_repos_and_groups(self):
284 self.log_user()
285
286 response = self.app.get(
287 url(controller='home', action='goto_switcher_data'),
288 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
289 result = json.loads(response.body)['results']
290
291 repos, groups, commits = assert_and_get_content(result)
292
293 assert len(repos) == len(Repository.get_all())
294 assert len(groups) == len(RepoGroup.get_all())
295 assert len(commits) == 0
296
297 def test_returns_list_of_repos_and_groups_filtered(self):
298 self.log_user()
299
300 response = self.app.get(
301 url(controller='home', action='goto_switcher_data'),
302 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
303 params={'query': 'abc'}, status=200)
304 result = json.loads(response.body)['results']
305
306 repos, groups, commits = assert_and_get_content(result)
307
308 assert len(repos) == 13
309 assert len(groups) == 5
310 assert len(commits) == 0
311
312 def test_returns_list_of_properly_sorted_and_filtered(self):
313 self.log_user()
314
315 response = self.app.get(
316 url(controller='home', action='goto_switcher_data'),
317 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
318 params={'query': 'abc'}, status=200)
319 result = json.loads(response.body)['results']
320
321 repos, groups, commits = assert_and_get_content(result)
322
323 test_repos = [x['text'] for x in repos[:4]]
324 assert ['abc', 'abcd', 'a/abc', 'abcde'] == test_repos
325
326 test_groups = [x['text'] for x in groups[:4]]
327 assert ['abc_repos', 'repos_abc',
328 'forked-abc', 'forked-abc/a'] == test_groups
329
330
331 class TestRepoListData(TestController):
332 def test_returns_list_of_repos_and_groups(self, user_util):
333 self.log_user()
334
335 response = self.app.get(
336 url(controller='home', action='repo_list_data'),
337 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
338 result = json.loads(response.body)['results']
339
340 repos, groups, commits = assert_and_get_content(result)
341
342 assert len(repos) == len(Repository.get_all())
343 assert len(groups) == 0
344 assert len(commits) == 0
345
346 def test_returns_list_of_repos_and_groups_filtered(self):
347 self.log_user()
348
349 response = self.app.get(
350 url(controller='home', action='repo_list_data'),
351 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
352 params={'query': 'vcs_test_git'}, status=200)
353 result = json.loads(response.body)['results']
354
355 repos, groups, commits = assert_and_get_content(result)
356
357 assert len(repos) == len(Repository.query().filter(
358 Repository.repo_name.ilike('%vcs_test_git%')).all())
359 assert len(groups) == 0
360 assert len(commits) == 0
361
362 def test_returns_list_of_repos_and_groups_filtered_with_type(self):
363 self.log_user()
364
365 response = self.app.get(
366 url(controller='home', action='repo_list_data'),
367 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
368 params={'query': 'vcs_test_git', 'repo_type': 'git'}, status=200)
369 result = json.loads(response.body)['results']
370
371 repos, groups, commits = assert_and_get_content(result)
372
373 assert len(repos) == len(Repository.query().filter(
374 Repository.repo_name.ilike('%vcs_test_git%')).all())
375 assert len(groups) == 0
376 assert len(commits) == 0
377
378 def test_returns_list_of_repos_non_ascii_query(self):
379 self.log_user()
380 response = self.app.get(
381 url(controller='home', action='repo_list_data'),
382 headers={'X-REQUESTED-WITH': 'XMLHttpRequest', },
383 params={'query': 'ć_vcs_test_ą', 'repo_type': 'git'}, status=200)
384 result = json.loads(response.body)['results']
385
386 repos, groups, commits = assert_and_get_content(result)
387
388 assert len(repos) == 0
389 assert len(groups) == 0
390 assert len(commits) == 0
@@ -25,7 +25,6 b' import formencode'
25 import logging
25 import logging
26 import urlparse
26 import urlparse
27
27
28 from pylons import url
29 from pyramid.httpexceptions import HTTPFound
28 from pyramid.httpexceptions import HTTPFound
30 from pyramid.view import view_config
29 from pyramid.view import view_config
31 from recaptcha.client.captcha import submit
30 from recaptcha.client.captcha import submit
@@ -34,6 +33,7 b' from rhodecode.apps._base import BaseApp'
34 from rhodecode.authentication.base import authenticate, HTTP_TYPE
33 from rhodecode.authentication.base import authenticate, HTTP_TYPE
35 from rhodecode.events import UserRegistered
34 from rhodecode.events import UserRegistered
36 from rhodecode.lib import helpers as h
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib import audit_logger
37 from rhodecode.lib.auth import (
37 from rhodecode.lib.auth import (
38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
38 AuthUser, HasPermissionAnyDecorator, CSRFRequired)
39 from rhodecode.lib.base import get_ip_addr
39 from rhodecode.lib.base import get_ip_addr
@@ -90,20 +90,21 b' def get_came_from(request):'
90 came_from = safe_str(request.GET.get('came_from', ''))
90 came_from = safe_str(request.GET.get('came_from', ''))
91 parsed = urlparse.urlparse(came_from)
91 parsed = urlparse.urlparse(came_from)
92 allowed_schemes = ['http', 'https']
92 allowed_schemes = ['http', 'https']
93 default_came_from = h.route_path('home')
93 if parsed.scheme and parsed.scheme not in allowed_schemes:
94 if parsed.scheme and parsed.scheme not in allowed_schemes:
94 log.error('Suspicious URL scheme detected %s for url %s' %
95 log.error('Suspicious URL scheme detected %s for url %s' %
95 (parsed.scheme, parsed))
96 (parsed.scheme, parsed))
96 came_from = url('home')
97 came_from = default_came_from
97 elif parsed.netloc and request.host != parsed.netloc:
98 elif parsed.netloc and request.host != parsed.netloc:
98 log.error('Suspicious NETLOC detected %s for url %s server url '
99 log.error('Suspicious NETLOC detected %s for url %s server url '
99 'is: %s' % (parsed.netloc, parsed, request.host))
100 'is: %s' % (parsed.netloc, parsed, request.host))
100 came_from = url('home')
101 came_from = default_came_from
101 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
102 elif any(bad_str in parsed.path for bad_str in ('\r', '\n')):
102 log.error('Header injection detected `%s` for url %s server url ' %
103 log.error('Header injection detected `%s` for url %s server url ' %
103 (parsed.path, parsed))
104 (parsed.path, parsed))
104 came_from = url('home')
105 came_from = default_came_from
105
106
106 return came_from or url('home')
107 return came_from or default_came_from
107
108
108
109
109 class LoginView(BaseAppView):
110 class LoginView(BaseAppView):
@@ -166,6 +167,15 b' class LoginView(BaseAppView):'
166 username=form_result['username'],
167 username=form_result['username'],
167 remember=form_result['remember'])
168 remember=form_result['remember'])
168 log.debug('Redirecting to "%s" after login.', c.came_from)
169 log.debug('Redirecting to "%s" after login.', c.came_from)
170
171 audit_user = audit_logger.UserWrap(
172 username=self.request.params.get('username'),
173 ip_addr=self.request.remote_addr)
174 action_data = {'user_agent': self.request.user_agent}
175 audit_logger.store_web(
176 'user.login.success', action_data=action_data,
177 user=audit_user, commit=True)
178
169 raise HTTPFound(c.came_from, headers=headers)
179 raise HTTPFound(c.came_from, headers=headers)
170 except formencode.Invalid as errors:
180 except formencode.Invalid as errors:
171 defaults = errors.value
181 defaults = errors.value
@@ -176,6 +186,14 b' class LoginView(BaseAppView):'
176 'errors': errors.error_dict,
186 'errors': errors.error_dict,
177 'defaults': defaults,
187 'defaults': defaults,
178 })
188 })
189
190 audit_user = audit_logger.UserWrap(
191 username=self.request.params.get('username'),
192 ip_addr=self.request.remote_addr)
193 action_data = {'user_agent': self.request.user_agent}
194 audit_logger.store_web(
195 'user.login.failure', action_data=action_data,
196 user=audit_user, commit=True)
179 return render_ctx
197 return render_ctx
180
198
181 except UserCreationError as e:
199 except UserCreationError as e:
@@ -191,8 +209,13 b' class LoginView(BaseAppView):'
191 def logout(self):
209 def logout(self):
192 auth_user = self._rhodecode_user
210 auth_user = self._rhodecode_user
193 log.info('Deleting session for user: `%s`', auth_user)
211 log.info('Deleting session for user: `%s`', auth_user)
212
213 action_data = {'user_agent': self.request.user_agent}
214 audit_logger.store_web(
215 'user.logout', action_data=action_data,
216 user=auth_user, commit=True)
194 self.session.delete()
217 self.session.delete()
195 return HTTPFound(url('home'))
218 return HTTPFound(h.route_path('home'))
196
219
197 @HasPermissionAnyDecorator(
220 @HasPermissionAnyDecorator(
198 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
221 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate')
@@ -338,6 +361,12 b' class LoginView(BaseAppView):'
338 form_result, password_reset_url)
361 form_result, password_reset_url)
339 # Display success message and redirect.
362 # Display success message and redirect.
340 self.session.flash(msg, queue='success')
363 self.session.flash(msg, queue='success')
364
365 action_data = {'email': user_email,
366 'user_agent': self.request.user_agent}
367 audit_logger.store_web(
368 'user.password.reset_request', action_data=action_data,
369 user=self._rhodecode_user, commit=True)
341 return HTTPFound(self.request.route_path('reset_password'))
370 return HTTPFound(self.request.route_path('reset_password'))
342
371
343 except formencode.Invalid as errors:
372 except formencode.Invalid as errors:
@@ -41,12 +41,45 b' def includeme(config):'
41 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
41 pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
42 config.add_route(
42 config.add_route(
43 name='my_account_auth_tokens_add',
43 name='my_account_auth_tokens_add',
44 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new',
44 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new')
45 )
46 config.add_route(
45 config.add_route(
47 name='my_account_auth_tokens_delete',
46 name='my_account_auth_tokens_delete',
48 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete',
47 pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete')
49 )
48
49 config.add_route(
50 name='my_account_emails',
51 pattern=ADMIN_PREFIX + '/my_account/emails')
52 config.add_route(
53 name='my_account_emails_add',
54 pattern=ADMIN_PREFIX + '/my_account/emails/new')
55 config.add_route(
56 name='my_account_emails_delete',
57 pattern=ADMIN_PREFIX + '/my_account/emails/delete')
58
59 config.add_route(
60 name='my_account_repos',
61 pattern=ADMIN_PREFIX + '/my_account/repos')
62
63 config.add_route(
64 name='my_account_watched',
65 pattern=ADMIN_PREFIX + '/my_account/watched')
66
67 config.add_route(
68 name='my_account_perms',
69 pattern=ADMIN_PREFIX + '/my_account/perms')
70
71 config.add_route(
72 name='my_account_notifications',
73 pattern=ADMIN_PREFIX + '/my_account/notifications')
74
75 config.add_route(
76 name='my_account_notifications_toggle_visibility',
77 pattern=ADMIN_PREFIX + '/my_account/toggle_visibility')
78
79 # channelstream test
80 config.add_route(
81 name='my_account_notifications_test_channelstream',
82 pattern=ADMIN_PREFIX + '/my_account/test_channelstream')
50
83
51 # Scan module for configuration decorators.
84 # Scan module for configuration decorators.
52 config.scan()
85 config.scan()
@@ -103,7 +103,7 b' class TestMyAccountAuthTokens(TestContro'
103
103
104 response = self.app.post(
104 response = self.app.post(
105 route_path('my_account_auth_tokens_delete'),
105 route_path('my_account_auth_tokens_delete'),
106 {'del_auth_token': keys[0].api_key, 'csrf_token': self.csrf_token})
106 {'del_auth_token': keys[0].user_api_key_id, 'csrf_token': self.csrf_token})
107 assert_session_flash(response, 'Auth token successfully deleted')
107 assert_session_flash(response, 'Auth token successfully deleted')
108
108
109 user = User.get(user_id)
109 user = User.get(user_id)
@@ -132,6 +132,7 b' class TestMyAccountPassword(TestControll'
132 self.app.post(route_path('my_account_password'), form_data)
132 self.app.post(route_path('my_account_password'), form_data)
133
133
134 response = self.app.get(route_path('home'))
134 response = self.app.get(route_path('home'))
135 new_password_hash = response.session['rhodecode_user']['password']
135 session = response.get_session_from_response()
136 new_password_hash = session['rhodecode_user']['password']
136
137
137 assert old_password_hash != new_password_hash No newline at end of file
138 assert old_password_hash != new_password_hash
@@ -19,18 +19,28 b''
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import datetime
22
23
24 import formencode
23 from pyramid.httpexceptions import HTTPFound
25 from pyramid.httpexceptions import HTTPFound
24 from pyramid.view import view_config
26 from pyramid.view import view_config
25
27
26 from rhodecode.apps._base import BaseAppView
28 from rhodecode.apps._base import BaseAppView
27 from rhodecode import forms
29 from rhodecode import forms
30 from rhodecode.lib import helpers as h
31 from rhodecode.lib import audit_logger
32 from rhodecode.lib.ext_json import json
28 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
33 from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
29 from rhodecode.lib import helpers as h
34 from rhodecode.lib.channelstream import channelstream_request, \
35 ChannelstreamException
30 from rhodecode.lib.utils2 import safe_int, md5
36 from rhodecode.lib.utils2 import safe_int, md5
31 from rhodecode.model.auth_token import AuthTokenModel
37 from rhodecode.model.auth_token import AuthTokenModel
38 from rhodecode.model.db import (
39 Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload)
32 from rhodecode.model.meta import Session
40 from rhodecode.model.meta import Session
41 from rhodecode.model.scm import RepoList
33 from rhodecode.model.user import UserModel
42 from rhodecode.model.user import UserModel
43 from rhodecode.model.repo import RepoModel
34 from rhodecode.model.validation_schema.schemas import user_schema
44 from rhodecode.model.validation_schema.schemas import user_schema
35
45
36 log = logging.getLogger(__name__)
46 log = logging.getLogger(__name__)
@@ -158,7 +168,7 b' class MyAccountView(BaseAppView):'
158 @NotAnonymous()
168 @NotAnonymous()
159 @CSRFRequired()
169 @CSRFRequired()
160 @view_config(
170 @view_config(
161 route_name='my_account_auth_tokens_add', request_method='POST')
171 route_name='my_account_auth_tokens_add', request_method='POST',)
162 def my_account_auth_tokens_add(self):
172 def my_account_auth_tokens_add(self):
163 _ = self.request.translate
173 _ = self.request.translate
164 c = self.load_default_context()
174 c = self.load_default_context()
@@ -169,7 +179,13 b' class MyAccountView(BaseAppView):'
169
179
170 token = AuthTokenModel().create(
180 token = AuthTokenModel().create(
171 c.user.user_id, description, lifetime, role)
181 c.user.user_id, description, lifetime, role)
182 token_data = token.get_api_data()
183
172 self.maybe_attach_token_scope(token)
184 self.maybe_attach_token_scope(token)
185 audit_logger.store_web(
186 'user.edit.token.add', action_data={
187 'data': {'token': token_data, 'user': 'self'}},
188 user=self._rhodecode_user, )
173 Session().commit()
189 Session().commit()
174
190
175 h.flash(_("Auth token successfully created"), category='success')
191 h.flash(_("Auth token successfully created"), category='success')
@@ -187,8 +203,197 b' class MyAccountView(BaseAppView):'
187 del_auth_token = self.request.POST.get('del_auth_token')
203 del_auth_token = self.request.POST.get('del_auth_token')
188
204
189 if del_auth_token:
205 if del_auth_token:
206 token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
207 token_data = token.get_api_data()
208
190 AuthTokenModel().delete(del_auth_token, c.user.user_id)
209 AuthTokenModel().delete(del_auth_token, c.user.user_id)
210 audit_logger.store_web(
211 'user.edit.token.delete', action_data={
212 'data': {'token': token_data, 'user': 'self'}},
213 user=self._rhodecode_user,)
191 Session().commit()
214 Session().commit()
192 h.flash(_("Auth token successfully deleted"), category='success')
215 h.flash(_("Auth token successfully deleted"), category='success')
193
216
194 return HTTPFound(h.route_path('my_account_auth_tokens'))
217 return HTTPFound(h.route_path('my_account_auth_tokens'))
218
219 @LoginRequired()
220 @NotAnonymous()
221 @view_config(
222 route_name='my_account_emails', request_method='GET',
223 renderer='rhodecode:templates/admin/my_account/my_account.mako')
224 def my_account_emails(self):
225 _ = self.request.translate
226
227 c = self.load_default_context()
228 c.active = 'emails'
229
230 c.user_email_map = UserEmailMap.query()\
231 .filter(UserEmailMap.user == c.user).all()
232 return self._get_template_context(c)
233
234 @LoginRequired()
235 @NotAnonymous()
236 @CSRFRequired()
237 @view_config(
238 route_name='my_account_emails_add', request_method='POST')
239 def my_account_emails_add(self):
240 _ = self.request.translate
241 c = self.load_default_context()
242
243 email = self.request.POST.get('new_email')
244
245 try:
246 UserModel().add_extra_email(c.user.user_id, email)
247 audit_logger.store_web(
248 'user.edit.email.add', action_data={
249 'data': {'email': email, 'user': 'self'}},
250 user=self._rhodecode_user,)
251
252 Session().commit()
253 h.flash(_("Added new email address `%s` for user account") % email,
254 category='success')
255 except formencode.Invalid as error:
256 h.flash(h.escape(error.error_dict['email']), category='error')
257 except Exception:
258 log.exception("Exception in my_account_emails")
259 h.flash(_('An error occurred during email saving'),
260 category='error')
261 return HTTPFound(h.route_path('my_account_emails'))
262
263 @LoginRequired()
264 @NotAnonymous()
265 @CSRFRequired()
266 @view_config(
267 route_name='my_account_emails_delete', request_method='POST')
268 def my_account_emails_delete(self):
269 _ = self.request.translate
270 c = self.load_default_context()
271
272 del_email_id = self.request.POST.get('del_email_id')
273 if del_email_id:
274 email = UserEmailMap.get_or_404(del_email_id, pyramid_exc=True).email
275 UserModel().delete_extra_email(c.user.user_id, del_email_id)
276 audit_logger.store_web(
277 'user.edit.email.delete', action_data={
278 'data': {'email': email, 'user': 'self'}},
279 user=self._rhodecode_user,)
280 Session().commit()
281 h.flash(_("Email successfully deleted"),
282 category='success')
283 return HTTPFound(h.route_path('my_account_emails'))
284
285 @LoginRequired()
286 @NotAnonymous()
287 @CSRFRequired()
288 @view_config(
289 route_name='my_account_notifications_test_channelstream',
290 request_method='POST', renderer='json_ext')
291 def my_account_notifications_test_channelstream(self):
292 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
293 self._rhodecode_user.username, datetime.datetime.now())
294 payload = {
295 # 'channel': 'broadcast',
296 'type': 'message',
297 'timestamp': datetime.datetime.utcnow(),
298 'user': 'system',
299 'pm_users': [self._rhodecode_user.username],
300 'message': {
301 'message': message,
302 'level': 'info',
303 'topic': '/notifications'
304 }
305 }
306
307 registry = self.request.registry
308 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
309 channelstream_config = rhodecode_plugins.get('channelstream', {})
310
311 try:
312 channelstream_request(channelstream_config, [payload], '/message')
313 except ChannelstreamException as e:
314 log.exception('Failed to send channelstream data')
315 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
316 return {"response": 'Channelstream data sent. '
317 'You should see a new live message now.'}
318
319 def _load_my_repos_data(self, watched=False):
320 if watched:
321 admin = False
322 follows_repos = Session().query(UserFollowing)\
323 .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
324 .options(joinedload(UserFollowing.follows_repository))\
325 .all()
326 repo_list = [x.follows_repository for x in follows_repos]
327 else:
328 admin = True
329 repo_list = Repository.get_all_repos(
330 user_id=self._rhodecode_user.user_id)
331 repo_list = RepoList(repo_list, perm_set=[
332 'repository.read', 'repository.write', 'repository.admin'])
333
334 repos_data = RepoModel().get_repos_as_dict(
335 repo_list=repo_list, admin=admin)
336 # json used to render the grid
337 return json.dumps(repos_data)
338
339 @LoginRequired()
340 @NotAnonymous()
341 @view_config(
342 route_name='my_account_repos', request_method='GET',
343 renderer='rhodecode:templates/admin/my_account/my_account.mako')
344 def my_account_repos(self):
345 c = self.load_default_context()
346 c.active = 'repos'
347
348 # json used to render the grid
349 c.data = self._load_my_repos_data()
350 return self._get_template_context(c)
351
352 @LoginRequired()
353 @NotAnonymous()
354 @view_config(
355 route_name='my_account_watched', request_method='GET',
356 renderer='rhodecode:templates/admin/my_account/my_account.mako')
357 def my_account_watched(self):
358 c = self.load_default_context()
359 c.active = 'watched'
360
361 # json used to render the grid
362 c.data = self._load_my_repos_data(watched=True)
363 return self._get_template_context(c)
364
365 @LoginRequired()
366 @NotAnonymous()
367 @view_config(
368 route_name='my_account_perms', request_method='GET',
369 renderer='rhodecode:templates/admin/my_account/my_account.mako')
370 def my_account_perms(self):
371 c = self.load_default_context()
372 c.active = 'perms'
373
374 c.perm_user = c.auth_user
375 return self._get_template_context(c)
376
377 @LoginRequired()
378 @NotAnonymous()
379 @view_config(
380 route_name='my_account_notifications', request_method='GET',
381 renderer='rhodecode:templates/admin/my_account/my_account.mako')
382 def my_notifications(self):
383 c = self.load_default_context()
384 c.active = 'notifications'
385
386 return self._get_template_context(c)
387
388 @LoginRequired()
389 @NotAnonymous()
390 @CSRFRequired()
391 @view_config(
392 route_name='my_account_notifications_toggle_visibility',
393 request_method='POST', renderer='json_ext')
394 def my_notifications_toggle_visibility(self):
395 user = self._rhodecode_db_user
396 new_status = not user.user_data.get('notification_status', True)
397 user.update_userdata(notification_status=new_status)
398 Session().commit()
399 return user.user_data['notification_status'] No newline at end of file
@@ -17,30 +17,137 b''
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import add_route_with_slash
20
21
21
22
22 def includeme(config):
23 def includeme(config):
23
24
25 # Summary
26 # NOTE(marcink): one additional route is defined in very bottom, catch
27 # all pattern
28 config.add_route(
29 name='repo_summary_explicit',
30 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
31 config.add_route(
32 name='repo_summary_commits',
33 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
34
35 # repo commits
36 config.add_route(
37 name='repo_commit',
38 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
39
40 # refs data
41 config.add_route(
42 name='repo_refs_data',
43 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
44
45 config.add_route(
46 name='repo_refs_changelog_data',
47 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
48
49 config.add_route(
50 name='repo_stats',
51 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
52
53 # Tags
54 config.add_route(
55 name='tags_home',
56 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
57
58 # Branches
59 config.add_route(
60 name='branches_home',
61 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
62
63 config.add_route(
64 name='bookmarks_home',
65 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
66
67 # Pull Requests
68 config.add_route(
69 name='pullrequest_show',
70 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
71 repo_route=True)
72
73 config.add_route(
74 name='pullrequest_show_all',
75 pattern='/{repo_name:.*?[^/]}/pull-request',
76 repo_route=True, repo_accepted_types=['hg', 'git'])
77
78 config.add_route(
79 name='pullrequest_show_all_data',
80 pattern='/{repo_name:.*?[^/]}/pull-request-data',
81 repo_route=True, repo_accepted_types=['hg', 'git'])
82
83 # Settings
84 config.add_route(
85 name='edit_repo',
86 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
87
88 # Settings advanced
89 config.add_route(
90 name='edit_repo_advanced',
91 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
92 config.add_route(
93 name='edit_repo_advanced_delete',
94 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
95 config.add_route(
96 name='edit_repo_advanced_locking',
97 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
98 config.add_route(
99 name='edit_repo_advanced_journal',
100 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
101 config.add_route(
102 name='edit_repo_advanced_fork',
103 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
104
105 # Caches
106 config.add_route(
107 name='edit_repo_caches',
108 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
109
110 # Permissions
111 config.add_route(
112 name='edit_repo_perms',
113 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
114
115 # Repo Review Rules
116 config.add_route(
117 name='repo_reviewers',
118 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
119
120 config.add_route(
121 name='repo_default_reviewers_data',
122 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
123
124 # Maintenance
24 config.add_route(
125 config.add_route(
25 name='repo_maintenance',
126 name='repo_maintenance',
26 pattern='/{repo_name:.*?[^/]}/maintenance', repo_route=True)
127 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
27
128
28 config.add_route(
129 config.add_route(
29 name='repo_maintenance_execute',
130 name='repo_maintenance_execute',
30 pattern='/{repo_name:.*?[^/]}/maintenance/execute', repo_route=True)
131 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
31
32
132
33 # Strip
133 # Strip
34 config.add_route(
134 config.add_route(
35 name='strip',
135 name='strip',
36 pattern='/{repo_name:.*?[^/]}/strip', repo_route=True)
136 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
37
137
38 config.add_route(
138 config.add_route(
39 name='strip_check',
139 name='strip_check',
40 pattern='/{repo_name:.*?[^/]}/strip_check', repo_route=True)
140 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
41
141
42 config.add_route(
142 config.add_route(
43 name='strip_execute',
143 name='strip_execute',
44 pattern='/{repo_name:.*?[^/]}/strip_execute', repo_route=True)
144 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
145
146 # NOTE(marcink): needs to be at the end for catch-all
147 add_route_with_slash(
148 config,
149 name='repo_summary',
150 pattern='/{repo_name:.*?[^/]}', repo_route=True)
151
45 # Scan module for configuration decorators.
152 # Scan module for configuration decorators.
46 config.scan()
153 config.scan()
@@ -18,24 +18,35 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 from rhodecode.model.db import Repository
22 from rhodecode.model.db import Repository
22 from rhodecode.tests import *
23
23
24
24
25 class TestBookmarksController(TestController):
25 def route_path(name, params=None, **kwargs):
26 import urllib
27
28 base_url = {
29 'bookmarks_home': '/{repo_name}/bookmarks',
30 }[name].format(**kwargs)
31
32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
34 return base_url
35
36
37 @pytest.mark.usefixtures('autologin_user', 'app')
38 class TestBookmarks(object):
26
39
27 def test_index(self, backend):
40 def test_index(self, backend):
28 self.log_user()
29 if backend.alias == 'hg':
41 if backend.alias == 'hg':
30 response = self.app.get(url(controller='bookmarks',
42 response = self.app.get(
31 action='index',
43 route_path('bookmarks_home', repo_name=backend.repo_name))
32 repo_name=backend.repo_name))
33
44
34 repo = Repository.get_by_repo_name(backend.repo_name)
45 repo = Repository.get_by_repo_name(backend.repo_name)
35 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
46 for commit_id, obj_name in repo.scm_instance().bookmarks.items():
36 assert commit_id in response
47 assert commit_id in response
37 assert obj_name in response
48 assert obj_name in response
38 else:
49 else:
39 self.app.get(url(controller='bookmarks',
50 self.app.get(
40 action='index',
51 route_path('bookmarks_home', repo_name=backend.repo_name),
41 repo_name=backend.repo_name), status=404) No newline at end of file
52 status=404)
@@ -18,17 +18,28 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 from rhodecode.model.db import Repository
22 from rhodecode.model.db import Repository
22 from rhodecode.tests import *
23
23
24
24
25 class TestBranchesController(TestController):
25 def route_path(name, params=None, **kwargs):
26 import urllib
27
28 base_url = {
29 'branches_home': '/{repo_name}/branches',
30 }[name].format(**kwargs)
31
32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
34 return base_url
35
36
37 @pytest.mark.usefixtures('autologin_user', 'app')
38 class TestBranchesController(object):
26
39
27 def test_index(self, backend):
40 def test_index(self, backend):
28 self.log_user()
41 response = self.app.get(
29 response = self.app.get(url(controller='branches',
42 route_path('branches_home', repo_name=backend.repo_name))
30 action='index',
31 repo_name=backend.repo_name))
32
43
33 repo = Repository.get_by_repo_name(backend.repo_name)
44 repo = Repository.get_by_repo_name(backend.repo_name)
34
45
@@ -23,16 +23,16 b' import re'
23 import mock
23 import mock
24 import pytest
24 import pytest
25
25
26 from rhodecode.controllers import summary
26 from rhodecode.apps.repository.views.repo_summary import RepoSummaryView
27 from rhodecode.lib import helpers as h
27 from rhodecode.lib import helpers as h
28 from rhodecode.lib.compat import OrderedDict
28 from rhodecode.lib.compat import OrderedDict
29 from rhodecode.lib.utils2 import AttributeDict
29 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
30 from rhodecode.model.db import Repository
31 from rhodecode.model.db import Repository
31 from rhodecode.model.meta import Session
32 from rhodecode.model.meta import Session
32 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.repo import RepoModel
33 from rhodecode.model.scm import ScmModel
34 from rhodecode.model.scm import ScmModel
34 from rhodecode.tests import (
35 from rhodecode.tests import assert_session_flash
35 TestController, url, HG_REPO, assert_session_flash)
36 from rhodecode.tests.fixture import Fixture
36 from rhodecode.tests.fixture import Fixture
37 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
37 from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
38
38
@@ -40,14 +40,31 b' from rhodecode.tests.utils import Assert'
40 fixture = Fixture()
40 fixture = Fixture()
41
41
42
42
43 class TestSummaryController(TestController):
43 def route_path(name, params=None, **kwargs):
44 def test_index(self, backend):
44 import urllib
45 self.log_user()
45
46 base_url = {
47 'repo_summary': '/{repo_name}',
48 'repo_stats': '/{repo_name}/repo_stats/{commit_id}',
49 'repo_refs_data': '/{repo_name}/refs-data',
50 'repo_refs_changelog_data': '/{repo_name}/refs-data-changelog'
51
52 }[name].format(**kwargs)
53
54 if params:
55 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
56 return base_url
57
58
59 @pytest.mark.usefixtures('app')
60 class TestSummaryView(object):
61 def test_index(self, autologin_user, backend, http_host_only_stub):
46 repo_id = backend.repo.repo_id
62 repo_id = backend.repo.repo_id
47 repo_name = backend.repo_name
63 repo_name = backend.repo_name
48 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
64 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
49 return_value=False):
65 return_value=False):
50 response = self.app.get(url('summary_home', repo_name=repo_name))
66 response = self.app.get(
67 route_path('repo_summary', repo_name=repo_name))
51
68
52 # repo type
69 # repo type
53 response.mustcontain(
70 response.mustcontain(
@@ -61,46 +78,47 b' class TestSummaryController(TestControll'
61 # clone url...
78 # clone url...
62 response.mustcontain(
79 response.mustcontain(
63 'id="clone_url" readonly="readonly"'
80 'id="clone_url" readonly="readonly"'
64 ' value="http://test_admin@test.example.com:80/%s"' % (repo_name, ))
81 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
65 response.mustcontain(
82 response.mustcontain(
66 'id="clone_url_id" readonly="readonly"'
83 'id="clone_url_id" readonly="readonly"'
67 ' value="http://test_admin@test.example.com:80/_%s"' % (repo_id, ))
84 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
68
85
69 def test_index_svn_without_proxy(self, backend_svn):
86 def test_index_svn_without_proxy(
70 self.log_user()
87 self, autologin_user, backend_svn, http_host_only_stub):
71 repo_id = backend_svn.repo.repo_id
88 repo_id = backend_svn.repo.repo_id
72 repo_name = backend_svn.repo_name
89 repo_name = backend_svn.repo_name
73 response = self.app.get(url('summary_home', repo_name=repo_name))
90 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
74 # clone url...
91 # clone url...
75 response.mustcontain(
92 response.mustcontain(
76 'id="clone_url" disabled'
93 'id="clone_url" disabled'
77 ' value="http://test_admin@test.example.com:80/%s"' % (repo_name, ))
94 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
78 response.mustcontain(
95 response.mustcontain(
79 'id="clone_url_id" disabled'
96 'id="clone_url_id" disabled'
80 ' value="http://test_admin@test.example.com:80/_%s"' % (repo_id, ))
97 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
81
98
82 def test_index_with_trailing_slash(self, autologin_user, backend):
99 def test_index_with_trailing_slash(
100 self, autologin_user, backend, http_host_only_stub):
101
83 repo_id = backend.repo.repo_id
102 repo_id = backend.repo.repo_id
84 repo_name = backend.repo_name
103 repo_name = backend.repo_name
85 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
104 with mock.patch('rhodecode.lib.helpers.is_svn_without_proxy',
86 return_value=False):
105 return_value=False):
87 response = self.app.get(
106 response = self.app.get(
88 url('summary_home', repo_name=repo_name) + '/',
107 route_path('repo_summary', repo_name=repo_name) + '/',
89 status=200)
108 status=200)
90
109
91 # clone url...
110 # clone url...
92 response.mustcontain(
111 response.mustcontain(
93 'id="clone_url" readonly="readonly"'
112 'id="clone_url" readonly="readonly"'
94 ' value="http://test_admin@test.example.com:80/%s"' % (repo_name, ))
113 ' value="http://test_admin@%s/%s"' % (http_host_only_stub, repo_name, ))
95 response.mustcontain(
114 response.mustcontain(
96 'id="clone_url_id" readonly="readonly"'
115 'id="clone_url_id" readonly="readonly"'
97 ' value="http://test_admin@test.example.com:80/_%s"' % (repo_id, ))
116 ' value="http://test_admin@%s/_%s"' % (http_host_only_stub, repo_id, ))
98
117
99 def test_index_by_id(self, backend):
118 def test_index_by_id(self, autologin_user, backend):
100 self.log_user()
101 repo_id = backend.repo.repo_id
119 repo_id = backend.repo.repo_id
102 response = self.app.get(url(
120 response = self.app.get(
103 'summary_home', repo_name='_%s' % (repo_id,)))
121 route_path('repo_summary', repo_name='_%s' % (repo_id,)))
104
122
105 # repo type
123 # repo type
106 response.mustcontain(
124 response.mustcontain(
@@ -111,10 +129,9 b' class TestSummaryController(TestControll'
111 """<i class="icon-unlock-alt">"""
129 """<i class="icon-unlock-alt">"""
112 )
130 )
113
131
114 def test_index_by_repo_having_id_path_in_name_hg(self):
132 def test_index_by_repo_having_id_path_in_name_hg(self, autologin_user):
115 self.log_user()
116 fixture.create_repo(name='repo_1')
133 fixture.create_repo(name='repo_1')
117 response = self.app.get(url('summary_home', repo_name='repo_1'))
134 response = self.app.get(route_path('repo_summary', repo_name='repo_1'))
118
135
119 try:
136 try:
120 response.mustcontain("repo_1")
137 response.mustcontain("repo_1")
@@ -122,11 +139,11 b' class TestSummaryController(TestControll'
122 RepoModel().delete(Repository.get_by_repo_name('repo_1'))
139 RepoModel().delete(Repository.get_by_repo_name('repo_1'))
123 Session().commit()
140 Session().commit()
124
141
125 def test_index_with_anonymous_access_disabled(self):
142 def test_index_with_anonymous_access_disabled(
126 with fixture.anon_access(False):
143 self, backend, disable_anonymous_user):
127 response = self.app.get(url('summary_home', repo_name=HG_REPO),
144 response = self.app.get(
128 status=302)
145 route_path('repo_summary', repo_name=backend.repo_name), status=302)
129 assert 'login' in response.location
146 assert 'login' in response.location
130
147
131 def _enable_stats(self, repo):
148 def _enable_stats(self, repo):
132 r = Repository.get_by_repo_name(repo)
149 r = Repository.get_by_repo_name(repo)
@@ -173,17 +190,15 b' class TestSummaryController(TestControll'
173 },
190 },
174 }
191 }
175
192
176 def test_repo_stats(self, backend, xhr_header):
193 def test_repo_stats(self, autologin_user, backend, xhr_header):
177 self.log_user()
178 response = self.app.get(
194 response = self.app.get(
179 url('repo_stats',
195 route_path(
180 repo_name=backend.repo_name, commit_id='tip'),
196 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
181 extra_environ=xhr_header,
197 extra_environ=xhr_header,
182 status=200)
198 status=200)
183 assert re.match(r'6[\d\.]+ KiB', response.json['size'])
199 assert re.match(r'6[\d\.]+ KiB', response.json['size'])
184
200
185 def test_repo_stats_code_stats_enabled(self, backend, xhr_header):
201 def test_repo_stats_code_stats_enabled(self, autologin_user, backend, xhr_header):
186 self.log_user()
187 repo_name = backend.repo_name
202 repo_name = backend.repo_name
188
203
189 # codes stats
204 # codes stats
@@ -191,8 +206,8 b' class TestSummaryController(TestControll'
191 ScmModel().mark_for_invalidation(repo_name)
206 ScmModel().mark_for_invalidation(repo_name)
192
207
193 response = self.app.get(
208 response = self.app.get(
194 url('repo_stats',
209 route_path(
195 repo_name=backend.repo_name, commit_id='tip'),
210 'repo_stats', repo_name=backend.repo_name, commit_id='tip'),
196 extra_environ=xhr_header,
211 extra_environ=xhr_header,
197 status=200)
212 status=200)
198
213
@@ -203,7 +218,7 b' class TestSummaryController(TestControll'
203
218
204 def test_repo_refs_data(self, backend):
219 def test_repo_refs_data(self, backend):
205 response = self.app.get(
220 response = self.app.get(
206 url('repo_refs_data', repo_name=backend.repo_name),
221 route_path('repo_refs_data', repo_name=backend.repo_name),
207 status=200)
222 status=200)
208
223
209 # Ensure that there is the correct amount of items in the result
224 # Ensure that there is the correct amount of items in the result
@@ -220,72 +235,68 b' class TestSummaryController(TestControll'
220 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
235 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
221
236
222 with scm_patcher:
237 with scm_patcher:
223 response = self.app.get(url('summary_home', repo_name=repo_name))
238 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
224 assert_response = AssertResponse(response)
239 assert_response = AssertResponse(response)
225 assert_response.element_contains(
240 assert_response.element_contains(
226 '.main .alert-warning strong', 'Missing requirements')
241 '.main .alert-warning strong', 'Missing requirements')
227 assert_response.element_contains(
242 assert_response.element_contains(
228 '.main .alert-warning',
243 '.main .alert-warning',
229 'These commits cannot be displayed, because this repository'
244 'Commits cannot be displayed, because this repository '
230 ' uses the Mercurial largefiles extension, which was not enabled.')
245 'uses one or more extensions, which was not enabled.')
231
246
232 def test_missing_requirements_page_does_not_contains_switch_to(
247 def test_missing_requirements_page_does_not_contains_switch_to(
233 self, backend):
248 self, autologin_user, backend):
234 self.log_user()
235 repo_name = backend.repo_name
249 repo_name = backend.repo_name
236 scm_patcher = mock.patch.object(
250 scm_patcher = mock.patch.object(
237 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
251 Repository, 'scm_instance', side_effect=RepositoryRequirementError)
238
252
239 with scm_patcher:
253 with scm_patcher:
240 response = self.app.get(url('summary_home', repo_name=repo_name))
254 response = self.app.get(route_path('repo_summary', repo_name=repo_name))
241 response.mustcontain(no='Switch To')
255 response.mustcontain(no='Switch To')
242
256
243
257
244 @pytest.mark.usefixtures('pylonsapp')
258 @pytest.mark.usefixtures('app')
245 class TestSwitcherReferenceData:
259 class TestRepoLocation(object):
246
260
247 def test_creates_reference_urls_based_on_name(self):
261 @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
248 references = {
262 def test_manual_delete(self, autologin_user, backend, suffix, csrf_token):
249 'name': 'commit_id',
263 repo = backend.create_repo(name_suffix=suffix)
250 }
264 repo_name = repo.repo_name
251 controller = summary.SummaryController()
265
252 is_svn = False
266 # delete from file system
253 result = controller._switcher_reference_data(
267 RepoModel()._delete_filesystem_repo(repo)
254 'repo_name', references, is_svn)
255 expected_url = h.url(
256 'files_home', repo_name='repo_name', revision='name',
257 at='name')
258 assert result[0]['files_url'] == expected_url
259
268
260 def test_urls_contain_commit_id_if_slash_in_name(self):
269 # test if the repo is still in the database
261 references = {
270 new_repo = RepoModel().get_by_repo_name(repo_name)
262 'name/with/slash': 'commit_id',
271 assert new_repo.repo_name == repo_name
263 }
264 controller = summary.SummaryController()
265 is_svn = False
266 result = controller._switcher_reference_data(
267 'repo_name', references, is_svn)
268 expected_url = h.url(
269 'files_home', repo_name='repo_name', revision='commit_id',
270 at='name/with/slash')
271 assert result[0]['files_url'] == expected_url
272
272
273 def test_adds_reference_to_path_for_svn(self):
273 # check if repo is not in the filesystem
274 references = {
274 assert not repo_on_filesystem(repo_name)
275 'name/with/slash': 'commit_id',
275 self.assert_repo_not_found_redirect(repo_name)
276 }
276
277 controller = summary.SummaryController()
277 def assert_repo_not_found_redirect(self, repo_name):
278 is_svn = True
278 # run the check page that triggers the other flash message
279 result = controller._switcher_reference_data(
279 response = self.app.get(h.url('repo_check_home', repo_name=repo_name))
280 'repo_name', references, is_svn)
280 assert_session_flash(
281 expected_url = h.url(
281 response, 'The repository at %s cannot be located.' % repo_name)
282 'files_home', repo_name='repo_name', f_path='name/with/slash',
283 revision='commit_id', at='name/with/slash')
284 assert result[0]['files_url'] == expected_url
285
282
286
283
287 @pytest.mark.usefixtures('pylonsapp')
284 @pytest.fixture()
288 class TestCreateReferenceData:
285 def summary_view(context_stub, request_stub, user_util):
286 """
287 Bootstrap view to test the view functions
288 """
289 request_stub.matched_route = AttributeDict(name='test_view')
290
291 request_stub.user = user_util.create_user().AuthUser
292 request_stub.db_repo = user_util.create_repo()
293
294 view = RepoSummaryView(context=context_stub, request=request_stub)
295 return view
296
297
298 @pytest.mark.usefixtures('app')
299 class TestCreateReferenceData(object):
289
300
290 @pytest.fixture
301 @pytest.fixture
291 def example_refs(self):
302 def example_refs(self):
@@ -296,14 +307,13 b' class TestCreateReferenceData:'
296 ]
307 ]
297 return example_refs
308 return example_refs
298
309
299 def test_generates_refs_based_on_commit_ids(self, example_refs):
310 def test_generates_refs_based_on_commit_ids(self, example_refs, summary_view):
300 repo = mock.Mock()
311 repo = mock.Mock()
301 repo.name = 'test-repo'
312 repo.name = 'test-repo'
302 repo.alias = 'git'
313 repo.alias = 'git'
303 full_repo_name = 'pytest-repo-group/' + repo.name
314 full_repo_name = 'pytest-repo-group/' + repo.name
304 controller = summary.SummaryController()
305
315
306 result = controller._create_reference_data(
316 result = summary_view._create_reference_data(
307 repo, full_repo_name, example_refs)
317 repo, full_repo_name, example_refs)
308
318
309 expected_files_url = '/{}/files/'.format(full_repo_name)
319 expected_files_url = '/{}/files/'.format(full_repo_name)
@@ -332,13 +342,13 b' class TestCreateReferenceData:'
332 }]
342 }]
333 assert result == expected_result
343 assert result == expected_result
334
344
335 def test_generates_refs_with_path_for_svn(self, example_refs):
345 def test_generates_refs_with_path_for_svn(self, example_refs, summary_view):
336 repo = mock.Mock()
346 repo = mock.Mock()
337 repo.name = 'test-repo'
347 repo.name = 'test-repo'
338 repo.alias = 'svn'
348 repo.alias = 'svn'
339 full_repo_name = 'pytest-repo-group/' + repo.name
349 full_repo_name = 'pytest-repo-group/' + repo.name
340 controller = summary.SummaryController()
350
341 result = controller._create_reference_data(
351 result = summary_view._create_reference_data(
342 repo, full_repo_name, example_refs)
352 repo, full_repo_name, example_refs)
343
353
344 expected_files_url = '/{}/files/'.format(full_repo_name)
354 expected_files_url = '/{}/files/'.format(full_repo_name)
@@ -372,35 +382,9 b' class TestCreateReferenceData:'
372 assert result == expected_result
382 assert result == expected_result
373
383
374
384
375 @pytest.mark.usefixtures("app")
385 class TestCreateFilesUrl(object):
376 class TestRepoLocation:
377
378 @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii'])
379 def test_manual_delete(self, autologin_user, backend, suffix, csrf_token):
380 repo = backend.create_repo(name_suffix=suffix)
381 repo_name = repo.repo_name
382
383 # delete from file system
384 RepoModel()._delete_filesystem_repo(repo)
385
386 # test if the repo is still in the database
387 new_repo = RepoModel().get_by_repo_name(repo_name)
388 assert new_repo.repo_name == repo_name
389
386
390 # check if repo is not in the filesystem
387 def test_creates_non_svn_url(self, summary_view):
391 assert not repo_on_filesystem(repo_name)
392 self.assert_repo_not_found_redirect(repo_name)
393
394 def assert_repo_not_found_redirect(self, repo_name):
395 # run the check page that triggers the other flash message
396 response = self.app.get(url('repo_check_home', repo_name=repo_name))
397 assert_session_flash(
398 response, 'The repository at %s cannot be located.' % repo_name)
399
400
401 class TestCreateFilesUrl(object):
402 def test_creates_non_svn_url(self):
403 controller = summary.SummaryController()
404 repo = mock.Mock()
388 repo = mock.Mock()
405 repo.name = 'abcde'
389 repo.name = 'abcde'
406 full_repo_name = 'test-repo-group/' + repo.name
390 full_repo_name = 'test-repo-group/' + repo.name
@@ -408,16 +392,15 b' class TestCreateFilesUrl(object):'
408 raw_id = 'deadbeef0123456789'
392 raw_id = 'deadbeef0123456789'
409 is_svn = False
393 is_svn = False
410
394
411 with mock.patch.object(summary.h, 'url') as url_mock:
395 with mock.patch('rhodecode.lib.helpers.url') as url_mock:
412 result = controller._create_files_url(
396 result = summary_view._create_files_url(
413 repo, full_repo_name, ref_name, raw_id, is_svn)
397 repo, full_repo_name, ref_name, raw_id, is_svn)
414 url_mock.assert_called_once_with(
398 url_mock.assert_called_once_with(
415 'files_home', repo_name=full_repo_name, f_path='',
399 'files_home', repo_name=full_repo_name, f_path='',
416 revision=ref_name, at=ref_name)
400 revision=ref_name, at=ref_name)
417 assert result == url_mock.return_value
401 assert result == url_mock.return_value
418
402
419 def test_creates_svn_url(self):
403 def test_creates_svn_url(self, summary_view):
420 controller = summary.SummaryController()
421 repo = mock.Mock()
404 repo = mock.Mock()
422 repo.name = 'abcde'
405 repo.name = 'abcde'
423 full_repo_name = 'test-repo-group/' + repo.name
406 full_repo_name = 'test-repo-group/' + repo.name
@@ -425,16 +408,15 b' class TestCreateFilesUrl(object):'
425 raw_id = 'deadbeef0123456789'
408 raw_id = 'deadbeef0123456789'
426 is_svn = True
409 is_svn = True
427
410
428 with mock.patch.object(summary.h, 'url') as url_mock:
411 with mock.patch('rhodecode.lib.helpers.url') as url_mock:
429 result = controller._create_files_url(
412 result = summary_view._create_files_url(
430 repo, full_repo_name, ref_name, raw_id, is_svn)
413 repo, full_repo_name, ref_name, raw_id, is_svn)
431 url_mock.assert_called_once_with(
414 url_mock.assert_called_once_with(
432 'files_home', repo_name=full_repo_name, f_path=ref_name,
415 'files_home', repo_name=full_repo_name, f_path=ref_name,
433 revision=raw_id, at=ref_name)
416 revision=raw_id, at=ref_name)
434 assert result == url_mock.return_value
417 assert result == url_mock.return_value
435
418
436 def test_name_has_slashes(self):
419 def test_name_has_slashes(self, summary_view):
437 controller = summary.SummaryController()
438 repo = mock.Mock()
420 repo = mock.Mock()
439 repo.name = 'abcde'
421 repo.name = 'abcde'
440 full_repo_name = 'test-repo-group/' + repo.name
422 full_repo_name = 'test-repo-group/' + repo.name
@@ -442,8 +424,8 b' class TestCreateFilesUrl(object):'
442 raw_id = 'deadbeef0123456789'
424 raw_id = 'deadbeef0123456789'
443 is_svn = False
425 is_svn = False
444
426
445 with mock.patch.object(summary.h, 'url') as url_mock:
427 with mock.patch('rhodecode.lib.helpers.url') as url_mock:
446 result = controller._create_files_url(
428 result = summary_view._create_files_url(
447 repo, full_repo_name, ref_name, raw_id, is_svn)
429 repo, full_repo_name, ref_name, raw_id, is_svn)
448 url_mock.assert_called_once_with(
430 url_mock.assert_called_once_with(
449 'files_home', repo_name=full_repo_name, f_path='', revision=raw_id,
431 'files_home', repo_name=full_repo_name, f_path='', revision=raw_id,
@@ -462,42 +444,39 b' class TestReferenceItems(object):'
462 def _format_function(name, id_):
444 def _format_function(name, id_):
463 return 'format_function_{}_{}'.format(name, id_)
445 return 'format_function_{}_{}'.format(name, id_)
464
446
465 def test_creates_required_amount_of_items(self):
447 def test_creates_required_amount_of_items(self, summary_view):
466 amount = 100
448 amount = 100
467 refs = {
449 refs = {
468 'ref{}'.format(i): '{0:040d}'.format(i)
450 'ref{}'.format(i): '{0:040d}'.format(i)
469 for i in range(amount)
451 for i in range(amount)
470 }
452 }
471
453
472 controller = summary.SummaryController()
454 url_patcher = mock.patch.object(summary_view, '_create_files_url')
473
455 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
474 url_patcher = mock.patch.object(
456 return_value=False)
475 controller, '_create_files_url')
476 svn_patcher = mock.patch.object(
477 summary.h, 'is_svn', return_value=False)
478
457
479 with url_patcher as url_mock, svn_patcher:
458 with url_patcher as url_mock, svn_patcher:
480 result = controller._create_reference_items(
459 result = summary_view._create_reference_items(
481 self.repo, self.repo_full_name, refs, self.ref_type,
460 self.repo, self.repo_full_name, refs, self.ref_type,
482 self._format_function)
461 self._format_function)
483 assert len(result) == amount
462 assert len(result) == amount
484 assert url_mock.call_count == amount
463 assert url_mock.call_count == amount
485
464
486 def test_single_item_details(self):
465 def test_single_item_details(self, summary_view):
487 ref_name = 'ref1'
466 ref_name = 'ref1'
488 ref_id = 'deadbeef'
467 ref_id = 'deadbeef'
489 refs = {
468 refs = {
490 ref_name: ref_id
469 ref_name: ref_id
491 }
470 }
492
471
493 controller = summary.SummaryController()
472 svn_patcher = mock.patch('rhodecode.lib.helpers.is_svn',
473 return_value=False)
474
494 url_patcher = mock.patch.object(
475 url_patcher = mock.patch.object(
495 controller, '_create_files_url', return_value=self.fake_url)
476 summary_view, '_create_files_url', return_value=self.fake_url)
496 svn_patcher = mock.patch.object(
497 summary.h, 'is_svn', return_value=False)
498
477
499 with url_patcher as url_mock, svn_patcher:
478 with url_patcher as url_mock, svn_patcher:
500 result = controller._create_reference_items(
479 result = summary_view._create_reference_items(
501 self.repo, self.repo_full_name, refs, self.ref_type,
480 self.repo, self.repo_full_name, refs, self.ref_type,
502 self._format_function)
481 self._format_function)
503
482
@@ -18,16 +18,27 b''
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import pytest
21 from rhodecode.model.db import Repository
22 from rhodecode.model.db import Repository
22 from rhodecode.tests import *
23
23
24
24
25 class TestTagsController(TestController):
25 def route_path(name, params=None, **kwargs):
26 import urllib
27
28 base_url = {
29 'tags_home': '/{repo_name}/tags',
30 }[name].format(**kwargs)
31
32 if params:
33 base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
34 return base_url
35
36
37 @pytest.mark.usefixtures('autologin_user', 'app')
38 class TestTagsController(object):
26 def test_index(self, backend):
39 def test_index(self, backend):
27 self.log_user()
40 response = self.app.get(
28 response = self.app.get(url(controller='tags',
41 route_path('tags_home', repo_name=backend.repo_name))
29 action='index',
30 repo_name=backend.repo_name))
31
42
32 repo = Repository.get_by_repo_name(backend.repo_name)
43 repo = Repository.get_by_repo_name(backend.repo_name)
33
44
@@ -41,7 +41,6 b' class RepoMaintenanceView(RepoAppView):'
41 return c
41 return c
42
42
43 @LoginRequired()
43 @LoginRequired()
44 @NotAnonymous()
45 @HasRepoPermissionAnyDecorator('repository.admin')
44 @HasRepoPermissionAnyDecorator('repository.admin')
46 @view_config(
45 @view_config(
47 route_name='repo_maintenance', request_method='GET',
46 route_name='repo_maintenance', request_method='GET',
@@ -54,7 +53,6 b' class RepoMaintenanceView(RepoAppView):'
54 return self._get_template_context(c)
53 return self._get_template_context(c)
55
54
56 @LoginRequired()
55 @LoginRequired()
57 @NotAnonymous()
58 @HasRepoPermissionAnyDecorator('repository.admin')
56 @HasRepoPermissionAnyDecorator('repository.admin')
59 @view_config(
57 @view_config(
60 route_name='repo_maintenance_execute', request_method='GET',
58 route_name='repo_maintenance_execute', request_method='GET',
@@ -1,6 +1,6 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2017 RhodeCode GmbH
3 # Copyright (C) 2017-2017 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
@@ -22,9 +22,11 b' import logging'
22 from pyramid.view import view_config
22 from pyramid.view import view_config
23
23
24 from rhodecode.apps._base import RepoAppView
24 from rhodecode.apps._base import RepoAppView
25 from rhodecode.lib import audit_logger
26 from rhodecode.lib import helpers as h
25 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator,
27 from rhodecode.lib.auth import (LoginRequired, HasRepoPermissionAnyDecorator,
26 NotAnonymous)
28 NotAnonymous, CSRFRequired)
27
29 from rhodecode.lib.ext_json import json
28
30
29 log = logging.getLogger(__name__)
31 log = logging.getLogger(__name__)
30
32
@@ -40,7 +42,6 b' class StripView(RepoAppView):'
40 return c
42 return c
41
43
42 @LoginRequired()
44 @LoginRequired()
43 @NotAnonymous()
44 @HasRepoPermissionAnyDecorator('repository.admin')
45 @HasRepoPermissionAnyDecorator('repository.admin')
45 @view_config(
46 @view_config(
46 route_name='strip', request_method='GET',
47 route_name='strip', request_method='GET',
@@ -53,41 +54,39 b' class StripView(RepoAppView):'
53 return self._get_template_context(c)
54 return self._get_template_context(c)
54
55
55 @LoginRequired()
56 @LoginRequired()
56 @NotAnonymous()
57 @HasRepoPermissionAnyDecorator('repository.admin')
57 @HasRepoPermissionAnyDecorator('repository.admin')
58 @CSRFRequired()
58 @view_config(
59 @view_config(
59 route_name='strip_check', request_method='POST',
60 route_name='strip_check', request_method='POST',
60 renderer='json', xhr=True
61 renderer='json', xhr=True)
61 )
62 def strip_check(self):
62 def strip_check(self):
63 from rhodecode.lib.vcs.backends.base import EmptyCommit
63 from rhodecode.lib.vcs.backends.base import EmptyCommit
64 data = {}
64 data = {}
65 rp = self.request.POST
65 rp = self.request.POST
66 for i in range(1, 11):
66 for i in range(1, 11):
67 chset = 'changeset_id-%d'%(i,)
67 chset = 'changeset_id-%d' % (i,)
68 check = rp.get(chset)
68 check = rp.get(chset)
69
69 if check:
70 if check:
70 data[i] = self.db_repo.get_changeset(rp[chset])
71 data[i] = self.db_repo.get_changeset(rp[chset])
71 if isinstance(data[i], EmptyCommit):
72 if isinstance(data[i], EmptyCommit):
72 data[i] = {'rev': None, 'commit': rp[chset]}
73 data[i] = {'rev': None, 'commit': h.escape(rp[chset])}
73 else:
74 else:
74 data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch, 'author': data[i].author,
75 data[i] = {'rev': data[i].raw_id, 'branch': data[i].branch,
76 'author': data[i].author,
75 'comment': data[i].message}
77 'comment': data[i].message}
76 else:
78 else:
77 break
79 break
78 return data
80 return data
79
81
80 @LoginRequired()
82 @LoginRequired()
81 @NotAnonymous()
82 @HasRepoPermissionAnyDecorator('repository.admin')
83 @HasRepoPermissionAnyDecorator('repository.admin')
84 @CSRFRequired()
83 @view_config(
85 @view_config(
84 route_name='strip_execute', request_method='POST',
86 route_name='strip_execute', request_method='POST',
85 renderer='json', xhr=True
87 renderer='json', xhr=True)
86 )
87 def strip_execute(self):
88 def strip_execute(self):
88
89 from rhodecode.model.scm import ScmModel
89 from rhodecode.model.scm import ScmModel
90 from rhodecode.lib.ext_json import json
91
90
92 c = self.load_default_context()
91 c = self.load_default_context()
93 user = self._rhodecode_user
92 user = self._rhodecode_user
@@ -95,16 +94,23 b' class StripView(RepoAppView):'
95 data = {}
94 data = {}
96 for idx in rp:
95 for idx in rp:
97 commit = json.loads(rp[idx])
96 commit = json.loads(rp[idx])
98 #If someone put two times the same branch
97 # If someone put two times the same branch
99 if commit['branch'] in data.keys():
98 if commit['branch'] in data.keys():
100 continue
99 continue
101 try:
100 try:
102 ScmModel().strip(repo=c.repo_info,
101 ScmModel().strip(
103 commit_id=commit['rev'], branch=commit['branch'])
102 repo=c.repo_info,
104 log.info('Stripped commit %s from repo `%s` by %s' % (commit['rev'], c.repo_info.repo_name, user))
103 commit_id=commit['rev'], branch=commit['branch'])
104 log.info('Stripped commit %s from repo `%s` by %s' % (
105 commit['rev'], c.repo_info.repo_name, user))
105 data[commit['rev']] = True
106 data[commit['rev']] = True
106 except Exception, e:
107
108 audit_logger.store_web(
109 'repo.commit.strip', action_data={'commit_id': commit['rev']},
110 repo=self.db_repo, user=self._rhodecode_user, commit=True)
111
112 except Exception as e:
107 data[commit['rev']] = False
113 data[commit['rev']] = False
108 log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % (commit['rev'],
114 log.debug('Stripped commit %s from repo `%s` failed by %s, exeption %s' % (
109 c.repo_info.repo_name, user, e.message))
115 commit['rev'], self.db_repo_name, user, e.message))
110 return data
116 return data
@@ -564,8 +564,6 b' def authenticate(username, password, env'
564 for plugin in authn_registry.get_plugins_for_authentication():
564 for plugin in authn_registry.get_plugins_for_authentication():
565 plugin.set_auth_type(auth_type)
565 plugin.set_auth_type(auth_type)
566 plugin.set_calling_scope_repo(acl_repo_name)
566 plugin.set_calling_scope_repo(acl_repo_name)
567 user = plugin.get_user(username)
568 display_user = user.username if user else username
569
567
570 if headers_only and not plugin.is_headers_auth:
568 if headers_only and not plugin.is_headers_auth:
571 log.debug('Auth type is for headers only and plugin `%s` is not '
569 log.debug('Auth type is for headers only and plugin `%s` is not '
@@ -71,15 +71,17 b' class LdapSettingsSchema(AuthnPluginSett'
71 host = colander.SchemaNode(
71 host = colander.SchemaNode(
72 colander.String(),
72 colander.String(),
73 default='',
73 default='',
74 description=_('Host of the LDAP Server \n'
74 description=_('Host[s] of the LDAP Server \n'
75 '(e.g., 192.168.2.154, or ldap-server.domain.com'),
75 '(e.g., 192.168.2.154, or ldap-server.domain.com.\n '
76 'Multiple servers can be specified using commas'),
76 preparer=strip_whitespace,
77 preparer=strip_whitespace,
77 title=_('LDAP Host'),
78 title=_('LDAP Host'),
78 widget='string')
79 widget='string')
79 port = colander.SchemaNode(
80 port = colander.SchemaNode(
80 colander.Int(),
81 colander.Int(),
81 default=389,
82 default=389,
82 description=_('Custom port that the LDAP server is listening on. Default: 389'),
83 description=_('Custom port that the LDAP server is listening on. '
84 'Default value is: 389'),
83 preparer=strip_whitespace,
85 preparer=strip_whitespace,
84 title=_('Port'),
86 title=_('Port'),
85 validator=colander.Range(min=0, max=65536),
87 validator=colander.Range(min=0, max=65536),
@@ -112,7 +114,9 b' class LdapSettingsSchema(AuthnPluginSett'
112 tls_reqcert = colander.SchemaNode(
114 tls_reqcert = colander.SchemaNode(
113 colander.String(),
115 colander.String(),
114 default=tls_reqcert_choices[0],
116 default=tls_reqcert_choices[0],
115 description=_('Require Cert over TLS?'),
117 description=_('Require Cert over TLS?. Self-signed and custom '
118 'certificates can be used when\n `RhodeCode Certificate` '
119 'found in admin > settings > system info page is extended.'),
116 title=_('Certificate Checks'),
120 title=_('Certificate Checks'),
117 validator=colander.OneOf(tls_reqcert_choices),
121 validator=colander.OneOf(tls_reqcert_choices),
118 widget='select')
122 widget='select')
@@ -1,11 +1,11 b''
1 {
1 {
2 "nodejs-4.3.1": {
2 "libnghttp2-1.7.1": {
3 "MIT License": "http://spdx.org/licenses/MIT"
3 "MIT License": "http://spdx.org/licenses/MIT"
4 },
4 },
5 "postgresql-9.5.1": {
5 "nodejs-4.3.1": {
6 "PostgreSQL License": "http://spdx.org/licenses/PostgreSQL"
6 "MIT License": "http://spdx.org/licenses/MIT"
7 },
7 },
8 "python-2.7.11": {
8 "python-2.7.12": {
9 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0"
9 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0"
10 },
10 },
11 "python2.7-Babel-1.3": {
11 "python2.7-Babel-1.3": {
@@ -14,19 +14,25 b''
14 "python2.7-Beaker-1.7.0": {
14 "python2.7-Beaker-1.7.0": {
15 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
15 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
16 },
16 },
17 "python2.7-Chameleon-2.24": {
18 "BSD-like": "http://repoze.org/license.html"
19 },
17 "python2.7-FormEncode-1.2.4": {
20 "python2.7-FormEncode-1.2.4": {
18 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0"
21 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0"
19 },
22 },
20 "python2.7-Mako-1.0.1": {
23 "python2.7-Jinja2-2.7.3": {
24 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
25 },
26 "python2.7-Mako-1.0.6": {
21 "MIT License": "http://spdx.org/licenses/MIT"
27 "MIT License": "http://spdx.org/licenses/MIT"
22 },
28 },
23 "python2.7-Markdown-2.6.2": {
29 "python2.7-Markdown-2.6.7": {
24 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
30 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
25 },
31 },
26 "python2.7-MarkupSafe-0.23": {
32 "python2.7-MarkupSafe-0.23": {
27 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
33 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
28 },
34 },
29 "python2.7-Paste-2.0.2": {
35 "python2.7-Paste-2.0.3": {
30 "MIT License": "http://spdx.org/licenses/MIT"
36 "MIT License": "http://spdx.org/licenses/MIT"
31 },
37 },
32 "python2.7-PasteDeploy-1.5.2": {
38 "python2.7-PasteDeploy-1.5.2": {
@@ -35,12 +41,12 b''
35 "python2.7-PasteScript-1.7.5": {
41 "python2.7-PasteScript-1.7.5": {
36 "MIT License": "http://spdx.org/licenses/MIT"
42 "MIT License": "http://spdx.org/licenses/MIT"
37 },
43 },
38 "python2.7-Pygments-2.0.2": {
44 "python2.7-Pygments-2.2.0": {
39 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
45 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
40 },
46 },
41 "python2.7-Pylons-1.0.1-patch1": {
47 "python2.7-Pylons-1.0.2.rhodecode-patch1": {
42 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
48 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
43 },
49 },
44 "python2.7-Routes-1.13": {
50 "python2.7-Routes-1.13": {
45 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
51 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
46 },
52 },
@@ -65,7 +71,7 b''
65 "python2.7-WebOb-1.3.1": {
71 "python2.7-WebOb-1.3.1": {
66 "MIT License": "http://spdx.org/licenses/MIT"
72 "MIT License": "http://spdx.org/licenses/MIT"
67 },
73 },
68 "python2.7-Whoosh-2.7.0": {
74 "python2.7-Whoosh-2.7.4": {
69 "BSD 2-clause \"Simplified\" License": "http://spdx.org/licenses/BSD-2-Clause",
75 "BSD 2-clause \"Simplified\" License": "http://spdx.org/licenses/BSD-2-Clause",
70 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
76 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
71 },
77 },
@@ -87,9 +93,18 b''
87 "python2.7-backport-ipaddress-0.1": {
93 "python2.7-backport-ipaddress-0.1": {
88 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0"
94 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0"
89 },
95 },
96 "python2.7-backports.shutil-get-terminal-size-1.0.0": {
97 "MIT License": "http://spdx.org/licenses/MIT"
98 },
99 "python2.7-bleach-1.5.0": {
100 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
101 },
90 "python2.7-celery-2.2.10": {
102 "python2.7-celery-2.2.10": {
91 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
103 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
92 },
104 },
105 "python2.7-channelstream-0.5.2": {
106 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
107 },
93 "python2.7-click-5.1": {
108 "python2.7-click-5.1": {
94 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
109 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
95 },
110 },
@@ -99,82 +114,169 b''
99 "python2.7-configobj-5.0.6": {
114 "python2.7-configobj-5.0.6": {
100 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
115 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
101 },
116 },
102 "python2.7-cssselect-0.9.1": {
117 "python2.7-configparser-3.5.0": {
118 "MIT License": "http://spdx.org/licenses/MIT"
119 },
120 "python2.7-cssselect-1.0.1": {
103 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
121 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
104 },
122 },
105 "python2.7-decorator-3.4.2": {
123 "python2.7-decorator-4.0.11": {
106 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
124 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
107 },
125 },
126 "python2.7-deform-2.0a2": {
127 "BSD-derived": "http://www.repoze.org/LICENSE.txt"
128 },
108 "python2.7-docutils-0.12": {
129 "python2.7-docutils-0.12": {
109 "BSD 2-clause \"Simplified\" License": "http://spdx.org/licenses/BSD-2-Clause"
130 "BSD 2-clause \"Simplified\" License": "http://spdx.org/licenses/BSD-2-Clause"
110 },
131 },
132 "python2.7-dogpile.cache-0.6.1": {
133 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
134 },
135 "python2.7-dogpile.core-0.4.1": {
136 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
137 },
111 "python2.7-elasticsearch-2.3.0": {
138 "python2.7-elasticsearch-2.3.0": {
112 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
139 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
113 },
140 },
114 "python2.7-elasticsearch-dsl-2.0.0": {
141 "python2.7-elasticsearch-dsl-2.2.0": {
115 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
142 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
116 },
143 },
144 "python2.7-entrypoints-0.2.2": {
145 "MIT License": "http://spdx.org/licenses/MIT"
146 },
147 "python2.7-enum34-1.1.6": {
148 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
149 },
150 "python2.7-functools32-3.2.3.post2": {
151 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0"
152 },
117 "python2.7-future-0.14.3": {
153 "python2.7-future-0.14.3": {
118 "MIT License": "http://spdx.org/licenses/MIT"
154 "MIT License": "http://spdx.org/licenses/MIT"
119 },
155 },
120 "python2.7-futures-3.0.2": {
156 "python2.7-futures-3.0.2": {
121 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
157 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
122 },
158 },
159 "python2.7-gevent-1.1.2": {
160 "MIT License": "http://spdx.org/licenses/MIT"
161 },
123 "python2.7-gnureadline-6.3.3": {
162 "python2.7-gnureadline-6.3.3": {
124 "GNU General Public License v1.0 only": "http://spdx.org/licenses/GPL-1.0"
163 "GNU General Public License v1.0 only": "http://spdx.org/licenses/GPL-1.0"
125 },
164 },
165 "python2.7-gprof2dot-2016.10.13": {
166 "GNU Lesser General Public License v3.0 or later": "http://spdx.org/licenses/LGPL-3.0+"
167 },
168 "python2.7-greenlet-0.4.10": {
169 "MIT License": "http://spdx.org/licenses/MIT"
170 },
126 "python2.7-gunicorn-19.6.0": {
171 "python2.7-gunicorn-19.6.0": {
127 "MIT License": "http://spdx.org/licenses/MIT"
172 "MIT License": "http://spdx.org/licenses/MIT"
128 },
173 },
174 "python2.7-html5lib-0.9999999": {
175 "MIT License": "http://spdx.org/licenses/MIT"
176 },
129 "python2.7-infrae.cache-1.0.1": {
177 "python2.7-infrae.cache-1.0.1": {
130 "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1"
178 "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1"
131 },
179 },
132 "python2.7-ipython-3.1.0": {
180 "python2.7-ipython-5.1.0": {
181 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
182 },
183 "python2.7-ipython-genutils-0.2.0": {
133 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
184 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
134 },
185 },
135 "python2.7-iso8601-0.1.11": {
186 "python2.7-iso8601-0.1.11": {
136 "MIT License": "http://spdx.org/licenses/MIT"
187 "MIT License": "http://spdx.org/licenses/MIT"
137 },
188 },
189 "python2.7-itsdangerous-0.24": {
190 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
191 },
192 "python2.7-jsonschema-2.6.0": {
193 "MIT License": "http://spdx.org/licenses/MIT"
194 },
195 "python2.7-jupyter-client-5.0.0": {
196 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
197 },
198 "python2.7-jupyter-core-4.3.0": {
199 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
200 },
138 "python2.7-kombu-1.5.1": {
201 "python2.7-kombu-1.5.1": {
139 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
202 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
140 },
203 },
141 "python2.7-msgpack-python-0.4.6": {
204 "python2.7-mistune-0.7.4": {
205 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
206 },
207 "python2.7-msgpack-python-0.4.8": {
142 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
208 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
143 },
209 },
210 "python2.7-nbconvert-5.1.1": {
211 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
212 },
213 "python2.7-nbformat-4.3.0": {
214 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
215 },
144 "python2.7-packaging-15.2": {
216 "python2.7-packaging-15.2": {
145 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
217 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
146 },
218 },
147 "python2.7-psutil-2.2.1": {
219 "python2.7-pandocfilters-1.4.1": {
148 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
220 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
149 },
221 },
150 "python2.7-psycopg2-2.6": {
222 "python2.7-pathlib2-2.1.0": {
223 "MIT License": "http://spdx.org/licenses/MIT"
224 },
225 "python2.7-peppercorn-0.5": {
226 "BSD-derived": "http://www.repoze.org/LICENSE.txt"
227 },
228 "python2.7-pexpect-4.2.1": {
229 "ISC License": "http://spdx.org/licenses/ISC"
230 },
231 "python2.7-pickleshare-0.7.4": {
232 "MIT License": "http://spdx.org/licenses/MIT"
233 },
234 "python2.7-prompt-toolkit-1.0.14": {
235 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
236 },
237 "python2.7-psutil-4.3.1": {
238 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
239 },
240 "python2.7-psycopg2-2.6.1": {
151 "GNU Lesser General Public License v3.0 or later": "http://spdx.org/licenses/LGPL-3.0+"
241 "GNU Lesser General Public License v3.0 or later": "http://spdx.org/licenses/LGPL-3.0+"
152 },
242 },
153 "python2.7-py-1.4.29": {
243 "python2.7-ptyprocess-0.5.1": {
244 "ISC License": "http://opensource.org/licenses/ISC"
245 },
246 "python2.7-py-1.4.31": {
154 "MIT License": "http://spdx.org/licenses/MIT"
247 "MIT License": "http://spdx.org/licenses/MIT"
155 },
248 },
156 "python2.7-py-bcrypt-0.4": {
249 "python2.7-py-bcrypt-0.4": {
157 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
250 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
158 },
251 },
252 "python2.7-py-gfm-0.1.3.rhodecode-upstream1": {
253 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
254 },
159 "python2.7-pycrypto-2.6.1": {
255 "python2.7-pycrypto-2.6.1": {
160 "Public Domain": null
256 "Public Domain": null
161 },
257 },
162 "python2.7-pycurl-7.19.5": {
258 "python2.7-pycurl-7.19.5": {
163 "MIT License": "http://spdx.org/licenses/MIT"
259 "MIT License": "http://spdx.org/licenses/MIT"
164 },
260 },
261 "python2.7-pygments-markdown-lexer-0.1.0.dev39": {
262 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
263 },
165 "python2.7-pyparsing-1.5.7": {
264 "python2.7-pyparsing-1.5.7": {
166 "MIT License": "http://spdx.org/licenses/MIT"
265 "MIT License": "http://spdx.org/licenses/MIT"
167 },
266 },
168 "python2.7-pyramid-1.6.1": {
267 "python2.7-pyramid-1.7.4": {
169 "Repoze License": "http://www.repoze.org/LICENSE.txt"
268 "Repoze License": "http://www.repoze.org/LICENSE.txt"
170 },
269 },
171 "python2.7-pyramid-beaker-0.8": {
270 "python2.7-pyramid-beaker-0.8": {
172 "Repoze License": "http://www.repoze.org/LICENSE.txt"
271 "Repoze License": "http://www.repoze.org/LICENSE.txt"
173 },
272 },
174 "python2.7-pyramid-debugtoolbar-2.4.2": {
273 "python2.7-pyramid-debugtoolbar-3.0.5": {
175 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause",
274 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause",
176 "Repoze License": "http://www.repoze.org/LICENSE.txt"
275 "Repoze License": "http://www.repoze.org/LICENSE.txt"
177 },
276 },
277 "python2.7-pyramid-jinja2-2.5": {
278 "BSD-derived": "http://www.repoze.org/LICENSE.txt"
279 },
178 "python2.7-pyramid-mako-1.0.2": {
280 "python2.7-pyramid-mako-1.0.2": {
179 "Repoze License": "http://www.repoze.org/LICENSE.txt"
281 "Repoze License": "http://www.repoze.org/LICENSE.txt"
180 },
282 },
@@ -182,16 +284,25 b''
182 "libpng License": "http://spdx.org/licenses/Libpng",
284 "libpng License": "http://spdx.org/licenses/Libpng",
183 "zlib License": "http://spdx.org/licenses/Zlib"
285 "zlib License": "http://spdx.org/licenses/Zlib"
184 },
286 },
185 "python2.7-pytest-2.8.5": {
287 "python2.7-pytest-3.0.5": {
288 "MIT License": "http://spdx.org/licenses/MIT"
289 },
290 "python2.7-pytest-profiling-1.2.2": {
291 "MIT License": "http://spdx.org/licenses/MIT"
292 },
293 "python2.7-pytest-runner-2.9": {
186 "MIT License": "http://spdx.org/licenses/MIT"
294 "MIT License": "http://spdx.org/licenses/MIT"
187 },
295 },
188 "python2.7-pytest-runner-2.7.1": {
296 "python2.7-pytest-sugar-0.7.1": {
297 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
298 },
299 "python2.7-pytest-timeout-1.2.0": {
189 "MIT License": "http://spdx.org/licenses/MIT"
300 "MIT License": "http://spdx.org/licenses/MIT"
190 },
301 },
191 "python2.7-python-dateutil-1.5": {
302 "python2.7-python-dateutil-2.1": {
192 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0"
303 "Simplified BSD": null
193 },
304 },
194 "python2.7-python-editor-1.0.1": {
305 "python2.7-python-editor-1.0.3": {
195 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
306 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
196 },
307 },
197 "python2.7-python-ldap-2.4.19": {
308 "python2.7-python-ldap-2.4.19": {
@@ -203,6 +314,9 b''
203 "python2.7-pytz-2015.4": {
314 "python2.7-pytz-2015.4": {
204 "MIT License": "http://spdx.org/licenses/MIT"
315 "MIT License": "http://spdx.org/licenses/MIT"
205 },
316 },
317 "python2.7-pyzmq-14.6.0": {
318 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
319 },
206 "python2.7-recaptcha-client-1.0.6": {
320 "python2.7-recaptcha-client-1.0.6": {
207 "MIT License": "http://spdx.org/licenses/MIT"
321 "MIT License": "http://spdx.org/licenses/MIT"
208 },
322 },
@@ -211,21 +325,35 b''
211 },
325 },
212 "python2.7-requests-2.9.1": {
326 "python2.7-requests-2.9.1": {
213 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
327 "Apache License 2.0": "http://spdx.org/licenses/Apache-2.0"
214 },
328 },
215 "python2.7-setuptools-19.4": {
329 "python2.7-setuptools-19.4": {
216 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0",
330 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0",
217 "Zope Public License 2.0": "http://spdx.org/licenses/ZPL-2.0"
331 "Zope Public License 2.0": "http://spdx.org/licenses/ZPL-2.0"
218 },
332 },
219 "python2.7-setuptools-scm-1.11.0": {
333 "python2.7-setuptools-scm-1.15.0": {
220 "MIT License": "http://spdx.org/licenses/MIT"
334 "MIT License": "http://spdx.org/licenses/MIT"
221 },
335 },
336 "python2.7-simplegeneric-0.8.1": {
337 "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1"
338 },
222 "python2.7-simplejson-3.7.2": {
339 "python2.7-simplejson-3.7.2": {
223 "Academic Free License": "http://spdx.org/licenses/AFL-2.1",
224 "MIT License": "http://spdx.org/licenses/MIT"
340 "MIT License": "http://spdx.org/licenses/MIT"
225 },
341 },
226 "python2.7-six-1.9.0": {
342 "python2.7-six-1.9.0": {
227 "MIT License": "http://spdx.org/licenses/MIT"
343 "MIT License": "http://spdx.org/licenses/MIT"
228 },
344 },
345 "python2.7-subprocess32-3.2.6": {
346 "Python Software Foundation License version 2": "http://spdx.org/licenses/Python-2.0"
347 },
348 "python2.7-termcolor-1.1.0": {
349 "MIT License": "http://spdx.org/licenses/MIT"
350 },
351 "python2.7-testpath-0.1": {
352 "MIT License": "http://spdx.org/licenses/MIT"
353 },
354 "python2.7-traitlets-4.3.2": {
355 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
356 },
229 "python2.7-translationstring-1.3": {
357 "python2.7-translationstring-1.3": {
230 "Repoze License": "http://www.repoze.org/LICENSE.txt"
358 "Repoze License": "http://www.repoze.org/LICENSE.txt"
231 },
359 },
@@ -235,9 +363,15 b''
235 "python2.7-venusian-1.0": {
363 "python2.7-venusian-1.0": {
236 "Repoze License": "http://www.repoze.org/LICENSE.txt"
364 "Repoze License": "http://www.repoze.org/LICENSE.txt"
237 },
365 },
238 "python2.7-waitress-0.8.9": {
366 "python2.7-waitress-1.0.1": {
239 "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1"
367 "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1"
240 },
368 },
369 "python2.7-wcwidth-0.1.7": {
370 "MIT License": "http://spdx.org/licenses/MIT"
371 },
372 "python2.7-ws4py-0.3.5": {
373 "BSD 4-clause \"Original\" or \"Old\" License": "http://spdx.org/licenses/BSD-4-Clause"
374 },
241 "python2.7-zope.cachedescriptors-4.0.0": {
375 "python2.7-zope.cachedescriptors-4.0.0": {
242 "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1"
376 "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1"
243 },
377 },
@@ -246,5 +380,9 b''
246 },
380 },
247 "python2.7-zope.interface-4.1.3": {
381 "python2.7-zope.interface-4.1.3": {
248 "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1"
382 "Zope Public License 2.1": "http://spdx.org/licenses/ZPL-2.1"
383 },
384 "xz-5.2.2": {
385 "GNU General Public License v2.0 or later": "http://spdx.org/licenses/GPL-2.0+",
386 "GNU Library General Public License v2.1 or later": "http://spdx.org/licenses/LGPL-2.1+"
249 }
387 }
250 } No newline at end of file
388 }
@@ -39,11 +39,15 b' from routes.middleware import RoutesMidd'
39 import routes.util
39 import routes.util
40
40
41 import rhodecode
41 import rhodecode
42
42 from rhodecode.model import meta
43 from rhodecode.model import meta
43 from rhodecode.config import patches
44 from rhodecode.config import patches
44 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 from rhodecode.config.routing import STATIC_FILE_PREFIX
45 from rhodecode.config.environment import (
46 from rhodecode.config.environment import (
46 load_environment, load_pyramid_environment)
47 load_environment, load_pyramid_environment)
48
49 from rhodecode.lib.vcs import VCSCommunicationError
50 from rhodecode.lib.exceptions import VCSServerUnavailable
47 from rhodecode.lib.middleware import csrf
51 from rhodecode.lib.middleware import csrf
48 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
52 from rhodecode.lib.middleware.appenlight import wrap_in_appenlight_if_enabled
49 from rhodecode.lib.middleware.error_handling import (
53 from rhodecode.lib.middleware.error_handling import (
@@ -51,10 +55,10 b' from rhodecode.lib.middleware.error_hand'
51 from rhodecode.lib.middleware.https_fixup import HttpsFixup
55 from rhodecode.lib.middleware.https_fixup import HttpsFixup
52 from rhodecode.lib.middleware.vcs import VCSMiddleware
56 from rhodecode.lib.middleware.vcs import VCSMiddleware
53 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
57 from rhodecode.lib.plugins.utils import register_rhodecode_plugin
54 from rhodecode.lib.utils2 import aslist as rhodecode_aslist
58 from rhodecode.lib.utils2 import aslist as rhodecode_aslist, AttributeDict
55 from rhodecode.subscribers import (
59 from rhodecode.subscribers import (
56 scan_repositories_if_enabled, write_metadata_if_needed,
60 scan_repositories_if_enabled, write_js_routes_if_enabled,
57 write_js_routes_if_enabled)
61 write_metadata_if_needed)
58
62
59
63
60 log = logging.getLogger(__name__)
64 log = logging.getLogger(__name__)
@@ -221,7 +225,7 b' def add_pylons_compat_data(registry, glo'
221
225
222 def error_handler(exception, request):
226 def error_handler(exception, request):
223 import rhodecode
227 import rhodecode
224 from rhodecode.lib.utils2 import AttributeDict
228 from rhodecode.lib import helpers
225
229
226 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
230 rhodecode_title = rhodecode.CONFIG.get('rhodecode_title') or 'RhodeCode'
227
231
@@ -229,6 +233,8 b' def error_handler(exception, request):'
229 # prefer original exception for the response since it may have headers set
233 # prefer original exception for the response since it may have headers set
230 if isinstance(exception, HTTPException):
234 if isinstance(exception, HTTPException):
231 base_response = exception
235 base_response = exception
236 elif isinstance(exception, VCSCommunicationError):
237 base_response = VCSServerUnavailable()
232
238
233 def is_http_error(response):
239 def is_http_error(response):
234 # error which should have traceback
240 # error which should have traceback
@@ -255,9 +261,10 b' def error_handler(exception, request):'
255 c.causes = []
261 c.causes = []
256 if hasattr(base_response, 'causes'):
262 if hasattr(base_response, 'causes'):
257 c.causes = base_response.causes
263 c.causes = base_response.causes
264 c.messages = helpers.flash.pop_messages()
258
265
259 response = render_to_response(
266 response = render_to_response(
260 '/errors/error_document.mako', {'c': c}, request=request,
267 '/errors/error_document.mako', {'c': c, 'h': helpers}, request=request,
261 response=base_response)
268 response=base_response)
262
269
263 return response
270 return response
@@ -284,11 +291,15 b' def includeme(config):'
284
291
285 # apps
292 # apps
286 config.include('rhodecode.apps._base')
293 config.include('rhodecode.apps._base')
294 config.include('rhodecode.apps.ops')
287
295
288 config.include('rhodecode.apps.admin')
296 config.include('rhodecode.apps.admin')
289 config.include('rhodecode.apps.channelstream')
297 config.include('rhodecode.apps.channelstream')
290 config.include('rhodecode.apps.login')
298 config.include('rhodecode.apps.login')
299 config.include('rhodecode.apps.home')
291 config.include('rhodecode.apps.repository')
300 config.include('rhodecode.apps.repository')
301 config.include('rhodecode.apps.repo_group')
302 config.include('rhodecode.apps.search')
292 config.include('rhodecode.apps.user_profile')
303 config.include('rhodecode.apps.user_profile')
293 config.include('rhodecode.apps.my_account')
304 config.include('rhodecode.apps.my_account')
294 config.include('rhodecode.apps.svn_support')
305 config.include('rhodecode.apps.svn_support')
@@ -307,6 +318,12 b' def includeme(config):'
307 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
318 config.add_subscriber(write_metadata_if_needed, ApplicationCreated)
308 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
319 config.add_subscriber(write_js_routes_if_enabled, ApplicationCreated)
309
320
321 # events
322 # TODO(marcink): this should be done when pyramid migration is finished
323 # config.add_subscriber(
324 # 'rhodecode.integrations.integrations_event_handler',
325 # 'rhodecode.events.RhodecodeEvent')
326
310 # Set the authorization policy.
327 # Set the authorization policy.
311 authz_policy = ACLAuthorizationPolicy()
328 authz_policy = ACLAuthorizationPolicy()
312 config.set_authorization_policy(authz_policy)
329 config.set_authorization_policy(authz_policy)
@@ -314,6 +331,10 b' def includeme(config):'
314 # Set the default renderer for HTML templates to mako.
331 # Set the default renderer for HTML templates to mako.
315 config.add_mako_renderer('.html')
332 config.add_mako_renderer('.html')
316
333
334 config.add_renderer(
335 name='json_ext',
336 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
337
317 # include RhodeCode plugins
338 # include RhodeCode plugins
318 includes = aslist(settings.get('rhodecode.includes', []))
339 includes = aslist(settings.get('rhodecode.includes', []))
319 for inc in includes:
340 for inc in includes:
@@ -395,7 +416,6 b' def wrap_app_in_wsgi_middlewares(pyramid'
395 pool = meta.Base.metadata.bind.engine.pool
416 pool = meta.Base.metadata.bind.engine.pool
396 log.debug('sa pool status: %s', pool.status())
417 log.debug('sa pool status: %s', pool.status())
397
418
398
399 return pyramid_app_with_cleanup
419 return pyramid_app_with_cleanup
400
420
401
421
@@ -32,8 +32,6 b' import os'
32 import re
32 import re
33 from routes import Mapper
33 from routes import Mapper
34
34
35 from rhodecode.config import routing_links
36
37 # prefix for non repository related links needs to be prefixed with `/`
35 # prefix for non repository related links needs to be prefixed with `/`
38 ADMIN_PREFIX = '/_admin'
36 ADMIN_PREFIX = '/_admin'
39 STATIC_FILE_PREFIX = '/_static'
37 STATIC_FILE_PREFIX = '/_static'
@@ -119,8 +117,9 b' class JSRoutesMapper(Mapper):'
119
117
120 def make_map(config):
118 def make_map(config):
121 """Create, configure and return the routes Mapper"""
119 """Create, configure and return the routes Mapper"""
122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
120 rmap = JSRoutesMapper(
123 always_scan=config['debug'])
121 directory=config['pylons.paths']['controllers'],
122 always_scan=config['debug'])
124 rmap.minimization = False
123 rmap.minimization = False
125 rmap.explicit = False
124 rmap.explicit = False
126
125
@@ -186,36 +185,7 b' def make_map(config):'
186 # CUSTOM ROUTES HERE
185 # CUSTOM ROUTES HERE
187 #==========================================================================
186 #==========================================================================
188
187
189 # MAIN PAGE
188 # ping and pylons error test
190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
192 action='goto_switcher_data')
193 rmap.connect('repo_list_data', '/_repos', controller='home',
194 action='repo_list_data')
195
196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
197 action='user_autocomplete_data', jsroute=True)
198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 action='user_group_autocomplete_data', jsroute=True)
200
201 # TODO: johbo: Static links, to be replaced by our redirection mechanism
202 rmap.connect('rst_help',
203 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
204 _static=True)
205 rmap.connect('markdown_help',
206 'http://daringfireball.net/projects/markdown/syntax',
207 _static=True)
208 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
209 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
210 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
211 # TODO: anderson - making this a static link since redirect won't play
212 # nice with POST requests
213 rmap.connect('enterprise_license_convert_from_old',
214 'https://rhodecode.com/u/license-upgrade',
215 _static=True)
216
217 routing_links.connect_redirection_links(rmap)
218
219 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
189 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
220 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
190 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
221
191
@@ -228,10 +198,6 b' def make_map(config):'
228 action='index', conditions={'method': ['GET']})
198 action='index', conditions={'method': ['GET']})
229 m.connect('new_repo', '/create_repository', jsroute=True,
199 m.connect('new_repo', '/create_repository', jsroute=True,
230 action='create_repository', conditions={'method': ['GET']})
200 action='create_repository', conditions={'method': ['GET']})
231 m.connect('/repos/{repo_name}',
232 action='update', conditions={'method': ['PUT'],
233 'function': check_repo},
234 requirements=URL_NAME_REQUIREMENTS)
235 m.connect('delete_repo', '/repos/{repo_name}',
201 m.connect('delete_repo', '/repos/{repo_name}',
236 action='delete', conditions={'method': ['DELETE']},
202 action='delete', conditions={'method': ['DELETE']},
237 requirements=URL_NAME_REQUIREMENTS)
203 requirements=URL_NAME_REQUIREMENTS)
@@ -321,19 +287,6 b' def make_map(config):'
321 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
287 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
322 action='edit_perms_summary', conditions={'method': ['GET']})
288 action='edit_perms_summary', conditions={'method': ['GET']})
323
289
324 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
325 action='edit_emails', conditions={'method': ['GET']})
326 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
327 action='add_email', conditions={'method': ['PUT']})
328 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
329 action='delete_email', conditions={'method': ['DELETE']})
330
331 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
332 action='edit_ips', conditions={'method': ['GET']})
333 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
334 action='add_ip', conditions={'method': ['PUT']})
335 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
336 action='delete_ip', conditions={'method': ['DELETE']})
337
290
338 # ADMIN USER GROUPS REST ROUTES
291 # ADMIN USER GROUPS REST ROUTES
339 with rmap.submapper(path_prefix=ADMIN_PREFIX,
292 with rmap.submapper(path_prefix=ADMIN_PREFIX,
@@ -519,37 +472,9 b' def make_map(config):'
519 m.connect('my_account_password', '/my_account/password',
472 m.connect('my_account_password', '/my_account/password',
520 action='my_account_password', conditions={'method': ['GET']})
473 action='my_account_password', conditions={'method': ['GET']})
521
474
522 m.connect('my_account_repos', '/my_account/repos',
523 action='my_account_repos', conditions={'method': ['GET']})
524
525 m.connect('my_account_watched', '/my_account/watched',
526 action='my_account_watched', conditions={'method': ['GET']})
527
528 m.connect('my_account_pullrequests', '/my_account/pull_requests',
475 m.connect('my_account_pullrequests', '/my_account/pull_requests',
529 action='my_account_pullrequests', conditions={'method': ['GET']})
476 action='my_account_pullrequests', conditions={'method': ['GET']})
530
477
531 m.connect('my_account_perms', '/my_account/perms',
532 action='my_account_perms', conditions={'method': ['GET']})
533
534 m.connect('my_account_emails', '/my_account/emails',
535 action='my_account_emails', conditions={'method': ['GET']})
536 m.connect('my_account_emails', '/my_account/emails',
537 action='my_account_emails_add', conditions={'method': ['POST']})
538 m.connect('my_account_emails', '/my_account/emails',
539 action='my_account_emails_delete', conditions={'method': ['DELETE']})
540
541 m.connect('my_account_notifications', '/my_account/notifications',
542 action='my_notifications',
543 conditions={'method': ['GET']})
544 m.connect('my_account_notifications_toggle_visibility',
545 '/my_account/toggle_visibility',
546 action='my_notifications_toggle_visibility',
547 conditions={'method': ['POST']})
548 m.connect('my_account_notifications_test_channelstream',
549 '/my_account/test_channelstream',
550 action='my_account_notifications_test_channelstream',
551 conditions={'method': ['POST']})
552
553 # NOTIFICATION REST ROUTES
478 # NOTIFICATION REST ROUTES
554 with rmap.submapper(path_prefix=ADMIN_PREFIX,
479 with rmap.submapper(path_prefix=ADMIN_PREFIX,
555 controller='admin/notifications') as m:
480 controller='admin/notifications') as m:
@@ -597,22 +522,6 b' def make_map(config):'
597 action='show', conditions={'method': ['GET']},
522 action='show', conditions={'method': ['GET']},
598 requirements=URL_NAME_REQUIREMENTS)
523 requirements=URL_NAME_REQUIREMENTS)
599
524
600 # ADMIN MAIN PAGES
601 with rmap.submapper(path_prefix=ADMIN_PREFIX,
602 controller='admin/admin') as m:
603 m.connect('admin_home', '', action='index')
604 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
605 action='add_repo')
606 m.connect(
607 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
608 action='pull_requests')
609 m.connect(
610 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}',
611 action='pull_requests')
612 m.connect(
613 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}',
614 action='pull_requests')
615
616 # USER JOURNAL
525 # USER JOURNAL
617 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
526 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
618 controller='journal', action='index')
527 controller='journal', action='index')
@@ -642,15 +551,6 b' def make_map(config):'
642 controller='journal', action='toggle_following', jsroute=True,
551 controller='journal', action='toggle_following', jsroute=True,
643 conditions={'method': ['POST']})
552 conditions={'method': ['POST']})
644
553
645 # FULL TEXT SEARCH
646 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
647 controller='search')
648 rmap.connect('search_repo_home', '/{repo_name}/search',
649 controller='search',
650 action='index',
651 conditions={'function': check_repo},
652 requirements=URL_NAME_REQUIREMENTS)
653
654 # FEEDS
554 # FEEDS
655 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
555 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
656 controller='feed', action='rss',
556 controller='feed', action='rss',
@@ -673,21 +573,6 b' def make_map(config):'
673 controller='admin/repos', action='repo_check',
573 controller='admin/repos', action='repo_check',
674 requirements=URL_NAME_REQUIREMENTS)
574 requirements=URL_NAME_REQUIREMENTS)
675
575
676 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
677 controller='summary', action='repo_stats',
678 conditions={'function': check_repo},
679 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
680
681 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
682 controller='summary', action='repo_refs_data',
683 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
684 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
685 controller='summary', action='repo_refs_changelog_data',
686 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
687 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
688 controller='summary', action='repo_default_reviewers_data',
689 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
690
691 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
576 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
692 controller='changeset', revision='tip',
577 controller='changeset', revision='tip',
693 conditions={'function': check_repo},
578 conditions={'function': check_repo},
@@ -702,21 +587,6 b' def make_map(config):'
702 requirements=URL_NAME_REQUIREMENTS)
587 requirements=URL_NAME_REQUIREMENTS)
703
588
704 # repo edit options
589 # repo edit options
705 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
706 controller='admin/repos', action='edit',
707 conditions={'method': ['GET'], 'function': check_repo},
708 requirements=URL_NAME_REQUIREMENTS)
709
710 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
711 jsroute=True,
712 controller='admin/repos', action='edit_permissions',
713 conditions={'method': ['GET'], 'function': check_repo},
714 requirements=URL_NAME_REQUIREMENTS)
715 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
716 controller='admin/repos', action='edit_permissions_update',
717 conditions={'method': ['PUT'], 'function': check_repo},
718 requirements=URL_NAME_REQUIREMENTS)
719
720 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
590 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
721 controller='admin/repos', action='edit_fields',
591 controller='admin/repos', action='edit_fields',
722 conditions={'method': ['GET'], 'function': check_repo},
592 conditions={'method': ['GET'], 'function': check_repo},
@@ -730,39 +600,11 b' def make_map(config):'
730 conditions={'method': ['DELETE'], 'function': check_repo},
600 conditions={'method': ['DELETE'], 'function': check_repo},
731 requirements=URL_NAME_REQUIREMENTS)
601 requirements=URL_NAME_REQUIREMENTS)
732
602
733 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
734 controller='admin/repos', action='edit_advanced',
735 conditions={'method': ['GET'], 'function': check_repo},
736 requirements=URL_NAME_REQUIREMENTS)
737
738 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
739 controller='admin/repos', action='edit_advanced_locking',
740 conditions={'method': ['PUT'], 'function': check_repo},
741 requirements=URL_NAME_REQUIREMENTS)
742 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
603 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
743 controller='admin/repos', action='toggle_locking',
604 controller='admin/repos', action='toggle_locking',
744 conditions={'method': ['GET'], 'function': check_repo},
605 conditions={'method': ['GET'], 'function': check_repo},
745 requirements=URL_NAME_REQUIREMENTS)
606 requirements=URL_NAME_REQUIREMENTS)
746
607
747 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
748 controller='admin/repos', action='edit_advanced_journal',
749 conditions={'method': ['PUT'], 'function': check_repo},
750 requirements=URL_NAME_REQUIREMENTS)
751
752 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
753 controller='admin/repos', action='edit_advanced_fork',
754 conditions={'method': ['PUT'], 'function': check_repo},
755 requirements=URL_NAME_REQUIREMENTS)
756
757 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
758 controller='admin/repos', action='edit_caches_form',
759 conditions={'method': ['GET'], 'function': check_repo},
760 requirements=URL_NAME_REQUIREMENTS)
761 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
762 controller='admin/repos', action='edit_caches',
763 conditions={'method': ['PUT'], 'function': check_repo},
764 requirements=URL_NAME_REQUIREMENTS)
765
766 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
608 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
767 controller='admin/repos', action='edit_remote_form',
609 controller='admin/repos', action='edit_remote_form',
768 conditions={'method': ['GET'], 'function': check_repo},
610 conditions={'method': ['GET'], 'function': check_repo},
@@ -931,13 +773,6 b' def make_map(config):'
931 'method': ['DELETE']},
773 'method': ['DELETE']},
932 requirements=URL_NAME_REQUIREMENTS)
774 requirements=URL_NAME_REQUIREMENTS)
933
775
934 rmap.connect('pullrequest_show_all',
935 '/{repo_name}/pull-request',
936 controller='pullrequests',
937 action='show_all', conditions={'function': check_repo,
938 'method': ['GET']},
939 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
940
941 rmap.connect('pullrequest_comment',
776 rmap.connect('pullrequest_comment',
942 '/{repo_name}/pull-request-comment/{pull_request_id}',
777 '/{repo_name}/pull-request-comment/{pull_request_id}',
943 controller='pullrequests',
778 controller='pullrequests',
@@ -951,31 +786,10 b' def make_map(config):'
951 conditions={'function': check_repo, 'method': ['DELETE']},
786 conditions={'function': check_repo, 'method': ['DELETE']},
952 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
787 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
953
788
954 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
955 controller='summary', conditions={'function': check_repo},
956 requirements=URL_NAME_REQUIREMENTS)
957
958 rmap.connect('branches_home', '/{repo_name}/branches',
959 controller='branches', conditions={'function': check_repo},
960 requirements=URL_NAME_REQUIREMENTS)
961
962 rmap.connect('tags_home', '/{repo_name}/tags',
963 controller='tags', conditions={'function': check_repo},
964 requirements=URL_NAME_REQUIREMENTS)
965
966 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
967 controller='bookmarks', conditions={'function': check_repo},
968 requirements=URL_NAME_REQUIREMENTS)
969
970 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
789 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
971 controller='changelog', conditions={'function': check_repo},
790 controller='changelog', conditions={'function': check_repo},
972 requirements=URL_NAME_REQUIREMENTS)
791 requirements=URL_NAME_REQUIREMENTS)
973
792
974 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
975 controller='changelog', action='changelog_summary',
976 conditions={'function': check_repo},
977 requirements=URL_NAME_REQUIREMENTS)
978
979 rmap.connect('changelog_file_home',
793 rmap.connect('changelog_file_home',
980 '/{repo_name}/changelog/{revision}/{f_path}',
794 '/{repo_name}/changelog/{revision}/{f_path}',
981 controller='changelog', f_path=None,
795 controller='changelog', f_path=None,
@@ -1128,26 +942,4 b' def make_map(config):'
1128 conditions={'function': check_repo},
942 conditions={'function': check_repo},
1129 requirements=URL_NAME_REQUIREMENTS)
943 requirements=URL_NAME_REQUIREMENTS)
1130
944
1131 # must be here for proper group/repo catching pattern
1132 _connect_with_slash(
1133 rmap, 'repo_group_home', '/{group_name}',
1134 controller='home', action='index_repo_group',
1135 conditions={'function': check_group},
1136 requirements=URL_NAME_REQUIREMENTS)
1137
1138 # catch all, at the end
1139 _connect_with_slash(
1140 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1141 controller='summary', action='index',
1142 conditions={'function': check_repo},
1143 requirements=URL_NAME_REQUIREMENTS)
1144
1145 return rmap
945 return rmap
1146
1147
1148 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1149 """
1150 Connect a route with an optional trailing slash in `path`.
1151 """
1152 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1153 mapper.connect(name, path, *args, **kwargs)
@@ -46,27 +46,56 b' you can see it working.'
46 # flake8: noqa
46 # flake8: noqa
47 from __future__ import unicode_literals
47 from __future__ import unicode_literals
48
48
49 link_config = [
50 {
51 "name": "enterprise_docs",
52 "target": "https://rhodecode.com/r1/enterprise/docs/",
53 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/",
54 },
55 {
56 "name": "enterprise_log_file_locations",
57 "target": "https://rhodecode.com/r1/enterprise/docs/admin-system-overview/",
58 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/system-overview.html#log-files",
59 },
60 {
61 "name": "enterprise_issue_tracker_settings",
62 "target": "https://rhodecode.com/r1/enterprise/docs/issue-trackers-overview/",
63 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/issue-trackers/issue-trackers.html",
64 },
65 {
66 "name": "enterprise_svn_setup",
67 "target": "https://rhodecode.com/r1/enterprise/docs/svn-setup/",
68 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/svn-http.html",
69 },
70 {
71 "name": "rst_help",
72 "target": "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
73 "external_target": "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
74 },
75 {
76 "name": "markdown_help",
77 "target": "https://daringfireball.net/projects/markdown/syntax",
78 "external_target": "https://daringfireball.net/projects/markdown/syntax",
79 },
80 {
81 "name": "rhodecode_official",
82 "target": "https://rhodecode.com",
83 "external_target": "https://rhodecode.com/",
84 },
85 {
86 "name": "rhodecode_support",
87 "target": "https://rhodecode.com/help/",
88 "external_target": "https://rhodecode.com/support",
89 },
90 {
91 "name": "rhodecode_translations",
92 "target": "https://rhodecode.com/translate/enterprise",
93 "external_target": "https://www.transifex.com/rhodecode/RhodeCode/",
94 },
49
95
50 link_config = [
51 {"name": "enterprise_docs",
52 "target": "https://rhodecode.com/r1/enterprise/docs/",
53 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/",
54 },
55 {"name": "enterprise_log_file_locations",
56 "target": "https://rhodecode.com/r1/enterprise/docs/admin-system-overview/",
57 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/system-overview.html#log-files",
58 },
59 {"name": "enterprise_issue_tracker_settings",
60 "target": "https://rhodecode.com/r1/enterprise/docs/issue-trackers-overview/",
61 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/issue-trackers/issue-trackers.html",
62 },
63 {"name": "enterprise_svn_setup",
64 "target": "https://rhodecode.com/r1/enterprise/docs/svn-setup/",
65 "external_target": "https://docs.rhodecode.com/RhodeCode-Enterprise/admin/svn-http.html",
66 },
67 ]
96 ]
68
97
69
98
70 def connect_redirection_links(rmap):
99 def connect_redirection_links(config):
71 for link in link_config:
100 for link in link_config:
72 rmap.connect(link['name'], link['target'], _static=True)
101 config.add_route(link['name'], link['target'], static=True)
@@ -24,35 +24,27 b' my account controller for RhodeCode admi'
24 """
24 """
25
25
26 import logging
26 import logging
27 import datetime
28
27
29 import formencode
28 import formencode
30 from formencode import htmlfill
29 from formencode import htmlfill
31 from pyramid.threadlocal import get_current_registry
32 from pyramid.httpexceptions import HTTPFound
30 from pyramid.httpexceptions import HTTPFound
33
31
34 from pylons import request, tmpl_context as c, url
32 from pylons import request, tmpl_context as c
35 from pylons.controllers.util import redirect
33 from pylons.controllers.util import redirect
36 from pylons.i18n.translation import _
34 from pylons.i18n.translation import _
37 from sqlalchemy.orm import joinedload
38
35
39 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
40 from rhodecode.lib import auth
37 from rhodecode.lib import auth
41 from rhodecode.lib.auth import (
38 from rhodecode.lib.auth import (
42 LoginRequired, NotAnonymous, AuthUser)
39 LoginRequired, NotAnonymous, AuthUser)
43 from rhodecode.lib.base import BaseController, render
40 from rhodecode.lib.base import BaseController, render
44 from rhodecode.lib.utils import jsonify
45 from rhodecode.lib.utils2 import safe_int, str2bool
41 from rhodecode.lib.utils2 import safe_int, str2bool
46 from rhodecode.lib.ext_json import json
42 from rhodecode.lib.ext_json import json
47 from rhodecode.lib.channelstream import channelstream_request, \
48 ChannelstreamException
49
43
50 from rhodecode.model.db import (
44 from rhodecode.model.db import (
51 Repository, PullRequest, UserEmailMap, User, UserFollowing)
45 Repository, PullRequest, UserEmailMap, User, UserFollowing)
52 from rhodecode.model.forms import UserForm
46 from rhodecode.model.forms import UserForm
53 from rhodecode.model.scm import RepoList
54 from rhodecode.model.user import UserModel
47 from rhodecode.model.user import UserModel
55 from rhodecode.model.repo import RepoModel
56 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
57 from rhodecode.model.pull_request import PullRequestModel
49 from rhodecode.model.pull_request import PullRequestModel
58 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.comment import CommentsModel
@@ -82,26 +74,6 b' class MyAccountController(BaseController'
82 c.auth_user = AuthUser(
74 c.auth_user = AuthUser(
83 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
75 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
84
76
85 def _load_my_repos_data(self, watched=False):
86 if watched:
87 admin = False
88 follows_repos = Session().query(UserFollowing)\
89 .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\
90 .options(joinedload(UserFollowing.follows_repository))\
91 .all()
92 repo_list = [x.follows_repository for x in follows_repos]
93 else:
94 admin = True
95 repo_list = Repository.get_all_repos(
96 user_id=c.rhodecode_user.user_id)
97 repo_list = RepoList(repo_list, perm_set=[
98 'repository.read', 'repository.write', 'repository.admin'])
99
100 repos_data = RepoModel().get_repos_as_dict(
101 repo_list=repo_list, admin=admin)
102 # json used to render the grid
103 return json.dumps(repos_data)
104
105 @auth.CSRFRequired()
77 @auth.CSRFRequired()
106 def my_account_update(self):
78 def my_account_update(self):
107 """
79 """
@@ -181,65 +153,6 b' class MyAccountController(BaseController'
181 force_defaults=False
153 force_defaults=False
182 )
154 )
183
155
184 def my_account_repos(self):
185 c.active = 'repos'
186 self.__load_data()
187
188 # json used to render the grid
189 c.data = self._load_my_repos_data()
190 return render('admin/my_account/my_account.mako')
191
192 def my_account_watched(self):
193 c.active = 'watched'
194 self.__load_data()
195
196 # json used to render the grid
197 c.data = self._load_my_repos_data(watched=True)
198 return render('admin/my_account/my_account.mako')
199
200 def my_account_perms(self):
201 c.active = 'perms'
202 self.__load_data()
203 c.perm_user = c.auth_user
204
205 return render('admin/my_account/my_account.mako')
206
207 def my_account_emails(self):
208 c.active = 'emails'
209 self.__load_data()
210
211 c.user_email_map = UserEmailMap.query()\
212 .filter(UserEmailMap.user == c.user).all()
213 return render('admin/my_account/my_account.mako')
214
215 @auth.CSRFRequired()
216 def my_account_emails_add(self):
217 email = request.POST.get('new_email')
218
219 try:
220 UserModel().add_extra_email(c.rhodecode_user.user_id, email)
221 Session().commit()
222 h.flash(_("Added new email address `%s` for user account") % email,
223 category='success')
224 except formencode.Invalid as error:
225 msg = error.error_dict['email']
226 h.flash(msg, category='error')
227 except Exception:
228 log.exception("Exception in my_account_emails")
229 h.flash(_('An error occurred during email saving'),
230 category='error')
231 return redirect(url('my_account_emails'))
232
233 @auth.CSRFRequired()
234 def my_account_emails_delete(self):
235 email_id = request.POST.get('del_email_id')
236 user_model = UserModel()
237 user_model.delete_extra_email(c.rhodecode_user.user_id, email_id)
238 Session().commit()
239 h.flash(_("Removed email address from user account"),
240 category='success')
241 return redirect(url('my_account_emails'))
242
243 def _extract_ordering(self, request):
156 def _extract_ordering(self, request):
244 column_index = safe_int(request.GET.get('order[0][column]'))
157 column_index = safe_int(request.GET.get('order[0][column]'))
245 order_dir = request.GET.get('order[0][dir]', 'desc')
158 order_dir = request.GET.get('order[0][dir]', 'desc')
@@ -320,45 +233,4 b' class MyAccountController(BaseController'
320 else:
233 else:
321 return json.dumps(data)
234 return json.dumps(data)
322
235
323 def my_notifications(self):
324 c.active = 'notifications'
325 return render('admin/my_account/my_account.mako')
326
236
327 @auth.CSRFRequired()
328 @jsonify
329 def my_notifications_toggle_visibility(self):
330 user = c.rhodecode_user.get_instance()
331 new_status = not user.user_data.get('notification_status', True)
332 user.update_userdata(notification_status=new_status)
333 Session().commit()
334 return user.user_data['notification_status']
335
336 @auth.CSRFRequired()
337 @jsonify
338 def my_account_notifications_test_channelstream(self):
339 message = 'Test message sent via Channelstream by user: {}, on {}'.format(
340 c.rhodecode_user.username, datetime.datetime.now())
341 payload = {
342 'type': 'message',
343 'timestamp': datetime.datetime.utcnow(),
344 'user': 'system',
345 #'channel': 'broadcast',
346 'pm_users': [c.rhodecode_user.username],
347 'message': {
348 'message': message,
349 'level': 'info',
350 'topic': '/notifications'
351 }
352 }
353
354 registry = get_current_registry()
355 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
356 channelstream_config = rhodecode_plugins.get('channelstream', {})
357
358 try:
359 channelstream_request(channelstream_config, [payload], '/message')
360 except ChannelstreamException as e:
361 log.exception('Failed to send channelstream data')
362 return {"response": 'ERROR: {}'.format(e.__class__.__name__)}
363 return {"response": 'Channelstream data sent. '
364 'You should see a new live message now.'}
@@ -48,10 +48,6 b' log = logging.getLogger(__name__)'
48
48
49 class NotificationsController(BaseController):
49 class NotificationsController(BaseController):
50 """REST Controller styled on the Atom Publishing Protocol"""
50 """REST Controller styled on the Atom Publishing Protocol"""
51 # To properly map this controller, ensure your config/routing.py
52 # file has a resource setup:
53 # map.resource('notification', 'notifications', controller='_admin/notifications',
54 # path_prefix='/_admin', name_prefix='_admin_')
55
51
56 @LoginRequired()
52 @LoginRequired()
57 @NotAnonymous()
53 @NotAnonymous()
@@ -62,8 +58,8 b' class NotificationsController(BaseContro'
62 """GET /_admin/notifications: All items in the collection"""
58 """GET /_admin/notifications: All items in the collection"""
63 # url('notifications')
59 # url('notifications')
64 c.user = c.rhodecode_user
60 c.user = c.rhodecode_user
65 notif = NotificationModel().get_for_user(c.rhodecode_user.user_id,
61 notif = NotificationModel().get_for_user(
66 filter_=request.GET.getall('type'))
62 c.rhodecode_user.user_id, filter_=request.GET.getall('type'))
67
63
68 p = safe_int(request.GET.get('page', 1), 1)
64 p = safe_int(request.GET.get('page', 1), 1)
69 notifications_url = webhelpers.paginate.PageURL(
65 notifications_url = webhelpers.paginate.PageURL(
@@ -86,7 +82,6 b' class NotificationsController(BaseContro'
86
82
87 return render('admin/notifications/notifications.mako')
83 return render('admin/notifications/notifications.mako')
88
84
89
90 @auth.CSRFRequired()
85 @auth.CSRFRequired()
91 def mark_all_read(self):
86 def mark_all_read(self):
92 if request.is_xhr:
87 if request.is_xhr:
@@ -115,15 +110,8 b' class NotificationsController(BaseContro'
115
110
116 @auth.CSRFRequired()
111 @auth.CSRFRequired()
117 def update(self, notification_id):
112 def update(self, notification_id):
118 """PUT /_admin/notifications/id: Update an existing item"""
113 no = Notification.get_or_404(notification_id)
119 # Forms posted to this method should contain a hidden field:
120 # <input type="hidden" name="_method" value="PUT" />
121 # Or using helpers:
122 # h.form(url('notification', notification_id=ID),
123 # method='put')
124 # url('notification', notification_id=ID)
125 try:
114 try:
126 no = Notification.get(notification_id)
127 if self._has_permissions(no):
115 if self._has_permissions(no):
128 # deletes only notification2user
116 # deletes only notification2user
129 NotificationModel().mark_read(c.rhodecode_user.user_id, no)
117 NotificationModel().mark_read(c.rhodecode_user.user_id, no)
@@ -136,15 +124,8 b' class NotificationsController(BaseContro'
136
124
137 @auth.CSRFRequired()
125 @auth.CSRFRequired()
138 def delete(self, notification_id):
126 def delete(self, notification_id):
139 """DELETE /_admin/notifications/id: Delete an existing item"""
127 no = Notification.get_or_404(notification_id)
140 # Forms posted to this method should contain a hidden field:
141 # <input type="hidden" name="_method" value="DELETE" />
142 # Or using helpers:
143 # h.form(url('notification', notification_id=ID),
144 # method='delete')
145 # url('notification', notification_id=ID)
146 try:
128 try:
147 no = Notification.get(notification_id)
148 if self._has_permissions(no):
129 if self._has_permissions(no):
149 # deletes only notification2user
130 # deletes only notification2user
150 NotificationModel().delete(c.rhodecode_user.user_id, no)
131 NotificationModel().delete(c.rhodecode_user.user_id, no)
@@ -156,10 +137,8 b' class NotificationsController(BaseContro'
156 raise HTTPBadRequest()
137 raise HTTPBadRequest()
157
138
158 def show(self, notification_id):
139 def show(self, notification_id):
159 """GET /_admin/notifications/id: Show a specific item"""
160 # url('notification', notification_id=ID)
161 c.user = c.rhodecode_user
140 c.user = c.rhodecode_user
162 no = Notification.get(notification_id)
141 no = Notification.get_or_404(notification_id)
163
142
164 if no and self._has_permissions(no):
143 if no and self._has_permissions(no):
165 unotification = NotificationModel()\
144 unotification = NotificationModel()\
@@ -64,12 +64,7 b' class PermissionsController(BaseControll'
64 c.active = 'application'
64 c.active = 'application'
65 self.__load_data()
65 self.__load_data()
66
66
67 c.user = User.get_default_user()
67 c.user = User.get_default_user(refresh=True)
68
69 # TODO: johbo: The default user might be based on outdated state which
70 # has been loaded from the cache. A call to refresh() ensures that the
71 # latest state from the database is used.
72 Session().refresh(c.user)
73
68
74 app_settings = SettingsModel().get_all_settings()
69 app_settings = SettingsModel().get_all_settings()
75 defaults = {
70 defaults = {
@@ -34,17 +34,18 b' from pylons.i18n.translation import _, u'
34
34
35 from rhodecode.lib import auth
35 from rhodecode.lib import auth
36 from rhodecode.lib import helpers as h
36 from rhodecode.lib import helpers as h
37 from rhodecode.lib import audit_logger
37 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
39 LoginRequired, NotAnonymous, HasPermissionAll,
40 LoginRequired, NotAnonymous, HasPermissionAll,
40 HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator)
41 HasRepoGroupPermissionAll, HasRepoGroupPermissionAnyDecorator)
41 from rhodecode.lib.base import BaseController, render
42 from rhodecode.lib.base import BaseController, render
43 from rhodecode.lib.utils2 import safe_int
42 from rhodecode.model.db import RepoGroup, User
44 from rhodecode.model.db import RepoGroup, User
43 from rhodecode.model.scm import RepoGroupList
45 from rhodecode.model.scm import RepoGroupList
44 from rhodecode.model.repo_group import RepoGroupModel
46 from rhodecode.model.repo_group import RepoGroupModel
45 from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm
47 from rhodecode.model.forms import RepoGroupForm, RepoGroupPermsForm
46 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
47 from rhodecode.lib.utils2 import safe_int
48
49
49
50
50 log = logging.getLogger(__name__)
51 log = logging.getLogger(__name__)
@@ -153,9 +154,6 b' class RepoGroupsController(BaseControlle'
153
154
154 @NotAnonymous()
155 @NotAnonymous()
155 def index(self):
156 def index(self):
156 """GET /repo_groups: All items in the collection"""
157 # url('repo_groups')
158
159 repo_group_list = RepoGroup.get_all_repo_groups()
157 repo_group_list = RepoGroup.get_all_repo_groups()
160 _perms = ['group.admin']
158 _perms = ['group.admin']
161 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
159 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
@@ -168,8 +166,6 b' class RepoGroupsController(BaseControlle'
168 @NotAnonymous()
166 @NotAnonymous()
169 @auth.CSRFRequired()
167 @auth.CSRFRequired()
170 def create(self):
168 def create(self):
171 """POST /repo_groups: Create a new item"""
172 # url('repo_groups')
173
169
174 parent_group_id = safe_int(request.POST.get('group_parent_id'))
170 parent_group_id = safe_int(request.POST.get('group_parent_id'))
175 can_create = self._can_create_repo_group(parent_group_id)
171 can_create = self._can_create_repo_group(parent_group_id)
@@ -183,20 +179,29 b' class RepoGroupsController(BaseControlle'
183 try:
179 try:
184 owner = c.rhodecode_user
180 owner = c.rhodecode_user
185 form_result = repo_group_form.to_python(dict(request.POST))
181 form_result = repo_group_form.to_python(dict(request.POST))
186 RepoGroupModel().create(
182 repo_group = RepoGroupModel().create(
187 group_name=form_result['group_name_full'],
183 group_name=form_result['group_name_full'],
188 group_description=form_result['group_description'],
184 group_description=form_result['group_description'],
189 owner=owner.user_id,
185 owner=owner.user_id,
190 copy_permissions=form_result['group_copy_permissions']
186 copy_permissions=form_result['group_copy_permissions']
191 )
187 )
188 Session().flush()
189
190 repo_group_data = repo_group.get_api_data()
191 audit_logger.store_web(
192 'repo_group.create', action_data={'data': repo_group_data},
193 user=c.rhodecode_user)
194
192 Session().commit()
195 Session().commit()
196
193 _new_group_name = form_result['group_name_full']
197 _new_group_name = form_result['group_name_full']
198
194 repo_group_url = h.link_to(
199 repo_group_url = h.link_to(
195 _new_group_name,
200 _new_group_name,
196 h.url('repo_group_home', group_name=_new_group_name))
201 h.route_path('repo_group_home', repo_group_name=_new_group_name))
197 h.flash(h.literal(_('Created repository group %s')
202 h.flash(h.literal(_('Created repository group %s')
198 % repo_group_url), category='success')
203 % repo_group_url), category='success')
199 # TODO: in futureaction_logger(, '', '', '', self.sa)
204
200 except formencode.Invalid as errors:
205 except formencode.Invalid as errors:
201 return htmlfill.render(
206 return htmlfill.render(
202 render('admin/repo_groups/repo_group_add.mako'),
207 render('admin/repo_groups/repo_group_add.mako'),
@@ -216,8 +221,6 b' class RepoGroupsController(BaseControlle'
216 # perm checks inside
221 # perm checks inside
217 @NotAnonymous()
222 @NotAnonymous()
218 def new(self):
223 def new(self):
219 """GET /repo_groups/new: Form to create a new item"""
220 # url('new_repo_group')
221 # perm check for admin, create_group perm or admin of parent_group
224 # perm check for admin, create_group perm or admin of parent_group
222 parent_group_id = safe_int(request.GET.get('parent_group'))
225 parent_group_id = safe_int(request.GET.get('parent_group'))
223 if not self._can_create_repo_group(parent_group_id):
226 if not self._can_create_repo_group(parent_group_id):
@@ -229,12 +232,6 b' class RepoGroupsController(BaseControlle'
229 @HasRepoGroupPermissionAnyDecorator('group.admin')
232 @HasRepoGroupPermissionAnyDecorator('group.admin')
230 @auth.CSRFRequired()
233 @auth.CSRFRequired()
231 def update(self, group_name):
234 def update(self, group_name):
232 """PUT /repo_groups/group_name: Update an existing item"""
233 # Forms posted to this method should contain a hidden field:
234 # <input type="hidden" name="_method" value="PUT" />
235 # Or using helpers:
236 # h.form(url('repos_group', group_name=GROUP_NAME), method='put')
237 # url('repo_group_home', group_name=GROUP_NAME)
238
235
239 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
236 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
240 can_create_in_root = self._can_create_repo_group()
237 can_create_in_root = self._can_create_repo_group()
@@ -250,16 +247,21 b' class RepoGroupsController(BaseControlle'
250 available_groups=c.repo_groups_choices,
247 available_groups=c.repo_groups_choices,
251 can_create_in_root=can_create_in_root, allow_disabled=True)()
248 can_create_in_root=can_create_in_root, allow_disabled=True)()
252
249
250 old_values = c.repo_group.get_api_data()
253 try:
251 try:
254 form_result = repo_group_form.to_python(dict(request.POST))
252 form_result = repo_group_form.to_python(dict(request.POST))
255 gr_name = form_result['group_name']
253 gr_name = form_result['group_name']
256 new_gr = RepoGroupModel().update(group_name, form_result)
254 new_gr = RepoGroupModel().update(group_name, form_result)
255
256 audit_logger.store_web(
257 'repo_group.edit', action_data={'old_data': old_values},
258 user=c.rhodecode_user)
259
257 Session().commit()
260 Session().commit()
258 h.flash(_('Updated repository group %s') % (gr_name,),
261 h.flash(_('Updated repository group %s') % (gr_name,),
259 category='success')
262 category='success')
260 # we now have new name !
263 # we now have new name !
261 group_name = new_gr.group_name
264 group_name = new_gr.group_name
262 # TODO: in future action_logger(, '', '', '', self.sa)
263 except formencode.Invalid as errors:
265 except formencode.Invalid as errors:
264 c.active = 'settings'
266 c.active = 'settings'
265 return htmlfill.render(
267 return htmlfill.render(
@@ -279,13 +281,6 b' class RepoGroupsController(BaseControlle'
279 @HasRepoGroupPermissionAnyDecorator('group.admin')
281 @HasRepoGroupPermissionAnyDecorator('group.admin')
280 @auth.CSRFRequired()
282 @auth.CSRFRequired()
281 def delete(self, group_name):
283 def delete(self, group_name):
282 """DELETE /repo_groups/group_name: Delete an existing item"""
283 # Forms posted to this method should contain a hidden field:
284 # <input type="hidden" name="_method" value="DELETE" />
285 # Or using helpers:
286 # h.form(url('repos_group', group_name=GROUP_NAME), method='delete')
287 # url('repo_group_home', group_name=GROUP_NAME)
288
289 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
284 gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name)
290 repos = gr.repositories.all()
285 repos = gr.repositories.all()
291 if repos:
286 if repos:
@@ -307,11 +302,16 b' class RepoGroupsController(BaseControlle'
307 return redirect(url('repo_groups'))
302 return redirect(url('repo_groups'))
308
303
309 try:
304 try:
305 old_values = gr.get_api_data()
310 RepoGroupModel().delete(group_name)
306 RepoGroupModel().delete(group_name)
307
308 audit_logger.store_web(
309 'repo_group.delete', action_data={'old_data': old_values},
310 user=c.rhodecode_user)
311
311 Session().commit()
312 Session().commit()
312 h.flash(_('Removed repository group %s') % group_name,
313 h.flash(_('Removed repository group %s') % group_name,
313 category='success')
314 category='success')
314 # TODO: in future action_logger(, '', '', '', self.sa)
315 except Exception:
315 except Exception:
316 log.exception("Exception during deletion of repository group")
316 log.exception("Exception during deletion of repository group")
317 h.flash(_('Error occurred during deletion of repository group %s')
317 h.flash(_('Error occurred during deletion of repository group %s')
@@ -321,8 +321,7 b' class RepoGroupsController(BaseControlle'
321
321
322 @HasRepoGroupPermissionAnyDecorator('group.admin')
322 @HasRepoGroupPermissionAnyDecorator('group.admin')
323 def edit(self, group_name):
323 def edit(self, group_name):
324 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
324
325 # url('edit_repo_group', group_name=GROUP_NAME)
326 c.active = 'settings'
325 c.active = 'settings'
327
326
328 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
327 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
@@ -346,8 +345,6 b' class RepoGroupsController(BaseControlle'
346
345
347 @HasRepoGroupPermissionAnyDecorator('group.admin')
346 @HasRepoGroupPermissionAnyDecorator('group.admin')
348 def edit_repo_group_advanced(self, group_name):
347 def edit_repo_group_advanced(self, group_name):
349 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
350 # url('edit_repo_group', group_name=GROUP_NAME)
351 c.active = 'advanced'
348 c.active = 'advanced'
352 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
349 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
353
350
@@ -355,8 +352,6 b' class RepoGroupsController(BaseControlle'
355
352
356 @HasRepoGroupPermissionAnyDecorator('group.admin')
353 @HasRepoGroupPermissionAnyDecorator('group.admin')
357 def edit_repo_group_perms(self, group_name):
354 def edit_repo_group_perms(self, group_name):
358 """GET /repo_groups/group_name/edit: Form to edit an existing item"""
359 # url('edit_repo_group', group_name=GROUP_NAME)
360 c.active = 'perms'
355 c.active = 'perms'
361 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
356 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
362 self.__load_defaults()
357 self.__load_defaults()
@@ -374,8 +369,6 b' class RepoGroupsController(BaseControlle'
374 def update_perms(self, group_name):
369 def update_perms(self, group_name):
375 """
370 """
376 Update permissions for given repository group
371 Update permissions for given repository group
377
378 :param group_name:
379 """
372 """
380
373
381 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
374 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
@@ -393,14 +386,20 b' class RepoGroupsController(BaseControlle'
393 # iterate over all members(if in recursive mode) of this groups and
386 # iterate over all members(if in recursive mode) of this groups and
394 # set the permissions !
387 # set the permissions !
395 # this can be potentially heavy operation
388 # this can be potentially heavy operation
396 RepoGroupModel().update_permissions(
389 changes = RepoGroupModel().update_permissions(
397 c.repo_group,
390 c.repo_group,
398 form['perm_additions'], form['perm_updates'],
391 form['perm_additions'], form['perm_updates'], form['perm_deletions'],
399 form['perm_deletions'], form['recursive'])
392 form['recursive'])
400
393
401 # TODO: implement this
394 action_data = {
402 # action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
395 'added': changes['added'],
403 # repo_name, self.ip_addr, self.sa)
396 'updated': changes['updated'],
397 'deleted': changes['deleted'],
398 }
399 audit_logger.store_web(
400 'repo_group.edit.permissions', action_data=action_data,
401 user=c.rhodecode_user)
402
404 Session().commit()
403 Session().commit()
405 h.flash(_('Repository Group permissions updated'), category='success')
404 h.flash(_('Repository Group permissions updated'), category='success')
406 return redirect(url('edit_repo_group_perms', group_name=group_name))
405 return redirect(url('edit_repo_group_perms', group_name=group_name))
@@ -41,15 +41,11 b' from rhodecode.lib.auth import ('
41 HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator)
41 HasRepoGroupPermissionAny, HasRepoPermissionAnyDecorator)
42 from rhodecode.lib.base import BaseRepoController, render
42 from rhodecode.lib.base import BaseRepoController, render
43 from rhodecode.lib.ext_json import json
43 from rhodecode.lib.ext_json import json
44 from rhodecode.lib.exceptions import AttachedForksError
44 from rhodecode.lib.utils import repo_name_slug, jsonify
45 from rhodecode.lib.utils import action_logger, repo_name_slug, jsonify
46 from rhodecode.lib.utils2 import safe_int, str2bool
45 from rhodecode.lib.utils2 import safe_int, str2bool
47 from rhodecode.lib.vcs import RepositoryError
46 from rhodecode.model.db import (Repository, RepoGroup, RepositoryField)
48 from rhodecode.model.db import (
49 User, Repository, UserFollowing, RepoGroup, RepositoryField)
50 from rhodecode.model.forms import (
47 from rhodecode.model.forms import (
51 RepoForm, RepoFieldForm, RepoPermsForm, RepoVcsSettingsForm,
48 RepoForm, RepoFieldForm, RepoVcsSettingsForm, IssueTrackerPatternsForm)
52 IssueTrackerPatternsForm)
53 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
54 from rhodecode.model.repo import RepoModel
50 from rhodecode.model.repo import RepoModel
55 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
51 from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
@@ -185,7 +181,7 b' class ReposController(BaseRepoController'
185 except Exception as e:
181 except Exception as e:
186 msg = self._log_creation_exception(e, form_result.get('repo_name'))
182 msg = self._log_creation_exception(e, form_result.get('repo_name'))
187 h.flash(msg, category='error')
183 h.flash(msg, category='error')
188 return redirect(url('home'))
184 return redirect(h.route_path('home'))
189
185
190 return redirect(h.url('repo_creating_home',
186 return redirect(h.url('repo_creating_home',
191 repo_name=form_result['repo_name_full'],
187 repo_name=form_result['repo_name_full'],
@@ -265,7 +261,7 b' class ReposController(BaseRepoController'
265 if task.failed():
261 if task.failed():
266 msg = self._log_creation_exception(task.result, c.repo)
262 msg = self._log_creation_exception(task.result, c.repo)
267 h.flash(msg, category='error')
263 h.flash(msg, category='error')
268 return redirect(url('home'), code=501)
264 return redirect(h.route_path('home'), code=501)
269
265
270 repo = Repository.get_by_repo_name(repo_name)
266 repo = Repository.get_by_repo_name(repo_name)
271 if repo and repo.repo_state == Repository.STATE_CREATED:
267 if repo and repo.repo_state == Repository.STATE_CREATED:
@@ -274,9 +270,9 b' class ReposController(BaseRepoController'
274 h.flash(_('Created repository %s from %s')
270 h.flash(_('Created repository %s from %s')
275 % (repo.repo_name, clone_uri), category='success')
271 % (repo.repo_name, clone_uri), category='success')
276 else:
272 else:
277 repo_url = h.link_to(repo.repo_name,
273 repo_url = h.link_to(
278 h.url('summary_home',
274 repo.repo_name,
279 repo_name=repo.repo_name))
275 h.route_path('repo_summary',repo_name=repo.repo_name))
280 fork = repo.fork
276 fork = repo.fork
281 if fork:
277 if fork:
282 fork_name = fork.repo_name
278 fork_name = fork.repo_name
@@ -288,165 +284,14 b' class ReposController(BaseRepoController'
288 return {'result': True}
284 return {'result': True}
289 return {'result': False}
285 return {'result': False}
290
286
291 @HasRepoPermissionAllDecorator('repository.admin')
292 @auth.CSRFRequired()
293 def update(self, repo_name):
294 """
295 PUT /repos/repo_name: Update an existing item"""
296 # Forms posted to this method should contain a hidden field:
297 # <input type="hidden" name="_method" value="PUT" />
298 # Or using helpers:
299 # h.form(url('repo', repo_name=ID),
300 # method='put')
301 # url('repo', repo_name=ID)
302
303 self.__load_data(repo_name)
304 c.active = 'settings'
305 c.repo_fields = RepositoryField.query()\
306 .filter(RepositoryField.repository == c.repo_info).all()
307
308 repo_model = RepoModel()
309 changed_name = repo_name
310
311 c.personal_repo_group = c.rhodecode_user.personal_repo_group
312 # override the choices with extracted revisions !
313 repo = Repository.get_by_repo_name(repo_name)
314 old_data = {
315 'repo_name': repo_name,
316 'repo_group': repo.group.get_dict() if repo.group else {},
317 'repo_type': repo.repo_type,
318 }
319 _form = RepoForm(
320 edit=True, old_data=old_data, repo_groups=c.repo_groups_choices,
321 landing_revs=c.landing_revs_choices, allow_disabled=True)()
322
323 try:
324 form_result = _form.to_python(dict(request.POST))
325 repo = repo_model.update(repo_name, **form_result)
326 ScmModel().mark_for_invalidation(repo_name)
327 h.flash(_('Repository %s updated successfully') % repo_name,
328 category='success')
329 changed_name = repo.repo_name
330 action_logger(c.rhodecode_user, 'admin_updated_repo',
331 changed_name, self.ip_addr, self.sa)
332 Session().commit()
333 except formencode.Invalid as errors:
334 defaults = self.__load_data(repo_name)
335 defaults.update(errors.value)
336 return htmlfill.render(
337 render('admin/repos/repo_edit.mako'),
338 defaults=defaults,
339 errors=errors.error_dict or {},
340 prefix_error=False,
341 encoding="UTF-8",
342 force_defaults=False)
343
344 except Exception:
345 log.exception("Exception during update of repository")
346 h.flash(_('Error occurred during update of repository %s') \
347 % repo_name, category='error')
348 return redirect(url('edit_repo', repo_name=changed_name))
349
350 @HasRepoPermissionAllDecorator('repository.admin')
351 @auth.CSRFRequired()
352 def delete(self, repo_name):
353 """
354 DELETE /repos/repo_name: Delete an existing item"""
355 # Forms posted to this method should contain a hidden field:
356 # <input type="hidden" name="_method" value="DELETE" />
357 # Or using helpers:
358 # h.form(url('repo', repo_name=ID),
359 # method='delete')
360 # url('repo', repo_name=ID)
361
362 repo_model = RepoModel()
363 repo = repo_model.get_by_repo_name(repo_name)
364 if not repo:
365 h.not_mapped_error(repo_name)
366 return redirect(url('repos'))
367 try:
368 _forks = repo.forks.count()
369 handle_forks = None
370 if _forks and request.POST.get('forks'):
371 do = request.POST['forks']
372 if do == 'detach_forks':
373 handle_forks = 'detach'
374 h.flash(_('Detached %s forks') % _forks, category='success')
375 elif do == 'delete_forks':
376 handle_forks = 'delete'
377 h.flash(_('Deleted %s forks') % _forks, category='success')
378 repo_model.delete(repo, forks=handle_forks)
379 action_logger(c.rhodecode_user, 'admin_deleted_repo',
380 repo_name, self.ip_addr, self.sa)
381 ScmModel().mark_for_invalidation(repo_name)
382 h.flash(_('Deleted repository %s') % repo_name, category='success')
383 Session().commit()
384 except AttachedForksError:
385 h.flash(_('Cannot delete %s it still contains attached forks')
386 % repo_name, category='warning')
387
388 except Exception:
389 log.exception("Exception during deletion of repository")
390 h.flash(_('An error occurred during deletion of %s') % repo_name,
391 category='error')
392
393 return redirect(url('repos'))
394
395 @HasPermissionAllDecorator('hg.admin')
287 @HasPermissionAllDecorator('hg.admin')
396 def show(self, repo_name, format='html'):
288 def show(self, repo_name, format='html'):
397 """GET /repos/repo_name: Show a specific item"""
289 """GET /repos/repo_name: Show a specific item"""
398 # url('repo', repo_name=ID)
290 # url('repo', repo_name=ID)
399
291
400 @HasRepoPermissionAllDecorator('repository.admin')
292 @HasRepoPermissionAllDecorator('repository.admin')
401 def edit(self, repo_name):
402 """GET /repo_name/settings: Form to edit an existing item"""
403 # url('edit_repo', repo_name=ID)
404 defaults = self.__load_data(repo_name)
405 if 'clone_uri' in defaults:
406 del defaults['clone_uri']
407
408 c.repo_fields = RepositoryField.query()\
409 .filter(RepositoryField.repository == c.repo_info).all()
410 c.personal_repo_group = c.rhodecode_user.personal_repo_group
411 c.active = 'settings'
412 return htmlfill.render(
413 render('admin/repos/repo_edit.mako'),
414 defaults=defaults,
415 encoding="UTF-8",
416 force_defaults=False)
417
418 @HasRepoPermissionAllDecorator('repository.admin')
419 def edit_permissions(self, repo_name):
420 """GET /repo_name/settings: Form to edit an existing item"""
421 # url('edit_repo', repo_name=ID)
422 c.repo_info = self._load_repo(repo_name)
423 c.active = 'permissions'
424 defaults = RepoModel()._get_defaults(repo_name)
425
426 return htmlfill.render(
427 render('admin/repos/repo_edit.mako'),
428 defaults=defaults,
429 encoding="UTF-8",
430 force_defaults=False)
431
432 @HasRepoPermissionAllDecorator('repository.admin')
433 @auth.CSRFRequired()
434 def edit_permissions_update(self, repo_name):
435 form = RepoPermsForm()().to_python(request.POST)
436 RepoModel().update_permissions(repo_name,
437 form['perm_additions'], form['perm_updates'], form['perm_deletions'])
438
439 #TODO: implement this
440 #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
441 # repo_name, self.ip_addr, self.sa)
442 Session().commit()
443 h.flash(_('Repository permissions updated'), category='success')
444 return redirect(url('edit_repo_perms', repo_name=repo_name))
445
446 @HasRepoPermissionAllDecorator('repository.admin')
447 def edit_fields(self, repo_name):
293 def edit_fields(self, repo_name):
448 """GET /repo_name/settings: Form to edit an existing item"""
294 """GET /repo_name/settings: Form to edit an existing item"""
449 # url('edit_repo', repo_name=ID)
450 c.repo_info = self._load_repo(repo_name)
295 c.repo_info = self._load_repo(repo_name)
451 c.repo_fields = RepositoryField.query()\
296 c.repo_fields = RepositoryField.query()\
452 .filter(RepositoryField.repository == c.repo_info).all()
297 .filter(RepositoryField.repository == c.repo_info).all()
@@ -490,106 +335,6 b' class ReposController(BaseRepoController'
490 h.flash(msg, category='error')
335 h.flash(msg, category='error')
491 return redirect(url('edit_repo_fields', repo_name=repo_name))
336 return redirect(url('edit_repo_fields', repo_name=repo_name))
492
337
493 @HasRepoPermissionAllDecorator('repository.admin')
494 def edit_advanced(self, repo_name):
495 """GET /repo_name/settings: Form to edit an existing item"""
496 # url('edit_repo', repo_name=ID)
497 c.repo_info = self._load_repo(repo_name)
498 c.default_user_id = User.get_default_user().user_id
499 c.in_public_journal = UserFollowing.query()\
500 .filter(UserFollowing.user_id == c.default_user_id)\
501 .filter(UserFollowing.follows_repository == c.repo_info).scalar()
502
503 c.active = 'advanced'
504 c.has_origin_repo_read_perm = False
505 if c.repo_info.fork:
506 c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
507 'repository.write', 'repository.read', 'repository.admin')(
508 c.repo_info.fork.repo_name, 'repo set as fork page')
509
510 if request.POST:
511 return redirect(url('repo_edit_advanced'))
512 return render('admin/repos/repo_edit.mako')
513
514 @HasRepoPermissionAllDecorator('repository.admin')
515 @auth.CSRFRequired()
516 def edit_advanced_journal(self, repo_name):
517 """
518 Set's this repository to be visible in public journal,
519 in other words assing default user to follow this repo
520
521 :param repo_name:
522 """
523
524 try:
525 repo_id = Repository.get_by_repo_name(repo_name).repo_id
526 user_id = User.get_default_user().user_id
527 self.scm_model.toggle_following_repo(repo_id, user_id)
528 h.flash(_('Updated repository visibility in public journal'),
529 category='success')
530 Session().commit()
531 except Exception:
532 h.flash(_('An error occurred during setting this'
533 ' repository in public journal'),
534 category='error')
535
536 return redirect(url('edit_repo_advanced', repo_name=repo_name))
537
538 @HasRepoPermissionAllDecorator('repository.admin')
539 @auth.CSRFRequired()
540 def edit_advanced_fork(self, repo_name):
541 """
542 Mark given repository as a fork of another
543
544 :param repo_name:
545 """
546
547 new_fork_id = request.POST.get('id_fork_of')
548 try:
549
550 if new_fork_id and not new_fork_id.isdigit():
551 log.error('Given fork id %s is not an INT', new_fork_id)
552
553 fork_id = safe_int(new_fork_id)
554 repo = ScmModel().mark_as_fork(repo_name, fork_id,
555 c.rhodecode_user.username)
556 fork = repo.fork.repo_name if repo.fork else _('Nothing')
557 Session().commit()
558 h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork),
559 category='success')
560 except RepositoryError as e:
561 log.exception("Repository Error occurred")
562 h.flash(str(e), category='error')
563 except Exception as e:
564 log.exception("Exception while editing fork")
565 h.flash(_('An error occurred during this operation'),
566 category='error')
567
568 return redirect(url('edit_repo_advanced', repo_name=repo_name))
569
570 @HasRepoPermissionAllDecorator('repository.admin')
571 @auth.CSRFRequired()
572 def edit_advanced_locking(self, repo_name):
573 """
574 Unlock repository when it is locked !
575
576 :param repo_name:
577 """
578 try:
579 repo = Repository.get_by_repo_name(repo_name)
580 if request.POST.get('set_lock'):
581 Repository.lock(repo, c.rhodecode_user.user_id,
582 lock_reason=Repository.LOCK_WEB)
583 h.flash(_('Locked repository'), category='success')
584 elif request.POST.get('set_unlock'):
585 Repository.unlock(repo)
586 h.flash(_('Unlocked repository'), category='success')
587 except Exception as e:
588 log.exception("Exception during unlocking")
589 h.flash(_('An error occurred during unlocking'),
590 category='error')
591 return redirect(url('edit_repo_advanced', repo_name=repo_name))
592
593 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
338 @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin')
594 @auth.CSRFRequired()
339 @auth.CSRFRequired()
595 def toggle_locking(self, repo_name):
340 def toggle_locking(self, repo_name):
@@ -617,32 +362,7 b' class ReposController(BaseRepoController'
617 log.exception("Exception during unlocking")
362 log.exception("Exception during unlocking")
618 h.flash(_('An error occurred during unlocking'),
363 h.flash(_('An error occurred during unlocking'),
619 category='error')
364 category='error')
620 return redirect(url('summary_home', repo_name=repo_name))
365 return redirect(h.route_path('repo_summary', repo_name=repo_name))
621
622 @HasRepoPermissionAllDecorator('repository.admin')
623 @auth.CSRFRequired()
624 def edit_caches(self, repo_name):
625 """PUT /{repo_name}/settings/caches: invalidate the repo caches."""
626 try:
627 ScmModel().mark_for_invalidation(repo_name, delete=True)
628 Session().commit()
629 h.flash(_('Cache invalidation successful'),
630 category='success')
631 except Exception:
632 log.exception("Exception during cache invalidation")
633 h.flash(_('An error occurred during cache invalidation'),
634 category='error')
635
636 return redirect(url('edit_repo_caches', repo_name=c.repo_name))
637
638 @HasRepoPermissionAllDecorator('repository.admin')
639 def edit_caches_form(self, repo_name):
640 """GET /repo_name/settings: Form to edit an existing item"""
641 # url('edit_repo', repo_name=ID)
642 c.repo_info = self._load_repo(repo_name)
643 c.active = 'caches'
644
645 return render('admin/repos/repo_edit.mako')
646
366
647 @HasRepoPermissionAllDecorator('repository.admin')
367 @HasRepoPermissionAllDecorator('repository.admin')
648 @auth.CSRFRequired()
368 @auth.CSRFRequired()
@@ -660,7 +380,6 b' class ReposController(BaseRepoController'
660 @HasRepoPermissionAllDecorator('repository.admin')
380 @HasRepoPermissionAllDecorator('repository.admin')
661 def edit_remote_form(self, repo_name):
381 def edit_remote_form(self, repo_name):
662 """GET /repo_name/settings: Form to edit an existing item"""
382 """GET /repo_name/settings: Form to edit an existing item"""
663 # url('edit_repo', repo_name=ID)
664 c.repo_info = self._load_repo(repo_name)
383 c.repo_info = self._load_repo(repo_name)
665 c.active = 'remote'
384 c.active = 'remote'
666
385
@@ -682,7 +401,6 b' class ReposController(BaseRepoController'
682 @HasRepoPermissionAllDecorator('repository.admin')
401 @HasRepoPermissionAllDecorator('repository.admin')
683 def edit_statistics_form(self, repo_name):
402 def edit_statistics_form(self, repo_name):
684 """GET /repo_name/settings: Form to edit an existing item"""
403 """GET /repo_name/settings: Form to edit an existing item"""
685 # url('edit_repo', repo_name=ID)
686 c.repo_info = self._load_repo(repo_name)
404 c.repo_info = self._load_repo(repo_name)
687 repo = c.repo_info.scm_instance()
405 repo = c.repo_info.scm_instance()
688
406
@@ -195,9 +195,12 b' class SettingsController(BaseController)'
195 pyramid_settings = get_current_registry().settings
195 pyramid_settings = get_current_registry().settings
196 c.svn_proxy_generate_config = pyramid_settings[generate_config]
196 c.svn_proxy_generate_config = pyramid_settings[generate_config]
197
197
198 defaults = self._form_defaults()
199
200 model.create_largeobjects_dirs_if_needed(defaults['paths_root_path'])
198 return htmlfill.render(
201 return htmlfill.render(
199 render('admin/settings/settings.mako'),
202 render('admin/settings/settings.mako'),
200 defaults=self._form_defaults(),
203 defaults=defaults,
201 encoding="UTF-8",
204 encoding="UTF-8",
202 force_defaults=False)
205 force_defaults=False)
203
206
@@ -35,9 +35,11 b' from sqlalchemy.orm import joinedload'
35
35
36 from rhodecode.lib import auth
36 from rhodecode.lib import auth
37 from rhodecode.lib import helpers as h
37 from rhodecode.lib import helpers as h
38 from rhodecode.lib import audit_logger
39 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.exceptions import UserGroupAssignedException,\
40 from rhodecode.lib.exceptions import UserGroupAssignedException,\
39 RepoGroupAssignmentError
41 RepoGroupAssignmentError
40 from rhodecode.lib.utils import jsonify, action_logger
42 from rhodecode.lib.utils import jsonify
41 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
43 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
42 from rhodecode.lib.auth import (
44 from rhodecode.lib.auth import (
43 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
45 LoginRequired, NotAnonymous, HasUserGroupPermissionAnyDecorator,
@@ -52,8 +54,7 b' from rhodecode.model.forms import ('
52 UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm,
54 UserGroupForm, UserGroupPermsForm, UserIndividualPermissionsForm,
53 UserPermissionsForm)
55 UserPermissionsForm)
54 from rhodecode.model.meta import Session
56 from rhodecode.model.meta import Session
55 from rhodecode.lib.utils import action_logger
57
56 from rhodecode.lib.ext_json import json
57
58
58 log = logging.getLogger(__name__)
59 log = logging.getLogger(__name__)
59
60
@@ -105,8 +106,6 b' class UserGroupsController(BaseControlle'
105 # permission check inside
106 # permission check inside
106 @NotAnonymous()
107 @NotAnonymous()
107 def index(self):
108 def index(self):
108 """GET /users_groups: All items in the collection"""
109 # url('users_groups')
110
109
111 from rhodecode.lib.utils import PartialRenderer
110 from rhodecode.lib.utils import PartialRenderer
112 _render = PartialRenderer('data_table/_dt_elements.mako')
111 _render = PartialRenderer('data_table/_dt_elements.mako')
@@ -142,8 +141,6 b' class UserGroupsController(BaseControlle'
142 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
141 @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
143 @auth.CSRFRequired()
142 @auth.CSRFRequired()
144 def create(self):
143 def create(self):
145 """POST /users_groups: Create a new item"""
146 # url('users_groups')
147
144
148 users_group_form = UserGroupForm()()
145 users_group_form = UserGroupForm()()
149 try:
146 try:
@@ -154,14 +151,16 b' class UserGroupsController(BaseControlle'
154 owner=c.rhodecode_user.user_id,
151 owner=c.rhodecode_user.user_id,
155 active=form_result['users_group_active'])
152 active=form_result['users_group_active'])
156 Session().flush()
153 Session().flush()
157
154 creation_data = user_group.get_api_data()
158 user_group_name = form_result['users_group_name']
155 user_group_name = form_result['users_group_name']
159 action_logger(c.rhodecode_user,
156
160 'admin_created_users_group:%s' % user_group_name,
157 audit_logger.store_web(
161 None, self.ip_addr, self.sa)
158 'user_group.create', action_data={'data': creation_data},
162 user_group_link = h.link_to(h.escape(user_group_name),
159 user=c.rhodecode_user)
163 url('edit_users_group',
160
164 user_group_id=user_group.users_group_id))
161 user_group_link = h.link_to(
162 h.escape(user_group_name),
163 url('edit_users_group', user_group_id=user_group.users_group_id))
165 h.flash(h.literal(_('Created user group %(user_group_link)s')
164 h.flash(h.literal(_('Created user group %(user_group_link)s')
166 % {'user_group_link': user_group_link}),
165 % {'user_group_link': user_group_link}),
167 category='success')
166 category='success')
@@ -191,13 +190,6 b' class UserGroupsController(BaseControlle'
191 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
190 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
192 @auth.CSRFRequired()
191 @auth.CSRFRequired()
193 def update(self, user_group_id):
192 def update(self, user_group_id):
194 """PUT /user_groups/user_group_id: Update an existing item"""
195 # Forms posted to this method should contain a hidden field:
196 # <input type="hidden" name="_method" value="PUT" />
197 # Or using helpers:
198 # h.form(url('users_group', user_group_id=ID),
199 # method='put')
200 # url('users_group', user_group_id=ID)
201
193
202 user_group_id = safe_int(user_group_id)
194 user_group_id = safe_int(user_group_id)
203 c.user_group = UserGroup.get_or_404(user_group_id)
195 c.user_group = UserGroup.get_or_404(user_group_id)
@@ -207,16 +199,22 b' class UserGroupsController(BaseControlle'
207 users_group_form = UserGroupForm(
199 users_group_form = UserGroupForm(
208 edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)()
200 edit=True, old_data=c.user_group.get_dict(), allow_disabled=True)()
209
201
202 old_values = c.user_group.get_api_data()
210 try:
203 try:
211 form_result = users_group_form.to_python(request.POST)
204 form_result = users_group_form.to_python(request.POST)
212 pstruct = peppercorn.parse(request.POST.items())
205 pstruct = peppercorn.parse(request.POST.items())
213 form_result['users_group_members'] = pstruct['user_group_members']
206 form_result['users_group_members'] = pstruct['user_group_members']
214
207
215 UserGroupModel().update(c.user_group, form_result)
208 user_group, added_members, removed_members = \
209 UserGroupModel().update(c.user_group, form_result)
216 updated_user_group = form_result['users_group_name']
210 updated_user_group = form_result['users_group_name']
217 action_logger(c.rhodecode_user,
211
218 'admin_updated_users_group:%s' % updated_user_group,
212 audit_logger.store_web(
219 None, self.ip_addr, self.sa)
213 'user_group.edit', action_data={'old_data': old_values},
214 user=c.rhodecode_user)
215
216 # TODO(marcink): use added/removed to set user_group.edit.member.add
217
220 h.flash(_('Updated user group %s') % updated_user_group,
218 h.flash(_('Updated user group %s') % updated_user_group,
221 category='success')
219 category='success')
222 Session().commit()
220 Session().commit()
@@ -241,19 +239,16 b' class UserGroupsController(BaseControlle'
241 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
239 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
242 @auth.CSRFRequired()
240 @auth.CSRFRequired()
243 def delete(self, user_group_id):
241 def delete(self, user_group_id):
244 """DELETE /user_groups/user_group_id: Delete an existing item"""
245 # Forms posted to this method should contain a hidden field:
246 # <input type="hidden" name="_method" value="DELETE" />
247 # Or using helpers:
248 # h.form(url('users_group', user_group_id=ID),
249 # method='delete')
250 # url('users_group', user_group_id=ID)
251 user_group_id = safe_int(user_group_id)
242 user_group_id = safe_int(user_group_id)
252 c.user_group = UserGroup.get_or_404(user_group_id)
243 c.user_group = UserGroup.get_or_404(user_group_id)
253 force = str2bool(request.POST.get('force'))
244 force = str2bool(request.POST.get('force'))
254
245
246 old_values = c.user_group.get_api_data()
255 try:
247 try:
256 UserGroupModel().delete(c.user_group, force=force)
248 UserGroupModel().delete(c.user_group, force=force)
249 audit_logger.store_web(
250 'user.delete', action_data={'old_data': old_values},
251 user=c.rhodecode_user)
257 Session().commit()
252 Session().commit()
258 h.flash(_('Successfully deleted user group'), category='success')
253 h.flash(_('Successfully deleted user group'), category='success')
259 except UserGroupAssignedException as e:
254 except UserGroupAssignedException as e:
@@ -330,9 +325,9 b' class UserGroupsController(BaseControlle'
330 except RepoGroupAssignmentError:
325 except RepoGroupAssignmentError:
331 h.flash(_('Target group cannot be the same'), category='error')
326 h.flash(_('Target group cannot be the same'), category='error')
332 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
327 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
333 #TODO: implement this
328
334 #action_logger(c.rhodecode_user, 'admin_changed_repo_permissions',
329 # TODO(marcink): implement global permissions
335 # repo_name, self.ip_addr, self.sa)
330 # audit_log.store_web('user_group.edit.permissions')
336 Session().commit()
331 Session().commit()
337 h.flash(_('User Group permissions updated'), category='success')
332 h.flash(_('User Group permissions updated'), category='success')
338 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
333 return redirect(url('edit_user_group_perms', user_group_id=user_group_id))
@@ -389,8 +384,6 b' class UserGroupsController(BaseControlle'
389 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
384 @HasUserGroupPermissionAnyDecorator('usergroup.admin')
390 @auth.CSRFRequired()
385 @auth.CSRFRequired()
391 def update_global_perms(self, user_group_id):
386 def update_global_perms(self, user_group_id):
392 """PUT /users_perm/user_group_id: Update an existing item"""
393 # url('users_group_perm', user_group_id=ID, method='put')
394 user_group_id = safe_int(user_group_id)
387 user_group_id = safe_int(user_group_id)
395 user_group = UserGroup.get_or_404(user_group_id)
388 user_group = UserGroup.get_or_404(user_group_id)
396 c.active = 'global_perms'
389 c.active = 'global_perms'
@@ -492,6 +485,9 b' class UserGroupsController(BaseControlle'
492 @XHRRequired()
485 @XHRRequired()
493 @jsonify
486 @jsonify
494 def user_group_members(self, user_group_id):
487 def user_group_members(self, user_group_id):
488 """
489 Return members of given user group
490 """
495 user_group_id = safe_int(user_group_id)
491 user_group_id = safe_int(user_group_id)
496 user_group = UserGroup.get_or_404(user_group_id)
492 user_group = UserGroup.get_or_404(user_group_id)
497 group_members_obj = sorted((x.user for x in user_group.members),
493 group_members_obj = sorted((x.user for x in user_group.members),
@@ -500,8 +496,8 b' class UserGroupsController(BaseControlle'
500 group_members = [
496 group_members = [
501 {
497 {
502 'id': user.user_id,
498 'id': user.user_id,
503 'first_name': user.name,
499 'first_name': user.first_name,
504 'last_name': user.lastname,
500 'last_name': user.last_name,
505 'username': user.username,
501 'username': user.username,
506 'icon_link': h.gravatar_url(user.email, 30),
502 'icon_link': h.gravatar_url(user.email, 30),
507 'value_display': h.person(user.email),
503 'value_display': h.person(user.email),
@@ -31,15 +31,17 b' from pylons.controllers.util import redi'
31 from pylons.i18n.translation import _
31 from pylons.i18n.translation import _
32
32
33 from rhodecode.authentication.plugins import auth_rhodecode
33 from rhodecode.authentication.plugins import auth_rhodecode
34
35 from rhodecode.lib import helpers as h
36 from rhodecode.lib import auth
37 from rhodecode.lib import audit_logger
38 from rhodecode.lib.auth import (
39 LoginRequired, HasPermissionAllDecorator, AuthUser)
40 from rhodecode.lib.base import BaseController, render
34 from rhodecode.lib.exceptions import (
41 from rhodecode.lib.exceptions import (
35 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
42 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
36 UserOwnsUserGroupsException, UserCreationError)
43 UserOwnsUserGroupsException, UserCreationError)
37 from rhodecode.lib import helpers as h
44 from rhodecode.lib.utils2 import safe_int, AttributeDict
38 from rhodecode.lib import auth
39 from rhodecode.lib.auth import (
40 LoginRequired, HasPermissionAllDecorator, AuthUser, generate_auth_token)
41 from rhodecode.lib.base import BaseController, render
42 from rhodecode.model.auth_token import AuthTokenModel
43
45
44 from rhodecode.model.db import (
46 from rhodecode.model.db import (
45 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
47 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
@@ -49,8 +51,6 b' from rhodecode.model.repo_group import R'
49 from rhodecode.model.user import UserModel
51 from rhodecode.model.user import UserModel
50 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
51 from rhodecode.model.permission import PermissionModel
53 from rhodecode.model.permission import PermissionModel
52 from rhodecode.lib.utils import action_logger
53 from rhodecode.lib.utils2 import datetime_to_time, safe_int, AttributeDict
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
@@ -88,7 +88,6 b' class UsersController(BaseController):'
88 @HasPermissionAllDecorator('hg.admin')
88 @HasPermissionAllDecorator('hg.admin')
89 @auth.CSRFRequired()
89 @auth.CSRFRequired()
90 def create(self):
90 def create(self):
91 """POST /users: Create a new item"""
92 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
91 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
93 user_model = UserModel()
92 user_model = UserModel()
94 user_form = UserForm()()
93 user_form = UserForm()()
@@ -96,9 +95,12 b' class UsersController(BaseController):'
96 form_result = user_form.to_python(dict(request.POST))
95 form_result = user_form.to_python(dict(request.POST))
97 user = user_model.create(form_result)
96 user = user_model.create(form_result)
98 Session().flush()
97 Session().flush()
98 creation_data = user.get_api_data()
99 username = form_result['username']
99 username = form_result['username']
100 action_logger(c.rhodecode_user, 'admin_created_user:%s' % username,
100
101 None, self.ip_addr, self.sa)
101 audit_logger.store_web(
102 'user.create', action_data={'data': creation_data},
103 user=c.rhodecode_user)
102
104
103 user_link = h.link_to(h.escape(username),
105 user_link = h.link_to(h.escape(username),
104 url('edit_user',
106 url('edit_user',
@@ -125,8 +127,6 b' class UsersController(BaseController):'
125
127
126 @HasPermissionAllDecorator('hg.admin')
128 @HasPermissionAllDecorator('hg.admin')
127 def new(self):
129 def new(self):
128 """GET /users/new: Form to create a new item"""
129 # url('new_user')
130 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
130 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
131 self._get_personal_repo_group_template_vars()
131 self._get_personal_repo_group_template_vars()
132 return render('admin/users/user_add.mako')
132 return render('admin/users/user_add.mako')
@@ -134,13 +134,7 b' class UsersController(BaseController):'
134 @HasPermissionAllDecorator('hg.admin')
134 @HasPermissionAllDecorator('hg.admin')
135 @auth.CSRFRequired()
135 @auth.CSRFRequired()
136 def update(self, user_id):
136 def update(self, user_id):
137 """PUT /users/user_id: Update an existing item"""
137
138 # Forms posted to this method should contain a hidden field:
139 # <input type="hidden" name="_method" value="PUT" />
140 # Or using helpers:
141 # h.form(url('update_user', user_id=ID),
142 # method='put')
143 # url('user', user_id=ID)
144 user_id = safe_int(user_id)
138 user_id = safe_int(user_id)
145 c.user = User.get_or_404(user_id)
139 c.user = User.get_or_404(user_id)
146 c.active = 'profile'
140 c.active = 'profile'
@@ -152,6 +146,7 b' class UsersController(BaseController):'
152 old_data={'user_id': user_id,
146 old_data={'user_id': user_id,
153 'email': c.user.email})()
147 'email': c.user.email})()
154 form_result = {}
148 form_result = {}
149 old_values = c.user.get_api_data()
155 try:
150 try:
156 form_result = _form.to_python(dict(request.POST))
151 form_result = _form.to_python(dict(request.POST))
157 skip_attrs = ['extern_type', 'extern_name']
152 skip_attrs = ['extern_type', 'extern_name']
@@ -160,12 +155,15 b' class UsersController(BaseController):'
160 # forbid updating username for external accounts
155 # forbid updating username for external accounts
161 skip_attrs.append('username')
156 skip_attrs.append('username')
162
157
163 UserModel().update_user(user_id, skip_attrs=skip_attrs, **form_result)
158 UserModel().update_user(
164 usr = form_result['username']
159 user_id, skip_attrs=skip_attrs, **form_result)
165 action_logger(c.rhodecode_user, 'admin_updated_user:%s' % usr,
160
166 None, self.ip_addr, self.sa)
161 audit_logger.store_web(
162 'user.edit', action_data={'old_data': old_values},
163 user=c.rhodecode_user)
164
165 Session().commit()
167 h.flash(_('User updated successfully'), category='success')
166 h.flash(_('User updated successfully'), category='success')
168 Session().commit()
169 except formencode.Invalid as errors:
167 except formencode.Invalid as errors:
170 defaults = errors.value
168 defaults = errors.value
171 e = errors.error_dict or {}
169 e = errors.error_dict or {}
@@ -188,13 +186,6 b' class UsersController(BaseController):'
188 @HasPermissionAllDecorator('hg.admin')
186 @HasPermissionAllDecorator('hg.admin')
189 @auth.CSRFRequired()
187 @auth.CSRFRequired()
190 def delete(self, user_id):
188 def delete(self, user_id):
191 """DELETE /users/user_id: Delete an existing item"""
192 # Forms posted to this method should contain a hidden field:
193 # <input type="hidden" name="_method" value="DELETE" />
194 # Or using helpers:
195 # h.form(url('delete_user', user_id=ID),
196 # method='delete')
197 # url('user', user_id=ID)
198 user_id = safe_int(user_id)
189 user_id = safe_int(user_id)
199 c.user = User.get_or_404(user_id)
190 c.user = User.get_or_404(user_id)
200
191
@@ -249,10 +240,16 b' class UsersController(BaseController):'
249 _('Deleted %s user groups') % len(_user_groups),
240 _('Deleted %s user groups') % len(_user_groups),
250 category='success')
241 category='success')
251
242
243 old_values = c.user.get_api_data()
252 try:
244 try:
253 UserModel().delete(c.user, handle_repos=handle_repos,
245 UserModel().delete(c.user, handle_repos=handle_repos,
254 handle_repo_groups=handle_repo_groups,
246 handle_repo_groups=handle_repo_groups,
255 handle_user_groups=handle_user_groups)
247 handle_user_groups=handle_user_groups)
248
249 audit_logger.store_web(
250 'user.delete', action_data={'old_data': old_values},
251 user=c.rhodecode_user)
252
256 Session().commit()
253 Session().commit()
257 set_handle_flash_repos()
254 set_handle_flash_repos()
258 set_handle_flash_repo_groups()
255 set_handle_flash_repo_groups()
@@ -272,19 +269,25 b' class UsersController(BaseController):'
272 def reset_password(self, user_id):
269 def reset_password(self, user_id):
273 """
270 """
274 toggle reset password flag for this user
271 toggle reset password flag for this user
275
276 :param user_id:
277 """
272 """
278 user_id = safe_int(user_id)
273 user_id = safe_int(user_id)
279 c.user = User.get_or_404(user_id)
274 c.user = User.get_or_404(user_id)
280 try:
275 try:
281 old_value = c.user.user_data.get('force_password_change')
276 old_value = c.user.user_data.get('force_password_change')
282 c.user.update_userdata(force_password_change=not old_value)
277 c.user.update_userdata(force_password_change=not old_value)
283 Session().commit()
278
284 if old_value:
279 if old_value:
285 msg = _('Force password change disabled for user')
280 msg = _('Force password change disabled for user')
281 audit_logger.store_web(
282 'user.edit.password_reset.disabled',
283 user=c.rhodecode_user)
286 else:
284 else:
287 msg = _('Force password change enabled for user')
285 msg = _('Force password change enabled for user')
286 audit_logger.store_web(
287 'user.edit.password_reset.enabled',
288 user=c.rhodecode_user)
289
290 Session().commit()
288 h.flash(msg, category='success')
291 h.flash(msg, category='success')
289 except Exception:
292 except Exception:
290 log.exception("Exception during password reset for user")
293 log.exception("Exception during password reset for user")
@@ -298,8 +301,6 b' class UsersController(BaseController):'
298 def create_personal_repo_group(self, user_id):
301 def create_personal_repo_group(self, user_id):
299 """
302 """
300 Create personal repository group for this user
303 Create personal repository group for this user
301
302 :param user_id:
303 """
304 """
304 from rhodecode.model.repo_group import RepoGroupModel
305 from rhodecode.model.repo_group import RepoGroupModel
305
306
@@ -381,8 +382,7 b' class UsersController(BaseController):'
381 return redirect(h.route_path('users'))
382 return redirect(h.route_path('users'))
382
383
383 c.active = 'advanced'
384 c.active = 'advanced'
384 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
385 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
385 c.personal_repo_group = c.perm_user.personal_repo_group
386 c.personal_repo_group_name = RepoGroupModel()\
386 c.personal_repo_group_name = RepoGroupModel()\
387 .get_personal_group_name(user)
387 .get_personal_group_name(user)
388 c.first_admin = User.get_first_super_admin()
388 c.first_admin = User.get_first_super_admin()
@@ -429,8 +429,6 b' class UsersController(BaseController):'
429 @HasPermissionAllDecorator('hg.admin')
429 @HasPermissionAllDecorator('hg.admin')
430 @auth.CSRFRequired()
430 @auth.CSRFRequired()
431 def update_global_perms(self, user_id):
431 def update_global_perms(self, user_id):
432 """PUT /users_perm/user_id: Update an existing item"""
433 # url('user_perm', user_id=ID, method='put')
434 user_id = safe_int(user_id)
432 user_id = safe_int(user_id)
435 user = User.get_or_404(user_id)
433 user = User.get_or_404(user_id)
436 c.active = 'global_perms'
434 c.active = 'global_perms'
@@ -457,11 +455,13 b' class UsersController(BaseController):'
457
455
458 PermissionModel().update_user_permissions(form_result)
456 PermissionModel().update_user_permissions(form_result)
459
457
458 # TODO(marcink): implement global permissions
459 # audit_log.store_web('user.edit.permissions')
460
460 Session().commit()
461 Session().commit()
461 h.flash(_('User global permissions updated successfully'),
462 h.flash(_('User global permissions updated successfully'),
462 category='success')
463 category='success')
463
464
464 Session().commit()
465 except formencode.Invalid as errors:
465 except formencode.Invalid as errors:
466 defaults = errors.value
466 defaults = errors.value
467 c.user = user
467 c.user = user
@@ -491,140 +491,3 b' class UsersController(BaseController):'
491
491
492 return render('admin/users/user_edit.mako')
492 return render('admin/users/user_edit.mako')
493
493
494 @HasPermissionAllDecorator('hg.admin')
495 def edit_emails(self, user_id):
496 user_id = safe_int(user_id)
497 c.user = User.get_or_404(user_id)
498 if c.user.username == User.DEFAULT_USER:
499 h.flash(_("You can't edit this user"), category='warning')
500 return redirect(h.route_path('users'))
501
502 c.active = 'emails'
503 c.user_email_map = UserEmailMap.query() \
504 .filter(UserEmailMap.user == c.user).all()
505
506 defaults = c.user.get_dict()
507 return htmlfill.render(
508 render('admin/users/user_edit.mako'),
509 defaults=defaults,
510 encoding="UTF-8",
511 force_defaults=False)
512
513 @HasPermissionAllDecorator('hg.admin')
514 @auth.CSRFRequired()
515 def add_email(self, user_id):
516 """POST /user_emails:Add an existing item"""
517 # url('user_emails', user_id=ID, method='put')
518 user_id = safe_int(user_id)
519 c.user = User.get_or_404(user_id)
520
521 email = request.POST.get('new_email')
522 user_model = UserModel()
523
524 try:
525 user_model.add_extra_email(user_id, email)
526 Session().commit()
527 h.flash(_("Added new email address `%s` for user account") % email,
528 category='success')
529 except formencode.Invalid as error:
530 msg = error.error_dict['email']
531 h.flash(msg, category='error')
532 except Exception:
533 log.exception("Exception during email saving")
534 h.flash(_('An error occurred during email saving'),
535 category='error')
536 return redirect(url('edit_user_emails', user_id=user_id))
537
538 @HasPermissionAllDecorator('hg.admin')
539 @auth.CSRFRequired()
540 def delete_email(self, user_id):
541 """DELETE /user_emails_delete/user_id: Delete an existing item"""
542 # url('user_emails_delete', user_id=ID, method='delete')
543 user_id = safe_int(user_id)
544 c.user = User.get_or_404(user_id)
545 email_id = request.POST.get('del_email_id')
546 user_model = UserModel()
547 user_model.delete_extra_email(user_id, email_id)
548 Session().commit()
549 h.flash(_("Removed email address from user account"), category='success')
550 return redirect(url('edit_user_emails', user_id=user_id))
551
552 @HasPermissionAllDecorator('hg.admin')
553 def edit_ips(self, user_id):
554 user_id = safe_int(user_id)
555 c.user = User.get_or_404(user_id)
556 if c.user.username == User.DEFAULT_USER:
557 h.flash(_("You can't edit this user"), category='warning')
558 return redirect(h.route_path('users'))
559
560 c.active = 'ips'
561 c.user_ip_map = UserIpMap.query() \
562 .filter(UserIpMap.user == c.user).all()
563
564 c.inherit_default_ips = c.user.inherit_default_permissions
565 c.default_user_ip_map = UserIpMap.query() \
566 .filter(UserIpMap.user == User.get_default_user()).all()
567
568 defaults = c.user.get_dict()
569 return htmlfill.render(
570 render('admin/users/user_edit.mako'),
571 defaults=defaults,
572 encoding="UTF-8",
573 force_defaults=False)
574
575 @HasPermissionAllDecorator('hg.admin')
576 @auth.CSRFRequired()
577 def add_ip(self, user_id):
578 """POST /user_ips:Add an existing item"""
579 # url('user_ips', user_id=ID, method='put')
580
581 user_id = safe_int(user_id)
582 c.user = User.get_or_404(user_id)
583 user_model = UserModel()
584 try:
585 ip_list = user_model.parse_ip_range(request.POST.get('new_ip'))
586 except Exception as e:
587 ip_list = []
588 log.exception("Exception during ip saving")
589 h.flash(_('An error occurred during ip saving:%s' % (e,)),
590 category='error')
591
592 desc = request.POST.get('description')
593 added = []
594 for ip in ip_list:
595 try:
596 user_model.add_extra_ip(user_id, ip, desc)
597 Session().commit()
598 added.append(ip)
599 except formencode.Invalid as error:
600 msg = error.error_dict['ip']
601 h.flash(msg, category='error')
602 except Exception:
603 log.exception("Exception during ip saving")
604 h.flash(_('An error occurred during ip saving'),
605 category='error')
606 if added:
607 h.flash(
608 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
609 category='success')
610 if 'default_user' in request.POST:
611 return redirect(url('admin_permissions_ips'))
612 return redirect(url('edit_user_ips', user_id=user_id))
613
614 @HasPermissionAllDecorator('hg.admin')
615 @auth.CSRFRequired()
616 def delete_ip(self, user_id):
617 """DELETE /user_ips_delete/user_id: Delete an existing item"""
618 # url('user_ips_delete', user_id=ID, method='delete')
619 user_id = safe_int(user_id)
620 c.user = User.get_or_404(user_id)
621
622 ip_id = request.POST.get('del_ip_id')
623 user_model = UserModel()
624 user_model.delete_extra_ip(user_id, ip_id)
625 Session().commit()
626 h.flash(_("Removed ip address from user whitelist"), category='success')
627
628 if 'default_user' in request.POST:
629 return redirect(url('admin_permissions_ips'))
630 return redirect(url('edit_user_ips', user_id=user_id))
@@ -46,27 +46,6 b' log = logging.getLogger(__name__)'
46 DEFAULT_CHANGELOG_SIZE = 20
46 DEFAULT_CHANGELOG_SIZE = 20
47
47
48
48
49 def _load_changelog_summary():
50 p = safe_int(request.GET.get('page'), 1)
51 size = safe_int(request.GET.get('size'), 10)
52
53 def url_generator(**kw):
54 return url('summary_home',
55 repo_name=c.rhodecode_db_repo.repo_name, size=size, **kw)
56
57 pre_load = ['author', 'branch', 'date', 'message']
58 try:
59 collection = c.rhodecode_repo.get_commits(pre_load=pre_load)
60 except EmptyRepositoryError:
61 collection = c.rhodecode_repo
62
63 c.repo_commits = RepoPage(
64 collection, page=p, items_per_page=size, url=url_generator)
65 page_ids = [x.raw_id for x in c.repo_commits]
66 c.comments = c.rhodecode_db_repo.get_comments(page_ids)
67 c.statuses = c.rhodecode_db_repo.statuses(page_ids)
68
69
70 class ChangelogController(BaseRepoController):
49 class ChangelogController(BaseRepoController):
71
50
72 def __before__(self):
51 def __before__(self):
@@ -88,13 +67,11 b' class ChangelogController(BaseRepoContro'
88 except EmptyRepositoryError:
67 except EmptyRepositoryError:
89 if not redirect_after:
68 if not redirect_after:
90 return None
69 return None
91 h.flash(h.literal(_('There are no commits yet')),
70 h.flash(_('There are no commits yet'), category='warning')
92 category='warning')
93 redirect(url('changelog_home', repo_name=repo.repo_name))
71 redirect(url('changelog_home', repo_name=repo.repo_name))
94 except RepositoryError as e:
72 except RepositoryError as e:
95 msg = safe_str(e)
73 log.exception(safe_str(e))
96 log.exception(msg)
74 h.flash(safe_str(h.escape(e)), category='warning')
97 h.flash(msg, category='warning')
98 if not partial:
75 if not partial:
99 redirect(h.url('changelog_home', repo_name=repo.repo_name))
76 redirect(h.url('changelog_home', repo_name=repo.repo_name))
100 raise HTTPBadRequest()
77 raise HTTPBadRequest()
@@ -134,7 +111,7 b' class ChangelogController(BaseRepoContro'
134
111
135 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
112 def _check_if_valid_branch(self, branch_name, repo_name, f_path):
136 if branch_name not in c.rhodecode_repo.branches_all:
113 if branch_name not in c.rhodecode_repo.branches_all:
137 h.flash('Branch {} is not found.'.format(branch_name),
114 h.flash('Branch {} is not found.'.format(h.escape(branch_name)),
138 category='warning')
115 category='warning')
139 redirect(url('changelog_file_home', repo_name=repo_name,
116 redirect(url('changelog_file_home', repo_name=repo_name,
140 revision=branch_name, f_path=f_path or ''))
117 revision=branch_name, f_path=f_path or ''))
@@ -210,12 +187,11 b' class ChangelogController(BaseRepoContro'
210 collection, p, chunk_size, c.branch_name, dynamic=f_path)
187 collection, p, chunk_size, c.branch_name, dynamic=f_path)
211
188
212 except EmptyRepositoryError as e:
189 except EmptyRepositoryError as e:
213 h.flash(safe_str(e), category='warning')
190 h.flash(safe_str(h.escape(e)), category='warning')
214 return redirect(url('summary_home', repo_name=repo_name))
191 return redirect(h.route_path('repo_summary', repo_name=repo_name))
215 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
192 except (RepositoryError, CommitDoesNotExistError, Exception) as e:
216 msg = safe_str(e)
193 log.exception(safe_str(e))
217 log.exception(msg)
194 h.flash(safe_str(h.escape(e)), category='error')
218 h.flash(msg, category='error')
219 return redirect(url('changelog_home', repo_name=repo_name))
195 return redirect(url('changelog_home', repo_name=repo_name))
220
196
221 if (request.environ.get('HTTP_X_PARTIAL_XHR')
197 if (request.environ.get('HTTP_X_PARTIAL_XHR')
@@ -279,12 +255,3 b' class ChangelogController(BaseRepoContro'
279 c.rhodecode_repo, c.pagination,
255 c.rhodecode_repo, c.pagination,
280 prev_data=prev_data, next_data=next_data)
256 prev_data=prev_data, next_data=next_data)
281 return render('changelog/changelog_elements.mako')
257 return render('changelog/changelog_elements.mako')
282
283 @LoginRequired()
284 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
285 'repository.admin')
286 def changelog_summary(self, repo_name):
287 if request.environ.get('HTTP_X_PJAX'):
288 _load_changelog_summary()
289 return render('changelog/changelog_summary_data.mako')
290 raise HTTPNotFound()
@@ -39,8 +39,8 b' from rhodecode.lib.base import BaseRepoC'
39 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.compat import OrderedDict
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
40 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 import rhodecode.lib.helpers as h
41 import rhodecode.lib.helpers as h
42 from rhodecode.lib.utils import action_logger, jsonify
42 from rhodecode.lib.utils import jsonify
43 from rhodecode.lib.utils2 import safe_unicode
43 from rhodecode.lib.utils2 import safe_unicode, safe_int
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
46 RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError)
@@ -48,7 +48,6 b' from rhodecode.model.db import Changeset'
48 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.changeset_status import ChangesetStatusModel
49 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.comment import CommentsModel
50 from rhodecode.model.meta import Session
50 from rhodecode.model.meta import Session
51 from rhodecode.model.repo import RepoModel
52
51
53
52
54 log = logging.getLogger(__name__)
53 log = logging.getLogger(__name__)
@@ -268,8 +267,10 b' class ChangesetController(BaseRepoContro'
268 repo_name=c.repo_name,
267 repo_name=c.repo_name,
269 source_node_getter=_node_getter(commit1),
268 source_node_getter=_node_getter(commit1),
270 target_node_getter=_node_getter(commit2),
269 target_node_getter=_node_getter(commit2),
271 comments=inline_comments
270 comments=inline_comments)
272 ).render_patchset(_parsed, commit1.raw_id, commit2.raw_id)
271 diffset = diffset.render_patchset(
272 _parsed, commit1.raw_id, commit2.raw_id)
273
273 c.changes[commit.raw_id] = diffset
274 c.changes[commit.raw_id] = diffset
274 else:
275 else:
275 # downloads/raw we only need RAW diff nothing else
276 # downloads/raw we only need RAW diff nothing else
@@ -368,7 +369,6 b' class ChangesetController(BaseRepoContro'
368 comment_type=comment_type,
369 comment_type=comment_type,
369 resolves_comment_id=resolves_comment_id
370 resolves_comment_id=resolves_comment_id
370 )
371 )
371 c.inline_comment = True if comment.line_no else False
372
372
373 # get status if set !
373 # get status if set !
374 if status:
374 if status:
@@ -433,20 +433,26 b' class ChangesetController(BaseRepoContro'
433 @auth.CSRFRequired()
433 @auth.CSRFRequired()
434 @jsonify
434 @jsonify
435 def delete_comment(self, repo_name, comment_id):
435 def delete_comment(self, repo_name, comment_id):
436 comment = ChangesetComment.get(comment_id)
436 comment = ChangesetComment.get_or_404(safe_int(comment_id))
437 if not comment:
437 if not comment:
438 log.debug('Comment with id:%s not found, skipping', comment_id)
438 log.debug('Comment with id:%s not found, skipping', comment_id)
439 # comment already deleted in another call probably
439 # comment already deleted in another call probably
440 return True
440 return True
441
441
442 owner = (comment.author.user_id == c.rhodecode_user.user_id)
443 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
442 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name)
444 if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner:
443 super_admin = h.HasPermissionAny('hg.admin')()
445 CommentsModel().delete(comment=comment)
444 comment_owner = (comment.author.user_id == c.rhodecode_user.user_id)
445 is_repo_comment = comment.repo.repo_name == c.repo_name
446 comment_repo_admin = is_repo_admin and is_repo_comment
447
448 if super_admin or comment_owner or comment_repo_admin:
449 CommentsModel().delete(comment=comment, user=c.rhodecode_user)
446 Session().commit()
450 Session().commit()
447 return True
451 return True
448 else:
452 else:
449 raise HTTPForbidden()
453 log.warning('No permissions for user %s to delete comment_id: %s',
454 c.rhodecode_user, comment_id)
455 raise HTTPNotFound()
450
456
451 @LoginRequired()
457 @LoginRequired()
452 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
458 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
@@ -24,7 +24,7 b' Compare controller for showing differenc'
24
24
25 import logging
25 import logging
26
26
27 from webob.exc import HTTPBadRequest
27 from webob.exc import HTTPBadRequest, HTTPNotFound
28 from pylons import request, tmpl_context as c, url
28 from pylons import request, tmpl_context as c, url
29 from pylons.controllers.util import redirect
29 from pylons.controllers.util import redirect
30 from pylons.i18n.translation import _
30 from pylons.i18n.translation import _
@@ -63,14 +63,13 b' class CompareController(BaseRepoControll'
63 return repo.scm_instance().EMPTY_COMMIT
63 return repo.scm_instance().EMPTY_COMMIT
64 h.flash(h.literal(_('There are no commits yet')),
64 h.flash(h.literal(_('There are no commits yet')),
65 category='warning')
65 category='warning')
66 redirect(url('summary_home', repo_name=repo.repo_name))
66 redirect(h.route_path('repo_summary', repo_name=repo.repo_name))
67
67
68 except RepositoryError as e:
68 except RepositoryError as e:
69 msg = safe_str(e)
69 log.exception(safe_str(e))
70 log.exception(msg)
70 h.flash(safe_str(h.escape(e)), category='warning')
71 h.flash(msg, category='warning')
72 if not partial:
71 if not partial:
73 redirect(h.url('summary_home', repo_name=repo.repo_name))
72 redirect(h.route_path('repo_summary', repo_name=repo.repo_name))
74 raise HTTPBadRequest()
73 raise HTTPBadRequest()
75
74
76 @LoginRequired()
75 @LoginRequired()
@@ -86,6 +85,10 b' class CompareController(BaseRepoControll'
86 target_repo = request.GET.get('target_repo', source_repo)
85 target_repo = request.GET.get('target_repo', source_repo)
87 c.source_repo = Repository.get_by_repo_name(source_repo)
86 c.source_repo = Repository.get_by_repo_name(source_repo)
88 c.target_repo = Repository.get_by_repo_name(target_repo)
87 c.target_repo = Repository.get_by_repo_name(target_repo)
88
89 if c.source_repo is None or c.target_repo is None:
90 raise HTTPNotFound()
91
89 c.source_ref = c.target_ref = _('Select commit')
92 c.source_ref = c.target_ref = _('Select commit')
90 c.source_ref_type = ""
93 c.source_ref_type = ""
91 c.target_ref_type = ""
94 c.target_ref_type = ""
@@ -141,18 +144,17 b' class CompareController(BaseRepoControll'
141 target_repo = Repository.get_by_repo_name(target_repo_name)
144 target_repo = Repository.get_by_repo_name(target_repo_name)
142
145
143 if source_repo is None:
146 if source_repo is None:
144 msg = _('Could not find the original repo: %(repo)s') % {
147 log.error('Could not find the source repo: {}'
145 'repo': source_repo}
148 .format(source_repo_name))
146
149 h.flash(_('Could not find the source repo: `{}`')
147 log.error(msg)
150 .format(h.escape(source_repo_name)), category='error')
148 h.flash(msg, category='error')
149 return redirect(url('compare_home', repo_name=c.repo_name))
151 return redirect(url('compare_home', repo_name=c.repo_name))
150
152
151 if target_repo is None:
153 if target_repo is None:
152 msg = _('Could not find the other repo: %(repo)s') % {
154 log.error('Could not find the target repo: {}'
153 'repo': target_repo_name}
155 .format(source_repo_name))
154 log.error(msg)
156 h.flash(_('Could not find the target repo: `{}`')
155 h.flash(msg, category='error')
157 .format(h.escape(target_repo_name)), category='error')
156 return redirect(url('compare_home', repo_name=c.repo_name))
158 return redirect(url('compare_home', repo_name=c.repo_name))
157
159
158 source_scm = source_repo.scm_instance()
160 source_scm = source_repo.scm_instance()
@@ -269,11 +271,13 b' class CompareController(BaseRepoControll'
269 return None
271 return None
270 return get_node
272 return get_node
271
273
272 c.diffset = codeblocks.DiffSet(
274 diffset = codeblocks.DiffSet(
273 repo_name=source_repo.repo_name,
275 repo_name=source_repo.repo_name,
274 source_node_getter=_node_getter(source_commit),
276 source_node_getter=_node_getter(source_commit),
275 target_node_getter=_node_getter(target_commit),
277 target_node_getter=_node_getter(target_commit),
276 ).render_patchset(_parsed, source_ref, target_ref)
278 )
279 c.diffset = diffset.render_patchset(
280 _parsed, source_ref, target_ref)
277
281
278 c.preview_mode = merge
282 c.preview_mode = merge
279 c.source_commit = source_commit
283 c.source_commit = source_commit
@@ -113,7 +113,7 b' class FeedController(BaseRepoController)'
113 def _generate_feed(cache_key):
113 def _generate_feed(cache_key):
114 feed = Atom1Feed(
114 feed = Atom1Feed(
115 title=self.title % repo_name,
115 title=self.title % repo_name,
116 link=url('summary_home', repo_name=repo_name, qualified=True),
116 link=h.route_url('repo_summary', repo_name=repo_name),
117 description=self.description % repo_name,
117 description=self.description % repo_name,
118 language=self.language,
118 language=self.language,
119 ttl=self.ttl
119 ttl=self.ttl
@@ -150,8 +150,7 b' class FeedController(BaseRepoController)'
150 def _generate_feed(cache_key):
150 def _generate_feed(cache_key):
151 feed = Rss201rev2Feed(
151 feed = Rss201rev2Feed(
152 title=self.title % repo_name,
152 title=self.title % repo_name,
153 link=url('summary_home', repo_name=repo_name,
153 link=h.route_url('repo_summary', repo_name=repo_name),
154 qualified=True),
155 description=self.description % repo_name,
154 description=self.description % repo_name,
156 language=self.language,
155 language=self.language,
157 ttl=self.ttl
156 ttl=self.ttl
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/templates/admin/admin.mako to rhodecode/templates/admin/admin_audit_logs.mako
NO CONTENT: file renamed from rhodecode/templates/admin/admin.mako to rhodecode/templates/admin/admin_audit_logs.mako
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file renamed from rhodecode/tests/controllers/test_search.py to rhodecode/tests/lib/test_search.py
NO CONTENT: file renamed from rhodecode/tests/controllers/test_search.py to rhodecode/tests/lib/test_search.py
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: modified file
NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
1 NO CONTENT: file was removed
NO CONTENT: file was removed
The requested commit or file is too big and content was truncated. Show full diff
General Comments 0
You need to be logged in to leave comments. Login now